[Issue]: Add Duplicate PO button #142

Closed
opened 2026-06-27 18:54:46 +00:00 by shad0w · 3 comments
Owner

Raised by

Eeshan Singh (eeshan.singh@pelagiamarine.com, TECHNICAL) — via portal Report Issue button

Description

For anyone who is allowed to create POs, browsing a PO should show a duplicate PO button that creates a new PO form pre-filled with the values from the duplicated PO.

Priority

P2 — Medium

Context

  • Page: /po/cmqwbs64y01e7uxaw4kfyd5uy
  • Reported at: 2026-06-27T18:54:46.480Z
### Raised by Eeshan Singh (eeshan.singh@pelagiamarine.com, TECHNICAL) — via portal Report Issue button ### Description For anyone who is allowed to create POs, browsing a PO should show a duplicate PO button that creates a new PO form pre-filled with the values from the duplicated PO. ### Priority P2 — Medium ### Context - Page: `/po/cmqwbs64y01e7uxaw4kfyd5uy` - Reported at: 2026-06-27T18:54:46.480Z
shad0w added the
portal
label 2026-06-27 18:54:46 +00:00
shad0w added the
feature
claude-queue
triaged
labels 2026-06-27 19:01:53 +00:00
Author
Owner

Claude triage

Triage — Issue #142: Add Duplicate PO button

Type: Feature (new capability — a "Duplicate" affordance on PO detail)
Routing: claude-queue
Priority: P2 — Medium

Interpretation

Anyone with create_po browsing a PO should see a Duplicate button. Clicking it opens
the New Purchase Order form pre-filled with the source PO's values, so the user can tweak
and submit a fresh PO. Crucially this is a prefilled form, not a created record — nothing is
written until the user saves/submits, exactly like the existing cart → new PO prefill flow.

The codebase already establishes this pattern: new/page.tsx reads a ?cart= query param and
maps it onto initialLineItems / initialVendorId props that NewPoForm consumes. Duplicate
is the same shape with a richer field set sourced from an existing PO by id.

Action items

  1. Add the Duplicate buttoncomponents/po/po-detail.tsx header action area
    (po-detail.tsx:198, alongside Edit / Export / Email). Render it gated by
    hasPermission(currentRole, "create_po") (the component already receives currentRole;
    import hasPermission from lib/permissions). A plain <Link href={/po/new?duplicate=${po.id}}>.
    Decide visibility: simplest is "any status the user can view"; acceptable since it only
    prefills a new form.
  2. Handle the duplicate paramapp/(portal)/po/new/page.tsx. When duplicate=<id> is
    present, fetch that PO (db.purchaseOrder.findUnique with lineItems, account, vendor,
    vessel, company) and map it onto the form's initial props. Reuse the existing
    create_po gate already at the top of the page.
  3. Extend NewPoForm initial propsapp/(portal)/po/new/new-po-form.tsx. It already
    accepts initialLineItems, initialVendorId, initialVesselId, initialCompanyId. Most
    other fields are uncontrolled (title, projectCode, dateRequired, placeOfDelivery,
    PI/quotation + requisition fields) and the accounting code / terms are controlled state.
    Add defaultValue/initial props for the fields to copy: initialTitle, initialAccountId
    (→ defaultAccountId), initialProjectCode, initialPlaceOfDelivery, initialTerms
    (→ terms state), and optionally the quotation/requisition fields. Mechanical, following
    the existing prop pattern.
  4. Map line items — reuse the lineItemsForEditor shape already in po-detail.tsx:111
    (Decimal → Number()), copying name/description/quantity/unit/size/unitPrice/gstRate
    (and accountId for multi-account POs). Drop productId? — keep it; it links back cleanly.
  5. Unit test — cover the duplicate→prefill mapping (e.g. a tests/unit test asserting the
    source PO maps to the expected initial props / rendered defaults).

Files / areas involved

  • components/po/po-detail.tsx — new Duplicate Link + create_po gate
  • app/(portal)/po/new/page.tsx — read ?duplicate=, fetch + map source PO
  • app/(portal)/po/new/new-po-form.tsx — additional initial-value props/defaults
  • lib/permissions.ts — read-only (hasPermission, create_po); no permission changes
  • tests/unit/ — new test for the mapping

What is explicitly NOT touched

No DB migration (no new model/field — prefill only, source PO unchanged), no auth/permissions
changes (only reads the existing create_po grant), no payments/money logic, no external
systems. Attachments, status/dates, payment refs, and audit history are not copied — a
duplicate starts as a clean draft.

Open questions (sensible defaults assumed, not blockers)

  • Which fields to copy? Default: all editable order fields (title, company, vessel,
    accounting code, project code, delivery, line items, vendor, T&C). Exclude attachments,
    PO/payment dates, status, payment data. (Quotation/requisition refs: lean toward copying.)
  • Vendor copy — copying an unverified vendor is fine; the create flow already permits a
    blank/unverified vendor.
  • Button visibility by status — assume available in any viewable status; could be limited
    to non-DRAFT if reviewers prefer, but no functional reason to restrict.
  • Title suffix — copy verbatim, or append "(copy)"? Default: copy verbatim.

These are low-risk presentation choices with clear defaults; none requires business content,
design sign-off, or schema/permission decisions.

Routing rationale: Localized 3-file UI prefill change that directly extends the existing cart→new-PO pattern, gated by an existing permission with no DB migration, auth/payments, or external dependency, and fully verifiable by type-check / lint / a unit test — fits an unattended claude-queue run.

Routing: claude-queue | Type: feature

## Claude triage # Triage — Issue #142: Add Duplicate PO button **Type:** Feature (new capability — a "Duplicate" affordance on PO detail) **Routing:** claude-queue **Priority:** P2 — Medium ## Interpretation Anyone with `create_po` browsing a PO should see a **Duplicate** button. Clicking it opens the **New Purchase Order** form pre-filled with the source PO's values, so the user can tweak and submit a fresh PO. Crucially this is a *prefilled form*, not a created record — nothing is written until the user saves/submits, exactly like the existing **cart → new PO** prefill flow. The codebase already establishes this pattern: `new/page.tsx` reads a `?cart=` query param and maps it onto `initialLineItems` / `initialVendorId` props that `NewPoForm` consumes. Duplicate is the same shape with a richer field set sourced from an existing PO by id. ## Action items 1. **Add the Duplicate button** — `components/po/po-detail.tsx` header action area (`po-detail.tsx:198`, alongside Edit / Export / Email). Render it gated by `hasPermission(currentRole, "create_po")` (the component already receives `currentRole`; import `hasPermission` from `lib/permissions`). A plain `<Link href={`/po/new?duplicate=${po.id}`}>`. Decide visibility: simplest is "any status the user can view"; acceptable since it only prefills a new form. 2. **Handle the `duplicate` param** — `app/(portal)/po/new/page.tsx`. When `duplicate=<id>` is present, fetch that PO (`db.purchaseOrder.findUnique` with `lineItems`, `account`, `vendor`, `vessel`, `company`) and map it onto the form's initial props. Reuse the existing `create_po` gate already at the top of the page. 3. **Extend `NewPoForm` initial props** — `app/(portal)/po/new/new-po-form.tsx`. It already accepts `initialLineItems`, `initialVendorId`, `initialVesselId`, `initialCompanyId`. Most other fields are **uncontrolled** (`title`, `projectCode`, `dateRequired`, `placeOfDelivery`, PI/quotation + requisition fields) and the accounting code / terms are controlled state. Add `defaultValue`/initial props for the fields to copy: `initialTitle`, `initialAccountId` (→ `defaultAccountId`), `initialProjectCode`, `initialPlaceOfDelivery`, `initialTerms` (→ `terms` state), and optionally the quotation/requisition fields. Mechanical, following the existing prop pattern. 4. **Map line items** — reuse the `lineItemsForEditor` shape already in `po-detail.tsx:111` (Decimal → `Number()`), copying `name/description/quantity/unit/size/unitPrice/gstRate` (and `accountId` for multi-account POs). Drop `productId`? — keep it; it links back cleanly. 5. **Unit test** — cover the duplicate→prefill mapping (e.g. a `tests/unit` test asserting the source PO maps to the expected initial props / rendered defaults). ## Files / areas involved - `components/po/po-detail.tsx` — new Duplicate Link + `create_po` gate - `app/(portal)/po/new/page.tsx` — read `?duplicate=`, fetch + map source PO - `app/(portal)/po/new/new-po-form.tsx` — additional initial-value props/defaults - `lib/permissions.ts` — read-only (`hasPermission`, `create_po`); **no permission changes** - `tests/unit/` — new test for the mapping ## What is explicitly NOT touched No DB migration (no new model/field — prefill only, source PO unchanged), no auth/permissions changes (only *reads* the existing `create_po` grant), no payments/money logic, no external systems. Attachments, status/dates, payment refs, and audit history are **not** copied — a duplicate starts as a clean draft. ## Open questions (sensible defaults assumed, not blockers) - **Which fields to copy?** Default: all editable order fields (title, company, vessel, accounting code, project code, delivery, line items, vendor, T&C). Exclude attachments, PO/payment dates, status, payment data. (Quotation/requisition refs: lean toward copying.) - **Vendor copy** — copying an unverified vendor is fine; the create flow already permits a blank/unverified vendor. - **Button visibility by status** — assume available in any viewable status; could be limited to non-DRAFT if reviewers prefer, but no functional reason to restrict. - **Title suffix** — copy verbatim, or append "(copy)"? Default: copy verbatim. These are low-risk presentation choices with clear defaults; none requires business content, design sign-off, or schema/permission decisions. Routing rationale: Localized 3-file UI prefill change that directly extends the existing cart→new-PO pattern, gated by an existing permission with no DB migration, auth/payments, or external dependency, and fully verifiable by type-check / lint / a unit test — fits an unattended claude-queue run. **Routing:** `claude-queue` | **Type:** `feature`
shad0w added
claude-working
and removed
claude-queue
labels 2026-06-27 19:04:25 +00:00
Author
Owner

[Claude] Started working on this issue on branch claude/issue-142.

<!-- ppms-bot --> [Claude] Started working on this issue on branch `claude/issue-142`.
shad0w added
claude-pr
and removed
claude-working
labels 2026-06-27 19:10:50 +00:00
Author
Owner

[Claude] Opened PR #145 with a proposed fix. Review and merge it, then create a release tag to deploy.

<!-- ppms-bot --> [Claude] Opened PR [#145](https://git.pelagiamarine.com/shad0w/pelagia-portal/pulls/145) with a proposed fix. Review and merge it, then create a release tag to deploy.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: shad0w/pelagia-portal#142
No description provided.