feat(po): colour the whole line, and give over-supply its own colour

A single coloured number at the far right of seven columns is easy to
miss. The description now carries the same tone as the outstanding
figure, so a line that still owes reads as one thing at a glance.

Over-supply moves from amber to blue, in the row and on the summary tile.
Amber sat next to red as another shade of "behind", which is the opposite
of what over-supply is: nothing is owed, something arrived that was not
ordered, and it is queried with the supplier rather than chased.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
DEls 2026-09-21 01:19:15 +02:00
parent 1c61a3fe96
commit 6bb0fb1da5

View file

@ -97,12 +97,21 @@ function amberTile(
value: React.ReactNode,
sub: string,
Icon: React.ComponentType<{ className?: string }>,
tone?: "warning",
// "info" is over-supply: a discrepancy to raise with the supplier, not
// a shortfall to chase. Blue keeps it distinct from what is still owed.
tone?: "warning" | "info",
) {
const strip =
tone === "warning" ? "from-red-500 to-amber-500" : "from-[#F2B165] to-[#E08A3C]";
const iconBg = tone === "warning" ? "bg-red-500/15" : "bg-[#F2B165]/15";
const iconFg = tone === "warning" ? "text-red-600 dark:text-red-400" : "text-[#F2B165]";
const strip = tone === "warning"
? "from-red-500 to-amber-500"
: tone === "info"
? "from-blue-500 to-sky-400"
: "from-[#F2B165] to-[#E08A3C]";
const iconBg = tone === "warning" ? "bg-red-500/15" : tone === "info" ? "bg-blue-500/15" : "bg-[#F2B165]/15";
const iconFg = tone === "warning"
? "text-red-600 dark:text-red-400"
: tone === "info"
? "text-blue-600 dark:text-blue-400"
: "text-[#F2B165]";
return (
<Card className="relative overflow-hidden border-border/60">
<span
@ -267,7 +276,7 @@ export function PoDetailView({
? `${overSuppliedLines} line${overSuppliedLines === 1 ? "" : "s"} over-supplied`
: `Fulfilment ${state.fulfillmentPct.toFixed(0)}%`,
PackageCheck,
overSuppliedLines > 0 ? "warning" : undefined,
overSuppliedLines > 0 ? "info" : undefined,
)}
{amberTile(
"Verified",
@ -308,7 +317,17 @@ export function PoDetailView({
</tr>
</thead>
<tbody>
{state.items.map((it) => (
{state.items.map((it) => {
// One tone per row, carried by the description and the
// outstanding figure, so a line that still owes reads
// as one thing at a glance rather than a lone coloured
// number at the far right of seven columns.
const tone = it.qtyOutstanding > 0
? "text-destructive"
: it.qtyOutstanding < 0
? "text-blue-700 dark:text-blue-300"
: "";
return (
<tr key={it.id} className="border-b border-border last:border-0">
<td className="px-3 py-2 text-xs">
<p className="font-mono">{it.materialCode || "—"}</p>
@ -316,7 +335,9 @@ export function PoDetailView({
{it.costCode || "—"}
</p>
</td>
<td className="px-3 py-2">{it.description || "—"}</td>
<td className={cn("px-3 py-2", tone && `${tone} font-medium`)}>
{it.description || "—"}
</td>
<td className="px-3 py-2 text-right tabular-nums">
{formatNumber(it.qtyOrdered)} {it.unit}
</td>
@ -333,16 +354,10 @@ export function PoDetailView({
</td>
{/* Outstanding is ordered minus delivered, so it
goes negative once a supplier over-delivers.
Still owed and over-supplied are different
problems and read differently: a bare "-5" in
the same amber as a shortfall says neither. */}
<td
className={cn(
"px-3 py-2 text-right tabular-nums font-medium",
it.qtyOutstanding > 0 && "text-destructive",
it.qtyOutstanding < 0 && "text-amber-700 dark:text-amber-300",
)}
>
Still owed and over-supplied are opposite
problems: one is chased, the other is queried.
A bare "-5" in the same colour says neither. */}
<td className={cn("px-3 py-2 text-right tabular-nums font-medium", tone)}>
{it.qtyOutstanding < 0 ? (
<>{formatNumber(Math.abs(it.qtyOutstanding))} {it.unit} over</>
) : (
@ -356,7 +371,8 @@ export function PoDetailView({
{formatCurrency(it.amount)}
</td>
</tr>
))}
);
})}
</tbody>
</table>
</div>