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 ( + + ); + })} +
+ ); +}