JEI_Import_Documents-App/app/page.tsx

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>
);
}