diff --git a/src/app/dashboard/financial/delivery-notes/inbox/inbox-board.tsx b/src/app/dashboard/financial/delivery-notes/inbox/inbox-board.tsx new file mode 100644 index 0000000..48ef954 --- /dev/null +++ b/src/app/dashboard/financial/delivery-notes/inbox/inbox-board.tsx @@ -0,0 +1,106 @@ +"use client"; + +import Link from "next/link"; +import { Badge } from "@/components/ui/badge"; +import { cn, formatNumber } from "@/lib/utils"; + +export interface BatchCard { + id: string; + filename: string; + siteName: string; + status: string; + pageCount: number; + notesDetected: number; + notesPromoted: number; + totalQty: number; + totalUnit: string; + failedChecks: string[]; + lastError: string; +} + +const COLUMNS: { key: string; label: string }[] = [ + { key: "inbox", label: "Inbox" }, + { key: "analysing", label: "Analysing" }, + { key: "needs_attention", label: "Needs attention" }, + { key: "processed", label: "Processed" }, +]; + +/** Processed batches are historical — cap the column and link to the full list. */ +const PROCESSED_LIMIT = 20; + +export function InboxBoard({ cards }: { cards: BatchCard[] }) { + return ( +
+ {COLUMNS.map((col) => { + const all = cards.filter((c) => c.status === col.key); + const shown = col.key === "processed" ? all.slice(0, PROCESSED_LIMIT) : all; + return ( +
+
+ {col.label} + {all.length} +
+
+ {shown.map((c) => ( + + ))} + {all.length === 0 && ( +

+ Nothing here +

+ )} + {all.length > shown.length && ( +

+ + {all.length - shown.length} older +

+ )} +
+
+ ); + })} +
+ ); +} + +function Card({ card }: { card: BatchCard }) { + const complete = + card.notesDetected > 0 && card.notesPromoted === card.notesDetected; + return ( + +

{card.filename}

+

+ {card.siteName} + {card.pageCount > 0 && ` · ${card.pageCount} page${card.pageCount === 1 ? "" : "s"}`} +

+ {card.notesDetected > 0 && ( +

+ {card.notesPromoted} of {card.notesDetected} notes +

+ )} + {card.totalQty > 0 && ( +

+ {formatNumber(card.totalQty)} {card.totalUnit} +

+ )} +
+ {card.status === "processed" && complete && ( + All checks passed + )} + {card.failedChecks.map((reason) => ( + + {reason} + + ))} + {card.lastError && {card.lastError.slice(0, 60)}} +
+ + ); +} \ No newline at end of file diff --git a/src/app/dashboard/financial/delivery-notes/inbox/page.tsx b/src/app/dashboard/financial/delivery-notes/inbox/page.tsx new file mode 100644 index 0000000..2a869cb --- /dev/null +++ b/src/app/dashboard/financial/delivery-notes/inbox/page.tsx @@ -0,0 +1,70 @@ +import { pbAdmin } from "@/lib/pb"; +import { Topbar } from "@/components/layout/sidebar"; +import { filterOpt, siteFilter, type SearchParams } from "@/lib/site-scope"; +import { getSiteScope } from "@/lib/site-scope-server"; +import { InboxBoard, type BatchCard } from "./inbox-board"; +import type { CheckResult } from "@/lib/dn-pipeline/types"; + +export const dynamic = "force-dynamic"; + +export default async function InboxPage({ + searchParams, +}: { + searchParams: Promise; +}) { + const scope = await getSiteScope(await searchParams); + const pb = await pbAdmin(); + + const batches = await pb.collection("dn_batches").getFullList({ + expand: "site", + sort: "-created", + ...filterOpt(siteFilter(scope)), + }); + + const noteRows = await pb.collection("dn_batch_notes").getFullList({ + fields: "batch,quantity,unit,status", + }); + + const totals = new Map(); + for (const n of noteRows) { + const row = n as unknown as { batch: string; quantity?: number; unit?: string }; + const cur = totals.get(row.batch) ?? { qty: 0, unit: row.unit ?? "" }; + cur.qty += row.quantity ?? 0; + if (!cur.unit && row.unit) cur.unit = row.unit; + totals.set(row.batch, cur); + } + + const cards: BatchCard[] = batches.map((b) => { + const site = b.expand?.site as { name?: string } | undefined; + const total = totals.get(b.id); + const checks = Array.isArray(b.checks) ? (b.checks as CheckResult[]) : []; + return { + id: b.id, + filename: b.file ?? "(no file)", + siteName: site?.name ?? "(unknown site)", + status: b.status ?? "inbox", + pageCount: b.page_count ?? 0, + notesDetected: b.notes_detected ?? 0, + notesPromoted: b.notes_promoted ?? 0, + totalQty: total?.qty ?? 0, + totalUnit: total?.unit ?? "", + failedChecks: checks + .filter((c) => c.passed === false) + .map((c) => c.reason) + .slice(0, 3), + lastError: b.last_error ?? "", + }; + }); + + return ( + <> + +
+ +
+ + ); +} \ No newline at end of file diff --git a/src/app/dashboard/financial/delivery-notes/page.tsx b/src/app/dashboard/financial/delivery-notes/page.tsx index 9030bae..73a509b 100644 --- a/src/app/dashboard/financial/delivery-notes/page.tsx +++ b/src/app/dashboard/financial/delivery-notes/page.tsx @@ -1,5 +1,5 @@ import Link from "next/link"; -import { ClipboardList } from "lucide-react"; +import { ClipboardList, Inbox } from "lucide-react"; import { pbAdmin } from "@/lib/pb"; import { Topbar } from "@/components/layout/sidebar"; import { Button } from "@/components/ui/button"; @@ -118,6 +118,12 @@ export default async function DeliveryNotesPage({ Upload status +