108 lines
4.6 KiB
TypeScript
108 lines
4.6 KiB
TypeScript
import Link from "next/link";
|
|
import { getPb, fmtGBP, fmtDate } from "./lib/pb";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export default async function Dashboard() {
|
|
const pb = getPb();
|
|
|
|
let invoiceCount = 0;
|
|
let totalSpent = 0;
|
|
let stockCodeCount = 0;
|
|
let latest: any[] = [];
|
|
let errorMsg: string | null = null;
|
|
|
|
try {
|
|
// NOTE: PB JS SDK auto-cancels concurrent requests to the same collection
|
|
// (see https://github.com/pocketbase/js-sdk#auto-cancellation). Fetch the
|
|
// invoice list ONCE and derive both "count" and "latest 5" from it.
|
|
const [invoicesResp, sc] = await Promise.all([
|
|
pb.collection("JEI_invoices").getList(1, 5000),
|
|
pb.collection("JEI_stock_codes").getList(1, 1),
|
|
]);
|
|
invoiceCount = invoicesResp.totalItems;
|
|
stockCodeCount = sc.totalItems;
|
|
// Latest 5 by invoice_date
|
|
const sorted = [...invoicesResp.items].sort((a: any, b: any) =>
|
|
(b.invoice_date || "").localeCompare(a.invoice_date || "")
|
|
);
|
|
latest = sorted.slice(0, 5);
|
|
totalSpent = invoicesResp.items.reduce((s: number, r: any) => s + (r.invoice_total || 0), 0);
|
|
} catch (e: any) {
|
|
const msg = String(e?.message || e);
|
|
// Detect auto-cancellation and give a clearer hint
|
|
if (/autocancelled|auto-cancelled|aborted/i.test(msg)) {
|
|
errorMsg = "Dashboard query was auto-cancelled (concurrent same-collection calls). Refresh the page.";
|
|
} else if (/401|Unauthorized/i.test(msg)) {
|
|
errorMsg = "PB token expired — refresh in PB admin UI and update .env.local";
|
|
} else {
|
|
errorMsg = msg;
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<h1 className="text-3xl font-bold text-brand-dark">Dashboard</h1>
|
|
|
|
{errorMsg && (
|
|
<div className="bg-red-50 border border-red-200 text-red-800 rounded p-4">
|
|
<div className="font-semibold">Cannot reach PocketBase</div>
|
|
<div className="text-sm mt-1">{errorMsg}</div>
|
|
<div className="text-xs mt-2">
|
|
Check PB_TOKEN in <code>.env.local</code> — it expires every ~24h.
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<div className="text-sm text-slate-500">Total invoices</div>
|
|
<div className="text-3xl font-semibold mt-2 tabular-nums">{invoiceCount}</div>
|
|
<Link href="/invoices" className="text-xs text-brand hover:underline mt-2 inline-block">View all →</Link>
|
|
</div>
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<div className="text-sm text-slate-500">Total spent (£)</div>
|
|
<div className="text-3xl font-semibold mt-2 tabular-nums">{fmtGBP(totalSpent)}</div>
|
|
<div className="text-xs text-slate-400 mt-2">Sum of invoice_total across all records</div>
|
|
</div>
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<div className="text-sm text-slate-500">Distinct stock codes</div>
|
|
<div className="text-3xl font-semibold mt-2 tabular-nums">{stockCodeCount}</div>
|
|
<Link href="/stock-codes" className="text-xs text-brand hover:underline mt-2 inline-block">View all →</Link>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<h2 className="text-lg font-semibold mb-3">Latest 5 invoices</h2>
|
|
{latest.length === 0 ? (
|
|
<p className="text-slate-500 text-sm">
|
|
No data yet. To populate: <code className="bg-slate-100 px-1 rounded">python scripts/extract_invoices.py</code> then <code className="bg-slate-100 px-1 rounded">python scripts/import_to_pb.py</code>.
|
|
</p>
|
|
) : (
|
|
<table className="w-full text-sm">
|
|
<thead className="text-left text-slate-500">
|
|
<tr>
|
|
<th className="pb-2">Date</th>
|
|
<th className="pb-2">No</th>
|
|
<th className="pb-2">Buyer</th>
|
|
<th className="pb-2 text-right">Total (£)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{latest.map((r) => (
|
|
<tr key={r.id} className="border-t">
|
|
<td className="py-2">{fmtDate(r.invoice_date)}</td>
|
|
<td className="py-2">
|
|
<Link href={`/invoices/${r.id}`} className="text-brand hover:underline">{r.invoice_no}</Link>
|
|
</td>
|
|
<td className="py-2 text-slate-600">{r.buyer_name_variant}</td>
|
|
<td className="py-2 text-right tabular-nums">{fmtGBP(r.invoice_total)}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|