Quick order-builder for the user. Pick a stock code, set a qty, hit Add. Default unit price is the HIGHEST price ever paid for each code (worst- case budget), with the reference invoice [no + date] shown in the table so the price source is auditable. Ties broken by most-recent date so the source reflects current market, not an old historical peak. UI: - Stock code dropdown shows code, description, default price, ref inv - Inline-editable qty + unit price per row - Running grand total at bottom - Clear button (with confirm) wipes the list - Export PDF button calls window.print() — browser "Save as PDF" gives a clean table with no chrome (header/nav/controls hidden via @media print stylesheet) Persistence: - List kept in localStorage key 'jei:order-list' across reloads Files: - app/order/page.tsx [server: fetches stock codes + lines, builds highest-price map] - app/order/OrderClient.tsx [client: UI, state, print stylesheet] - app/layout.tsx [nav: added Order link] PB query note: when using `fields=...` to limit the response, you must also include 'expand.invoice.invoice_no' and 'expand.invoice.invoice_date' in the field selector, otherwise PB drops the expand data entirely.
87 lines
No EOL
3.1 KiB
TypeScript
87 lines
No EOL
3.1 KiB
TypeScript
import { getPb } from "../lib/pb";
|
|
import OrderClient from "./OrderClient";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
type StockWithHighestPrice = {
|
|
id: string;
|
|
code: string;
|
|
description: string;
|
|
highestUnitPrice: number | null;
|
|
refInvoiceNo: string | null; // invoice_no where the highest price was paid
|
|
refInvoiceDate: string | null; // invoice_date of that invoice
|
|
};
|
|
|
|
export default async function OrderPage() {
|
|
const pb = getPb();
|
|
let stockCodes: StockWithHighestPrice[] = [];
|
|
let errorMsg: string | null = null;
|
|
|
|
try {
|
|
// 1) All stock codes (just the fields we need)
|
|
const scRes = await pb.collection("JEI_stock_codes").getList(1, 5000, {
|
|
sort: "code",
|
|
fields: "id,code,description_goods",
|
|
});
|
|
|
|
// 2) All invoice lines + their parent invoice (so we can grab the
|
|
// invoice_no + invoice_date of whichever line carries the MAX
|
|
// unit_price for each stock code).
|
|
// PB only returns the expanded record when the field selector
|
|
// includes `expand.invoice.<field>`; `invoice` alone just returns
|
|
// the relation id and drops the expansion.
|
|
const linesRes = await pb.collection("JEI_invoice_lines").getList(1, 5000, {
|
|
expand: "invoice",
|
|
fields: "id,stock_code,unit_price,expand.invoice.invoice_no,expand.invoice.invoice_date",
|
|
});
|
|
|
|
// 3) For each stock_code, keep the line with the highest unit_price
|
|
// and remember which invoice (no + date) it came from. Ties broken
|
|
// by most-recent invoice_date so the price source reflects the
|
|
// current market rather than an old historical peak.
|
|
type Highest = { price: number; date: string; invNo: string };
|
|
const highestByStock = new Map<string, Highest>();
|
|
for (const line of linesRes.items) {
|
|
const scId = line.stock_code as string;
|
|
if (!scId) continue;
|
|
const inv = line.expand?.invoice as any;
|
|
const invNo: string = inv?.invoice_no || "";
|
|
const invDate: string = inv?.invoice_date || "";
|
|
const price = Number(line.unit_price) || 0;
|
|
const cur = highestByStock.get(scId);
|
|
const isHigher = !cur || price > cur.price;
|
|
const isTieNewer =
|
|
cur && price === cur.price && invDate > cur.date;
|
|
if (isHigher || isTieNewer) {
|
|
highestByStock.set(scId, { price, date: invDate, invNo });
|
|
}
|
|
}
|
|
|
|
stockCodes = scRes.items.map((s: any) => {
|
|
const h = highestByStock.get(s.id);
|
|
return {
|
|
id: s.id,
|
|
code: s.code,
|
|
description: s.description_goods || "",
|
|
highestUnitPrice: h?.price ?? null,
|
|
refInvoiceNo: h?.invNo || null,
|
|
refInvoiceDate: h?.date ? h.date.slice(0, 10) : null,
|
|
};
|
|
});
|
|
} catch (e: any) {
|
|
errorMsg = String(e?.message || e);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{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>
|
|
) : (
|
|
<OrderClient stockCodes={stockCodes} />
|
|
)}
|
|
</div>
|
|
);
|
|
} |