From 06311c6de2de8beb7916229c5d6069390826bdeb Mon Sep 17 00:00:00 2001 From: DEls Date: Thu, 17 Sep 2026 20:53:23 +0200 Subject: [PATCH] fix(dn-batches): add uploaded_at so ordering works PocketBase's created is empty on every dn_batches record, making the board's -created sort a no-op and the tick's oldest-first meaningless. Co-Authored-By: Claude Sonnet 5 --- scripts/pb-add-dn-batch-uploaded-at.py | 95 +++++++++++++++++++ src/app/api/delivery-notes/upload/route.ts | 1 + src/app/api/dn-batches/tick/route.ts | 2 +- .../financial/delivery-notes/inbox/page.tsx | 2 +- src/lib/pb-types.ts | 2 + 5 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 scripts/pb-add-dn-batch-uploaded-at.py diff --git a/scripts/pb-add-dn-batch-uploaded-at.py b/scripts/pb-add-dn-batch-uploaded-at.py new file mode 100644 index 0000000..1115140 --- /dev/null +++ b/scripts/pb-add-dn-batch-uploaded-at.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +""" +Adds dn_batches.uploaded_at and backfills it. + +PocketBase's own `created` is an empty string on every dn_batches record — +the collection was created without autodate — so the board's "-created" +sort is a no-op and the tick's "oldest first" means nothing. The rest of +this codebase already works around the same quirk (delivery_notes sorts by +date_received), so an explicit field is the consistent fix. + +Backfill uses the batch's earliest dn_batch_notes date where one exists. +Batches with nothing to go on are LEFT EMPTY rather than given an invented +timestamp; they sort last. + +Idempotent. +""" +import sys + +from pb_env import PB, http, superuser_token + + +def get_all(token, coll, fields=None, filt=None): + out, page = [], 1 + while True: + q = f"?page={page}&perPage=200" + if fields: + q += f"&fields={fields}" + if filt: + q += f"&filter={filt}" + code, resp = http("GET", f"/api/collections/{coll}/records{q}", token=token) + if code != 200: + sys.exit(f"GET {coll} failed: {code} {resp}") + out.extend(resp["items"]) + if page >= resp["totalPages"]: + return out + page += 1 + + +def main(): + token = superuser_token() + print(f"\n→ dn_batches on {PB}") + + code, coll = http("GET", "/api/collections/dn_batches", token=token) + if code != 200: + sys.exit(f"GET collection failed: {code} {coll}") + fields = list(coll.get("fields", [])) + if any(f.get("name") == "uploaded_at" for f in fields): + print(" • uploaded_at already exists") + else: + code, resp = http( + "PATCH", + f"/api/collections/{coll['id']}", + {"fields": fields + [{"name": "uploaded_at", "type": "date", "required": False}]}, + token, + ) + if code != 200: + sys.exit(f"PATCH failed: {code} {resp}") + print(" ✓ added uploaded_at") + + notes = get_all(token, "dn_batch_notes", "batch,date") + earliest = {} + for n in notes: + d = (n.get("date") or "").strip() + b = n.get("batch") + if not d or not b: + continue + if b not in earliest or d < earliest[b]: + earliest[b] = d + + batches = get_all(token, "dn_batches", "id,uploaded_at") + filled = skipped = blank = 0 + for b in batches: + if (b.get("uploaded_at") or "").strip(): + skipped += 1 + continue + d = earliest.get(b["id"]) + if not d: + blank += 1 + continue + code, resp = http( + "PATCH", + f"/api/collections/dn_batches/records/{b['id']}", + {"uploaded_at": d}, + token, + ) + if code != 200: + sys.exit(f"PATCH {b['id']} failed: {code} {resp}") + filled += 1 + + print(f" backfilled {filled}, already set {skipped}, left empty {blank}") + + +if __name__ == "__main__": + main() + print("\nDone.") diff --git a/src/app/api/delivery-notes/upload/route.ts b/src/app/api/delivery-notes/upload/route.ts index 0e34f96..1873ec9 100644 --- a/src/app/api/delivery-notes/upload/route.ts +++ b/src/app/api/delivery-notes/upload/route.ts @@ -180,6 +180,7 @@ export async function POST(request: Request) { source: "web", uploaded_by: "web-upload", status: "inbox", + uploaded_at: new Date().toISOString(), page_count: 0, notes_detected: 0, notes_promoted: 0, diff --git a/src/app/api/dn-batches/tick/route.ts b/src/app/api/dn-batches/tick/route.ts index 897ca2b..6865784 100644 --- a/src/app/api/dn-batches/tick/route.ts +++ b/src/app/api/dn-batches/tick/route.ts @@ -38,7 +38,7 @@ export async function POST(request: Request) { candidates = await pb.collection("dn_batches").getFullList({ filter: `status='inbox'`, fields: "id", - sort: "created", + sort: "uploaded_at", }); } catch (err) { console.error("[dn-tick] list failed", err); diff --git a/src/app/dashboard/financial/delivery-notes/inbox/page.tsx b/src/app/dashboard/financial/delivery-notes/inbox/page.tsx index 2a869cb..1bdc9cc 100644 --- a/src/app/dashboard/financial/delivery-notes/inbox/page.tsx +++ b/src/app/dashboard/financial/delivery-notes/inbox/page.tsx @@ -17,7 +17,7 @@ export default async function InboxPage({ const batches = await pb.collection("dn_batches").getFullList({ expand: "site", - sort: "-created", + sort: "-uploaded_at", ...filterOpt(siteFilter(scope)), }); diff --git a/src/lib/pb-types.ts b/src/lib/pb-types.ts index 642b44d..835eb88 100644 --- a/src/lib/pb-types.ts +++ b/src/lib/pb-types.ts @@ -381,6 +381,8 @@ export interface DnBatchRecord extends RecordModel { claimed_at: string; /** Last error message from n8n. Empty when the batch is healthy. */ last_error: string; + /** Set explicitly at upload — PocketBase's `created` is unusable here. */ + uploaded_at: string; expand?: { site?: SiteRecord }; }