pelagia-portal/App/app/(portal)/po/new/page.tsx
Claude (auto-fix) 4dc10b834c
All checks were successful
PR checks / checks (pull_request) Successful in 52s
PR checks / integration (pull_request) Successful in 30s
feat(po): add Duplicate PO button to prefill a new PO
Anyone with create_po browsing a PO now sees a Duplicate action that
opens the New Purchase Order form prefilled from the source PO. Like the
existing cart→new-PO prefill, nothing is written until the user saves or
submits — a duplicate is just a clean draft of the editable order fields.

- po-detail.tsx: Duplicate link in the header, gated by
  hasPermission(currentRole, "create_po") + !readOnly, linking to
  /po/new?duplicate=<id>.
- po/new/page.tsx: when ?duplicate=<id> is present, fetch the source PO
  and map it onto the form's initial props via the new pure helper.
- new-po-form.tsx: accept initial-value props for title, accounting code
  (+ per-item toggle), project code, place of delivery, date required,
  quotation/requisition refs, terms — following the existing prop pattern.
- lib/duplicate-po.ts: pure, unit-tested mapping (Decimals→numbers, dates
  →yyyy-MM-dd, saved-terms snapshot with legacy tc* fallback). Attachments,
  status/dates, payment data and audit history are intentionally not copied.

Fixes #142

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 00:40:36 +05:30

117 lines
5.1 KiB
TypeScript

import { auth } from "@/auth";
import { db } from "@/lib/db";
import { hasPermission } from "@/lib/permissions";
import { redirect } from "next/navigation";
import { NewPoForm } from "./new-po-form";
import { buildAccountGroups } from "@/lib/cost-centre-groups";
import { formatDeliveryLocation } from "@/lib/delivery-location";
import { getTermsCatalogue, getDefaultPoTerms } from "@/lib/terms-data";
import { buildDuplicatePrefill, type DuplicatePrefill } from "@/lib/duplicate-po";
import type { Metadata } from "next";
import type { LineItemInput } from "@/lib/validations/po";
import type { CartItem } from "@/lib/cart";
export const metadata: Metadata = { title: "New Purchase Order" };
interface Props {
searchParams: Promise<{ cart?: string; vesselId?: string; duplicate?: string }>;
}
export default async function NewPoPage({ searchParams }: Props) {
const session = await auth();
if (!session?.user) redirect("/login");
if (!hasPermission(session.user.role, "create_po")) redirect("/dashboard");
const { cart, vesselId, duplicate } = await searchParams;
// Duplicate-PO prefill (issue #142): copy a source PO's editable order fields
// onto a fresh draft. Nothing is written until the user saves/submits — same
// shape as the cart→new-PO prefill below, just a richer field set.
let dup: DuplicatePrefill | null = null;
let initialLineItems: LineItemInput[] | undefined;
let initialVendorId: string | undefined;
let initialVesselId: string | undefined = vesselId;
if (duplicate) {
const source = await db.purchaseOrder.findUnique({
where: { id: duplicate },
include: { lineItems: { orderBy: { sortOrder: "asc" } } },
});
if (source) dup = buildDuplicatePrefill(source);
} else if (cart) {
try {
const cartItems: CartItem[] = JSON.parse(decodeURIComponent(cart));
if (Array.isArray(cartItems) && cartItems.length > 0) {
initialLineItems = cartItems.map((item) => ({
name: item.name,
description: item.description ?? "",
quantity: item.quantity,
unit: item.unit,
size: "",
unitPrice: item.unitPrice,
gstRate: 0.18,
productId: item.productId,
}));
const vendorIds = [...new Set(cartItems.map((i) => i.vendorId).filter(Boolean))];
if (vendorIds.length === 1) initialVendorId = vendorIds[0];
}
} catch {
// malformed cart param — ignore
}
}
const [vessels, leafAccounts, vendors, companies, deliveryLocations, projectCodes] = await Promise.all([
db.vessel.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, name: true, code: true } }),
db.account.findMany({
where: { isActive: true, children: { none: {} } },
orderBy: { code: "asc" },
select: { id: true, code: true, name: true, parent: { select: { name: true, code: true, parent: { select: { name: true, code: true } } } } },
}),
db.vendor.findMany({ where: { isActive: true }, orderBy: { name: "asc" } }),
db.company.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, name: true, code: true } }),
db.deliveryLocation.findMany({ where: { isActive: true }, orderBy: { createdAt: "asc" }, include: { company: { select: { name: true } } } }),
db.projectCode.findMany({ where: { isActive: true }, orderBy: { code: "asc" }, select: { code: true } }),
]);
const accounts = buildAccountGroups(leafAccounts);
const deliveryOptions = deliveryLocations.map((l) => formatDeliveryLocation(l.company.name, l.address));
const projectCodeOptions = projectCodes.map((c) => c.code);
const [termsCatalogue, defaultTerms] = await Promise.all([getTermsCatalogue(), getDefaultPoTerms()]);
return (
<div className="max-w-6xl">
<div className="mb-6">
<h1 className="text-2xl font-semibold text-neutral-900">New Purchase Order</h1>
<p className="mt-1 text-sm text-neutral-500">
Fill in the details below. You can save as draft or submit directly for approval.
</p>
</div>
<NewPoForm
vessels={vessels}
accounts={accounts}
vendors={vendors}
companies={companies}
deliveryOptions={deliveryOptions}
projectCodeOptions={projectCodeOptions}
termsCatalogue={termsCatalogue}
defaultTerms={defaultTerms}
initialLineItems={dup?.initialLineItems ?? initialLineItems}
initialVendorId={dup?.initialVendorId ?? initialVendorId}
initialVesselId={dup?.initialVesselId ?? initialVesselId}
initialCompanyId={dup?.initialCompanyId}
initialTitle={dup?.initialTitle}
initialAccountId={dup?.initialAccountId}
initialMultiAccount={dup?.initialMultiAccount}
initialProjectCode={dup?.initialProjectCode}
initialPlaceOfDelivery={dup?.initialPlaceOfDelivery}
initialDateRequired={dup?.initialDateRequired}
initialPiQuotationNo={dup?.initialPiQuotationNo}
initialPiQuotationDate={dup?.initialPiQuotationDate}
initialRequisitionNo={dup?.initialRequisitionNo}
initialRequisitionDate={dup?.initialRequisitionDate}
initialTerms={dup?.initialTerms}
/>
</div>
);
}