2026-09-17 21:07:56 +00:00
|
|
|
import Link from "next/link";
|
|
|
|
|
import { notFound } from "next/navigation";
|
|
|
|
|
import { getPb, fmtGBP, fmtNum, fmtDate } from "../../lib/pb";
|
|
|
|
|
|
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
|
|
|
|
|
|
export default async function StockCodeDetail({ params }: { params: { code: string } }) {
|
|
|
|
|
const pb = getPb();
|
|
|
|
|
const code = decodeURIComponent(params.code);
|
|
|
|
|
|
|
|
|
|
let sc: any = null;
|
|
|
|
|
let lines: any[] = [];
|
|
|
|
|
try {
|
|
|
|
|
// Find stock code by its `code` field
|
|
|
|
|
const found = await pb.collection("JEI_stock_codes").getList(1, 1, {
|
|
|
|
|
filter: `code="${code.replace(/"/g, '\\"')}"`,
|
|
|
|
|
});
|
|
|
|
|
sc = found.items[0];
|
|
|
|
|
if (!sc) return notFound();
|
|
|
|
|
// All lines for this stock code, with their invoice (expand)
|
|
|
|
|
const lr = await pb.collection("JEI_invoice_lines").getList(1, 500, {
|
|
|
|
|
filter: `stock_code="${sc.id}"`,
|
|
|
|
|
expand: "invoice",
|
|
|
|
|
});
|
2026-09-17 21:17:58 +00:00
|
|
|
sc = found.items[0];
|
|
|
|
|
if (!sc) return notFound();
|
|
|
|
|
// All lines for this stock code, with their invoice (expand)
|
|
|
|
|
const lr = await pb.collection("JEI_invoice_lines").getList(1, 500, {
|
|
|
|
|
filter: `stock_code="${sc.id}"`,
|
|
|
|
|
expand: "invoice",
|
|
|
|
|
});
|
2026-09-17 21:07:56 +00:00
|
|
|
lines = lr.items;
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<h1 className="text-2xl font-bold text-brand-dark">Stock code {code}</h1>
|
|
|
|
|
<div className="bg-red-50 border border-red-200 text-red-800 rounded p-4">
|
|
|
|
|
{String(e?.message || e)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (!sc) return notFound();
|
|
|
|
|
|
|
|
|
|
// Stats
|
|
|
|
|
const totalQty = lines.reduce((s, l) => s + (l.quantity || 0), 0);
|
|
|
|
|
const totalValue = lines.reduce((s, l) => s + (l.net_amount || 0), 0);
|
|
|
|
|
const unitPrices = lines.map((l) => l.unit_price).filter((p) => p != null && p > 0);
|
|
|
|
|
const avgUnitPrice = unitPrices.length ? unitPrices.reduce((a, b) => a + b, 0) / unitPrices.length : 0;
|
|
|
|
|
const minUnitPrice = unitPrices.length ? Math.min(...unitPrices) : 0;
|
|
|
|
|
const maxUnitPrice = unitPrices.length ? Math.max(...unitPrices) : 0;
|
|
|
|
|
const invoiceIds = new Set(lines.map((l) => l.invoice));
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<div>
|
|
|
|
|
<Link href="/stock-codes" className="text-sm text-slate-500 hover:underline">← All stock codes</Link>
|
|
|
|
|
<h1 className="text-2xl font-bold text-brand-dark mt-1">{sc.code}</h1>
|
|
|
|
|
<p className="text-slate-700 mt-1">{sc.description_goods}</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-2 md:grid-cols-6 gap-3">
|
|
|
|
|
<Stat label="Total qty" value={fmtNum(totalQty)} />
|
|
|
|
|
<Stat label="Total £" value={fmtGBP(totalValue)} />
|
|
|
|
|
<Stat label="Invoices" value={String(invoiceIds.size)} />
|
|
|
|
|
<Stat label="Avg £/unit" value={fmtGBP(avgUnitPrice)} />
|
|
|
|
|
<Stat label="Min £/unit" value={fmtGBP(minUnitPrice)} />
|
|
|
|
|
<Stat label="Max £/unit" value={fmtGBP(maxUnitPrice)} />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="bg-white rounded-lg shadow">
|
|
|
|
|
<div className="px-6 py-3 border-b font-semibold">
|
|
|
|
|
Purchase history ({lines.length} line(s) across {invoiceIds.size} invoice(s))
|
|
|
|
|
</div>
|
|
|
|
|
<table className="w-full text-sm">
|
|
|
|
|
<thead className="bg-slate-50 text-left">
|
|
|
|
|
<tr>
|
|
|
|
|
<th className="px-3 py-2">Date</th>
|
|
|
|
|
<th className="px-3 py-2">Invoice</th>
|
|
|
|
|
<th className="px-3 py-2 text-right">Qty</th>
|
|
|
|
|
<th className="px-3 py-2 text-right">Unit price (£)</th>
|
|
|
|
|
<th className="px-3 py-2 text-right">Net (£)</th>
|
|
|
|
|
<th className="px-3 py-2">Commodity</th>
|
|
|
|
|
<th className="px-3 py-2">Origin</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{lines.length === 0 && (
|
|
|
|
|
<tr>
|
|
|
|
|
<td colSpan={7} className="px-3 py-6 text-center text-slate-500">
|
|
|
|
|
No purchases recorded for this stock code yet.
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
)}
|
|
|
|
|
{lines.map((l) => {
|
|
|
|
|
const inv = l.expand?.invoice;
|
|
|
|
|
return (
|
|
|
|
|
<tr key={l.id} className="border-t">
|
|
|
|
|
<td className="px-3 py-2 text-slate-600">
|
|
|
|
|
{inv ? fmtDate(inv.invoice_date) : "—"}
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-3 py-2">
|
|
|
|
|
{inv ? (
|
|
|
|
|
<Link href={`/invoices/${inv.id}`} className="text-brand hover:underline">
|
|
|
|
|
{inv.invoice_no}
|
|
|
|
|
</Link>
|
|
|
|
|
) : (
|
|
|
|
|
"—"
|
|
|
|
|
)}
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-3 py-2 text-right tabular-nums">{fmtNum(l.quantity)}</td>
|
|
|
|
|
<td className="px-3 py-2 text-right tabular-nums">{fmtGBP(l.unit_price)}</td>
|
|
|
|
|
<td className="px-3 py-2 text-right tabular-nums">{fmtGBP(l.net_amount)}</td>
|
|
|
|
|
<td className="px-3 py-2 text-slate-600">{l.commodity_code || "—"}</td>
|
|
|
|
|
<td className="px-3 py-2 text-slate-600">{l.country_of_origin || "—"}</td>
|
|
|
|
|
</tr>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Stat({ label, value }: { label: string; value: string }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="bg-white rounded-lg shadow p-4">
|
|
|
|
|
<div className="text-xs text-slate-500">{label}</div>
|
|
|
|
|
<div className="text-xl font-semibold mt-1 tabular-nums">{value}</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|