diff --git a/src/components/orders-review-modal.test.tsx b/src/components/orders-review-modal.test.tsx
index d8e0718..f265bbe 100644
--- a/src/components/orders-review-modal.test.tsx
+++ b/src/components/orders-review-modal.test.tsx
@@ -248,4 +248,89 @@ describe("OrdersReviewModal", () => {
// Cost should now show 33.33.
expect(screen.getByDisplayValue("33.33")).toBeInTheDocument();
});
+
+ it("renders PDF preview via iframe when first attachment is a PDF", () => {
+ render(
+ ,
+ );
+ const iframe = document.querySelector("iframe");
+ expect(iframe).toBeInTheDocument();
+ expect(iframe?.getAttribute("src")).toBe(
+ "/api/v1/files/coll-1/att-1/acme-token",
+ );
+ });
+
+ it("renders image preview via
when first attachment is an image", () => {
+ render(
+ ,
+ );
+ const img = document.querySelector("img");
+ expect(img).toBeInTheDocument();
+ expect(img?.getAttribute("src")).toBe(
+ "/api/v1/files/coll-1/att-1/photo-token",
+ );
+ });
+
+ it("renders 'no preview' list when attachments are not PDF/image", () => {
+ render(
+ ,
+ );
+ expect(screen.getByText(/no preview/i)).toBeInTheDocument();
+ expect(screen.getByText("doc.docx")).toBeInTheDocument();
+ });
});
diff --git a/src/components/orders-review-modal.tsx b/src/components/orders-review-modal.tsx
index 711ffd9..cbcb44c 100644
--- a/src/components/orders-review-modal.tsx
+++ b/src/components/orders-review-modal.tsx
@@ -514,7 +514,10 @@ export function OrdersReviewModal({
);
}
-// --- Source-of-truth panel (filled out in Task 5) -------------------------
+// --- Source-of-truth panel -----------------------------------------------
+// Inline preview of the source attachment so the operator can scan
+// the actual PDF/image against the order items in one screen.
+// Falls back to the email body when no attachments exist.
function SourceOfTruth({
quoteEmail,
attachments,
@@ -523,32 +526,143 @@ function SourceOfTruth({
attachments: QuoteAttachment[];
}) {
if (attachments.length === 0) {
- return (
-
-
-
- Email
-
-
{quoteEmail.subject || "(no subject)"}
-
- {quoteEmail.body || "(no body)"}
-
-
- );
+ return ;
}
+
+ // Pick the first PDF if any, else first image, else the first
+ // entry of any other kind (which gets the "no preview" treatment).
+ const firstPdf = attachments.find((a) => a.kind === "pdf");
+ const firstImage = attachments.find((a) => a.kind === "image");
+ const firstOther = attachments.find(
+ (a) => a.kind !== "pdf" && a.kind !== "image",
+ );
+
+ const previewTarget = firstPdf ?? firstImage ?? null;
+ const othersAreUnpreviewable = attachments.some(
+ (a) => a.kind !== "pdf" && a.kind !== "image",
+ );
+
return (
-
-
+
+
- Attachments ({attachments.length}) — preview added in Task 5
+ Attachments ({attachments.length})
-
+
+ {/* Tab strip — one chip per attachment, primary chip = previewTarget */}
+ {attachments.length > 1 && (
+
+ {attachments.map((a) => {
+ const isActive = previewTarget && a.id === previewTarget.id;
+ const previewable = a.kind === "pdf" || a.kind === "image";
+ return (
+
+ {previewable ? "●" : "○"} {a.filename}
+
+ );
+ })}
+
+ )}
+
+ {previewTarget && previewTarget.kind === "pdf" && (
+
+ )}
+
+ {previewTarget && previewTarget.kind === "image" && (
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+
})
+
+ )}
+
+ {/* No previewable attachment at all — show a no-preview message. */}
+ {!previewTarget && firstOther && (
+
+
+ {`No preview available for this ${firstOther.kind ?? "file"} type.`}
+
+
+ )}
+
+ {othersAreUnpreviewable && previewTarget && (
+
+
+ + {attachments.filter(
+ (a) => a.kind !== "pdf" && a.kind !== "image",
+ ).length}{" "}
+ unpreviewable attachment(s) — open to view.
+
+
+ )}
+
+
{attachments.map((a) => (
- -
- {a.filename}
+
-
+
+ {a.filename}
+
+
+ open
+
))}
);
}
+
+function EmailFallback({
+ quoteEmail,
+}: {
+ quoteEmail: { subject: string; body: string };
+}) {
+ return (
+
+
+
+ Email
+
+
+ {quoteEmail.subject || "(no subject)"}
+
+
+ {quoteEmail.body || "(no body)"}
+
+
+ );
+}
+
+function attachmentUrl(a: QuoteAttachment): string {
+ const token = Array.isArray(a.file) ? a.file[0] : a.file;
+ return `/api/v1/files/${a.collectionId}/${a.id}/${token}`;
+}