feat(collection-confirm): add PhotoStrip component

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
SJCoetz 2026-08-03 01:16:09 +02:00
parent 7360e14c58
commit afa3876316
2 changed files with 116 additions and 0 deletions

View file

@ -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(<PhotoStrip rows={rows} />);
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(<PhotoStrip rows={rows} />);
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(<PhotoStrip rows={rows} />);
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(<PhotoStrip rows={rows} />);
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",
);
});
});

View file

@ -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 (
<div className="rounded-xl border border-dashed border-muted-foreground/30 p-4 text-sm text-muted-foreground">
<p>No invoice photo attached.</p>
<p className="mt-1 text-xs">
The driver should attach the invoice photo via the Attach dialog. The operator can still
confirm without a photo.
</p>
</div>
);
}
return (
<div className="flex flex-wrap gap-2">
{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 (
<Button
key={filename}
type="button"
variant="outline"
size="sm"
className="h-auto max-w-full py-2"
aria-label={`Invoice ${index + 1} ${filename}`}
onClick={() => window.open(url, "_blank", "noopener,noreferrer")} >
<span className="font-medium">Invoice {index + 1}</span>
<span className="truncate text-muted-foreground">{filename}</span>
</Button>
);
})}
</div>
);
}