feat(assign-dialog): gate submit on ready_for_collection with warning

This commit is contained in:
SJCoetz 2026-07-12 00:40:41 +02:00
parent d78d479091
commit b5cd061da1
4 changed files with 131 additions and 1 deletions

15
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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;
}) => (
<input
data-testid="driver-search"
value={value}
onChange={(e) => onValueChange(e.target.value)}
placeholder={placeholder}
/>
),
}));
function openDialog(overrides: Partial<React.ComponentProps<typeof AssignDriverDialog>> = {}) {
const onSubmit = vi.fn();
const onOpenChange = vi.fn();
const result = render(
<AssignDriverDialog
open
onOpenChange={onOpenChange}
onSubmit={onSubmit}
initialDriverId=""
initialDate="2026-07-12"
requireReadyForCollection
readyForCollection={false}
{...overrides}
/>,
);
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();
});
});

View file

@ -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 (
<ModalShell.Dialog
@ -210,6 +235,22 @@ function AssignDriverDialogInner({
}
>
<div className="space-y-5">
{requireReadyForCollection && readyForCollection === false && (
<p
role="status"
className="mb-4 rounded-md border border-amber-300 bg-amber-50 px-3 py-2 text-xs text-amber-900"
>
Supplier hasn&apos;t confirmed this item is ready for collection. Mark it
ready in{" "}
<a
href="/collection-pool"
className="underline underline-offset-2"
>
/collection-pool
</a>{" "}
before assigning a driver.
</p>
)}
{/* Driver picker */}
<div className="space-y-2">
<Label htmlFor="assign-driver">Driver</Label>