diff --git a/src/app/(app)/collection-confirm/page.tsx b/src/app/(app)/collection-confirm/page.tsx index bf4fb8d..7560205 100644 --- a/src/app/(app)/collection-confirm/page.tsx +++ b/src/app/(app)/collection-confirm/page.tsx @@ -18,7 +18,6 @@ import type { StockItem } from "@/lib/types"; import { appendConfirmingNote, appendNote, - buildConfirmNote, buildConfirmPatch, buildReturnPatch, canConfirmRow, @@ -30,6 +29,8 @@ import { type NoteEntry, } from "./view-model"; +import { ConfirmInvoiceDialog } from "@/components/collection-confirm/confirm-invoice-dialog"; + /** * Where the row came from. The collection-confirm / collection-pool * pages now read from BOTH `order_items` (customer deliveries) and @@ -1459,15 +1460,25 @@ function ConfirmCard({ { + rowCount={ + confirmModal?.bulkRows && confirmModal.bulkRows.length > 0 + ? confirmModal.bulkRows.length + : 1 + } + defaultValues={ + invoice ? { date: invoice.date, number: invoice.number } : undefined + } + costOverrides={{}} + onOpenChange={(state) => { + if (!state) onCloseConfirm(); + }} + onSubmit={async ({ invoice: submittedInvoice }) => { if (confirmModal?.bulkRows && confirmModal.bulkRows.length > 0) { - await onSubmitBulkConfirm(confirmModal.bulkRows, invoice); + await onSubmitBulkConfirm(confirmModal.bulkRows, submittedInvoice); } else { - await onSubmitConfirm(row, invoice); + await onSubmitConfirm(row, submittedInvoice); } }} /> @@ -1676,97 +1687,6 @@ function AttachDialog({ ); } -function ConfirmInvoiceDialog({ - open, - row, - busy, - bulkRows, - onClose, - onSubmit, -}: { - open: boolean; - row: ConfirmRow; - busy: boolean; - bulkRows?: ConfirmRow[]; - onClose: () => void; - onSubmit: (invoice: { date: string; number: string }) => Promise; -}) { - const today = new Date().toISOString().slice(0, 10); - const existing = readInvoiceDetails(row.order_item_details); - const [date, setDate] = useState(existing?.date ?? today); - const [number, setNumber] = useState(existing?.number ?? ""); - - useEffect(() => { - if (open) { - // eslint-disable-next-line react-hooks/set-state-in-effect -- reset modal state when (re)opened - setDate(existing?.date ?? today); - // eslint-disable-next-line react-hooks/set-state-in-effect -- reset modal state when (re)opened - setNumber(existing?.number ?? ""); - } - }, [open, existing?.date, existing?.number, today]); - - const trimmedNumber = number.trim(); - const autoNote = buildConfirmNote({ date, number: trimmedNumber }); - const valid = date.length === 10 && trimmedNumber.length > 0 && !busy; - const bulkCount = bulkRows?.length ?? 0; - - return ( - { - if (!state) onClose(); - }} - size="md" - eyebrow={`${row.expand?.purchase_order?.po_number ?? "Item"}`} - title="Confirm for invoicing" - description="Capture the supplier invoice details so invoicing can pick the row up." - footer={ -
- - -
- } - > -
-
- - setDate(e.target.value)} - className="mt-1 w-full rounded-md border bg-background px-3 py-2 text-sm" - /> -
-
- - setNumber(e.target.value)} - className="mt-1 w-full rounded-md border bg-background px-3 py-2 text-sm" - placeholder="INV-..." - /> -
-

- Audit note: {autoNote} -

- {bulkCount > 1 && ( -

- This will confirm {bulkCount} items in this PO with the invoice details above. -

- )} -
-
- ); -} - function ReturnToPoolDialog({ open, row, diff --git a/src/components/collection-confirm/confirm-invoice-dialog.tsx b/src/components/collection-confirm/confirm-invoice-dialog.tsx new file mode 100644 index 0000000..a5929ca --- /dev/null +++ b/src/components/collection-confirm/confirm-invoice-dialog.tsx @@ -0,0 +1,151 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Loader2 } from "lucide-react"; + +import { ModalShell } from "@/components/ui/modal-shell"; +import { Button } from "@/components/ui/button"; + +type ConfirmInvoiceDialogProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + /** + * Optional pre-filled invoice details. The parent typically derives + * these from the row being confirmed (e.g. via + * `readInvoiceDetails(row.order_item_details)`); when omitted, the + * date defaults to today and the number is left blank. + */ + defaultValues?: { + date?: string; + number?: string; + }; + /** + * Map of `itemId → new cost`. Surfaced in the summary line at the + * bottom of the dialog body so the operator can see at a glance + * whether the confirm patch will change any `cost_price` values. + * Tasks 10–12 will populate this from the surrounding flows; the + * dialog itself just reads it for display. + */ + costOverrides?: Record; + /** + * Number of rows this confirm covers. Drives the button label + * ("Confirm" vs "Confirm N items") and the summary line plural. + * Defaults to 1 (single-row confirm). + */ + rowCount?: number; + /** Small label above the title (e.g. the PO number). */ + eyebrow?: React.ReactNode; + /** Disables the submit button and shows a spinner while the parent persists. */ + busy?: boolean; + /** + * Called when the operator clicks Confirm. Receives the trimmed + * invoice date/number plus the cost-overrides map so callers can + * stitch them into the confirm patch. + */ + onSubmit: (args: { + invoice: { date: string; number: string }; + costOverrides: Record; + }) => Promise | void; +}; + +export function ConfirmInvoiceDialog({ + open, + onOpenChange, + defaultValues, + costOverrides = {}, + rowCount = 1, + eyebrow, + busy = false, + onSubmit, +}: ConfirmInvoiceDialogProps) { + const today = new Date().toISOString().slice(0, 10); + const [date, setDate] = useState(defaultValues?.date ?? today); + const [number, setNumber] = useState(defaultValues?.number ?? ""); + + useEffect(() => { + if (open) { + // eslint-disable-next-line react-hooks/set-state-in-effect -- reset modal state when (re)opened + setDate(defaultValues?.date ?? today); + // eslint-disable-next-line react-hooks/set-state-in-effect -- reset modal state when (re)opened + setNumber(defaultValues?.number ?? ""); + } + }, [open, defaultValues?.date, defaultValues?.number, today]); + + const trimmedNumber = number.trim(); + const valid = date.length === 10 && trimmedNumber.length > 0 && !busy; + const overridesCount = Object.keys(costOverrides).length; + + return ( + + + + + } + > +
+
+ + setDate(e.target.value)} + className="mt-1 w-full rounded-md border bg-background px-3 py-2 text-sm" + /> +
+
+ + setNumber(e.target.value)} + className="mt-1 w-full rounded-md border bg-background px-3 py-2 text-sm" + placeholder="INV-..." + /> +
+

+ Confirming {rowCount} row{rowCount === 1 ? "" : "s"} ·{" "} + {overridesCount} cost override{overridesCount === 1 ? "" : "s"} applied +

+
+
+ ); +}