mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 16:19:01 +00:00
feat(work-orders): add technician affordance in wo vendor dialog
This commit is contained in:
parent
a0214cc01f
commit
ed9c380efb
8 changed files with 513 additions and 7 deletions
|
|
@ -0,0 +1,53 @@
|
|||
import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order";
|
||||
|
||||
type ResolveVendorCompanyAnchorInput = {
|
||||
company: string;
|
||||
resolvedCompany: string;
|
||||
selectedVendor: VendorDropdownItem | undefined;
|
||||
vendors: VendorDropdownItem[];
|
||||
};
|
||||
|
||||
export function resolveVendorCompanyAnchor({
|
||||
company,
|
||||
resolvedCompany,
|
||||
selectedVendor,
|
||||
vendors,
|
||||
}: ResolveVendorCompanyAnchorInput): {
|
||||
companyName: string;
|
||||
companyVendor: VendorDropdownItem | null;
|
||||
} {
|
||||
const companyName = resolvedCompany || company;
|
||||
if (!companyName) {
|
||||
return { companyName: "", companyVendor: null };
|
||||
}
|
||||
|
||||
const companyVendor =
|
||||
selectedVendor ??
|
||||
vendors.find((vendor) => vendor.companyName === companyName) ??
|
||||
({
|
||||
id: "",
|
||||
companyName,
|
||||
contactName: "",
|
||||
tradeSpecialties: "",
|
||||
address: "",
|
||||
distanceMiles: null,
|
||||
} satisfies VendorDropdownItem);
|
||||
|
||||
return { companyName, companyVendor };
|
||||
}
|
||||
|
||||
export function mergeVendorDropdownItems(
|
||||
vendors: VendorDropdownItem[],
|
||||
addedVendors: VendorDropdownItem[],
|
||||
): VendorDropdownItem[] {
|
||||
const byId = new Map<string, VendorDropdownItem>();
|
||||
for (const vendor of vendors) {
|
||||
if (vendor.id === "") continue;
|
||||
byId.set(String(vendor.id), vendor);
|
||||
}
|
||||
for (const vendor of addedVendors) {
|
||||
if (vendor.id === "") continue;
|
||||
byId.set(String(vendor.id), vendor);
|
||||
}
|
||||
return Array.from(byId.values());
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
import { useState } from "react";
|
||||
import { Plus } from "lucide-react";
|
||||
import {
|
||||
DLG_INPUT_CLS,
|
||||
DLG_INPUT_STYLE,
|
||||
} from "@/app/(protected)/workorders/_components/wizard/wizard-input-styles";
|
||||
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 { 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 { FieldLabel } from "@/app/(protected)/workorders/_components/list/table/cells/dialog-field-label";
|
||||
|
||||
type VendorDialogAddTechnicianProps = {
|
||||
companyName: string;
|
||||
companyVendor: VendorDropdownItem;
|
||||
onCreated: (vendor: VendorDropdownItem, phone: string) => void;
|
||||
};
|
||||
|
||||
export function VendorDialogAddTechnician({
|
||||
companyName,
|
||||
companyVendor,
|
||||
onCreated,
|
||||
}: VendorDialogAddTechnicianProps) {
|
||||
const createVendor = useCreateVendor();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [contactName, setContactName] = useState("");
|
||||
const [phone, setPhone] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const resetForm = () => {
|
||||
setContactName("");
|
||||
setPhone("");
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
const payload = buildTechnicianCreatePayload({
|
||||
companyVendor,
|
||||
contactName,
|
||||
phone,
|
||||
});
|
||||
const parsed = vendorSchema.safeParse(payload);
|
||||
if (!parsed.success) {
|
||||
const fieldErrors = parsed.error.flatten().fieldErrors;
|
||||
const message =
|
||||
fieldErrors.contactName?.[0] ??
|
||||
fieldErrors.phone?.[0] ??
|
||||
fieldErrors.companyName?.[0] ??
|
||||
"Enter a valid technician name";
|
||||
setError(message);
|
||||
return;
|
||||
}
|
||||
|
||||
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");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (!expanded) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded(true)}
|
||||
className="flex items-center gap-1.5 text-[12px] font-medium transition-opacity hover:opacity-80"
|
||||
style={{ color: "var(--color-primary)" }}
|
||||
>
|
||||
<Plus size={12} className="shrink-0" aria-hidden />+ Add technician
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="space-y-2 rounded-lg p-3"
|
||||
style={{
|
||||
background: "var(--color-bg-muted)",
|
||||
border: "1px solid var(--color-border)",
|
||||
}}
|
||||
>
|
||||
<Text as="p" variant="caption" sx={{ fontSize: 12, color: "var(--muted-foreground)" }}>
|
||||
New technician for {companyName}
|
||||
</Text>
|
||||
<div>
|
||||
<FieldLabel>Technician Name</FieldLabel>
|
||||
<input
|
||||
autoFocus
|
||||
value={contactName}
|
||||
onChange={(e) => setContactName(e.target.value)}
|
||||
placeholder="Full name"
|
||||
className={DLG_INPUT_CLS}
|
||||
style={DLG_INPUT_STYLE}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FieldLabel>Technician Phone</FieldLabel>
|
||||
<input
|
||||
type="tel"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
placeholder="(000) 000-0000"
|
||||
className={DLG_INPUT_CLS}
|
||||
style={DLG_INPUT_STYLE}
|
||||
/>
|
||||
</div>
|
||||
{Boolean(error) && (
|
||||
<Text as="p" variant="caption" sx={{ fontSize: 11.5, color: "var(--color-destructive)" }}>
|
||||
{error}
|
||||
</Text>
|
||||
)}
|
||||
<div className="flex items-center justify-end gap-2 pt-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="min-h-0 px-2 py-1 text-[12px]"
|
||||
disabled={createVendor.isPending}
|
||||
onClick={() => {
|
||||
resetForm();
|
||||
setExpanded(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="min-h-0 px-3 py-1 text-[12px]"
|
||||
disabled={createVendor.isPending || contactName.trim() === ""}
|
||||
onClick={handleCreate}
|
||||
>
|
||||
{createVendor.isPending ? "Creating…" : "Create"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -16,6 +16,11 @@ import {
|
|||
phoneForOption,
|
||||
} from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-options";
|
||||
import { VendorDialogCompanyCard } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-company-card";
|
||||
import { VendorDialogAddTechnician } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-add-technician";
|
||||
import {
|
||||
mergeVendorDropdownItems,
|
||||
resolveVendorCompanyAnchor,
|
||||
} from "@/app/(protected)/workorders/_components/list/table/cells/resolve-vendor-company-anchor";
|
||||
|
||||
export type VendorPatch = {
|
||||
vendorId: string;
|
||||
|
|
@ -46,10 +51,16 @@ export function VendorDialog({
|
|||
const [t, setT] = useState(tech);
|
||||
const [phone, setPhone] = useState(techPhone);
|
||||
const [notes, setNotes] = useState("");
|
||||
const [addedVendors, setAddedVendors] = useState<VendorDropdownItem[]>([]);
|
||||
|
||||
const allVendors = useMemo(
|
||||
() => mergeVendorDropdownItems(vendors, addedVendors),
|
||||
[vendors, addedVendors],
|
||||
);
|
||||
|
||||
const technicianOptions = useMemo(
|
||||
() => buildTechnicianOptions(vendors, tech, company),
|
||||
[vendors, tech, company],
|
||||
() => buildTechnicianOptions(allVendors, tech, company),
|
||||
[allVendors, tech, company],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -57,6 +68,7 @@ export function VendorDialog({
|
|||
setT(tech);
|
||||
setPhone(techPhone || phoneForOption(tech, technicianOptions));
|
||||
setNotes("");
|
||||
setAddedVendors([]);
|
||||
}
|
||||
}, [open, tech, techPhone, technicianOptions]);
|
||||
|
||||
|
|
@ -64,8 +76,16 @@ export function VendorDialog({
|
|||
technicianOptions.find((o) => o.name === t && (!company || o.company === company)) ??
|
||||
technicianOptions.find((o) => o.name === t || o.key === t);
|
||||
const selectedKey = selected?.key ?? "";
|
||||
const resolvedCompany = t ? (selected?.company ?? company) : "";
|
||||
const selectedVendor = selected?.vendor ?? vendors.find((v) => v.companyName === resolvedCompany);
|
||||
const resolvedCompany = t ? (selected?.company ?? company) : company;
|
||||
const selectedVendor =
|
||||
selected?.vendor ?? allVendors.find((v) => v.companyName === resolvedCompany);
|
||||
|
||||
const { companyName: anchorCompanyName, companyVendor } = resolveVendorCompanyAnchor({
|
||||
company,
|
||||
resolvedCompany,
|
||||
selectedVendor,
|
||||
vendors: allVendors,
|
||||
});
|
||||
|
||||
const handlePick = (key: string) => {
|
||||
const option = technicianOptions.find((o) => o.key === key);
|
||||
|
|
@ -74,6 +94,12 @@ export function VendorDialog({
|
|||
setPhone(phoneForOption(name, technicianOptions));
|
||||
};
|
||||
|
||||
const handleTechnicianCreated = (created: VendorDropdownItem, createdPhone: string) => {
|
||||
setAddedVendors((current) => [...current, created]);
|
||||
setT(created.contactName);
|
||||
setPhone(createdPhone);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
|
|
@ -116,6 +142,15 @@ export function VendorDialog({
|
|||
}))}
|
||||
onPick={handlePick}
|
||||
/>
|
||||
{companyVendor != null && (
|
||||
<div className="mt-2">
|
||||
<VendorDialogAddTechnician
|
||||
companyName={anchorCompanyName}
|
||||
companyVendor={companyVendor}
|
||||
onCreated={handleTechnicianCreated}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{Boolean(t) && (
|
||||
|
|
@ -131,7 +166,7 @@ export function VendorDialog({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{Boolean(t) && (
|
||||
{Boolean(resolvedCompany) && (
|
||||
<div>
|
||||
<FieldLabel>Company</FieldLabel>
|
||||
<VendorDialogCompanyCard company={resolvedCompany} vendor={selectedVendor} />
|
||||
|
|
@ -162,9 +197,17 @@ export function VendorDialog({
|
|||
<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: selectedVendor ? String(selectedVendor.id) : "",
|
||||
company: resolvedCompany,
|
||||
vendorId: saveVendor ? String(saveVendor.id) : "",
|
||||
company: resolvedCompany || anchorCompanyName,
|
||||
tech: t,
|
||||
techPhone: phone,
|
||||
});
|
||||
|
|
|
|||
36
src/domain/vendors/utils/build-technician-create-payload.ts
vendored
Normal file
36
src/domain/vendors/utils/build-technician-create-payload.ts
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order";
|
||||
import type { VendorFormValues } from "@/domain/vendors/schemas/vendor-schema";
|
||||
|
||||
type BuildTechnicianCreatePayloadInput = {
|
||||
companyVendor: Pick<VendorDropdownItem, "companyName" | "email" | "address" | "tradeSpecialties">;
|
||||
contactName: string;
|
||||
phone?: string;
|
||||
companyId?: string | number | null;
|
||||
};
|
||||
|
||||
/** Builds a vendor create payload for a new technician under an existing company. */
|
||||
export function buildTechnicianCreatePayload({
|
||||
companyVendor,
|
||||
contactName,
|
||||
phone = "",
|
||||
companyId = null,
|
||||
}: BuildTechnicianCreatePayloadInput): VendorFormValues {
|
||||
return {
|
||||
companyName: companyVendor.companyName,
|
||||
contactName: contactName.trim(),
|
||||
email: companyVendor.email ?? "",
|
||||
phone: phone.trim(),
|
||||
companyPhone: "",
|
||||
googleMapsUrl: "",
|
||||
notes: "",
|
||||
totalJobs: 0,
|
||||
address: companyVendor.address ?? "",
|
||||
city: "",
|
||||
state: "",
|
||||
zip: "",
|
||||
tradeSpecialties: companyVendor.tradeSpecialties ?? "",
|
||||
isActive: true,
|
||||
companyId,
|
||||
preferredContact: "Phone",
|
||||
};
|
||||
}
|
||||
15
src/domain/vendors/utils/vendor-to-dropdown-item.ts
vendored
Normal file
15
src/domain/vendors/utils/vendor-to-dropdown-item.ts
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order";
|
||||
import type { Vendor } from "@/domain/vendors/types/vendor";
|
||||
|
||||
export function vendorToDropdownItem(vendor: Vendor): VendorDropdownItem {
|
||||
const email = vendor.email.trim();
|
||||
return {
|
||||
id: vendor.id ?? "",
|
||||
companyName: vendor.companyName,
|
||||
contactName: vendor.contactName,
|
||||
tradeSpecialties: vendor.tradeSpecialties,
|
||||
address: vendor.address,
|
||||
distanceMiles: null,
|
||||
email: email || undefined,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import { fireEvent, screen, waitFor } 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 { 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",
|
||||
contactName: "Adam Whyte",
|
||||
tradeSpecialties: "Plumbing",
|
||||
address: "1 Main St",
|
||||
distanceMiles: 4.2,
|
||||
email: "dispatch@gateway.test",
|
||||
};
|
||||
|
||||
describe("VendorDialogAddTechnician", () => {
|
||||
it("creates a technician under the company and reports selection on success", async () => {
|
||||
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"
|
||||
companyVendor={companyVendor}
|
||||
onCreated={onCreated}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /\+ add technician/i }));
|
||||
fireEvent.change(screen.getByPlaceholderText("Full name"), {
|
||||
target: { value: "Jordan Lee" },
|
||||
});
|
||||
fireEvent.change(screen.getByPlaceholderText("(000) 000-0000"), {
|
||||
target: { value: "3145550199" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: /^create$/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
companyName: "Gateway Plumbing",
|
||||
contactName: "Jordan Lee",
|
||||
}),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
expect(onCreated).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 55,
|
||||
contactName: "Jordan Lee",
|
||||
companyName: "Gateway Plumbing",
|
||||
}),
|
||||
"(314) 555-0199",
|
||||
);
|
||||
});
|
||||
});
|
||||
106
src/test/domain/vendors/utils/build-technician-create-payload.test.ts
vendored
Normal file
106
src/test/domain/vendors/utils/build-technician-create-payload.test.ts
vendored
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
mergeVendorDropdownItems,
|
||||
resolveVendorCompanyAnchor,
|
||||
} from "@/app/(protected)/workorders/_components/list/table/cells/resolve-vendor-company-anchor";
|
||||
import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order";
|
||||
import { buildTechnicianCreatePayload } from "@/domain/vendors/utils/build-technician-create-payload";
|
||||
import { vendorToDropdownItem } from "@/domain/vendors/utils/vendor-to-dropdown-item";
|
||||
import type { Vendor } from "@/domain/vendors/types/vendor";
|
||||
|
||||
describe("buildTechnicianCreatePayload", () => {
|
||||
it("copies company fields and sets the new technician identity", () => {
|
||||
const payload = buildTechnicianCreatePayload({
|
||||
companyVendor: {
|
||||
companyName: "Gateway Plumbing",
|
||||
email: "dispatch@gateway.test",
|
||||
address: "1 Main St",
|
||||
tradeSpecialties: "Plumbing",
|
||||
},
|
||||
contactName: "Jordan Lee",
|
||||
phone: "(314) 555-0100",
|
||||
companyId: 42,
|
||||
});
|
||||
|
||||
expect(payload).toMatchObject({
|
||||
companyName: "Gateway Plumbing",
|
||||
contactName: "Jordan Lee",
|
||||
phone: "(314) 555-0100",
|
||||
email: "dispatch@gateway.test",
|
||||
address: "1 Main St",
|
||||
tradeSpecialties: "Plumbing",
|
||||
companyId: 42,
|
||||
isActive: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveVendorCompanyAnchor", () => {
|
||||
const vendors: VendorDropdownItem[] = [
|
||||
{
|
||||
id: 10,
|
||||
companyName: "Gateway Plumbing",
|
||||
contactName: "Adam Whyte",
|
||||
tradeSpecialties: "Plumbing",
|
||||
address: "1 Main St",
|
||||
distanceMiles: null,
|
||||
},
|
||||
];
|
||||
|
||||
it("returns null anchor when no company context exists", () => {
|
||||
expect(
|
||||
resolveVendorCompanyAnchor({
|
||||
company: "",
|
||||
resolvedCompany: "",
|
||||
selectedVendor: undefined,
|
||||
vendors,
|
||||
}),
|
||||
).toEqual({ companyName: "", companyVendor: null });
|
||||
});
|
||||
|
||||
it("merges newly created vendors without dropping existing options", () => {
|
||||
const added: VendorDropdownItem = {
|
||||
id: 55,
|
||||
companyName: "Gateway Plumbing",
|
||||
contactName: "Jordan Lee",
|
||||
tradeSpecialties: "Plumbing",
|
||||
address: "1 Main St",
|
||||
distanceMiles: null,
|
||||
};
|
||||
expect(mergeVendorDropdownItems(vendors, [added])).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("vendorToDropdownItem", () => {
|
||||
it("maps a created vendor into the work-order dropdown shape", () => {
|
||||
const vendor: Vendor = {
|
||||
id: 99,
|
||||
companyId: "co-1",
|
||||
companyName: "Gateway Plumbing",
|
||||
contactName: "Jordan Lee",
|
||||
email: "jordan@gateway.test",
|
||||
phone: "(314) 555-0100",
|
||||
companyPhone: "",
|
||||
googleMapsUrl: "",
|
||||
notes: "",
|
||||
totalJobs: 0,
|
||||
address: "1 Main St",
|
||||
city: "St. Louis",
|
||||
state: "MO",
|
||||
zip: "63101",
|
||||
tradeSpecialties: "Plumbing",
|
||||
isActive: true,
|
||||
preferredContact: "Phone",
|
||||
};
|
||||
|
||||
expect(vendorToDropdownItem(vendor)).toEqual({
|
||||
id: 99,
|
||||
companyName: "Gateway Plumbing",
|
||||
contactName: "Jordan Lee",
|
||||
tradeSpecialties: "Plumbing",
|
||||
address: "1 Main St",
|
||||
distanceMiles: null,
|
||||
email: "jordan@gateway.test",
|
||||
});
|
||||
});
|
||||
});
|
||||
21
tmp/pr-descriptions/pr7-add-technician.md
Normal file
21
tmp/pr-descriptions/pr7-add-technician.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
## Summary
|
||||
|
||||
- Adds **+ Add technician** to the WO board vendor dialog when a company context is known (from the row or the current selection).
|
||||
- Creates the technician via the existing **`POST vendors`** REST endpoint (`vendorsApi.create` / `useCreateVendor`), reusing the selected company's profile fields and registering a new `contactName` under that company.
|
||||
- After a successful create, the new technician is merged into the dialog options and auto-selected for the single-tech WO assignment.
|
||||
|
||||
## Backend
|
||||
|
||||
Uses the established vendor create API (`API_PATHS.rest.vendors` → `POST /vendors`). Each vendor record represents one technician; linking to an existing company is done by sending the same `companyName` (and optional `companyId` when available from facets elsewhere). **No new endpoint** — confirm with BE that duplicate `companyName` + distinct `contactName` rows are the intended model for multiple technicians per company.
|
||||
|
||||
## Test plan
|
||||
|
||||
- [ ] Open a WO with an assigned company on the board → vendor cell → **+ Add technician** is visible.
|
||||
- [ ] Create a technician with name (+ optional phone) → new tech appears selected in the dialog.
|
||||
- [ ] Save → WO row shows the new technician and vendor id.
|
||||
- [ ] Open dialog on a row with no company → add affordance hidden until a company/tech context exists.
|
||||
- [ ] `npm run verify`
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Multi-POC, multi-tech arrays on WO, vendors directory redesign.
|
||||
Loading…
Add table
Reference in a new issue