feat(orders): add shared quote-line context component

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 <noreply@anthropic.com>
This commit is contained in:
Dave 2026-08-30 12:20:39 +02:00
parent 23b50a32a9
commit 33fc549e10

View file

@ -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 (
<div className="space-y-2.5 text-xs">
{tier === "possible" && (
<div className="rounded border border-warn/40 bg-warn/5 dark:bg-warn/10 px-2.5 py-2">
<div className="flex items-start gap-2">
<HelpCircle className="h-3.5 w-3.5 shrink-0 mt-0.5 text-warn-foreground dark:text-warn" />
<div className="flex-1 min-w-0">
<div className="font-medium text-warn-foreground dark:text-warn">
Possible match — is this the right quote line?
</div>
<p className="text-muted-foreground mt-0.5">
The wording differs enough that we&apos;re not certain. Confirm to
link them permanently.
</p>
</div>
{onConfirm && (
<Button
size="sm"
variant="outline"
className="h-7 text-[11px] shrink-0"
onClick={onConfirm}
disabled={confirming}
>
{confirming ? "Linking…" : "Confirm"}
</Button>
)}
</div>
</div>
)}
{quoteDescription && quoteDescription !== orderDescription && (
<div className="rounded border bg-muted/30 px-2.5 py-1.5">
<div className="text-[10px] uppercase tracking-wide text-muted-foreground mb-0.5">
Quote line says
</div>
<div className="whitespace-pre-wrap break-words">{quoteDescription}</div>
</div>
)}
{notes.trim() !== "" && (
<div className="rounded border border-warn/30 bg-warn/5 dark:bg-warn/10 px-2.5 py-1.5">
<div className="text-[10px] uppercase tracking-wide text-warn-foreground dark:text-warn mb-0.5 inline-flex items-center gap-1">
<MessageSquare className="h-3 w-3" />
Buyer note
</div>
<div className="whitespace-pre-wrap break-words text-foreground/90">
{notes}
</div>
</div>
)}
{perItemAttachments.length > 0 && (
<AttachmentList label="Attached to this line" attachments={perItemAttachments} />
)}
{quoteAttachments.length > 0 && (
<AttachmentList label="Quote attachments" attachments={quoteAttachments} />
)}
</div>
);
}
/** 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 (
<div className="rounded border border-dashed px-2.5 py-1.5 text-xs text-muted-foreground">
No matching line on the linked quote.
</div>
);
}