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
+