From b5cd061da184693fa1d803aa1e219d1b06858b69 Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Sun, 12 Jul 2026 00:40:41 +0200 Subject: [PATCH] feat(assign-dialog): gate submit on ready_for_collection with warning --- package-lock.json | 15 ++++ package.json | 1 + .../ui/assign-driver-dialog.test.tsx | 73 +++++++++++++++++++ src/components/ui/assign-driver-dialog.tsx | 43 ++++++++++- 4 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 src/components/ui/assign-driver-dialog.test.tsx diff --git a/package-lock.json b/package-lock.json index a101dcb..497d79d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -3331,6 +3332,20 @@ } } }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@ts-morph/common": { "version": "0.27.0", "license": "MIT", diff --git a/package.json b/package.json index a510a8e..cf2fa87 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/src/components/ui/assign-driver-dialog.test.tsx b/src/components/ui/assign-driver-dialog.test.tsx new file mode 100644 index 0000000..3173800 --- /dev/null +++ b/src/components/ui/assign-driver-dialog.test.tsx @@ -0,0 +1,73 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { AssignDriverDialog } from "./assign-driver-dialog"; + +vi.mock("@/components/ui/user-search", () => ({ + UserSearch: ({ + value, + onValueChange, + placeholder, + }: { + value: string; + onValueChange: (v: string) => void; + placeholder?: string; + }) => ( + onValueChange(e.target.value)} + placeholder={placeholder} + /> + ), +})); + +function openDialog(overrides: Partial> = {}) { + const onSubmit = vi.fn(); + const onOpenChange = vi.fn(); + const result = render( + , + ); + return { ...result, onSubmit, onOpenChange }; +} + +describe("AssignDriverDialog ready-gate", () => { + afterEach(() => { + cleanup(); + }); + + it("shows the amber warning and keeps Submit disabled when requireReadyForCollection and not ready", () => { + openDialog(); + expect( + screen.getByText(/Supplier hasn't confirmed this item is ready/i), + ).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /Assign driver/i })).toBeDisabled(); + }); + + it("allows Submit when requireReadyForCollection=true and readyForCollection=true", async () => { + const user = userEvent.setup(); + const { onSubmit } = openDialog({ readyForCollection: true }); + // Type a driver id so canSubmit would otherwise be true. + await user.type(screen.getByTestId("driver-search"), "drv-1"); + const btn = screen.getByRole("button", { name: /Assign driver/i }); + expect(btn).toBeEnabled(); + await user.click(btn); + expect(onSubmit).toHaveBeenCalledWith({ driverId: "drv-1", date: "2026-07-12" }); + }); + + it("does not show the warning when requireReadyForCollection is unset (other call sites)", () => { + openDialog({ requireReadyForCollection: undefined }); + expect( + screen.queryByText(/Supplier hasn't confirmed this item is ready/i), + ).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/ui/assign-driver-dialog.tsx b/src/components/ui/assign-driver-dialog.tsx index a62c300..6723ce6 100644 --- a/src/components/ui/assign-driver-dialog.tsx +++ b/src/components/ui/assign-driver-dialog.tsx @@ -67,6 +67,19 @@ export interface AssignDriverDialogProps { eyebrow?: React.ReactNode; /** Disable the submit button. */ busy?: boolean; + /** + * When true, the dialog blocks the Assign path until the row's + * `ready_for_collection` flag is true. The warning copy links to + * `/collection-pool` — that's where the operator flips the flag. + * Defaults to false (so other call sites that don't care about + * the supplier gate keep their current behavior). The flag is + * intentionally caller-supplied because the dialog stays + * row-agnostic. + */ + requireReadyForCollection?: boolean; + /** Current value of the row's `ready_for_collection` flag. When + * `requireReadyForCollection` is unset, this is ignored. */ + readyForCollection?: boolean; } function todayAsYmd(): string { @@ -88,6 +101,8 @@ export function AssignDriverDialog({ bulkCount = 1, eyebrow, busy = false, + requireReadyForCollection = false, + readyForCollection, }: AssignDriverDialogProps) { const isBulk = bulkCount > 1; const singleIntent = intent ?? (initialDriverId ? "reschedule" : "assign"); @@ -122,6 +137,8 @@ export function AssignDriverDialog({ submitLabel={submitLabel} eyebrow={eyebrow} busy={busy} + requireReadyForCollection={requireReadyForCollection} + readyForCollection={readyForCollection} /> ); } @@ -138,6 +155,8 @@ type AssignDriverDialogInnerProps = { submitLabel: string; eyebrow?: React.ReactNode; busy: boolean; + requireReadyForCollection: boolean; + readyForCollection: boolean | undefined; }; function AssignDriverDialogInner({ @@ -152,6 +171,8 @@ function AssignDriverDialogInner({ submitLabel, eyebrow, busy, + requireReadyForCollection, + readyForCollection, }: AssignDriverDialogInnerProps) { const [driverId, setDriverId] = useState(initialDriverId); const [date, setDate] = useState(initialDate ?? todayAsYmd()); @@ -169,7 +190,11 @@ function AssignDriverDialogInner({ [], ); - const canSubmit = date.length === 10 && driverId.length > 0 && !busy; + const isAssignment = driverId.length > 0; + const isReadyBlocked = + isAssignment && requireReadyForCollection && readyForCollection === false; + const canSubmit = + date.length === 10 && driverId.length > 0 && !busy && !isReadyBlocked; return (
+ {requireReadyForCollection && readyForCollection === false && ( +

+ Supplier hasn't confirmed this item is ready for collection. Mark it + ready in{" "} + + /collection-pool + {" "} + before assigning a driver. +

+ )} {/* Driver picker */}