From 146491fafbdd8b0e1c985ad45f20f0d95dbd3806 Mon Sep 17 00:00:00 2001 From: Codex Review Integration Date: Thu, 20 Aug 2026 20:43:52 -0300 Subject: [PATCH] fix(vendors): persist technician removal --- .../_components/use-vendor-roster-resolver.ts | 28 +++++-- .../_components/vendor-detail-drawer.tsx | 1 + .../_components/vendor-roster-form-fields.tsx | 33 ++++---- .../vendors/vendor-detail-drawer.test.tsx | 77 ++++++++++++++++++- 4 files changed, 114 insertions(+), 25 deletions(-) diff --git a/src/app/(protected)/vendors/_components/use-vendor-roster-resolver.ts b/src/app/(protected)/vendors/_components/use-vendor-roster-resolver.ts index 12e1810a..864bdb4a 100644 --- a/src/app/(protected)/vendors/_components/use-vendor-roster-resolver.ts +++ b/src/app/(protected)/vendors/_components/use-vendor-roster-resolver.ts @@ -8,19 +8,35 @@ import { import { getSingleStatusOnlyChange } from "@/domain/vendors/use-cases/use-save-vendor-company-roster"; 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; + const keys = Object.keys(candidate); + const isGeneratedStatusStub = + keys.length === 1 && keys[0] === "isActive" && typeof candidate.isActive === "boolean"; + return !isGeneratedStatusStub; + }), + }; +} + export function useVendorRosterResolver( routeRoster: VendorCompanyRoster | undefined, ): Resolver { const strictResolver = useMemo(() => zodResolver(vendorCompanyRosterSchema), []); return useMemo>( () => async (values, context, options) => { - if ( - routeRoster && - getSingleStatusOnlyChange(routeRoster, values as VendorCompanyRosterFormValues) - ) { - return { values, errors: {} }; + const normalizedValues = withoutTechnicianStubs(values); + if (routeRoster && getSingleStatusOnlyChange(routeRoster, normalizedValues)) { + return { values: normalizedValues, errors: {} }; } - return strictResolver(values, context, options); + return strictResolver(normalizedValues, context, options); }, [routeRoster, strictResolver], ); diff --git a/src/app/(protected)/vendors/_components/vendor-detail-drawer.tsx b/src/app/(protected)/vendors/_components/vendor-detail-drawer.tsx index 349abe96..22ab7cd0 100644 --- a/src/app/(protected)/vendors/_components/vendor-detail-drawer.tsx +++ b/src/app/(protected)/vendors/_components/vendor-detail-drawer.tsx @@ -315,6 +315,7 @@ function DrawerEditor({ ( {field.value ? "Active" : "Inactive"} diff --git a/src/app/(protected)/vendors/_components/vendor-roster-form-fields.tsx b/src/app/(protected)/vendors/_components/vendor-roster-form-fields.tsx index 178b271b..df32ae32 100644 --- a/src/app/(protected)/vendors/_components/vendor-roster-form-fields.tsx +++ b/src/app/(protected)/vendors/_components/vendor-roster-form-fields.tsx @@ -348,23 +348,22 @@ function TechnicianRow({ tradeOptions={tradeOptions} tradeOptionsLoading={tradeOptionsLoading} /> - {showStatus && ( - ( - field.onChange(checked)} - /> - } - label={field.value ? "Active" : "Inactive"} - /> - )} - /> - )} + ( + field.onChange(checked)} + /> + } + label={field.value ? "Active" : "Inactive"} + /> + )} + /> ); } diff --git a/src/test/app/(protected)/vendors/vendor-detail-drawer.test.tsx b/src/test/app/(protected)/vendors/vendor-detail-drawer.test.tsx index af64c2e0..a882b91f 100644 --- a/src/test/app/(protected)/vendors/vendor-detail-drawer.test.tsx +++ b/src/test/app/(protected)/vendors/vendor-detail-drawer.test.tsx @@ -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 { 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 { renderWithProviders } from "@/test/test-utils"; import type { VendorListItem } from "@/domain/vendors/types/vendor"; const useVendorCompanyRoster = vi.fn(); +const saveMutate = vi.fn(); vi.mock("@/domain/vendors/use-cases/use-vendor-company-roster", () => ({ 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 = { id: 1, companyId: "co-1", @@ -50,7 +61,69 @@ function rosterWith(technicians: Array>) { }; } +beforeEach(() => { + saveMutate.mockReset(); +}); + 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( + , + { 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 () => { const roster = rosterWith([ {