diff --git a/src/components/collection-confirm/photo-strip.test.tsx b/src/components/collection-confirm/photo-strip.test.tsx
new file mode 100644
index 0000000..4330930
--- /dev/null
+++ b/src/components/collection-confirm/photo-strip.test.tsx
@@ -0,0 +1,66 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+import type { ConfirmRow } from "@/lib/server/collection-confirm-service";
+import { PhotoStrip } from "./photo-strip";
+
+afterEach(() => {
+ vi.restoreAllMocks();
+});
+
+describe("PhotoStrip", () => {
+ it("renders one chip per distinct filename", () => {
+ const rows: ConfirmRow[] = [
+ { id: "row-1", order_item_attachements: ["invoice.pdf"] },
+ { id: "row-2", order_item_attachements: ["receipt.jpg"] },
+ ];
+
+ render();
+
+ expect(screen.getByRole("button", { name: /Invoice 1 invoice\.pdf/i })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /Invoice 2 receipt\.jpg/i })).toBeInTheDocument();
+ });
+
+ it("dedupes filenames across rows", () => {
+ const rows: ConfirmRow[] = [
+ { id: "row-1", order_item_attachements: ["invoice.pdf"] },
+ { id: "row-2", order_item_attachements: ["invoice.pdf"] },
+ ];
+
+ render();
+
+ expect(screen.getAllByRole("button")).toHaveLength(1);
+ expect(screen.getByText("Invoice 1")).toBeInTheDocument();
+ });
+
+ it("renders the empty state when no rows have attachments", () => {
+ const rows: ConfirmRow[] = [
+ { id: "row-1", order_item_attachements: [] },
+ { id: "row-2" },
+ ];
+
+ render();
+
+ expect(screen.getByText(/No invoice photo attached/i)).toBeInTheDocument();
+ expect(screen.getByText(/Attach dialog/i)).toBeInTheDocument();
+ });
+
+ it("opens the file from its first row in a new tab", async () => {
+ const user = userEvent.setup();
+ const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
+ const rows: ConfirmRow[] = [
+ { id: "row/one", order_item_attachements: ["invoice copy.pdf"] },
+ { id: "row-2", order_item_attachements: ["invoice copy.pdf"] },
+ ];
+
+ render();
+ await user.click(screen.getByRole("button", { name: /Invoice 1 invoice copy\.pdf/i }));
+
+ expect(openSpy).toHaveBeenCalledWith(
+ "/api/v1/files/order_items/row%2Fone/invoice%20copy.pdf",
+ "_blank",
+ "noopener,noreferrer",
+ );
+ });
+});
diff --git a/src/components/collection-confirm/photo-strip.tsx b/src/components/collection-confirm/photo-strip.tsx
new file mode 100644
index 0000000..dc3f01d
--- /dev/null
+++ b/src/components/collection-confirm/photo-strip.tsx
@@ -0,0 +1,50 @@
+"use client";
+
+import { dedupeAttachments } from "@/app/(app)/collection-confirm/view-model";
+import { Button } from "@/components/ui/button";
+import type { ConfirmRow } from "@/lib/server/collection-confirm-service";
+
+type PhotoStripProps = {
+ rows: ConfirmRow[];
+};
+
+export function PhotoStrip({ rows }: PhotoStripProps) {
+ const attachments = dedupeAttachments(rows);
+
+ if (attachments.length === 0) {
+ return (
+
+
No invoice photo attached.
+
+ The driver should attach the invoice photo via the Attach dialog. The operator can still
+ confirm without a photo.
+
+
+ );
+ }
+
+ return (
+
+ {attachments.map(({ filename, itemIds }, index) => {
+ const firstItemId = itemIds[0];
+ if (!firstItemId) return null;
+
+ const url = `/api/v1/files/order_items/${encodeURIComponent(firstItemId)}/${encodeURIComponent(filename)}`;
+
+ return (
+
+ );
+ })}
+
+ );
+}