mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 08:03:13 +00:00
fix(vendor-portal): clear stale refusal state
This commit is contained in:
parent
3f4d64d7b6
commit
bc5659eb2a
6 changed files with 195 additions and 9 deletions
|
|
@ -76,7 +76,7 @@ export function DispatchDetailSections({
|
|||
existing={data.signoffs ?? []}
|
||||
signoffType="customer"
|
||||
nameLabel="Customer name"
|
||||
submissionLocked={data.status === "Refused"}
|
||||
submissionLocked={locked}
|
||||
onAdded={() => void refresh()}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -89,7 +89,7 @@ export function DispatchDetailSections({
|
|||
existing={data.signoffs ?? []}
|
||||
signoffType="vendor"
|
||||
nameLabel="Your name"
|
||||
submissionLocked={data.status === "Refused"}
|
||||
submissionLocked={locked}
|
||||
onAdded={() => void refresh()}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export function SignoffSection({
|
|||
}
|
||||
|
||||
if (submissionLocked) {
|
||||
return <Text variant="feedback">Signoff is unavailable after this dispatch was refused.</Text>;
|
||||
return <Text variant="feedback">Signoff is unavailable for this dispatch.</Text>;
|
||||
}
|
||||
|
||||
const submit = async (event: React.FormEvent) => {
|
||||
|
|
|
|||
99
src/test/app/v/dispatch-detail-sections.test.tsx
Normal file
99
src/test/app/v/dispatch-detail-sections.test.tsx
Normal file
|
|
@ -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: () => <div data-testid="checklist-mock" />,
|
||||
}));
|
||||
vi.mock("@/app/v/_components/vendor-portal-documents", () => ({
|
||||
VendorPortalDocuments: () => <div data-testid="documents-mock" />,
|
||||
}));
|
||||
vi.mock("@/app/v/[token]/dispatch/_components/uplift-requests-section", () => ({
|
||||
UpliftRequestsSection: () => <div data-testid="uplift-mock" />,
|
||||
}));
|
||||
vi.mock("@/app/v/[token]/dispatch/_components/comments-section", () => ({
|
||||
CommentsSection: () => <div data-testid="comments-mock" />,
|
||||
}));
|
||||
|
||||
function buildData(
|
||||
overrides: Partial<VendorPortalDispatchDetail> = {},
|
||||
): 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(
|
||||
<DispatchDetailSections
|
||||
token="portal-token"
|
||||
dispatchId={9}
|
||||
data={data}
|
||||
locked={locked}
|
||||
refresh={vi.fn()}
|
||||
updateLocal={vi.fn()}
|
||||
/>,
|
||||
{ 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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) => (
|
||||
<VendorPortalContext.Provider value={portalValue}>
|
||||
<RefuseDispatchDialog open={open} dispatchId={9} onClose={onClose} onRefused={onRefused} />
|
||||
</VendorPortalContext.Provider>
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
46
src/test/app/v/signoff-section.test.tsx
Normal file
46
src/test/app/v/signoff-section.test.tsx
Normal file
|
|
@ -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<React.ComponentProps<typeof SignoffSection>> = {}) {
|
||||
return render(
|
||||
<SignoffSection
|
||||
token="portal-token"
|
||||
dispatchId={9}
|
||||
existing={[]}
|
||||
signoffType="customer"
|
||||
nameLabel="Customer name"
|
||||
onAdded={vi.fn()}
|
||||
{...overrides}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue