Suppliers: dark-mode contrast for monthly figures and coloured text; heading drawn in the view (React key warning)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
David 2026-10-29 21:16:54 +02:00
parent 56305fc599
commit 4248f3d37f
2 changed files with 24 additions and 29 deletions

View file

@ -1,4 +1,3 @@
import { Store } from "lucide-react";
import { serverAuth } from "@/lib/pb";
import { buildSupplierSpend } from "@/lib/supplier-spend";
import { Suppliers2View } from "@/components/Suppliers2View";
@ -48,15 +47,6 @@ export default async function Suppliers2Page() {
rows={rows}
year={now.getFullYear()}
month={now.getMonth() + 1}
title={
<div className="flex items-center gap-3">
<div className="w-10 h-10 shrink-0 rounded-xl bg-gradient-to-br from-[#30638E]/15 to-[#00798C]/15 grid place-items-center text-[#30638E]"><Store className="w-5 h-5" /></div>
<div>
<h1 className="text-2xl font-semibold text-[#003D5B] dark:text-white">Suppliers</h1>
<p className="text-gray-500 dark:text-gray-400 text-sm">Spend and payables per supplier</p>
</div>
</div>
}
/>
</div>
);

View file

@ -2,7 +2,7 @@
import { useState, useMemo } from "react";
import { useRouter } from "next/navigation";
import { ChevronRight, Plus, Trash2, X, Loader2, Pencil } from "lucide-react";
import { Store, ChevronRight, Plus, Trash2, X, Loader2, Pencil } from "lucide-react";
import { Sparkline } from "@/components/Sparkline";
import { MONTH_NAMES } from "@/lib/data";
import { ZAR, ZAR2 } from "@/lib/utils";
@ -18,12 +18,11 @@ const monthLabel = (ym: string) => {
};
export function Suppliers2View({
rows, year, month, title,
rows, year, month,
}: {
rows: SupplierSpendRow[];
year: number;
month: number;
title: React.ReactNode;
}) {
// Cash suppliers are paid on the spot — nothing to chase — so they stay out of the way until asked for.
const [show, setShow] = useState<"account" | "all" | "cash">("account");
@ -56,7 +55,13 @@ export function Suppliers2View({
return (
<>
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-4 mb-6">
{title}
<div className="flex items-center gap-3">
<div className="w-10 h-10 shrink-0 rounded-xl bg-gradient-to-br from-[#30638E]/15 to-[#00798C]/15 grid place-items-center text-[#30638E] dark:text-[#8DB8E0]"><Store className="w-5 h-5" /></div>
<div>
<h1 className="text-2xl font-semibold text-[#003D5B] dark:text-white">Suppliers</h1>
<p className="text-gray-500 dark:text-gray-400 text-sm">Spend and payables per supplier</p>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div className="bg-gradient-to-br from-[#30638E] to-[#003D5B] rounded-2xl px-5 py-3 text-white shadow-lg shadow-blue-900/20">
<div className="text-xs text-white/80">Spend in {MONTH_NAMES[month - 1]} {year}</div>
@ -79,7 +84,7 @@ export function Suppliers2View({
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-5">
<div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400 flex-wrap">
<span className="px-2.5 py-1 rounded-md bg-[#30638E]/10 text-[#30638E] font-medium">{visible.length} supplier{visible.length !== 1 && "s"}</span>
<span className="px-2.5 py-1 rounded-md bg-[#30638E]/10 text-[#30638E] dark:text-[#8DB8E0] font-medium">{visible.length} supplier{visible.length !== 1 && "s"}</span>
{show === "account" && cashN > 0 && (
<span className="px-2.5 py-1 rounded-md bg-gray-100 dark:bg-white/5 font-medium">{cashN} cash supplier{cashN !== 1 && "s"} hidden</span>
)}
@ -144,7 +149,7 @@ export function Suppliers2View({
<button onClick={() => setExpanded(isOpen ? null : r.id)} aria-expanded={isOpen} className="w-full text-left p-4 pl-5">
<div className="flex items-center gap-3 sm:gap-4 min-h-[44px]">
{r.code && (
<span className="hidden sm:inline font-mono text-xs px-2 py-1 rounded-md bg-[#00798C]/10 text-[#00798C] font-semibold shrink-0">{r.code}</span>
<span className="hidden sm:inline font-mono text-xs px-2 py-1 rounded-md bg-[#00798C]/10 text-[#00798C] dark:text-[#00A6BE] font-semibold shrink-0">{r.code}</span>
)}
<div className="flex-1 min-w-0">
<div
@ -168,8 +173,8 @@ export function Suppliers2View({
</div>
</div>
<div className="text-right shrink-0 w-32 hidden md:block">
<div className="text-xs uppercase tracking-wider text-[#00798C]">Paid</div>
<div className={`text-sm font-semibold tabular-nums ${r.paidNow > 0 ? "text-[#00798C]" : "text-gray-300 dark:text-text-soft"}`}>
<div className="text-xs uppercase tracking-wider text-[#00798C] dark:text-[#00A6BE]">Paid</div>
<div className={`text-sm font-semibold tabular-nums ${r.paidNow > 0 ? "text-[#00798C] dark:text-[#00A6BE]" : "text-gray-300 dark:text-text-soft"}`}>
{r.paidNow > 0 ? ZAR2(r.paidNow) : "—"}
</div>
</div>
@ -190,7 +195,7 @@ export function Suppliers2View({
<div className={`text-sm font-semibold tabular-nums ${r.payableNet > 0.005 ? "text-[#EDAE49]" : "text-gray-300 dark:text-text-soft"}`}>
{r.payableNet > 0.005 ? ZAR2(r.payableNet) : "—"}
</div>
{r.paidNow > 0 && <div className="text-xs text-[#00798C] tabular-nums">paid {ZAR(r.paidNow)}</div>}
{r.paidNow > 0 && <div className="text-xs text-[#00798C] dark:text-[#00A6BE] tabular-nums">paid {ZAR(r.paidNow)}</div>}
</div>
<div className="text-right min-w-0">
<div className="text-xs text-gray-400 uppercase tracking-wider">YTD</div>
@ -205,7 +210,7 @@ export function Suppliers2View({
{r.months.map((v, i) => (
<div key={i} className="text-center">
<div className="text-gray-400 dark:text-text-soft text-xs mb-1">{MONTH_NAMES[i]}</div>
<div className={`py-1.5 rounded tabular-nums ${v ? "bg-[#30638E]/5 text-[#30638E] dark:bg-[#30638E]/15" : "text-gray-300 dark:text-text-soft"}`}>
<div className={`py-1.5 rounded tabular-nums ${v ? "bg-[#30638E]/5 text-[#30638E] dark:bg-[#30638E]/25 dark:text-gray-100" : "text-gray-300 dark:text-text-soft"}`}>
{v ? ZAR2(v) : "—"}
</div>
<MonthPayLine due={r.dueByMonth[i]} paid={r.paidByMonth[i]} past={i < month} />
@ -217,7 +222,7 @@ export function Suppliers2View({
<div className="mt-4 pt-3 border-t border-gray-100 dark:border-border">
<div className="flex items-baseline justify-between gap-3 mb-2">
<div className="text-xs uppercase tracking-wider text-gray-400">Payments</div>
<div className={`text-xs font-semibold tabular-nums ${r.owingToDate > 0.005 ? "text-[#D1495B]" : "text-[#00798C]"}`}>
<div className={`text-xs font-semibold tabular-nums ${r.owingToDate > 0.005 ? "text-[#D1495B] dark:text-[#D94F60]" : "text-[#00798C] dark:text-[#00A6BE]"}`}>
{r.owingToDate > 0.005 ? `${ZAR2(r.owingToDate)} still owing to date` : r.owingToDate < -0.005 ? `${ZAR2(-r.owingToDate)} paid ahead` : "Settled to date"}
</div>
</div>
@ -243,7 +248,7 @@ export function Suppliers2View({
<span className="font-mono font-semibold text-[#003D5B] dark:text-white">{p.po_no}</span>
<span className="flex-1 min-w-0 truncate text-gray-500 dark:text-gray-400">
{p.invoiceCount === 0 ? "no invoice yet" : p.status === "over"
? <span className="font-semibold text-[#8B5CF6]">{ZAR2(p.invoiced)} invoiced — {ZAR2(p.invoiced - p.total)} OVER the PO</span>
? <span className="font-semibold text-[#8B5CF6] dark:text-[#A78BFA]">{ZAR2(p.invoiced)} invoiced — {ZAR2(p.invoiced - p.total)} OVER the PO</span>
: `${ZAR2(p.invoiced)} of ${ZAR2(p.total)} invoiced`}
{p.dueMonth && ` · pay ${monthLabel(p.dueMonth)}`}
</span>
@ -272,11 +277,11 @@ function MonthPayLine({ due, paid, past }: { due: number; paid: number; past: bo
return (
<div className="mt-1 space-y-0.5 text-xs tabular-nums">
{due > 0 && <div className="text-[#EDAE49]" title="Fell due this month">due {ZAR(due)}</div>}
{paid > 0 && <div className="text-[#00798C] font-medium" title="Paid this month">paid {ZAR(paid)}</div>}
{paid > 0 && <div className="text-[#00798C] dark:text-[#00A6BE] font-medium" title="Paid this month">paid {ZAR(paid)}</div>}
{past && Math.abs(diff) > 0.005 && (
diff > 0
? <div className="text-[#00798C] font-semibold">+{ZAR(diff)} surplus</div>
: <div className="text-[#D1495B] font-semibold">{ZAR(-diff)} short</div>
? <div className="text-[#00798C] dark:text-[#00A6BE] font-semibold">+{ZAR(diff)} surplus</div>
: <div className="text-[#D1495B] dark:text-[#D94F60] font-semibold">{ZAR(-diff)} short</div>
)}
</div>
);
@ -298,7 +303,7 @@ function PaymentRow({ p, editMode }: { p: { id: string; paid_on: string; descrip
<li className="flex items-center gap-3 py-1 text-xs">
<span className="tabular-nums text-gray-500 dark:text-gray-400 shrink-0">{p.paid_on}</span>
<span className="flex-1 min-w-0 truncate text-gray-600 dark:text-gray-300">{p.description || "—"}</span>
<span className="font-semibold tabular-nums text-[#00798C] shrink-0">{ZAR2(p.amount)}</span>
<span className="font-semibold tabular-nums text-[#00798C] dark:text-[#00A6BE] shrink-0">{ZAR2(p.amount)}</span>
{editMode && (
<button onClick={remove} disabled={busy} aria-label="Remove payment"
className="min-h-[44px] min-w-[44px] grid place-items-center rounded-lg text-gray-400 hover:text-[#D1495B] hover:bg-gray-50 dark:hover:bg-white/5 disabled:opacity-50">
@ -374,11 +379,11 @@ function AddPaymentModal({ rows, month, onClose }: { rows: SupplierSpendRow[]; m
{sup && (
<div className="text-xs text-gray-500 dark:text-gray-400">
{MONTH_NAMES[month - 1]}: <span className="text-[#EDAE49] font-medium">{ZAR2(sup.payableNow)} due</span>
{" · "}<span className="text-[#00798C] font-medium">{ZAR2(sup.paidNow)} paid</span>
{" · "}<span className="text-[#00798C] dark:text-[#00A6BE] font-medium">{ZAR2(sup.paidNow)} paid</span>
{" · "}
{sup.owingToDate > 0.005
? <span className="text-[#D1495B] font-medium">{ZAR2(sup.owingToDate)} owing to date</span>
: <span className="text-[#00798C] font-medium">settled to date</span>}
? <span className="text-[#D1495B] dark:text-[#D94F60] font-medium">{ZAR2(sup.owingToDate)} owing to date</span>
: <span className="text-[#00798C] dark:text-[#00A6BE] font-medium">settled to date</span>}
</div>
)}
<label className="block text-[12px] text-gray-500 dark:text-gray-400">