fix(vendors): persist technician removal

This commit is contained in:
Codex Review Integration 2026-08-20 20:43:52 -03:00
parent 35bfb9b7c8
commit 146491fafb
4 changed files with 114 additions and 25 deletions

View file

@ -8,19 +8,35 @@ import {
import { getSingleStatusOnlyChange } from "@/domain/vendors/use-cases/use-save-vendor-company-roster"; import { getSingleStatusOnlyChange } from "@/domain/vendors/use-cases/use-save-vendor-company-roster";
import type { VendorCompanyRoster } from "@/domain/vendors/types/vendor"; import type { VendorCompanyRoster } from "@/domain/vendors/types/vendor";
function withoutTechnicianStubs(
values: VendorCompanyRosterFormValues,
): VendorCompanyRosterFormValues {
return {
...values,
technicians: values.technicians.filter((technician) => {
if (technician === null || typeof technician !== "object" || Array.isArray(technician)) {
return true;
}
const candidate = technician as unknown as Record<string, unknown>;
const keys = Object.keys(candidate);
const isGeneratedStatusStub =
keys.length === 1 && keys[0] === "isActive" && typeof candidate.isActive === "boolean";
return !isGeneratedStatusStub;
}),
};
}
export function useVendorRosterResolver( export function useVendorRosterResolver(
routeRoster: VendorCompanyRoster | undefined, routeRoster: VendorCompanyRoster | undefined,
): Resolver<VendorCompanyRosterFormValues> { ): Resolver<VendorCompanyRosterFormValues> {
const strictResolver = useMemo(() => zodResolver(vendorCompanyRosterSchema), []); const strictResolver = useMemo(() => zodResolver(vendorCompanyRosterSchema), []);
return useMemo<Resolver<VendorCompanyRosterFormValues>>( return useMemo<Resolver<VendorCompanyRosterFormValues>>(
() => async (values, context, options) => { () => async (values, context, options) => {
if ( const normalizedValues = withoutTechnicianStubs(values);
routeRoster && if (routeRoster && getSingleStatusOnlyChange(routeRoster, normalizedValues)) {
getSingleStatusOnlyChange(routeRoster, values as VendorCompanyRosterFormValues) return { values: normalizedValues, errors: {} };
) {
return { values, errors: {} };
} }
return strictResolver(values, context, options); return strictResolver(normalizedValues, context, options);
}, },
[routeRoster, strictResolver], [routeRoster, strictResolver],
); );

View file

@ -315,6 +315,7 @@ function DrawerEditor({
<Controller <Controller
control={form.control} control={form.control}
name={`technicians.${selectedIndex}.isActive`} name={`technicians.${selectedIndex}.isActive`}
shouldUnregister
render={({ field }) => ( render={({ field }) => (
<Stack direction="row" spacing={1} sx={{ alignItems: "center" }}> <Stack direction="row" spacing={1} sx={{ alignItems: "center" }}>
<Text variant="description">{field.value ? "Active" : "Inactive"}</Text> <Text variant="description">{field.value ? "Active" : "Inactive"}</Text>

View file

@ -348,23 +348,22 @@ function TechnicianRow({
tradeOptions={tradeOptions} tradeOptions={tradeOptions}
tradeOptionsLoading={tradeOptionsLoading} tradeOptionsLoading={tradeOptionsLoading}
/> />
{showStatus && ( <Controller
<Controller control={control}
control={control} name={`technicians.${index}.isActive`}
name={`technicians.${index}.isActive`} render={({ field }) => (
render={({ field }) => ( <FormControlLabel
<FormControlLabel sx={{ display: showStatus ? undefined : "none" }}
control={ control={
<Switch <Switch
checked={field.value} checked={Boolean(field.value)}
onChange={(_event, checked) => field.onChange(checked)} onChange={(_event, checked) => field.onChange(checked)}
/> />
} }
label={field.value ? "Active" : "Inactive"} label={field.value ? "Active" : "Inactive"}
/> />
)} )}
/> />
)}
</Stack> </Stack>
); );
} }

View file

@ -1,16 +1,27 @@
import { screen } from "@testing-library/react"; import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { VendorDetailDrawer } from "@/app/(protected)/vendors/_components/vendor-detail-drawer"; import { VendorDetailDrawer } from "@/app/(protected)/vendors/_components/vendor-detail-drawer";
import { renderWithProviders } from "@/test/test-utils"; import { renderWithProviders } from "@/test/test-utils";
import type { VendorListItem } from "@/domain/vendors/types/vendor"; import type { VendorListItem } from "@/domain/vendors/types/vendor";
const useVendorCompanyRoster = vi.fn(); const useVendorCompanyRoster = vi.fn();
const saveMutate = vi.fn();
vi.mock("@/domain/vendors/use-cases/use-vendor-company-roster", () => ({ vi.mock("@/domain/vendors/use-cases/use-vendor-company-roster", () => ({
useVendorCompanyRoster: (...args: unknown[]) => useVendorCompanyRoster(...args), useVendorCompanyRoster: (...args: unknown[]) => useVendorCompanyRoster(...args),
})); }));
vi.mock("@/domain/vendors/use-cases/use-save-vendor-company-roster", async () => {
const actual = await vi.importActual<
typeof import("@/domain/vendors/use-cases/use-save-vendor-company-roster")
>("@/domain/vendors/use-cases/use-save-vendor-company-roster");
return {
...actual,
useSaveVendorCompanyRoster: () => ({ mutate: saveMutate, isPending: false }),
};
});
const vendor: VendorListItem = { const vendor: VendorListItem = {
id: 1, id: 1,
companyId: "co-1", companyId: "co-1",
@ -50,7 +61,69 @@ function rosterWith(technicians: Array<Record<string, unknown>>) {
}; };
} }
beforeEach(() => {
saveMutate.mockReset();
});
describe("VendorDetailDrawer selected-technician display", () => { describe("VendorDetailDrawer selected-technician display", () => {
it.each([
{ removedPosition: 1, remainingId: 2, remainingName: "Beth" },
{ removedPosition: 2, remainingId: 1, remainingName: "Adam" },
])(
"reconciles the remaining roster after removing technician $removedPosition",
async ({ removedPosition, remainingId, remainingName }) => {
useVendorCompanyRoster.mockReturnValue(
rosterWith([
{
id: 1,
contactName: "Adam",
phone: "314-555-0198",
email: "",
tradeSpecialties: "",
isActive: removedPosition === 1,
totalJobs: 5,
},
{
id: 2,
contactName: "Beth",
phone: "314-555-0199",
email: "",
tradeSpecialties: "",
isActive: removedPosition === 2,
totalJobs: 2,
},
]),
);
renderWithProviders(
<VendorDetailDrawer open={true} vendor={vendor} startInEdit={true} onClose={vi.fn()} />,
{ route: "/vendors", withAuth: false },
);
await userEvent.click(
screen.getByRole("button", { name: `Remove technician ${removedPosition}` }),
);
await userEvent.click(screen.getByRole("button", { name: "Save changes" }));
await waitFor(() => expect(saveMutate).toHaveBeenCalledTimes(1));
expect(saveMutate).toHaveBeenCalledWith(
expect.objectContaining({
mode: "update",
values: expect.objectContaining({
technicians: [
expect.objectContaining({
id: remainingId,
contactName: remainingName,
isActive: false,
}),
],
}),
}),
expect.any(Object),
);
},
);
it("keeps Save changes available after removing a technician from a legacy roster", async () => { it("keeps Save changes available after removing a technician from a legacy roster", async () => {
const roster = rosterWith([ const roster = rosterWith([
{ {