feat(orders): label lines split off a short collection

Shows "Short delivered · on <PO>" next to the description on /orders and
the order page so the buyer doesn't re-order units already on back order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dave 2026-10-22 07:59:57 +02:00
parent 7fceb47005
commit e4fbcf3748
4 changed files with 67 additions and 1 deletions

View file

@ -70,6 +70,7 @@ import type { MatchedAttachment } from "@/components/orders/attachment-list";
import { AiItemHistoryTrigger } from "@/components/ai-item-history-trigger";
import type { Order, OrderItem, PurchaseOrder, Quote, ItemIntel, ItemStatus } from "@/lib/types";
import { ShortDeliveredPill } from "@/components/orders/short-delivered-pill";
import type { ScoreBreakdown, LineMatchBreakdown } from "@/lib/server/order-quote-match";
import type { OrderDetail } from "@/lib/server/orders-service";
@ -687,6 +688,7 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
className="mt-0.5"
/>
<span>{item.description}</span>
<ShortDeliveredPill item={item} />
</div>
{item.po_description && item.po_description !== item.description && (
<div className="text-xs text-muted-foreground mt-1 whitespace-pre-wrap break-words">

View file

@ -37,6 +37,7 @@ import type {
} from "@/lib/server/orders-service";
import { AiItemHistoryTrigger } from "@/components/ai-item-history-trigger";
import { ItemStatusPill } from "@/components/item-status-pill";
import { ShortDeliveredPill } from "@/components/orders/short-delivered-pill";
import {
TAB_GROUP_VISUAL,
TAB_GROUP_ITEM_STATUSES,
@ -1004,7 +1005,10 @@ function OrderCard({
intel={null}
className="-my-1"
/>
<span className="flex-1 truncate">{item.description}</span>
<span className="flex-1 min-w-0 flex items-center gap-1.5">
<span className="truncate">{item.description}</span>
<ShortDeliveredPill item={item} />
</span>
<span className="text-muted-foreground shrink-0">×{item.quantity ?? "—"}</span>
<span className="shrink-0 tabular-nums">
{item.selling_price !== null && item.quantity !== null

View file

@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { shortDeliveredLabel } from "./short-delivered-pill";
describe("shortDeliveredLabel", () => {
it("names the PO the remainder is already on", () => {
const label = shortDeliveredLabel({
order_item_details: JSON.stringify({
shortCollection: { sourceItemId: "o", ordered: 770, collected: 549, remainderQty: 221, poNumber: "PO202610005BID", reason: "[Other]" },
}),
});
expect(label?.text).toBe("Short delivered · on PO202610005BID");
expect(label?.title).toContain("Only 549 of 770");
});
it("is null for the collected part and for ordinary lines", () => {
expect(shortDeliveredLabel({ order_item_details: { shortCollection: { remainderItemId: "r" } } })).toBeNull();
expect(shortDeliveredLabel({ order_item_details: null })).toBeNull();
});
});

View file

@ -0,0 +1,41 @@
"use client";
import { readShortCollectionMarker } from "@/lib/short-collection";
/**
* Label for a line split off a short collection (see
* `src/lib/short-collection.ts`), shown next to its description so the
* buyer doesn't order it a second time: the supplier already has it on
* the original PO, it just came short.
*/
export function shortDeliveredLabel(item: {
order_item_details?: string | Record<string, unknown> | null;
}): { text: string; title: string } | null {
const marker = readShortCollectionMarker(item.order_item_details);
if (!marker || marker.role !== "remainder") return null;
const po = marker.poNumber || "the original PO";
return {
text: `Short delivered · on ${po}`,
title:
`Only ${marker.collected} of ${marker.ordered} came in on ${po}. ` +
`These ${marker.remainderQty} were already ordered — check with the supplier before ordering again.` +
(marker.reason ? ` Driver: ${marker.reason}` : ""),
};
}
export function ShortDeliveredPill({
item,
}: {
item: { order_item_details?: string | Record<string, unknown> | null };
}) {
const label = shortDeliveredLabel(item);
if (!label) return null;
return (
<span
title={label.title}
className="shrink-0 rounded-full border border-warn/40 bg-warn/10 px-2 py-0.5 text-[11px] font-medium text-warn-foreground dark:text-warn"
>
{label.text}
</span>
);
}