fix(work-orders): defer technician create until vendor dialog save

This commit is contained in:
arthur.bassi 2026-08-03 13:53:59 -03:00
parent ed9c380efb
commit bfee557070
4 changed files with 101 additions and 88 deletions

View file

@ -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({
<Button
variant="ghost"
className="min-h-0 px-2 py-1 text-[12px]"
disabled={createVendor.isPending}
onClick={() => {
resetForm();
setExpanded(false);
@ -133,10 +123,10 @@ export function VendorDialogAddTechnician({
</Button>
<Button
className="min-h-0 px-3 py-1 text-[12px]"
disabled={createVendor.isPending || contactName.trim() === ""}
onClick={handleCreate}
disabled={contactName.trim() === ""}
onClick={handleAdd}
>
{createVendor.isPending ? "Creating…" : "Create"}
Add
</Button>
</div>
</div>

View file

@ -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<VendorDropdownItem[]>([]);
const [pendingCreates, setPendingCreates] = useState<Record<string, VendorFormValues>>({});
const [saveError, setSaveError] = useState<string | null>(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 (
<Dialog
open={open}
@ -186,35 +233,27 @@ export function VendorDialog({
</div>
</div>
{Boolean(saveError) && (
<Text as="p" variant="caption" sx={{ fontSize: 11.5, color: "var(--color-destructive)" }}>
{saveError}
</Text>
)}
<div className="mt-4 flex items-center justify-end gap-2">
<Button
variant="ghost"
className="min-h-0 px-2 py-1 text-[12px]"
disabled={createVendor.isPending}
onClick={() => onOpenChange(false)}
>
Cancel
</Button>
<Button
className="min-h-0 px-3 py-1 text-[12px]"
onClick={() => {
const saveVendor =
allVendors.find(
(vendor) =>
vendor.contactName === t &&
vendor.companyName === (resolvedCompany || anchorCompanyName),
) ??
allVendors.find((vendor) => String(vendor.id) === selectedKey) ??
selectedVendor;
onSave({
vendorId: saveVendor ? String(saveVendor.id) : "",
company: resolvedCompany || anchorCompanyName,
tech: t,
techPhone: phone,
});
onOpenChange(false);
}}
disabled={createVendor.isPending}
onClick={() => void handleSave()}
>
Save
{createVendor.isPending ? "Saving…" : "Save"}
</Button>
</div>
</DialogContent>

View file

@ -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,
};
}

View file

@ -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(
<VendorDialogAddTechnician
companyName="Gateway Plumbing"
@ -64,25 +34,18 @@ describe("VendorDialogAddTechnician", () => {
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",
);
});
});