diff --git a/src/app/(protected)/workorders/_components/list/table/build-inline-draft-create-input.ts b/src/app/(protected)/workorders/_components/list/table/build-inline-draft-create-input.ts index 5f911bd8..169f062e 100644 --- a/src/app/(protected)/workorders/_components/list/table/build-inline-draft-create-input.ts +++ b/src/app/(protected)/workorders/_components/list/table/build-inline-draft-create-input.ts @@ -19,6 +19,7 @@ export function buildInlineDraftCreateInput( locationId: String(draft.locationId || ""), locationName, siteCode: location ? resolveSiteCode(location) : resolveSiteCode({ name: locationName }), + customer: draft.customer.trim(), woNumber: draft.woNumber, type: draft.type || "PM", severity: draft.severity, diff --git a/src/app/(protected)/workorders/_components/list/table/cells/build-site-dialog-patch.ts b/src/app/(protected)/workorders/_components/list/table/cells/build-site-dialog-patch.ts index 3fe930cd..1b9e23c3 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/build-site-dialog-patch.ts +++ b/src/app/(protected)/workorders/_components/list/table/cells/build-site-dialog-patch.ts @@ -8,6 +8,7 @@ type BuildSiteDialogPatchArgs = { code: string; locId: string; selected: LocationOption | undefined; + customer?: string; pocName: string; pocPhone: string; pocNotes: string; @@ -20,6 +21,7 @@ export function buildSiteDialogPatch({ code, locId, selected, + customer, pocName, pocPhone, pocNotes, @@ -31,6 +33,7 @@ export function buildSiteDialogPatch({ site: code || (selected ? resolveSiteCode(selected) : ""), location: selected ? extractLocationLabel(selected) : "", locationId: locId, + ...(customer !== undefined ? { customer } : {}), pocName, pocPhone, pocNotes, diff --git a/src/app/(protected)/workorders/_components/list/table/cells/site-cell.tsx b/src/app/(protected)/workorders/_components/list/table/cells/site-cell.tsx index 4c40ae06..04077833 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/site-cell.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/site-cell.tsx @@ -11,6 +11,7 @@ type SiteCellProps = { value: string; location: string; locationId: string | number; + customer?: string; city?: string; state?: string; pocName?: string; @@ -29,6 +30,7 @@ export function SiteCell({ value, location, locationId, + customer, city, state, pocName, @@ -95,6 +97,7 @@ export function SiteCell({ onOpenChange={setOpen} value={value} locationId={locationId} + customer={customer} pocName={pocName} pocPhone={pocPhone} pocNotes={pocNotes} diff --git a/src/app/(protected)/workorders/_components/list/table/cells/site-dialog-types.ts b/src/app/(protected)/workorders/_components/list/table/cells/site-dialog-types.ts index 499af6aa..956d54ba 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/site-dialog-types.ts +++ b/src/app/(protected)/workorders/_components/list/table/cells/site-dialog-types.ts @@ -4,6 +4,7 @@ export type SitePatch = { site: string; location: string; locationId: string | number; + customer?: string; pocName: string; pocPhone: string; pocNotes: string; diff --git a/src/app/(protected)/workorders/_components/list/table/cells/site-dialog.tsx b/src/app/(protected)/workorders/_components/list/table/cells/site-dialog.tsx index 0843e331..ecf6e3ba 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/site-dialog.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/site-dialog.tsx @@ -9,6 +9,7 @@ import type { LocationOption } from "@/domain/work-orders/types/work-order"; import { FieldLabel } from "@/app/(protected)/workorders/_components/list/table/cells/dialog-field-label"; import { SiteDialogDetailPanel } from "@/app/(protected)/workorders/_components/list/table/cells/site-dialog-detail-panel"; import { SiteDialogFooter } from "@/app/(protected)/workorders/_components/list/table/cells/site-dialog-footer"; +import { WizardStepLocationAccountSelect } from "@/app/(protected)/workorders/_components/wizard/wizard-step-location-account-select"; import { SiteDialogPocSection } from "@/app/(protected)/workorders/_components/list/table/cells/site-dialog-poc-section"; import type { SitePatch } from "@/app/(protected)/workorders/_components/list/table/cells/site-dialog-types"; import { useSiteDialogState } from "@/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-state"; @@ -20,6 +21,7 @@ type SiteDialogProps = { onOpenChange: (open: boolean) => void; value: string; locationId: string | number; + customer?: string; pocName?: string; pocPhone?: string; pocNotes?: string; @@ -35,6 +37,7 @@ export function SiteDialog({ onOpenChange, value, locationId, + customer = "", pocName = "", pocPhone = "", pocNotes = "", @@ -49,6 +52,7 @@ export function SiteDialog({ onOpenChange, value, locationId, + customer, pocName, pocPhone, pocNotes, @@ -81,6 +85,13 @@ export function SiteDialog({
+ {createMode && ( + + )}
Site void; value: string; locationId: string | number; + customer?: string; pocName: string; pocPhone: string; pocNotes: string; @@ -29,6 +30,7 @@ export function useSiteDialogState({ onOpenChange, value, locationId, + customer = "", pocName, pocPhone, pocNotes, @@ -38,6 +40,7 @@ export function useSiteDialogState({ onSave, }: UseSiteDialogStateArgs) { const [code, setCode] = useState(value); + const [accountName, setAccountName] = useState(customer); const [locId, setLocId] = useState(() => resolveLocationId(locationId, value, sites)); const [pn, setPn] = useState(pocName); const [pp, setPp] = useState(pocPhone); @@ -60,8 +63,9 @@ export function useSiteDialogState({ const selected = sites.find((s) => String(s.id) === locId); const addressPreview = locationDetail ? formatLocationAddressPreview(locationDetail) : ""; const siteMissing = !locId; + const accountMissing = createMode && !accountName.trim(); const pocMissing = !pn.trim() || !pp.trim(); - const canConfirm = !siteMissing && (!createMode || !pocMissing); + const canConfirm = !siteMissing && (!createMode || (!pocMissing && !accountMissing)); useEffect(() => { if (!open) { @@ -77,6 +81,7 @@ export function useSiteDialogState({ } const resolved = resolveLocationId(locationId, value, sites); setCode(value); + setAccountName(customer); setLocId(resolved); setPn(pocName); setPp(pocPhone); @@ -84,7 +89,18 @@ export function useSiteDialogState({ setExtraContacts(additionalContacts); setBaselineHadContacts(additionalContacts.length > 0); setPocFilledFor(pocName.trim() || pocPhone.trim() ? resolved : ""); - }, [open, dirty, value, locationId, pocName, pocPhone, pocNotes, additionalContacts, sites]); + }, [ + open, + dirty, + value, + locationId, + customer, + pocName, + pocPhone, + pocNotes, + additionalContacts, + sites, + ]); useEffect(() => { if (!open || !locId || !locationDetail || pocFilledFor === locId) { @@ -121,6 +137,7 @@ export function useSiteDialogState({ code, locId, selected, + ...(createMode ? { customer: accountName } : {}), pocName: pn, pocPhone: pp, pocNotes: notes, @@ -152,6 +169,8 @@ export function useSiteDialogState({ return { locId, code, + accountName, + accountMissing, selected, locationDetail, locationDetailLoading, @@ -164,6 +183,10 @@ export function useSiteDialogState({ showErrors, siteMissing, handlePick, + setAccountName: (next: string) => { + setDirty(true); + setAccountName(next); + }, attemptSave, setPn: (next: string) => markPocDirty(() => setPn(next)), setPp: (next: string) => markPocDirty(() => setPp(next)), diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-inline-identity-cells.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-inline-identity-cells.tsx index 47ca7b46..f650a040 100644 --- a/src/app/(protected)/workorders/_components/list/table/wo-table-inline-identity-cells.tsx +++ b/src/app/(protected)/workorders/_components/list/table/wo-table-inline-identity-cells.tsx @@ -50,6 +50,7 @@ export function WoTableInlineIdentityCells({ value={draft.site} location={draft.location} locationId={draft.locationId} + customer={draft.customer} city={sitePlace?.city} state={sitePlace?.state} pocName={draft.pocName} @@ -63,6 +64,7 @@ export function WoTableInlineIdentityCells({ site: p.site, location: p.location, locationId: p.locationId, + ...(p.customer !== undefined ? { customer: p.customer } : {}), pocName: p.pocName, pocPhone: p.pocPhone, pocNotes: p.pocNotes, diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row.tsx index 0e29c529..94431d21 100644 --- a/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row.tsx +++ b/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row.tsx @@ -78,7 +78,9 @@ export function WoTableInlineRow({ const siteCellRef = useRef(null); const woCellRef = useRef(null); - const siteCellMissing = missing.some((f) => f === "site" || f === "pocName" || f === "pocPhone"); + const siteCellMissing = missing.some( + (f) => f === "customer" || f === "site" || f === "pocName" || f === "pocPhone", + ); const siteShowError = showErrors && siteCellMissing; const woShowError = showErrors && missing.includes("woNumber"); diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-sidebar-summary-rows.ts b/src/app/(protected)/workorders/_components/wizard/wizard-sidebar-summary-rows.ts index e49e267a..3cd23c7d 100644 --- a/src/app/(protected)/workorders/_components/wizard/wizard-sidebar-summary-rows.ts +++ b/src/app/(protected)/workorders/_components/wizard/wizard-sidebar-summary-rows.ts @@ -46,6 +46,7 @@ export function buildWizardSidebarSummaryRows(draft: WorkOrderWizardDraft): [str ["Schedule on", scheduleOnLabel(draft)], ["Due date", fmtDate(draft.dueDate) || "—"], ["Assigned to", draft.assigneeName || "—"], + ["Account", draft.customer.trim() || "—"], ["Site", draft.siteCode || draft.locationName || "—"], ["POC", pocLabel(draft)], ["Additional POCs", formatAdditionalPocSummary(draft)], diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-step-location-account-select.tsx b/src/app/(protected)/workorders/_components/wizard/wizard-step-location-account-select.tsx new file mode 100644 index 00000000..5ab5a3e1 --- /dev/null +++ b/src/app/(protected)/workorders/_components/wizard/wizard-step-location-account-select.tsx @@ -0,0 +1,49 @@ +import { Building2, Info } from "lucide-react"; +import { Text } from "@/components/ui/text"; +import { WizardFieldSelect } from "@/app/(protected)/workorders/_components/wizard/wizard-field-select"; +import { WizLabel } from "@/app/(protected)/workorders/_components/wizard/wizard-labels"; +import { useAccountsList } from "@/domain/accounts/use-cases/use-accounts-list"; + +type WizardStepLocationAccountSelectProps = { + customer: string; + onCustomerChange: (customer: string) => void; + showError?: boolean; +}; + +export function WizardStepLocationAccountSelect({ + customer, + onCustomerChange, + showError = false, +}: WizardStepLocationAccountSelectProps) { + const { data } = useAccountsList({ page: 1, pageSize: 100, search: "" }); + const accounts = data?.items ?? []; + const options = accounts + .map((account) => account.name.trim()) + .filter(Boolean) + .map((name) => ({ key: name, label: name })); + + return ( +
+ Account + + } + searchPlaceholder="Search account name…" + options={options} + onPick={onCustomerChange} + /> + + Account is required to create a work order. + +
+ ); +} diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-step-location-service.tsx b/src/app/(protected)/workorders/_components/wizard/wizard-step-location-service.tsx index 950ca5c8..17425052 100644 --- a/src/app/(protected)/workorders/_components/wizard/wizard-step-location-service.tsx +++ b/src/app/(protected)/workorders/_components/wizard/wizard-step-location-service.tsx @@ -2,6 +2,7 @@ import { Text } from "@/components/ui/text"; import type { LocationOption } from "@/domain/work-orders/types/work-order"; import type { Location } from "@/domain/locations/types/location"; import type { WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order-wizard"; +import { WizardStepLocationAccountSelect } from "@/app/(protected)/workorders/_components/wizard/wizard-step-location-account-select"; import { WizardStepLocationPocFields } from "@/app/(protected)/workorders/_components/wizard/wizard-step-location-poc-fields"; import { WizardStepLocationServiceSelect } from "@/app/(protected)/workorders/_components/wizard/wizard-step-location-service-select"; import { WizardStepLocationSiteDetails } from "@/app/(protected)/workorders/_components/wizard/wizard-step-location-site-details"; @@ -44,6 +45,11 @@ export function WizardStepLocationService({ return (
+ onPatch({ customer })} + /> = { + customer: "Account", site: "Site", woNumber: "WO #", pocName: "POC name", @@ -9,6 +10,7 @@ export const INLINE_CREATE_REQ_LABELS: Record = { }; export type InlineCreateDraftFields = { + customer?: string; site?: string; locationId?: string | number; woNumber?: string; @@ -18,6 +20,9 @@ export type InlineCreateDraftFields = { export function getInlineCreateMissing(d: InlineCreateDraftFields): InlineCreateReqField[] { const miss: InlineCreateReqField[] = []; + if (!d.customer?.trim()) { + miss.push("customer"); + } const hasSite = Boolean(d.site?.trim()) || Boolean(String(d.locationId ?? "").trim()); if (!hasSite) { miss.push("site"); @@ -43,6 +48,9 @@ export function inlineCreateSaveHint(miss: InlineCreateReqField[]): string { } export function inlineCreateSiteError(miss: InlineCreateReqField[]): string { + if (miss.includes("customer")) { + return "Account is required"; + } if (miss.includes("site")) { return "Site is required"; } diff --git a/src/test/app/(protected)/workorders/build-inline-draft-create-input.test.ts b/src/test/app/(protected)/workorders/build-inline-draft-create-input.test.ts index ccccd7d3..16b30ee8 100644 --- a/src/test/app/(protected)/workorders/build-inline-draft-create-input.test.ts +++ b/src/test/app/(protected)/workorders/build-inline-draft-create-input.test.ts @@ -8,6 +8,7 @@ describe("buildInlineDraftCreateInput", () => { const draft = { ...EMPTY_TABLE_DRAFT, site: "BK5", + customer: "Amazon", locationId: "9", woNumber: "25001", pocName: "Jane", @@ -30,6 +31,8 @@ describe("buildInlineDraftCreateInput", () => { expect(payload.scheduleWeekOnly).toBeUndefined(); expect(payload.woNumber).toBe("25001"); expect(payload.pocName).toBe("Jane"); + expect(payload.customer).toBe("Amazon"); + expect(input.customer).toBe("Amazon"); }); it("keeps week-only schedule when targetWeek is set", () => { diff --git a/src/test/app/(protected)/workorders/wizard-step-location-account-select.test.tsx b/src/test/app/(protected)/workorders/wizard-step-location-account-select.test.tsx new file mode 100644 index 00000000..d0bc6a2c --- /dev/null +++ b/src/test/app/(protected)/workorders/wizard-step-location-account-select.test.tsx @@ -0,0 +1,48 @@ +import { fireEvent, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { WizardStepLocationAccountSelect } from "@/app/(protected)/workorders/_components/wizard/wizard-step-location-account-select"; +import { renderWithProviders } from "@/test/test-utils"; + +vi.mock("@/domain/accounts/use-cases/use-accounts-list", () => ({ + useAccountsList: () => ({ + data: { + items: [ + { + id: 1, + name: "Amazon", + number: "", + industry: "", + website: "", + primaryContact: "", + phoneNumber: "", + owner: "", + }, + { + id: 2, + name: "Sea Haven", + number: "", + industry: "", + website: "", + primaryContact: "", + phoneNumber: "", + owner: "", + }, + ], + }, + }), +})); + +describe("WizardStepLocationAccountSelect", () => { + it("sets draft.customer to the exact account name on pick", () => { + const onCustomerChange = vi.fn(); + renderWithProviders( + , + { withAuth: false }, + ); + + fireEvent.click(screen.getByRole("button", { name: /select account/i })); + fireEvent.click(screen.getByRole("button", { name: "Amazon" })); + + expect(onCustomerChange).toHaveBeenCalledWith("Amazon"); + }); +}); diff --git a/src/test/domain/work-orders/api/work-orders-api.test.ts b/src/test/domain/work-orders/api/work-orders-api.test.ts index 388ad617..fabf80af 100644 --- a/src/test/domain/work-orders/api/work-orders-api.test.ts +++ b/src/test/domain/work-orders/api/work-orders-api.test.ts @@ -114,6 +114,7 @@ describe("workOrdersApi.createFromBoard", () => { type: "PM", siteCode: "BK5", woNumber: "12345", + customer: "Amazon", }); expect(apiPost).toHaveBeenCalledWith(API_PATHS.workOrder.boardCreate, { @@ -122,6 +123,7 @@ describe("workOrdersApi.createFromBoard", () => { lifecycleStatus: 1, woNumber: "12345", isAddOn: false, + customer: "Amazon", }); }); }); diff --git a/src/test/domain/work-orders/mappers/wizard-draft-mapper.test.ts b/src/test/domain/work-orders/mappers/wizard-draft-mapper.test.ts index 3c128e93..fe70bd74 100644 --- a/src/test/domain/work-orders/mappers/wizard-draft-mapper.test.ts +++ b/src/test/domain/work-orders/mappers/wizard-draft-mapper.test.ts @@ -123,7 +123,7 @@ describe("wizard step validation", () => { expect(isWizardStepValid(2, draft)).toBe(false); }); - it("allows step 2 when site and POC are set", () => { + it("blocks step 2 without customer", () => { const draft = { ...EMPTY_WIZARD_DRAFT, type: "PM" as const, @@ -133,6 +133,21 @@ describe("wizard step validation", () => { pocName: "Jordan", pocPhone: "555-0100", }; + expect(canContinueWizardStep2(draft)).toBe(false); + expect(isWizardStepValid(2, draft)).toBe(false); + }); + + it("allows step 2 when site, account, and POC are set", () => { + const draft = { + ...EMPTY_WIZARD_DRAFT, + type: "PM" as const, + woNumber: "12345", + locationId: "99", + siteCode: "BK5", + customer: "Amazon", + pocName: "Jordan", + pocPhone: "555-0100", + }; expect(canContinueWizardStep2(draft)).toBe(true); expect(isWizardStepValid(2, draft)).toBe(true); }); @@ -148,13 +163,14 @@ describe("wizard step validation", () => { expect(isWizardStepValid(2, draft)).toBe(false); }); - it("allows create when type, woNumber, site, and POC are set", () => { + it("allows create when type, woNumber, site, account, and POC are set", () => { const draft = { ...EMPTY_WIZARD_DRAFT, type: "PM" as const, woNumber: "12345678901234567890", locationId: "99", siteCode: "BK5", + customer: "Amazon", pocName: "Jordan", pocPhone: "555-0100", }; diff --git a/src/test/domain/work-orders/mappers/work-order-board-mapper.test.ts b/src/test/domain/work-orders/mappers/work-order-board-mapper.test.ts index b9b1511d..ee3d5c27 100644 --- a/src/test/domain/work-orders/mappers/work-order-board-mapper.test.ts +++ b/src/test/domain/work-orders/mappers/work-order-board-mapper.test.ts @@ -60,6 +60,28 @@ describe("mapWizardDraftToBoardCreatePayload", () => { }); }); + it("includes customer when the draft has an account name", () => { + const payload = mapWizardDraftToBoardCreatePayload({ + ...EMPTY_WIZARD_DRAFT, + type: "PM", + siteCode: "BK5", + customer: " Amazon ", + }); + + expect(payload.customer).toBe("Amazon"); + }); + + it("omits customer when the draft account is blank", () => { + const payload = mapWizardDraftToBoardCreatePayload({ + ...EMPTY_WIZARD_DRAFT, + type: "PM", + siteCode: "BK5", + customer: " ", + }); + + expect(payload.customer).toBeUndefined(); + }); + it("sets isAddOn from cutoff preview when scheduled (create hint; not frozen)", () => { const payload = mapWizardDraftToBoardCreatePayload({ ...EMPTY_WIZARD_DRAFT, diff --git a/src/test/domain/work-orders/utils/get-inline-create-missing.test.ts b/src/test/domain/work-orders/utils/get-inline-create-missing.test.ts index 3ddd8dca..6d2204df 100644 --- a/src/test/domain/work-orders/utils/get-inline-create-missing.test.ts +++ b/src/test/domain/work-orders/utils/get-inline-create-missing.test.ts @@ -7,9 +7,10 @@ import { } from "@/domain/work-orders/utils/get-inline-create-missing"; describe("getInlineCreateMissing", () => { - it("returns empty when Site, WO#, and POC are filled even without schedule", () => { + it("returns empty when Account, Site, WO#, and POC are filled even without schedule", () => { expect( getInlineCreateMissing({ + customer: "Amazon", site: "BK5", locationId: "12", woNumber: "25001", @@ -22,6 +23,7 @@ describe("getInlineCreateMissing", () => { it("accepts locationId alone as site presence", () => { expect( getInlineCreateMissing({ + customer: "Amazon", site: "", locationId: 42, woNumber: "25001", @@ -34,6 +36,7 @@ describe("getInlineCreateMissing", () => { it("lists missing POC fields without requiring schedule", () => { expect( getInlineCreateMissing({ + customer: "Amazon", site: "BK5", woNumber: "25001", pocName: "", @@ -42,8 +45,14 @@ describe("getInlineCreateMissing", () => { ).toEqual(["pocName", "pocPhone"]); }); - it("lists site and woNumber when empty", () => { - expect(getInlineCreateMissing({})).toEqual(["site", "woNumber", "pocName", "pocPhone"]); + it("lists account, site, and woNumber when empty", () => { + expect(getInlineCreateMissing({})).toEqual([ + "customer", + "site", + "woNumber", + "pocName", + "pocPhone", + ]); }); }); @@ -58,6 +67,10 @@ describe("inlineCreateSaveHint", () => { }); describe("inlineCreateSiteError", () => { + it("prefers account-required message when account is missing", () => { + expect(inlineCreateSiteError(["customer", "site"])).toBe("Account is required"); + }); + it("prefers site-required message when site is missing", () => { expect(inlineCreateSiteError(["site", "pocName"])).toBe("Site is required"); });