doc compare
This commit is contained in:
@@ -8,11 +8,67 @@ import { Link, useParams } from "react-router-dom";
|
||||
|
||||
import { useAuth } from "../../auth/AuthProvider";
|
||||
import { ConfirmActionDialog } from "../../components/ConfirmActionDialog";
|
||||
import { DocumentRevisionComparison } from "../../components/DocumentRevisionComparison";
|
||||
import { FileAttachmentsPanel } from "../../components/FileAttachmentsPanel";
|
||||
import { api, ApiError } from "../../lib/api";
|
||||
import { emptyPurchaseReceiptInput, purchaseStatusOptions } from "./config";
|
||||
import { PurchaseStatusBadge } from "./PurchaseStatusBadge";
|
||||
|
||||
function formatCurrency(value: number) {
|
||||
return `$${value.toFixed(2)}`;
|
||||
}
|
||||
|
||||
function mapPurchaseDocumentForComparison(
|
||||
document: Pick<
|
||||
PurchaseOrderDetailDto,
|
||||
| "documentNumber"
|
||||
| "vendorName"
|
||||
| "status"
|
||||
| "issueDate"
|
||||
| "taxPercent"
|
||||
| "taxAmount"
|
||||
| "freightAmount"
|
||||
| "subtotal"
|
||||
| "total"
|
||||
| "notes"
|
||||
| "paymentTerms"
|
||||
| "currencyCode"
|
||||
| "lines"
|
||||
| "receipts"
|
||||
>
|
||||
) {
|
||||
return {
|
||||
title: document.documentNumber,
|
||||
subtitle: document.vendorName,
|
||||
status: document.status,
|
||||
metaFields: [
|
||||
{ label: "Issue Date", value: new Date(document.issueDate).toLocaleDateString() },
|
||||
{ label: "Payment Terms", value: document.paymentTerms || "N/A" },
|
||||
{ label: "Currency", value: document.currencyCode || "USD" },
|
||||
{ label: "Receipts", value: document.receipts.length.toString() },
|
||||
],
|
||||
totalFields: [
|
||||
{ label: "Subtotal", value: formatCurrency(document.subtotal) },
|
||||
{ label: "Tax", value: `${formatCurrency(document.taxAmount)} (${document.taxPercent.toFixed(2)}%)` },
|
||||
{ label: "Freight", value: formatCurrency(document.freightAmount) },
|
||||
{ label: "Total", value: formatCurrency(document.total) },
|
||||
],
|
||||
notes: document.notes,
|
||||
lines: document.lines.map((line) => ({
|
||||
key: line.id || `${line.itemId}-${line.position}`,
|
||||
title: `${line.itemSku} | ${line.itemName}`,
|
||||
subtitle: line.description,
|
||||
quantity: `${line.quantity} ${line.unitOfMeasure}`,
|
||||
unitLabel: line.unitOfMeasure,
|
||||
amountLabel: formatCurrency(line.unitCost),
|
||||
totalLabel: formatCurrency(line.lineTotal),
|
||||
extraLabel:
|
||||
`${line.receivedQuantity} received | ${line.remainingQuantity} remaining` +
|
||||
(line.salesOrderNumber ? ` | Demand ${line.salesOrderNumber}` : ""),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function PurchaseDetailPage() {
|
||||
const { token, user } = useAuth();
|
||||
const { orderId } = useParams();
|
||||
@@ -244,6 +300,9 @@ export function PurchaseDetailPage() {
|
||||
<p className="mt-1 text-sm text-text">{activeDocument.vendorName}</p>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<PurchaseStatusBadge status={activeDocument.status} />
|
||||
<span className="inline-flex items-center rounded-full border border-line/70 px-2 py-1 text-xs font-semibold uppercase tracking-[0.16em] text-muted">
|
||||
Rev {activeDocument.revisions[0]?.revisionNumber ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
@@ -297,6 +356,68 @@ export function PurchaseDetailPage() {
|
||||
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Payment Terms</p><div className="mt-2 text-base font-bold text-text">{activeDocument.paymentTerms || "N/A"}</div></article>
|
||||
<article className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Currency</p><div className="mt-2 text-base font-bold text-text">{activeDocument.currencyCode || "USD"}</div></article>
|
||||
</section>
|
||||
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Revision History</p>
|
||||
<p className="mt-2 text-sm text-muted">Automatic snapshots are recorded when the purchase order changes or receipts are posted.</p>
|
||||
</div>
|
||||
</div>
|
||||
{activeDocument.revisions.length === 0 ? (
|
||||
<div className="mt-6 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">
|
||||
No revisions have been recorded yet.
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-6 space-y-3">
|
||||
{activeDocument.revisions.map((revision) => (
|
||||
<article key={revision.id} className="rounded-[18px] border border-line/70 bg-page/60 p-3">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="font-semibold text-text">Rev {revision.revisionNumber}</div>
|
||||
<div className="mt-1 text-sm text-text">{revision.reason}</div>
|
||||
</div>
|
||||
<div className="text-right text-xs text-muted">
|
||||
<div>{new Date(revision.createdAt).toLocaleString()}</div>
|
||||
<div className="mt-1">{revision.createdByName ?? "System"}</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
{activeDocument.revisions.length > 0 ? (
|
||||
<DocumentRevisionComparison
|
||||
title="Revision Comparison"
|
||||
description="Compare earlier purchase-order revisions against the current document or another revision to review commercial, receiving, and line-level changes."
|
||||
currentLabel="Current document"
|
||||
currentDocument={mapPurchaseDocumentForComparison(activeDocument)}
|
||||
revisions={activeDocument.revisions.map((revision) => ({
|
||||
id: revision.id,
|
||||
label: `Rev ${revision.revisionNumber}`,
|
||||
meta: `${new Date(revision.createdAt).toLocaleString()} | ${revision.createdByName ?? "System"}`,
|
||||
}))}
|
||||
getRevisionDocument={(revisionId) => {
|
||||
if (revisionId === "current") {
|
||||
return mapPurchaseDocumentForComparison(activeDocument);
|
||||
}
|
||||
|
||||
const revision = activeDocument.revisions.find((entry) => entry.id === revisionId);
|
||||
if (!revision) {
|
||||
return mapPurchaseDocumentForComparison(activeDocument);
|
||||
}
|
||||
|
||||
return mapPurchaseDocumentForComparison({
|
||||
...revision.snapshot,
|
||||
lines: revision.snapshot.lines.map((line) => ({
|
||||
id: `${line.itemId}-${line.position}`,
|
||||
...line,
|
||||
})),
|
||||
receipts: revision.snapshot.receipts,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<div className="grid gap-3 xl:grid-cols-[minmax(0,1.05fr)_minmax(320px,0.95fr)]">
|
||||
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Vendor</p>
|
||||
|
||||
Reference in New Issue
Block a user