diff --git a/src/app/v/[token]/dispatch/_components/dispatch-detail-sections.tsx b/src/app/v/[token]/dispatch/_components/dispatch-detail-sections.tsx index f400f8f1..0712f6ef 100644 --- a/src/app/v/[token]/dispatch/_components/dispatch-detail-sections.tsx +++ b/src/app/v/[token]/dispatch/_components/dispatch-detail-sections.tsx @@ -76,7 +76,7 @@ export function DispatchDetailSections({ existing={data.signoffs ?? []} signoffType="customer" nameLabel="Customer name" - submissionLocked={data.status === "Refused"} + submissionLocked={locked} onAdded={() => void refresh()} /> @@ -89,7 +89,7 @@ export function DispatchDetailSections({ existing={data.signoffs ?? []} signoffType="vendor" nameLabel="Your name" - submissionLocked={data.status === "Refused"} + submissionLocked={locked} onAdded={() => void refresh()} /> diff --git a/src/app/v/[token]/dispatch/_components/refuse-dispatch-dialog.tsx b/src/app/v/[token]/dispatch/_components/refuse-dispatch-dialog.tsx index f0cb4cf4..89e053f3 100644 --- a/src/app/v/[token]/dispatch/_components/refuse-dispatch-dialog.tsx +++ b/src/app/v/[token]/dispatch/_components/refuse-dispatch-dialog.tsx @@ -45,12 +45,6 @@ export function RefuseDispatchDialog({ const reasonValue = useWatch({ control, name: "reason" }) ?? ""; const isOverLimit = reasonValue.length > REFUSE_REASON_MAX_LENGTH; - useEffect(() => { - if (!open) { - reset({ reason: "" }); - } - }, [open, reset]); - const refuseMutation = useMutation({ mutationFn: (values: RefuseDispatchValues) => { const trimmed = values.reason.trim(); @@ -70,6 +64,15 @@ export function RefuseDispatchDialog({ }, }); + const { reset: resetMutation } = refuseMutation; + + useEffect(() => { + if (!open) { + reset({ reason: "" }); + resetMutation(); + } + }, [open, reset, resetMutation]); + const submit = handleSubmit((values) => refuseMutation.mutate(values)); const errorMessage = refuseMutation.error ? "Unable to refuse this dispatch. Please try again." diff --git a/src/app/v/[token]/dispatch/_components/signoff-section.tsx b/src/app/v/[token]/dispatch/_components/signoff-section.tsx index 58e8eaf4..298e1737 100644 --- a/src/app/v/[token]/dispatch/_components/signoff-section.tsx +++ b/src/app/v/[token]/dispatch/_components/signoff-section.tsx @@ -53,7 +53,7 @@ export function SignoffSection({ } if (submissionLocked) { - return Signoff is unavailable after this dispatch was refused.; + return Signoff is unavailable for this dispatch.; } const submit = async (event: React.FormEvent) => { diff --git a/src/test/app/v/dispatch-detail-sections.test.tsx b/src/test/app/v/dispatch-detail-sections.test.tsx new file mode 100644 index 00000000..fad2f8ae --- /dev/null +++ b/src/test/app/v/dispatch-detail-sections.test.tsx @@ -0,0 +1,99 @@ +import { screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { DispatchDetailSections } from "@/app/v/[token]/dispatch/_components/dispatch-detail-sections"; +import { isVendorPortalLockedStatus } from "@/domain/vendor-portal/lib/status-helpers"; +import type { + VendorPortalDispatchDetail, + VendorPortalSignoff, +} from "@/domain/vendor-portal/types/vendor-portal"; +import { renderWithProviders } from "@/test/test-utils"; + +vi.mock("@/app/v/[token]/dispatch/_components/checklist-section", () => ({ + ChecklistSection: () =>
, +})); +vi.mock("@/app/v/_components/vendor-portal-documents", () => ({ + VendorPortalDocuments: () =>
, +})); +vi.mock("@/app/v/[token]/dispatch/_components/uplift-requests-section", () => ({ + UpliftRequestsSection: () =>
, +})); +vi.mock("@/app/v/[token]/dispatch/_components/comments-section", () => ({ + CommentsSection: () =>
, +})); + +function buildData( + overrides: Partial = {}, +): VendorPortalDispatchDetail { + return { + id: 9, + dispatchNumber: "DSP-9", + status: "In Progress", + nteAmount: 500, + checklist: [], + comments: [], + upliftRequests: [], + signoffs: [], + documents: [], + ...overrides, + }; +} + +function renderSections({ + status, + signoffs = [], +}: { + status: string; + signoffs?: VendorPortalSignoff[]; +}) { + const data = buildData({ status, signoffs }); + const locked = isVendorPortalLockedStatus(status); + + return renderWithProviders( + , + { withAuth: false }, + ); +} + +describe("DispatchDetailSections signoff locking", () => { + it.each(["Verified", "Cancelled", "Refused"])( + "locks both customer and vendor signoff forms when status is %s", + (status) => { + renderSections({ status }); + + expect(screen.getAllByText(/Signoff is unavailable for this dispatch/i)).toHaveLength(2); + expect(screen.queryAllByRole("button", { name: "Submit Signoff" })).toHaveLength(0); + }, + ); + + it.each(["Completed", "In Progress"])( + "keeps both signoff forms available when status is %s", + (status) => { + renderSections({ status }); + + expect(screen.getAllByRole("button", { name: "Submit Signoff" })).toHaveLength(2); + expect(screen.queryByText(/Signoff is unavailable/i)).not.toBeInTheDocument(); + }, + ); + + it("keeps an existing signoff visible while the dispatch is locked", () => { + const customerSignoff: VendorPortalSignoff = { + signoffType: "customer", + name: "Ada Lovelace", + signatureMethod: "drawn", + signedAt: "2024-01-02T03:04:05Z", + }; + renderSections({ status: "Refused", signoffs: [customerSignoff] }); + + expect(screen.getByText("Ada Lovelace")).toBeInTheDocument(); + expect(screen.getByText("drawn")).toBeInTheDocument(); + expect(screen.getByText(/Signoff is unavailable/i)).toBeInTheDocument(); + expect(screen.queryAllByRole("button", { name: "Submit Signoff" })).toHaveLength(0); + }); +}); diff --git a/src/test/app/v/refuse-dispatch-dialog.test.tsx b/src/test/app/v/refuse-dispatch-dialog.test.tsx index 5850a511..ef0c9fe7 100644 --- a/src/test/app/v/refuse-dispatch-dialog.test.tsx +++ b/src/test/app/v/refuse-dispatch-dialog.test.tsx @@ -169,4 +169,42 @@ describe("RefuseDispatchDialog", () => { await waitFor(() => expect(onClose).toHaveBeenCalled()); }); + + it("clears the stale error and reason after closing and reopening", async () => { + vi.spyOn(vendorPortalApi, "refuse").mockRejectedValueOnce(new Error("nope")); + const onClose = vi.fn(); + const onRefused = vi.fn(); + + const portalValue: VendorPortalContextValue = { + token: "portal-token", + vendor: { companyName: "Gateway Plumbing", vendorId: 101 }, + status: "ready", + error: null, + }; + const renderEl = (open: boolean) => ( + + + + ); + + const { rerender } = renderWithProviders(renderEl(true), { withAuth: false }); + + fireEvent.change(screen.getByLabelText(/Reason/i), { target: { value: "too busy" } }); + await act(async () => { + fireEvent.click(screen.getByRole("button", { name: "Refuse dispatch" })); + }); + await waitFor(() => expect(screen.getByRole("alert")).toHaveTextContent(/Unable to refuse/i)); + expect(screen.getByText(`8/${REFUSE_REASON_MAX_LENGTH}`)).toBeInTheDocument(); + + await act(async () => { + rerender(renderEl(false)); + }); + + await act(async () => { + rerender(renderEl(true)); + }); + + await waitFor(() => expect(screen.queryByRole("alert")).not.toBeInTheDocument()); + expect(screen.getByText(`0/${REFUSE_REASON_MAX_LENGTH}`)).toBeInTheDocument(); + }); }); diff --git a/src/test/app/v/signoff-section.test.tsx b/src/test/app/v/signoff-section.test.tsx new file mode 100644 index 00000000..b83b0b7e --- /dev/null +++ b/src/test/app/v/signoff-section.test.tsx @@ -0,0 +1,46 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { SignoffSection } from "@/app/v/[token]/dispatch/_components/signoff-section"; + +function renderSection(overrides: Partial> = {}) { + return render( + , + ); +} + +describe("SignoffSection", () => { + it("shows status-neutral locked copy when submission is locked", () => { + renderSection({ submissionLocked: true }); + + expect(screen.getByText(/Signoff is unavailable for this dispatch/i)).toBeInTheDocument(); + expect(screen.queryByText(/refused/i)).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Submit Signoff" })).not.toBeInTheDocument(); + }); + + it("shows the submission form when unlocked", () => { + renderSection(); + + expect(screen.getByRole("button", { name: "Submit Signoff" })).toBeInTheDocument(); + }); + + it("shows an existing signoff even while locked", () => { + renderSection({ + signoffType: "vendor", + nameLabel: "Your name", + submissionLocked: true, + existing: [{ signoffType: "vendor", name: "Grace Hopper", signatureMethod: "typed" }], + }); + + expect(screen.getByText("Grace Hopper")).toBeInTheDocument(); + expect(screen.queryByText(/Signoff is unavailable/i)).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Submit Signoff" })).not.toBeInTheDocument(); + }); +});