diff --git a/src/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-add-technician.tsx b/src/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-add-technician.tsx index 7327e816..f559bb7d 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-add-technician.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-add-technician.tsx @@ -7,16 +7,15 @@ import { import { Button } from "@/components/ui/button"; import { Text } from "@/components/ui/text"; import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order"; -import { vendorSchema } from "@/domain/vendors/schemas/vendor-schema"; +import { vendorSchema, type VendorFormValues } from "@/domain/vendors/schemas/vendor-schema"; import { buildTechnicianCreatePayload } from "@/domain/vendors/utils/build-technician-create-payload"; -import { vendorToDropdownItem } from "@/domain/vendors/utils/vendor-to-dropdown-item"; -import { useCreateVendor } from "@/domain/vendors/use-cases/use-create-vendor"; +import { createDraftVendorDropdownItem } from "@/domain/vendors/utils/draft-vendor"; import { FieldLabel } from "@/app/(protected)/workorders/_components/list/table/cells/dialog-field-label"; type VendorDialogAddTechnicianProps = { companyName: string; companyVendor: VendorDropdownItem; - onCreated: (vendor: VendorDropdownItem, phone: string) => void; + onCreated: (vendor: VendorDropdownItem, phone: string, payload: VendorFormValues) => void; }; export function VendorDialogAddTechnician({ @@ -24,7 +23,6 @@ export function VendorDialogAddTechnician({ companyVendor, onCreated, }: VendorDialogAddTechnicianProps) { - const createVendor = useCreateVendor(); const [expanded, setExpanded] = useState(false); const [contactName, setContactName] = useState(""); const [phone, setPhone] = useState(""); @@ -36,7 +34,7 @@ export function VendorDialogAddTechnician({ setError(null); }; - const handleCreate = () => { + const handleAdd = () => { const payload = buildTechnicianCreatePayload({ companyVendor, contactName, @@ -55,17 +53,10 @@ export function VendorDialogAddTechnician({ } setError(null); - createVendor.mutate(parsed.data, { - onSuccess: (created) => { - const dropdownItem = vendorToDropdownItem(created); - onCreated(dropdownItem, created.phone || phone.trim()); - resetForm(); - setExpanded(false); - }, - onError: (mutationError: Error) => { - setError(mutationError.message || "Failed to create technician"); - }, - }); + const draftItem = createDraftVendorDropdownItem(parsed.data); + onCreated(draftItem, parsed.data.phone || phone.trim(), parsed.data); + resetForm(); + setExpanded(false); }; if (!expanded) { @@ -123,7 +114,6 @@ export function VendorDialogAddTechnician({ diff --git a/src/app/(protected)/workorders/_components/list/table/cells/vendor-dialog.tsx b/src/app/(protected)/workorders/_components/list/table/cells/vendor-dialog.tsx index 665a16e6..57c65d6d 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/vendor-dialog.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/vendor-dialog.tsx @@ -10,6 +10,10 @@ import { Button } from "@/components/ui/button"; import { Text } from "@/components/ui/text"; import { dialogBlurBackdropSlotProps } from "@/components/ui/dialog-blur-backdrop"; import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order"; +import type { VendorFormValues } from "@/domain/vendors/schemas/vendor-schema"; +import { isDraftVendorId } from "@/domain/vendors/utils/draft-vendor"; +import { vendorToDropdownItem } from "@/domain/vendors/utils/vendor-to-dropdown-item"; +import { useCreateVendor } from "@/domain/vendors/use-cases/use-create-vendor"; import { FieldLabel } from "@/app/(protected)/workorders/_components/list/table/cells/dialog-field-label"; import { buildTechnicianOptions, @@ -48,10 +52,13 @@ export function VendorDialog({ vendors, onSave, }: VendorDialogProps) { + const createVendor = useCreateVendor(); const [t, setT] = useState(tech); const [phone, setPhone] = useState(techPhone); const [notes, setNotes] = useState(""); const [addedVendors, setAddedVendors] = useState([]); + const [pendingCreates, setPendingCreates] = useState>({}); + const [saveError, setSaveError] = useState(null); const allVendors = useMemo( () => mergeVendorDropdownItems(vendors, addedVendors), @@ -69,6 +76,8 @@ export function VendorDialog({ setPhone(techPhone || phoneForOption(tech, technicianOptions)); setNotes(""); setAddedVendors([]); + setPendingCreates({}); + setSaveError(null); } }, [open, tech, techPhone, technicianOptions]); @@ -94,12 +103,50 @@ export function VendorDialog({ setPhone(phoneForOption(name, technicianOptions)); }; - const handleTechnicianCreated = (created: VendorDropdownItem, createdPhone: string) => { + const handleTechnicianCreated = ( + created: VendorDropdownItem, + createdPhone: string, + payload: VendorFormValues, + ) => { setAddedVendors((current) => [...current, created]); + setPendingCreates((current) => ({ ...current, [String(created.id)]: payload })); setT(created.contactName); setPhone(createdPhone); }; + const resolveSaveVendor = () => + allVendors.find( + (vendor) => + vendor.contactName === t && vendor.companyName === (resolvedCompany || anchorCompanyName), + ) ?? + allVendors.find((vendor) => String(vendor.id) === selectedKey) ?? + selectedVendor; + + const handleSave = async () => { + setSaveError(null); + let saveVendor = resolveSaveVendor(); + const pendingPayload = saveVendor ? pendingCreates[String(saveVendor.id)] : undefined; + + if (saveVendor != null && isDraftVendorId(saveVendor.id) && pendingPayload) { + try { + const created = await createVendor.mutateAsync(pendingPayload); + saveVendor = vendorToDropdownItem(created); + } catch (error) { + const message = error instanceof Error ? error.message : "Failed to create technician"; + setSaveError(message); + return; + } + } + + onSave({ + vendorId: saveVendor ? String(saveVendor.id) : "", + company: resolvedCompany || anchorCompanyName, + tech: t, + techPhone: phone, + }); + onOpenChange(false); + }; + return ( + {Boolean(saveError) && ( + + {saveError} + + )} +
diff --git a/src/domain/vendors/utils/draft-vendor.ts b/src/domain/vendors/utils/draft-vendor.ts new file mode 100644 index 00000000..514eef68 --- /dev/null +++ b/src/domain/vendors/utils/draft-vendor.ts @@ -0,0 +1,21 @@ +import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order"; +import type { VendorFormValues } from "@/domain/vendors/schemas/vendor-schema"; + +export const DRAFT_VENDOR_ID_PREFIX = "draft:"; + +export function isDraftVendorId(id: string | number | null | undefined): boolean { + return String(id ?? "").startsWith(DRAFT_VENDOR_ID_PREFIX); +} + +export function createDraftVendorDropdownItem(payload: VendorFormValues): VendorDropdownItem { + const email = payload.email.trim(); + return { + id: `${DRAFT_VENDOR_ID_PREFIX}${crypto.randomUUID()}`, + companyName: payload.companyName, + contactName: payload.contactName, + tradeSpecialties: payload.tradeSpecialties, + address: payload.address, + distanceMiles: null, + email: email || undefined, + }; +} diff --git a/src/test/app/(protected)/workorders/vendor-dialog-add-technician.test.tsx b/src/test/app/(protected)/workorders/vendor-dialog-add-technician.test.tsx index f5134d29..0f8ee6a5 100644 --- a/src/test/app/(protected)/workorders/vendor-dialog-add-technician.test.tsx +++ b/src/test/app/(protected)/workorders/vendor-dialog-add-technician.test.tsx @@ -1,18 +1,10 @@ -import { fireEvent, screen, waitFor } from "@testing-library/react"; +import { fireEvent, screen } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { VendorDialogAddTechnician } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-add-technician"; import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order"; +import { isDraftVendorId } from "@/domain/vendors/utils/draft-vendor"; import { renderWithProviders } from "@/test/test-utils"; -const mutate = vi.fn(); - -vi.mock("@/domain/vendors/use-cases/use-create-vendor", () => ({ - useCreateVendor: () => ({ - mutate, - isPending: false, - }), -})); - const companyVendor: VendorDropdownItem = { id: 10, companyName: "Gateway Plumbing", @@ -24,31 +16,9 @@ const companyVendor: VendorDropdownItem = { }; describe("VendorDialogAddTechnician", () => { - it("creates a technician under the company and reports selection on success", async () => { + it("stages a draft technician locally without calling the create API", () => { const onCreated = vi.fn(); - mutate.mockImplementation((_payload, options) => { - options?.onSuccess?.({ - id: 55, - companyId: "co-1", - companyName: "Gateway Plumbing", - contactName: "Jordan Lee", - email: "dispatch@gateway.test", - phone: "(314) 555-0199", - companyPhone: "", - googleMapsUrl: "", - notes: "", - totalJobs: 0, - address: "1 Main St", - city: "St. Louis", - state: "MO", - zip: "63101", - tradeSpecialties: "Plumbing", - isActive: true, - preferredContact: "Phone", - }); - }); - renderWithProviders( { fireEvent.change(screen.getByPlaceholderText("(000) 000-0000"), { target: { value: "3145550199" }, }); - fireEvent.click(screen.getByRole("button", { name: /^create$/i })); + fireEvent.click(screen.getByRole("button", { name: /^add$/i })); - await waitFor(() => { - expect(mutate).toHaveBeenCalledWith( - expect.objectContaining({ - companyName: "Gateway Plumbing", - contactName: "Jordan Lee", - }), - expect.any(Object), - ); - }); - - expect(onCreated).toHaveBeenCalledWith( + expect(onCreated).toHaveBeenCalledTimes(1); + const [draftVendor, phone, payload] = onCreated.mock.calls[0] ?? []; + expect(isDraftVendorId(draftVendor.id)).toBe(true); + expect(draftVendor.contactName).toBe("Jordan Lee"); + expect(phone).toBeTruthy(); + expect(payload).toEqual( expect.objectContaining({ - id: 55, - contactName: "Jordan Lee", companyName: "Gateway Plumbing", + contactName: "Jordan Lee", }), - "(314) 555-0199", ); }); });