mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 21:42:06 +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 ?? []}
|
existing={data.signoffs ?? []}
|
||||||
signoffType="customer"
|
signoffType="customer"
|
||||||
nameLabel="Customer name"
|
nameLabel="Customer name"
|
||||||
submissionLocked={data.status === "Refused"}
|
submissionLocked={locked}
|
||||||
onAdded={() => void refresh()}
|
onAdded={() => void refresh()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -89,7 +89,7 @@ export function DispatchDetailSections({
|
||||||
existing={data.signoffs ?? []}
|
existing={data.signoffs ?? []}
|
||||||
signoffType="vendor"
|
signoffType="vendor"
|
||||||
nameLabel="Your name"
|
nameLabel="Your name"
|
||||||
submissionLocked={data.status === "Refused"}
|
submissionLocked={locked}
|
||||||
onAdded={() => void refresh()}
|
onAdded={() => void refresh()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -45,12 +45,6 @@ export function RefuseDispatchDialog({
|
||||||
const reasonValue = useWatch({ control, name: "reason" }) ?? "";
|
const reasonValue = useWatch({ control, name: "reason" }) ?? "";
|
||||||
const isOverLimit = reasonValue.length > REFUSE_REASON_MAX_LENGTH;
|
const isOverLimit = reasonValue.length > REFUSE_REASON_MAX_LENGTH;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) {
|
|
||||||
reset({ reason: "" });
|
|
||||||
}
|
|
||||||
}, [open, reset]);
|
|
||||||
|
|
||||||
const refuseMutation = useMutation({
|
const refuseMutation = useMutation({
|
||||||
mutationFn: (values: RefuseDispatchValues) => {
|
mutationFn: (values: RefuseDispatchValues) => {
|
||||||
const trimmed = values.reason.trim();
|
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 submit = handleSubmit((values) => refuseMutation.mutate(values));
|
||||||
const errorMessage = refuseMutation.error
|
const errorMessage = refuseMutation.error
|
||||||
? "Unable to refuse this dispatch. Please try again."
|
? "Unable to refuse this dispatch. Please try again."
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ export function SignoffSection({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (submissionLocked) {
|
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) => {
|
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());
|
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