import Link from "next/link"; import { PoStatusBadge } from "@/components/po/po-status-badge"; import { LineItemsEditor } from "@/components/po/po-line-items-editor"; import { DiscardDraftButton } from "@/components/po/discard-draft-button"; import { SubmitDraftButton } from "@/components/po/submit-draft-button"; import { CancelPoButton, SupersedeForm } from "@/components/po/cancel-po-controls"; import { EmailVendorButton } from "@/components/po/email-vendor-button"; import { formatCurrency, formatDate, formatDateTime } from "@/lib/utils"; import { generateDownloadUrl } from "@/lib/storage"; import { groupAttachments } from "@/lib/attachments"; import { TC_FIXED_LINE } from "@/lib/validations/po"; import { parsePoTerms } from "@/lib/terms"; import { actionLabel } from "@/lib/po-activity"; import { hasPermission } from "@/lib/permissions"; import type { LineItemInput } from "@/lib/validations/po"; import type { Role } from "@prisma/client"; type PoWithRelations = { id: string; poNumber: string; title: string; status: import("@prisma/client").POStatus; totalAmount: import("@prisma/client").Prisma.Decimal; currency: string; poDate: Date | null; projectCode: string | null; dateRequired: Date | null; managerNote: string | null; paymentRef: string | null; paymentDate?: Date | null; paidAmount?: import("@prisma/client").Prisma.Decimal | null; suggestedAdvancePayment?: import("@prisma/client").Prisma.Decimal | null; piQuotationNo?: string | null; piQuotationDate?: Date | null; requisitionNo?: string | null; requisitionDate?: Date | null; placeOfDelivery?: string | null; tcDelivery?: string | null; tcDispatch?: string | null; tcInspection?: string | null; tcTransitInsurance?: string | null; tcPaymentTerms?: string | null; tcOthers?: string | null; terms?: import("@prisma/client").Prisma.JsonValue; createdAt: Date; submittedAt: Date | null; approvedAt: Date | null; paidAt: Date | null; closedAt: Date | null; cancelledAt?: Date | null; cancellationReason?: string | null; supersededBy?: { id: string; poNumber: string } | null; supersedes?: { id: string; poNumber: string }[]; submitter: { id: string; name: string; email: string }; vessel: { id: string; name: string }; account: { id: string; name: string; code: string }; vendor: { id: string; name: string; vendorId: string | null; address?: string | null; gstin?: string | null; contactName?: string | null; contactMobile?: string | null; contactEmail?: string | null; } | null; lineItems: { id: string; name: string; description?: string | null; quantity: import("@prisma/client").Prisma.Decimal; unit: string; size?: string | null; unitPrice: import("@prisma/client").Prisma.Decimal; totalPrice: import("@prisma/client").Prisma.Decimal; gstRate?: import("@prisma/client").Prisma.Decimal | null; sortOrder: number; }[]; documents: { id: string; fileName: string; fileSize: number; storageKey: string; uploadedAt: Date }[]; actions: { id: string; actionType: string; note: string | null; metadata: import("@prisma/client").Prisma.JsonValue; createdAt: Date; actor: { name: string } }[]; }; interface Props { po: PoWithRelations; currentUserId: string; currentRole: Role; readOnly?: boolean; // Vendor's primary contact email — enables the "Email to vendor" action (issue #14). vendorEmail?: string | null; } export async function PoDetail({ po, currentUserId, currentRole, readOnly = false, vendorEmail = null }: Props) { const lineItemsForEditor = po.lineItems.map((li) => ({ name: li.name, description: li.description ?? undefined, quantity: Number(li.quantity), unit: li.unit, size: li.size ?? undefined, unitPrice: Number(li.unitPrice), gstRate: li.gstRate != null ? Number(li.gstRate) : 0.18, })); const managerEditAction = [...po.actions] .reverse() .find((a) => a.actionType === "MANAGER_LINE_EDIT"); const noteAction = [...po.actions] .reverse() .find((a) => ["EDITS_REQUESTED", "REJECTED", "APPROVED", "APPROVED_WITH_NOTE"].includes(a.actionType) && a.note ); const managerNoteAuthor = noteAction?.actor.name ?? null; // Resubmit snapshot: stored in the most recent SUBMITTED action's metadata // when the submitter resubmits after EDITS_REQUESTED. type ResubmitSnapshot = { lineItems: LineItemInput[]; fields: { title: string; vessel: string | null; vesselId: string; account: string; accountId: string; vendor: string | null; vendorId: string | null; poDate: string | null; projectCode: string | null; dateRequired: string | null; placeOfDelivery: string | null; }; }; const resubmitAction = [...po.actions] .reverse() .find( (a) => a.actionType === "SUBMITTED" && !!(a.metadata as { editSnapshot?: unknown } | null)?.editSnapshot ); const resubmitSnapshot = resubmitAction ? (resubmitAction.metadata as { editSnapshot: ResubmitSnapshot }).editSnapshot : null; // Resubmit snapshot takes priority over manager line edit diff. const originalLineItems: LineItemInput[] | undefined = resubmitSnapshot?.lineItems ?? (managerEditAction ? (managerEditAction.metadata as { original: typeof lineItemsForEditor } | null)?.original : undefined); const lineItemsDiffLabel = resubmitSnapshot ? "Submitter updated these line items after edits were requested. Previous values shown with strikethrough." : "Line items were amended by manager. Current values shown; original values shown with strikethrough."; const docsWithUrls = await Promise.all( po.documents.map(async (doc) => ({ ...doc, url: await generateDownloadUrl(doc.storageKey), })) ); const attachmentGroups = groupAttachments(docsWithUrls); const canConfirmReceipt = (po.status === "PAID_DELIVERED" || po.status === "PARTIALLY_CLOSED" || po.status === "PARTIALLY_PAID") && (po.submitter.id === currentUserId || currentRole === "SUPERUSER") && !readOnly; // Find the approver from actions const approvalAction = [...po.actions] .reverse() .find((a) => a.actionType === "APPROVED" || a.actionType === "APPROVED_WITH_NOTE"); // PO date: submitter-set date → approved date → creation date const poDisplayDate = po.poDate ?? po.approvedAt ?? po.createdAt; return (
Cancelled{po.cancelledAt ? ` on ${formatDate(po.cancelledAt)}` : ""}
{po.cancellationReason && (Reason: {po.cancellationReason}
)}Superseded by{" "} {po.supersededBy.poNumber}
) : ["MANAGER", "SUPERUSER"].includes(currentRole) && !readOnly ? (Optionally link the PO that replaces this one:
Supersedes{" "} {po.supersedes.map((s, i) => ( {i > 0 && ", "} {s.poNumber} ))}
{managerNoteAuthor ? `Note from ${managerNoteAuthor}` : "Manager note"}
{po.managerNote}
Advance payment requested
Pay {formatCurrency(Number(po.suggestedAdvancePayment), po.currency)} first (of{" "} {formatCurrency(Number(po.totalAmount), po.currency)}). The balance follows the usual part-payment flow.
Submitter updated the following fields after edits were requested
| Field | Before | After |
|---|---|---|
| {label} | {before} | {after} |
No vendor assigned to this PO.
{group.meta.description}
)}{po.status === "PARTIALLY_CLOSED" ? "Partially received" : po.status === "PARTIALLY_PAID" ? "Advance payment received" : "Payment confirmed"}
{po.status === "PARTIALLY_CLOSED" ? "Some items are still outstanding. Confirm remaining deliveries." : po.status === "PARTIALLY_PAID" ? `Advance payment received (${formatCurrency(Number(po.paidAmount ?? 0), po.currency)} of ${formatCurrency(Number(po.totalAmount), po.currency)}). Items can be received now — PO closes when fully paid and delivered.` : "Please confirm that you have received all items."}
"{action.note}"
)}