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 <help@stably.ai>
This commit is contained in:
SJCoetz 2026-07-29 17:41:29 +02:00
parent 7801b602f5
commit d6570dca71
2 changed files with 81 additions and 9 deletions

View file

@ -72,6 +72,67 @@ const EMPTY_COUNTS: Record<TabGroup, number> = {
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) {

View file

@ -59,6 +59,12 @@ const ALLOWED_COLLECTIONS = new Set<string>([
// 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