feat(sprint-modal): show markup + order desc hint + explicit Show/Hide
Three small additions to the process modal: - Markup cell added to the pricing grid (4 cols now: PO Qty, Cost, Selling, Markup). Read-only and dashed like Selling — derived from selling/cost so the operator sees the margin they're pricing at. Green when selling ≥ cost, destructive when selling < cost. - Above the PO description textarea, the order's original item description is now shown as a read-only reference hint. Lets the operator see what the customer asked for vs what they're sending to the supplier without having to scroll back to the modal header. - AI Insights toggle now has an explicit "Show" / "Hide" label next to the chevron so the affordance is obvious. The chevron direction was the only signal before; the label makes the toggle self-documenting. Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
b9dfec8d28
commit
20b57ce2bb
2 changed files with 80 additions and 6 deletions
|
|
@ -282,6 +282,31 @@ describe("OrderSprintProcessModal", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("shows the computed markup percentage next to the locked selling", () => {
|
||||
// baseItem.selling_price = 50; cost starts at 0 → markup shows '—'.
|
||||
renderModal();
|
||||
expect(screen.getByLabelText(/markup/i).textContent).toMatch(/—/);
|
||||
// Set cost → markup should turn into "100.0%".
|
||||
return setField("Cost (ZAR)", "25").then(() => {
|
||||
expect(screen.getByLabelText(/markup/i).textContent).toMatch(/100\.0%/);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows the order description as a read-only hint above the PO description", () => {
|
||||
renderModal();
|
||||
// Order description should render above the textarea (read-only).
|
||||
expect(
|
||||
screen.getByLabelText(/order description \(read-only\)/i),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByLabelText(/order description \(read-only\)/i),
|
||||
).toHaveTextContent("HSS drill bit");
|
||||
// The PO description textarea is still editable below it.
|
||||
expect(
|
||||
screen.getByPlaceholderText("Description the supplier sees on the PO"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("'Use AI suggestion' button", () => {
|
||||
it("is disabled when ai_item_insights has no top_historical_matches", () => {
|
||||
renderModal({ item: { ...baseItem, ai_item_insights: { procurement_summary: "hi" } } });
|
||||
|
|
|
|||
|
|
@ -334,7 +334,7 @@ export function OrderSprintProcessModal({ item, suppliers, open, onClose, onProc
|
|||
type="button"
|
||||
onClick={() => setAiInsightsExpanded(v => !v)}
|
||||
aria-expanded={aiInsightsExpanded}
|
||||
className="flex items-center gap-2 text-left"
|
||||
className="flex items-center gap-2 text-left group/ai-toggle"
|
||||
>
|
||||
{aiInsightsExpanded ? (
|
||||
<ChevronUp className="h-4 w-4 text-warn/70" />
|
||||
|
|
@ -350,6 +350,9 @@ export function OrderSprintProcessModal({ item, suppliers, open, onClose, onProc
|
|||
suggestion available
|
||||
</span>
|
||||
)}
|
||||
<span className="ml-2 text-[10px] font-medium text-warn-foreground/70 group-hover/ai-toggle:text-warn-foreground underline-offset-2 group-hover/ai-toggle:underline transition-colors">
|
||||
{aiInsightsExpanded ? "Hide" : "Show"}
|
||||
</span>
|
||||
</button>
|
||||
<Button
|
||||
size="xs"
|
||||
|
|
@ -495,10 +498,11 @@ export function OrderSprintProcessModal({ item, suppliers, open, onClose, onProc
|
|||
)}
|
||||
</section>
|
||||
|
||||
{/* Pricing — qty + cost + selling (read-only). Selling is
|
||||
locked because it comes from the order; we still surface
|
||||
it so the operator sees the margin they're pricing. */}
|
||||
<section className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
{/* Pricing — qty + cost + selling (read-only) + markup
|
||||
(derived). Selling is locked because it comes from the
|
||||
order; markup is derived from selling/cost so the
|
||||
operator sees the margin they're pricing at. */}
|
||||
<section className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="order-sprint-qty" className="text-xs">
|
||||
PO Qty
|
||||
|
|
@ -546,6 +550,34 @@ export function OrderSprintProcessModal({ item, suppliers, open, onClose, onProc
|
|||
From order · not editable
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs flex items-center gap-1">
|
||||
<Lock className="h-3 w-3 text-muted-foreground" />
|
||||
Markup
|
||||
</Label>
|
||||
<div
|
||||
className="flex h-9 items-center rounded-md border border-dashed border-border/60 bg-muted/30 px-3 text-sm tabular-nums"
|
||||
aria-label="Markup (derived from cost and selling, read-only)"
|
||||
title="Derived from selling vs cost"
|
||||
>
|
||||
{cost > 0 && selling > 0 ? (
|
||||
<span
|
||||
className={cn(
|
||||
"font-medium",
|
||||
selling >= cost ? "text-emerald-700 dark:text-emerald-400" : "text-destructive",
|
||||
)}
|
||||
>
|
||||
{(((selling - cost) / cost) * 100).toFixed(1)}%
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground italic">—</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground flex items-center gap-1">
|
||||
<Info className="h-2.5 w-2.5" />
|
||||
Derived · not editable
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Supplier */}
|
||||
|
|
@ -563,7 +595,11 @@ export function OrderSprintProcessModal({ item, suppliers, open, onClose, onProc
|
|||
{/* PO description — what the supplier sees on the PO. Defaults
|
||||
to the order description so the buyer-tasks pending →
|
||||
processed transition fires for the common no-op case.
|
||||
Mirrors the canonical /orders/[id]/process gate. */}
|
||||
Mirrors the canonical /orders/[id]/process gate. The
|
||||
order's original item description is shown read-only
|
||||
above the textarea as a reference, so the operator can
|
||||
see what the customer asked for vs what they're sending
|
||||
to the supplier. */}
|
||||
<section className="space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs">PO description</Label>
|
||||
|
|
@ -577,6 +613,19 @@ export function OrderSprintProcessModal({ item, suppliers, open, onClose, onProc
|
|||
</button>
|
||||
)}
|
||||
</div>
|
||||
{item.description && (
|
||||
<div
|
||||
className="rounded-md border border-dashed border-border/60 bg-muted/20 px-3 py-2 text-xs text-muted-foreground"
|
||||
aria-label="Order description (read-only)"
|
||||
>
|
||||
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/70">
|
||||
Order description
|
||||
</span>
|
||||
<div className="mt-0.5 text-foreground/80 italic">
|
||||
{item.description}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Textarea
|
||||
value={poDescription}
|
||||
onChange={(e) => setPoDescription(e.target.value)}
|
||||
|
|
|
|||
Loading…
Reference in a new issue