mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 16:19:01 +00:00
fix(work-orders): defer technician create until vendor dialog save
This commit is contained in:
parent
ed9c380efb
commit
bfee557070
4 changed files with 101 additions and 88 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
21
src/domain/vendors/utils/draft-vendor.ts
vendored
Normal file
21
src/domain/vendors/utils/draft-vendor.ts
vendored
Normal 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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue