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:
parent
7360e14c58
commit
afa3876316
2 changed files with 116 additions and 0 deletions
66
src/components/collection-confirm/photo-strip.test.tsx
Normal file
66
src/components/collection-confirm/photo-strip.test.tsx
Normal 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",
|
||||
);
|
||||
});
|
||||
});
|
||||
50
src/components/collection-confirm/photo-strip.tsx
Normal file
50
src/components/collection-confirm/photo-strip.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue