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:
parent
23b50a32a9
commit
33fc549e10
1 changed files with 118 additions and 0 deletions
118
src/components/orders/quote-line-context.tsx
Normal file
118
src/components/orders/quote-line-context.tsx
Normal 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'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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue