diff --git a/src/app/(app)/orders/[id]/page.tsx b/src/app/(app)/orders/[id]/page.tsx
index ef0c60a..afb5a25 100644
--- a/src/app/(app)/orders/[id]/page.tsx
+++ b/src/app/(app)/orders/[id]/page.tsx
@@ -36,6 +36,7 @@ import {
RefreshCw,
History,
Send,
+ Paperclip,
} from "lucide-react";
import {
Dialog,
@@ -108,6 +109,13 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
// build a non-misleading body, so the textarea is disabled until
// the count is resolved.
const [sendMessageLoading, setSendMessageLoading] = useState(false);
+ // Additional attachments (supplier quote, drawing, NDA, etc.) sent
+ // alongside the PO PDF. PB stores these as file tokens in
+ // `po.additional_attachments`; the modal reads them on open and
+ // writes back via the upload/delete routes.
+ const [attachments, setAttachments] = useState([]);
+ const [attachmentsLoading, setAttachmentsLoading] = useState(false);
+ const [attachmentUploading, setAttachmentUploading] = useState(false);
const [loading, setLoading] = useState(true);
const [deleteOpen, setDeleteOpen] = useState(false);
@@ -331,6 +339,12 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
toast.error("Couldn't load line count for preview; check the PO before sending.");
} finally {
setSendMessageLoading(false);
+ // Seed the attachments list from the PO record that just opened
+ // the modal. PB returns a multi-file field as either an array
+ // or (for legacy single-file rows) a bare string — normalise
+ // to string[] so the rest of the modal can treat it uniformly.
+ const rawAtt = po.additional_attachments ?? [];
+ setAttachments(Array.isArray(rawAtt) ? rawAtt : rawAtt ? [rawAtt] : []);
}
};
@@ -402,6 +416,58 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
}
};
+ // ─── Additional attachments handlers ──────────────────────────
+ // Upload one or more files to `po.additional_attachments` via the
+ // dedicated route. We refresh `attachments` from each successful
+ // response so the list always reflects the canonical PB state.
+ const uploadAttachments = async (files: FileList | null) => {
+ if (!sendPo || !files || files.length === 0) return;
+ setAttachmentUploading(true);
+ try {
+ for (const file of Array.from(files)) {
+ const fd = new FormData();
+ fd.append("file", file);
+ const res = await fetch(
+ `/api/v1/purchase-orders/${sendPo.id}/attachments/upload`,
+ { method: "POST", body: fd },
+ );
+ if (!res.ok) {
+ const body = await res.json().catch(() => ({}));
+ toast.error(
+ `Upload failed for ${file.name}: ${body.error ?? res.status}`,
+ );
+ continue;
+ }
+ const data = (await res.json()) as { attachments: string[] };
+ setAttachments(data.attachments ?? []);
+ }
+ } finally {
+ setAttachmentUploading(false);
+ }
+ };
+
+ // Remove a single attachment by its PB token. The token is what
+ // appears in `additional_attachments[]`; it may differ from the
+ // operator's original filename because PB slug-normalises on disk.
+ const removeAttachment = async (token: string) => {
+ if (!sendPo) return;
+ setAttachmentUploading(true);
+ try {
+ const res = await fetch(
+ `/api/v1/purchase-orders/${sendPo.id}/attachments/${encodeURIComponent(token)}`,
+ { method: "DELETE" },
+ );
+ if (!res.ok) {
+ toast.error("Couldn't remove attachment");
+ return;
+ }
+ const data = (await res.json()) as { attachments: string[] };
+ setAttachments(data.attachments ?? []);
+ } finally {
+ setAttachmentUploading(false);
+ }
+ };
+
// Order-level "Complete" — derived from items, not stored anywhere.
const complete = isOrderComplete(items);
@@ -1100,6 +1166,59 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
: `${sendMessage.length}/2000`}
+
+
+
+
Additional attachments
+
+ Files sent alongside the PO PDF (supplier quotes, drawings, etc.).
+
+
+
+
+ {attachments.length === 0 ? (
+
No additional files attached.
+ ) : (
+
+ {attachments.map((token) => (
+ -
+
+ {token}
+
+
+
+ ))}
+
+ )}
+
{sendPo?.status === "mail_failed" && sendPo?.email_error && (
Previous failure:{" "}