diff --git a/AGENTS.md b/AGENTS.md index 3db6828..331baa2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,8 @@ MRP Codex is a modular Manufacturing Resource Planning platform intended to be a - CRM customers/vendors, hierarchy, contacts, lifecycle metadata, and attachments - inventory items, BOMs, warehouses, locations, transactions, item attachments, and item pricing - inventory transfers, reservations, available-stock visibility, and work-order reservation automation -- sales quotes, sales orders, approvals, revision history, and purchase orders +- sales quotes, sales orders, approvals, revision history/comparison, and purchase orders +- purchase-order revision history and revision comparison across document and receipt changes - purchase-order supporting documents and vendor-side purchasing visibility - shipping shipments, packing-slip PDFs, shipping labels, bills of lading, and logistics attachments - projects with customer/commercial/shipment linkage, owners, due dates, notes, and attachments @@ -33,7 +34,7 @@ MRP Codex is a modular Manufacturing Resource Planning platform intended to be a - CRM/shipping audit coverage and startup validation surfaced through the admin diagnostics workflow - backup/restore guidance, richer startup diagnostics, and exportable support bundles in the admin diagnostics workflow - backup verification checklist and restore-drill runbook in the admin diagnostics workflow -- support-log viewing and support debugging helpers in the admin diagnostics workflow +- support-log viewing, filtering, retention cleanup, and support debugging helpers in the admin diagnostics workflow - Puppeteer PDF foundation - single-container Docker deployment @@ -131,8 +132,8 @@ If implementation changes invalidate those docs, update them in the same change Near-term priorities are: -1. Support-log filtering, retention controls, and broader support-package polish -2. Revision comparison UX for changed sales and purchasing documents +1. Project milestones and project-side rollup visibility +2. Manufacturing routing/work-center depth, labor capture, and capacity-aware execution views When adding new modules, preserve the ability to extend the system without refactoring the existing app shell. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7182574..df99d2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,12 @@ This file is the running release and change log for MRP Codex. Keep it updated w ### Added +- Revision comparison views for sales quotes, sales orders, and purchase orders with field- and line-level before/after diffs +- Purchase-order revision snapshots covering document edits, status changes, and receipt posting - Session review cues on admin auth sessions, including flagged stale activity, multi-session counts, and multi-IP warnings - Session filters and text search for admin-side access review across user, email, IP, user agent, and review reasons +- Support-log filtering by severity, source, search text, and retention window in admin diagnostics +- Support-log export and support-snapshot export now carry filter context, summary counts, available sources, and retention metadata - Shared destructive-action confirmation dialog with impact and recovery guidance for high-risk operational actions - Typed confirmation for sensitive admin actions such as account deactivation, current-session revocation, and terminal manufacturing/inventory postings - Destructive-action confirmation and recovery coverage for sales approvals, quote conversion, purchase receiving, purchase status changes, and shipment status changes @@ -54,7 +58,9 @@ This file is the running release and change log for MRP Codex. Keep it updated w ### Changed +- Sales and purchasing detail pages now expose revision comparison directly alongside chronological revision history - `ROADMAP.md` now tracks remaining work only, and shipped phase history now lives in `SHIPPED.md` +- Support logs now prune retained entries by age instead of only trimming by count, and admin diagnostics now reviews filtered support-log summaries instead of an unbounded flat dump - Admin diagnostics now summarizes sessions that need review, and startup now prunes old expired or revoked auth-session records - Admin, sales, purchasing, shipping, inventory, manufacturing, project, warehouse, and attachment workflows now use explicit destructive-action confirmation and recovery messaging instead of immediate irreversible clicks - Admin operations now combine user management with live session visibility so operators can inspect and revoke sign-ins without changing user records diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md index 2cb50ce..3c1986e 100644 --- a/INSTRUCTIONS.md +++ b/INSTRUCTIONS.md @@ -17,10 +17,11 @@ This repository implements the platform foundation milestone: - inventory master data, BOM, warehouse, stock-location, transactions, and item attachments - inventory transfers, reservations, available-stock visibility, and work-order reservation automation - sales quotes and sales orders with quick actions and quote conversion -- sales approvals, approval stamps, and automatic revision history on quotes and sales orders +- sales approvals, approval stamps, automatic revision history, and revision comparison on quotes and sales orders - purchase orders with quick actions and searchable vendor/SKU entry - purchase orders restricted to inventory items flagged as purchasable - purchase receiving foundation with inventory posting and receipt history +- purchase-order revision history and revision comparison across document and receipt changes - branded sales and purchasing PDFs through the shared Puppeteer document pipeline - purchase-order supporting documents and vendor-side purchasing visibility - shipping shipments linked to sales orders with packing slips, shipping labels, bills of lading, and logistics attachments @@ -37,7 +38,7 @@ This repository implements the platform foundation milestone: - CRM/shipping audit coverage and startup validation surfaced through diagnostics - backup/restore guidance, richer startup diagnostics, and exportable support bundles in diagnostics - backup verification checklist and restore-drill runbook in diagnostics -- support-log viewing and support debugging helpers in diagnostics +- support-log viewing, filtering, retention cleanup, and support debugging helpers in diagnostics - Dockerized single-container deployment - Puppeteer PDF pipeline foundation @@ -73,5 +74,5 @@ This repository implements the platform foundation milestone: ## Next roadmap candidates -- support-log filtering, retention controls, and broader support-package polish -- revision comparison UX for changed sales and purchasing documents +- project milestones and project-side rollup visibility +- manufacturing routing/work-center depth, labor capture, and capacity-aware execution views diff --git a/README.md b/README.md index 6bcae29..c423f72 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,9 @@ Current foundation scope includes: - inventory item master, BOM, warehouse, stock-location, and stock-transaction flows - inventory transfers, reservations, and available-stock visibility - sales quotes and sales orders with searchable customer and SKU entry -- sales approvals, approval stamps, and automatic revision history on quotes and sales orders +- sales approvals, approval stamps, automatic revision history, and revision comparison on quotes and sales orders - purchase orders with searchable vendor and SKU entry, restricted to purchasable inventory items +- purchase-order revision history and revision comparison across commercial and receipt changes - purchase receiving with warehouse/location posting and receipt history against purchase orders - branded quote, sales-order, and purchase-order PDFs through the shared backend document pipeline - purchase-order supporting documents for vendor invoices, acknowledgements, certifications, and backup files @@ -36,7 +37,7 @@ Current foundation scope includes: - CRM and shipping audit coverage plus startup validation surfaced through the admin diagnostics page - backup/restore guidance, richer startup diagnostics, and exportable support bundles in the admin diagnostics workflow - backup verification checklist and restore-drill runbook surfaced in admin diagnostics -- support-log viewing and support debugging helpers in admin diagnostics +- support-log viewing, filtering, retention cleanup, and richer support-debug export helpers in admin diagnostics - route-level code-splitting and vendor chunking for lighter initial client loads - file storage and PDF rendering @@ -60,14 +61,13 @@ Current completed foundation areas: Near-term priorities: -1. Support-log filtering, retention controls, and broader support-package polish -2. Revision comparison UX for changed sales and purchasing documents +1. Project milestones and project-side rollup visibility +2. Manufacturing routing/work-center depth, labor capture, and capacity-aware execution views Revisit / deferred items: - local Windows Prisma migration reliability -- support-log filtering, retention controls, and broader support-package polish -- revision comparison UX for changed sales and purchasing documents +- project milestones and project-side rollup visibility Dashboard direction: @@ -377,13 +377,14 @@ The current admin operations slice supports: - startup now prunes stale expired or revoked auth-session records before serving requests - backup and restore guidance now surfaces directly in diagnostics, along with exportable support bundles for support handoff - support logs now capture startup warnings, HTTP failures, and server errors for admin-side debugging review +- support logs now support admin-side filtering by severity, source, search text, and retention window, and exports include summary metadata - backup verification items and restore-drill expected outcomes now live in the admin runbook surface - operator-facing review of recent high-impact changes without direct database access Current follow-up direction: -- support-log filtering, retention controls, and broader support-package polish - revision comparison UX for changed sales and purchasing documents +- project milestones and project-side rollup visibility ## UI Notes diff --git a/ROADMAP.md b/ROADMAP.md index d81e2f9..7159586 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -14,11 +14,10 @@ This file tracks work that still needs to be completed. Shipped phase history an ## Near-term priority order -1. Support-log filtering, retention controls, and broader support-package polish -2. Revision comparison UX for changed sales and purchasing documents -3. Project milestones, project rollups, and deeper project-side execution visibility -4. Manufacturing routing/work-center depth, labor capture, and capacity-aware execution views -5. Dashboard KPI, alert, recent-activity, and exception-widget expansion +1. Project milestones, project rollups, and deeper project-side execution visibility +2. Manufacturing routing/work-center depth, labor capture, and capacity-aware execution views +3. Dashboard KPI, alert, recent-activity, and exception-widget expansion +4. Longer-term session history and audit depth beyond the current review filtering and retention cleanup ## Active roadmap @@ -59,7 +58,6 @@ This file tracks work that still needs to be completed. Shipped phase history an - Vendor exception handling for acknowledgements, invoice matching, receipt discrepancies, and related inbound follow-up - Deeper carrier/commercial defaults where they improve order-entry speed -- Revision comparison UX for changed customer-facing and purchasing documents - Line duplication, drag ordering, and keyboard-first line editing - Saved customer defaults for tax, freight, and commercial terms - Richer dashboard widgets for recent quotes, open orders, purchasing queues, and shipping exceptions @@ -123,7 +121,6 @@ This file tracks work that still needs to be completed. Shipped phase history an ### Security, audit, and operations maturity -- Support-log filtering, retention controls, and broader support-package polish - Admin diagnostics depth for permissions, migrations, storage, and PDF health - Longer-term session history and audit depth beyond the current review filtering and retention cleanup - More explicit environment validation on startup diff --git a/SHIPPED.md b/SHIPPED.md index 05c3edb..f02c4bd 100644 --- a/SHIPPED.md +++ b/SHIPPED.md @@ -38,6 +38,7 @@ This file tracks roadmap phases, slices, and major foundations that have already - Manufacturing stations, item routing templates, and automatic work-order operation planning for gantt scheduling - Vendor invoice/supporting-document attachments directly on purchase orders - Vendor-detail purchasing visibility with recent purchase-order activity +- Revision comparison UX for changed sales and purchasing documents, including purchase-order revision persistence - Audit trail coverage across core settings, inventory, purchasing, project, sales, and manufacturing write flows - Admin diagnostics screen with runtime footprint, record counts, storage-path visibility, and recent audit activity - Dedicated user-management screen for account creation, activation, role assignment, and role-permission editing @@ -120,3 +121,4 @@ This file tracks roadmap phases, slices, and major foundations that have already - Route-level lazy loading and vendor chunking for a lighter initial client payload - Persisted auth-session review filtering and admin-side access review cues - Destructive-action confirmation coverage expanded into project customer/document unlinking and form-row removals in sales, purchasing, inventory, and warehouse editors +- Support-log filtering, retention cleanup, and richer filtered support-bundle exports in admin diagnostics diff --git a/client/src/components/DocumentRevisionComparison.tsx b/client/src/components/DocumentRevisionComparison.tsx new file mode 100644 index 0000000..4d3c28c --- /dev/null +++ b/client/src/components/DocumentRevisionComparison.tsx @@ -0,0 +1,279 @@ +import { useEffect, useState } from "react"; + +type RevisionOption = { + id: string; + label: string; + meta: string; +}; + +type ComparisonField = { + label: string; + value: string; +}; + +type ComparisonLine = { + key: string; + title: string; + subtitle?: string; + quantity: string; + unitLabel: string; + amountLabel: string; + totalLabel?: string; + extraLabel?: string; +}; + +type ComparisonDocument = { + title: string; + subtitle: string; + status: string; + metaFields: ComparisonField[]; + totalFields: ComparisonField[]; + notes: string; + lines: ComparisonLine[]; +}; + +type DiffRow = { + key: string; + status: "ADDED" | "REMOVED" | "CHANGED"; + left?: ComparisonLine; + right?: ComparisonLine; +}; + +function buildLineMap(lines: ComparisonLine[]) { + return new Map(lines.map((line) => [line.key, line])); +} + +function lineSignature(line?: ComparisonLine) { + if (!line) { + return ""; + } + + return [line.title, line.subtitle ?? "", line.quantity, line.unitLabel, line.amountLabel, line.totalLabel ?? "", line.extraLabel ?? ""].join("|"); +} + +function buildDiffRows(left: ComparisonDocument, right: ComparisonDocument): DiffRow[] { + const leftLines = buildLineMap(left.lines); + const rightLines = buildLineMap(right.lines); + const orderedKeys = [...new Set([...left.lines.map((line) => line.key), ...right.lines.map((line) => line.key)])]; + const rows: DiffRow[] = []; + + for (const key of orderedKeys) { + const leftLine = leftLines.get(key); + const rightLine = rightLines.get(key); + + if (leftLine && !rightLine) { + rows.push({ key, status: "REMOVED", left: leftLine }); + continue; + } + + if (!leftLine && rightLine) { + rows.push({ key, status: "ADDED", right: rightLine }); + continue; + } + + if (lineSignature(leftLine) !== lineSignature(rightLine)) { + rows.push({ key, status: "CHANGED", left: leftLine, right: rightLine }); + } + } + + return rows; +} + +function buildFieldChanges(left: ComparisonField[], right: ComparisonField[]): Array<{ label: string; leftValue: string; rightValue: string }> { + const rightByLabel = new Map(right.map((field) => [field.label, field.value])); + + return left.flatMap((field) => { + const rightValue = rightByLabel.get(field.label); + if (rightValue == null || rightValue === field.value) { + return []; + } + + return [ + { + label: field.label, + leftValue: field.value, + rightValue, + }, + ]; + }); +} + +function ComparisonCard({ label, document }: { label: string; document: ComparisonDocument }) { + return ( +
+
+
+

{label}

+

{document.title}

+

{document.subtitle}

+
+ + {document.status} + +
+
+ {document.metaFields.map((field) => ( +
+
{field.label}
+
{field.value}
+
+ ))} +
+
+ {document.totalFields.map((field) => ( +
+
{field.label}
+
{field.value}
+
+ ))} +
+
+
Notes
+

{document.notes || "No notes recorded."}

+
+
+ ); +} + +export function DocumentRevisionComparison({ + title, + description, + currentLabel, + currentDocument, + revisions, + getRevisionDocument, +}: { + title: string; + description: string; + currentLabel: string; + currentDocument: ComparisonDocument; + revisions: RevisionOption[]; + getRevisionDocument: (revisionId: string | "current") => ComparisonDocument; +}) { + const [leftRevisionId, setLeftRevisionId] = useState(revisions[0]?.id ?? "current"); + const [rightRevisionId, setRightRevisionId] = useState("current"); + + useEffect(() => { + setLeftRevisionId((current) => (current === "current" || revisions.some((revision) => revision.id === current) ? current : revisions[0]?.id ?? "current")); + }, [revisions]); + + const leftDocument = getRevisionDocument(leftRevisionId); + const rightDocument = getRevisionDocument(rightRevisionId); + const diffRows = buildDiffRows(leftDocument, rightDocument); + const metaChanges = buildFieldChanges(leftDocument.metaFields, rightDocument.metaFields); + const totalChanges = buildFieldChanges(leftDocument.totalFields, rightDocument.totalFields); + + return ( +
+
+
+

{title}

+

{description}

+
+
+ + +
+
+
+ + +
+
+
+

Field Changes

+ {metaChanges.length === 0 && totalChanges.length === 0 ? ( +
No header or total changes between the selected revisions.
+ ) : ( +
+ {[...metaChanges, ...totalChanges].map((change) => ( +
+
{change.label}
+
+ {change.leftValue} {"->"} {change.rightValue} +
+
+ ))} +
+ )} +
+
+

Line Changes

+
+
+
Added
+
{diffRows.filter((row) => row.status === "ADDED").length}
+
+
+
Removed
+
{diffRows.filter((row) => row.status === "REMOVED").length}
+
+
+
Changed
+
{diffRows.filter((row) => row.status === "CHANGED").length}
+
+
+ {diffRows.length === 0 ? ( +
No line-level changes between the selected revisions.
+ ) : ( +
+ {diffRows.map((row) => ( +
+
+
{row.right?.title ?? row.left?.title}
+ {row.status} +
+
+
+
Baseline
+
+ {row.left ? `${row.left.quantity} | ${row.left.amountLabel}${row.left.totalLabel ? ` | ${row.left.totalLabel}` : ""}` : "Not present"} +
+ {row.left?.subtitle ?
{row.left.subtitle}
: null} + {row.left?.extraLabel ?
{row.left.extraLabel}
: null} +
+
+
Compare To
+
+ {row.right ? `${row.right.quantity} | ${row.right.amountLabel}${row.right.totalLabel ? ` | ${row.right.totalLabel}` : ""}` : "Not present"} +
+ {row.right?.subtitle ?
{row.right.subtitle}
: null} + {row.right?.extraLabel ?
{row.right.extraLabel}
: null} +
+
+
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index 4e0144c..41da386 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -6,6 +6,8 @@ import type { AdminRoleDto, AdminRoleInput, SupportLogEntryDto, + SupportLogFiltersDto, + SupportLogListDto, SupportSnapshotDto, AdminUserDto, AdminUserInput, @@ -81,6 +83,7 @@ import type { import type { PurchaseOrderDetailDto, PurchaseOrderInput, + PurchaseOrderRevisionDto, PurchaseOrderStatus, PurchaseOrderSummaryDto, PurchaseVendorOptionDto, @@ -152,8 +155,33 @@ export const api = { getSupportSnapshot(token: string) { return request("/api/v1/admin/support-snapshot", undefined, token); }, - getSupportLogs(token: string) { - return request("/api/v1/admin/support-logs", undefined, token); + getSupportSnapshotWithFilters(token: string, filters?: SupportLogFiltersDto) { + return request( + `/api/v1/admin/support-snapshot${buildQueryString({ + level: filters?.level, + source: filters?.source, + query: filters?.query, + start: filters?.start, + end: filters?.end, + limit: filters?.limit?.toString(), + })}`, + undefined, + token + ); + }, + getSupportLogs(token: string, filters?: SupportLogFiltersDto) { + return request( + `/api/v1/admin/support-logs${buildQueryString({ + level: filters?.level, + source: filters?.source, + query: filters?.query, + start: filters?.start, + end: filters?.end, + limit: filters?.limit?.toString(), + })}`, + undefined, + token + ); }, getAdminPermissions(token: string) { return request("/api/v1/admin/permissions", undefined, token); @@ -683,6 +711,9 @@ export const api = { getPurchaseOrder(token: string, orderId: string) { return request(`/api/v1/purchasing/orders/${orderId}`, undefined, token); }, + getPurchaseOrderRevisions(token: string, orderId: string) { + return request(`/api/v1/purchasing/orders/${orderId}/revisions`, undefined, token); + }, createPurchaseOrder(token: string, payload: PurchaseOrderInput) { return request("/api/v1/purchasing/orders", { method: "POST", body: JSON.stringify(payload) }, token); }, diff --git a/client/src/modules/purchasing/PurchaseDetailPage.tsx b/client/src/modules/purchasing/PurchaseDetailPage.tsx index c5b715c..4863341 100644 --- a/client/src/modules/purchasing/PurchaseDetailPage.tsx +++ b/client/src/modules/purchasing/PurchaseDetailPage.tsx @@ -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() {

{activeDocument.vendorName}

+ + Rev {activeDocument.revisions[0]?.revisionNumber ?? 0} +
@@ -297,6 +356,68 @@ export function PurchaseDetailPage() {

Payment Terms

{activeDocument.paymentTerms || "N/A"}

Currency

{activeDocument.currencyCode || "USD"}
+
+
+
+

Revision History

+

Automatic snapshots are recorded when the purchase order changes or receipts are posted.

+
+
+ {activeDocument.revisions.length === 0 ? ( +
+ No revisions have been recorded yet. +
+ ) : ( +
+ {activeDocument.revisions.map((revision) => ( +
+
+
+
Rev {revision.revisionNumber}
+
{revision.reason}
+
+
+
{new Date(revision.createdAt).toLocaleString()}
+
{revision.createdByName ?? "System"}
+
+
+
+ ))} +
+ )} +
+ {activeDocument.revisions.length > 0 ? ( + ({ + 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}

Vendor

diff --git a/client/src/modules/purchasing/PurchaseFormPage.tsx b/client/src/modules/purchasing/PurchaseFormPage.tsx index dd08a65..ea4920c 100644 --- a/client/src/modules/purchasing/PurchaseFormPage.tsx +++ b/client/src/modules/purchasing/PurchaseFormPage.tsx @@ -140,6 +140,7 @@ export function PurchaseFormPage({ mode }: { mode: "create" | "edit" }) { taxPercent: document.taxPercent, freightAmount: document.freightAmount, notes: document.notes, + revisionReason: "", lines: document.lines.map((line: { itemId: string; description: string; quantity: number; unitOfMeasure: PurchaseLineInput["unitOfMeasure"]; unitCost: number; position: number; salesOrderId: string | null; salesOrderLineId: string | null }) => ({ itemId: line.itemId, description: line.description, @@ -344,6 +345,17 @@ export function PurchaseFormPage({ mode }: { mode: "create" | "edit" }) { Notes