From 33fc549e10ac3b4c1eb50a1cb039196054f15d36 Mon Sep 17 00:00:00 2001 From: Dave Date: Sun, 30 Aug 2026 12:20:39 +0200 Subject: [PATCH] feat(orders): add shared quote-line context component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shared presentation of a paired quote line — description (when it differs from the order), buyer note, line attachments, quote attachments, plus a 'possible match' warning banner for the operator's confirm button. Used by both the order detail table (Task 13) and the process page (Task 12) so the two never drift. The process page used to own the only copy; the detail page showed nothing. tier='possible' framing is the load-bearing part: a buyer is never shown a note from the wrong line without being told it might be wrong. Co-Authored-By: Claude --- src/components/orders/quote-line-context.tsx | 118 +++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/components/orders/quote-line-context.tsx diff --git a/src/components/orders/quote-line-context.tsx b/src/components/orders/quote-line-context.tsx new file mode 100644 index 0000000..676ac6c --- /dev/null +++ b/src/components/orders/quote-line-context.tsx @@ -0,0 +1,118 @@ +"use client"; + +import { MessageSquare, HelpCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + AttachmentList, + type MatchedAttachment, +} from "@/components/orders/attachment-list"; + +/** + * QuoteLineContext — everything the buyer / operator needs to know + * about the quote line behind one order line. + * + * Shared by the order detail table (Task 13) and the process page + * (Task 12) so the two never drift. Before this existed, the process + * page owned the only copy and the detail page showed nothing at all. + * + * `tier` drives the framing: a "confident" pair is stated as fact, a + * "possible" pair is explicitly presented as a guess with a confirm + * button, so a buyer is never shown a note from the wrong line + * without being told it might be wrong. + */ + +export interface QuoteLineContextProps { + /** Quote line description. Rendered only when it differs from the order's. */ + quoteDescription: string; + orderDescription: string; + /** Free-text note the estimator left on the quote line. */ + notes: string; + perItemAttachments: MatchedAttachment[]; + quoteAttachments: MatchedAttachment[]; + tier: "confident" | "possible"; + /** Called when the operator confirms a "possible" pair. */ + onConfirm?: () => void; + confirming?: boolean; +} + +export function QuoteLineContext({ + quoteDescription, + orderDescription, + notes, + perItemAttachments, + quoteAttachments, + tier, + onConfirm, + confirming = false, +}: QuoteLineContextProps) { + return ( +
+ {tier === "possible" && ( +
+
+ +
+
+ Possible match — is this the right quote line? +
+

+ The wording differs enough that we're not certain. Confirm to + link them permanently. +

+
+ {onConfirm && ( + + )} +
+
+ )} + + {quoteDescription && quoteDescription !== orderDescription && ( +
+
+ Quote line says +
+
{quoteDescription}
+
+ )} + + {notes.trim() !== "" && ( +
+
+ + Buyer note +
+
+ {notes} +
+
+ )} + + {perItemAttachments.length > 0 && ( + + )} + {quoteAttachments.length > 0 && ( + + )} +
+ ); +} + +/** Shown in place of the panel when the order line has no counterpart + * on the linked quote. Previously this case rendered nothing at all, + * which was indistinguishable from "the quote line had no note." */ +export function QuoteLineNoMatch() { + return ( +
+ No matching line on the linked quote. +
+ ); +}