From d6570dca71ad0fedc7dd00fe4593a90f2106e448 Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Wed, 29 Jul 2026 17:41:29 +0200 Subject: [PATCH] fix(orders): source attachments from order_attachements field The orders review modal's source-of-truth panel now reads from the order_attachements multi-file field on the orders collection (pbc_3527180448) instead of the quote_attachments collection rows on the parent quote. Older orders were migrated on 2026-07-29; the order-level field is authoritative going forward. - Added orderAttachementsToQuotes() helper in src/app/(app)/orders/page.tsx that synthesises the QuoteAttachment[] shape from the order's file-field array, deriving kind (pdf/image/doc) and content_type from the filename extension. PB file fields don't carry a content-type from the wire, so we infer from extension (png/jpg/gif/webp/svg/bmp/pdf/docx/txt). - Replaced the quote_attachments collection query in OrdersReviewModalMount with the helper call. - Added orders + pbc_3527180448 to the ALLOWED_COLLECTIONS whitelist in the file proxy route so the review modal's PDF/image preview iframes can stream through /api/v1/files/... . Co-authored-by: Orca --- src/app/(app)/orders/page.tsx | 84 +++++++++++++++++-- .../[recordId]/[filename]/route.ts | 6 ++ 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/src/app/(app)/orders/page.tsx b/src/app/(app)/orders/page.tsx index c93cd8a..8f3e422 100644 --- a/src/app/(app)/orders/page.tsx +++ b/src/app/(app)/orders/page.tsx @@ -72,6 +72,67 @@ const EMPTY_COUNTS: Record = { const SEARCH_DEBOUNCE_MS = 250; +/** + * Convert the order's `order_attachements` file field into the + * `QuoteAttachment[]` shape the review modal expects. The PB file field + * stores bare filenames (no collection-id token), so we synthesise the + * `kind` / `content_type` pairs from the filename extension. `size_bytes` + * is not available from a PB file field, so we leave it at 0. + * + * Kept inline because it's tightly coupled to the modal's + * `QuoteAttachment` interface — extracting it would just push the same + * code into another file. + */ +function orderAttachementsToQuotes( + order: import("@/lib/types").Order, +): import("@/lib/types").QuoteAttachment[] { + const files = order.order_attachements ?? []; + return files.map((filename) => { + const lower = filename.toLowerCase(); + let kind: "pdf" | "image" | "doc" = "doc"; + let content_type = "application/octet-stream"; + if (lower.endsWith(".pdf")) { + kind = "pdf"; + content_type = "application/pdf"; + } else if (/\.(png|jpe?g|gif|webp|bmp|svg)$/.test(lower)) { + const ext = lower.split(".").pop() ?? ""; + const imageMime = + ext === "png" + ? "png" + : ext === "gif" + ? "gif" + : ext === "webp" + ? "webp" + : ext === "svg" + ? "svg+xml" + : ext === "bmp" + ? "bmp" + : "jpeg"; + kind = "image"; + content_type = `image/${imageMime}`; + } else if (lower.endsWith(".docx")) { + content_type = + "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; + } else if (lower.endsWith(".txt")) { + content_type = "text/plain"; + } + return { + id: order.id, + created: order.created, + updated: order.updated, + collectionId: order.collectionId, + collectionName: "orders", + quote: order.quote, + filename, + file: [filename], + content_type, + size_bytes: 0, + email_message_id: "", + kind, + }; + }); +} + /* ============================================================================ * OrdersReviewModalMount — fetches the order's full details, line items, * the source quote's email + attachments, and the customer/company lists, @@ -107,7 +168,8 @@ function OrdersReviewModalMount({ // Use the BROWSER PB instance (not the admin one) — admin PB // requires server-only env vars and would break in the browser // bundle. The user's session PB already has read access to - // customers + quote_attachments, so we don't need privileges. + // customers, so we don't need privileges for the order-level + // attachments (they live on the order record itself). const { getPocketBase } = await import("@/lib/pocketbase"); const pb = getPocketBase(); const [order, items, customersResult] = await Promise.all([ @@ -128,13 +190,17 @@ function OrdersReviewModalMount({ }) | undefined; - const attachments = quote - ? await pb - .collection("quote_attachments") - .getFullList({ - filter: `quote = "${quote.id}"`, - }) - : []; + // Source attachments from `order_attachements` — the multi-file + // field ON the order record itself (orders collection + // `pbc_3527180448`, `maxSelect: 10`, accepts any MIME). Older + // orders may still have attachments stored under + // `quote_attachments` on the parent quote, but those records + // were migrated to the order-level field on 2026-07-29, so + // reading from the order is authoritative going forward. + // The PB file field stores bare filenames (no collection-id + // token); we synthesise the QuoteAttachment shape the modal + // expects (kind + content_type) from the filename extension. + const attachments = orderAttachementsToQuotes(order); if (cancelled) return; @@ -163,7 +229,7 @@ function OrdersReviewModalMount({ subject: quote?.email_subject || "", body: quote?.email_body || "", }, - attachments: attachments as import("@/lib/types").QuoteAttachment[], + attachments, }); } catch (err) { if (!cancelled) { diff --git a/src/app/api/v1/files/[collection]/[recordId]/[filename]/route.ts b/src/app/api/v1/files/[collection]/[recordId]/[filename]/route.ts index c966478..939f83a 100644 --- a/src/app/api/v1/files/[collection]/[recordId]/[filename]/route.ts +++ b/src/app/api/v1/files/[collection]/[recordId]/[filename]/route.ts @@ -59,6 +59,12 @@ const ALLOWED_COLLECTIONS = new Set([ // Order items — attachment chips and the Attach upload on the // Collection Confirm page proxy through here too. "order_items", "pbc_2456927940", + // Orders — the `order_attachements` file field on the orders + // collection. The orders review modal proxies PDF/image previews + // through here (each file is keyed by the order's own record id). + // Added 2026-07-29 when the review modal's attachment source moved + // from `quote_attachments` to the order-level field. + "orders", "pbc_3527180448", // Stock purchases — stock-PO PDFs (rendered into // `stock_purchases.documents`) and any other attachments the // operator uploads to a stock PO route through here. Added