From 3a902bfebe18d94af4e3933807fc5cdf1e8c2963 Mon Sep 17 00:00:00 2001 From: Codex Review Integration Date: Wed, 16 Sep 2026 18:26:55 -0300 Subject: [PATCH 1/3] feat(work-orders): use services registry picker --- src/api/api-paths.ts | 3 + .../table/build-inline-draft-create-input.ts | 1 + .../_components/wizard/new-wo-wizard.tsx | 5 +- .../wizard/use-new-wo-wizard-controller.ts | 7 +- .../wizard-step-location-service-select.tsx | 2 + src/domain/services/api/services-api.ts | 30 ++++++++ src/domain/services/mappers/service-mapper.ts | 71 +++++++++++++++++++ src/domain/services/types/service.ts | 11 +++ .../use-services-for-work-order-type.ts | 25 +++++++ .../assign-wizard-service-poc-fields.ts | 4 ++ .../mappers/work-order-table-row-mapper.ts | 1 + .../work-orders/types/work-order-board.ts | 3 + .../work-orders/types/work-order-table-row.ts | 1 + .../work-orders/types/work-order-wizard.ts | 3 + src/infra/query-key/query-key.ts | 6 ++ .../wizard-service-clears-vendor.test.tsx | 1 + .../domain/services/api/services-api.test.ts | 48 +++++++++++++ .../work-orders/api/work-orders-api.test.ts | 19 +++++ 18 files changed, 236 insertions(+), 5 deletions(-) create mode 100644 src/domain/services/api/services-api.ts create mode 100644 src/domain/services/mappers/service-mapper.ts create mode 100644 src/domain/services/types/service.ts create mode 100644 src/domain/services/use-cases/use-services-for-work-order-type.ts create mode 100644 src/test/domain/services/api/services-api.test.ts diff --git a/src/api/api-paths.ts b/src/api/api-paths.ts index 33db519b..0b1eb129 100644 --- a/src/api/api-paths.ts +++ b/src/api/api-paths.ts @@ -155,6 +155,9 @@ export const API_PATHS = { update: "DropdownOptions/Update", delete: "DropdownOptions", }, + services: { + list: "services", + }, user: { list: "User", }, 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 2e6dceb9..0058d354 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 @@ -28,6 +28,7 @@ export function buildInlineDraftCreateInput( scheduledOn: draft.scheduledOn, dueDate: draft.dueDate, pm: draft.pm, + serviceId: null, extraServices: draft.extraServices ?? [], avetaRequired: Boolean(draft.avetaRequired), pmNote: draft.serviceNotes ?? "", diff --git a/src/app/(protected)/workorders/_components/wizard/new-wo-wizard.tsx b/src/app/(protected)/workorders/_components/wizard/new-wo-wizard.tsx index 4d92a8f0..039770ac 100644 --- a/src/app/(protected)/workorders/_components/wizard/new-wo-wizard.tsx +++ b/src/app/(protected)/workorders/_components/wizard/new-wo-wizard.tsx @@ -82,7 +82,10 @@ export function NewWoWizard({ locationDetail={wizard.locationDetail} locationDetailLoading={wizard.locationDetailLoading} locationDetailError={wizard.locationDetailError} - services={wizard.problems.map((p) => ({ id: p.id, value: p.value }))} + services={wizard.services.map((service) => ({ + id: service.id, + value: service.name, + }))} showErrors={wizard.showErrors} svcOpen={wizard.svcOpen} onSvcOpenChange={wizard.setSvcOpen} diff --git a/src/app/(protected)/workorders/_components/wizard/use-new-wo-wizard-controller.ts b/src/app/(protected)/workorders/_components/wizard/use-new-wo-wizard-controller.ts index 908c6d1a..0a5f394b 100644 --- a/src/app/(protected)/workorders/_components/wizard/use-new-wo-wizard-controller.ts +++ b/src/app/(protected)/workorders/_components/wizard/use-new-wo-wizard-controller.ts @@ -2,8 +2,6 @@ import { useEffect } from "react"; import { guardDialogClose } from "@/lib/dialog-guard"; -import { useDropdownOptionsByCategory } from "@/domain/settings/dropdown-options/use-cases/use-dropdown-options-by-category"; - import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; import type { WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order-wizard"; @@ -16,6 +14,7 @@ import { useCreateWorkOrderFromWizard } from "@/domain/work-orders/use-cases/use import { useNewWoWizardDraft } from "@/app/(protected)/workorders/_components/wizard/use-new-wo-wizard-draft"; import { useWizardDuplicateActions } from "@/app/(protected)/workorders/_components/wizard/use-wizard-duplicate-actions"; import { useWizardPocAutofill } from "@/app/(protected)/workorders/_components/wizard/use-wizard-poc-autofill"; +import { useServicesForWorkOrderType } from "@/domain/services/use-cases/use-services-for-work-order-type"; type UseNewWoWizardControllerArgs = { open: boolean; @@ -32,7 +31,7 @@ export function useNewWoWizardController({ const draftState = useNewWoWizardDraft(open); const { data: locations = [] } = useLocationSites(); - const { data: problems = [] } = useDropdownOptionsByCategory("Problem"); + const { data: registryServices = [] } = useServicesForWorkOrderType(draftState.draft.type); const { data: locationDetail, isLoading: locationDetailLoading, @@ -78,7 +77,7 @@ export function useNewWoWizardController({ ...draftState, patchDraft, locations, - problems, + services: registryServices, locationDetail, locationDetailLoading, locationDetailError, diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-step-location-service-select.tsx b/src/app/(protected)/workorders/_components/wizard/wizard-step-location-service-select.tsx index bfefcf5d..4b9889df 100644 --- a/src/app/(protected)/workorders/_components/wizard/wizard-step-location-service-select.tsx +++ b/src/app/(protected)/workorders/_components/wizard/wizard-step-location-service-select.tsx @@ -32,8 +32,10 @@ export function WizardStepLocationServiceSelect({ nextPm !== draft.pm ? { vendorId: "", vendorName: "", techPhone: "" } : {}; const selectSvc = (p: string) => { + const selected = services.find((service) => service.value === p); onPatch({ pm: p, + serviceId: selected?.id ?? null, extraServices: [], ...clearVendorIfPrimaryChanged(p), }); diff --git a/src/domain/services/api/services-api.ts b/src/domain/services/api/services-api.ts new file mode 100644 index 00000000..58ba217c --- /dev/null +++ b/src/domain/services/api/services-api.ts @@ -0,0 +1,30 @@ +import { apiGet } from "@/api/api"; +import { API_PATHS } from "@/api/api-paths"; +import { handleApiResponse } from "@/api/handle-api-response"; +import { mapServices } from "@/domain/services/mappers/service-mapper"; +import type { Service } from "@/domain/services/types/service"; +import { WORK_ORDER_TYPE_API } from "@/domain/work-orders/types/work-order-board"; +import type { WOType } from "@/domain/work-orders/types/work-order-wizard"; + +const TYPE_TO_API: Record, number> = { + PM: WORK_ORDER_TYPE_API.PM, + Reactive: WORK_ORDER_TYPE_API.Reactive, + Emergency: WORK_ORDER_TYPE_API.Emergency, +}; + +export const servicesApi = { + getActive: async (): Promise => { + const data = await apiGet(API_PATHS.services.list, { + searchParams: { isActive: true }, + }); + return mapServices(handleApiResponse(data)); + }, + + getActiveForWorkOrderType: async (workOrderType: WOType): Promise => { + if (workOrderType === "Overdue") return []; + const data = await apiGet(API_PATHS.services.list, { + searchParams: { isActive: true, workOrderType: TYPE_TO_API[workOrderType] }, + }); + return mapServices(handleApiResponse(data)); + }, +}; diff --git a/src/domain/services/mappers/service-mapper.ts b/src/domain/services/mappers/service-mapper.ts new file mode 100644 index 00000000..4b6d6f6e --- /dev/null +++ b/src/domain/services/mappers/service-mapper.ts @@ -0,0 +1,71 @@ +import type { Service } from "@/domain/services/types/service"; +import type { WOType } from "@/domain/work-orders/types/work-order-wizard"; + +function asRecord(raw: unknown): Record { + return typeof raw === "object" && raw !== null ? (raw as Record) : {}; +} + +function readId(item: Record): string | number { + const value = item.id ?? item.Id; + return typeof value === "string" || typeof value === "number" ? value : ""; +} + +function readString(item: Record, ...keys: string[]): string { + for (const key of keys) { + const value = item[key]; + if (typeof value === "string") return value.trim(); + } + return ""; +} + +function readBool(item: Record, ...keys: string[]): boolean { + for (const key of keys) { + const value = item[key]; + if (typeof value === "boolean") return value; + } + return false; +} + +function mapWorkOrderType(value: unknown): WOType | null { + if (typeof value === "number") { + return value === 2 ? "PM" : value === 3 ? "Emergency" : value === 6 ? "Reactive" : null; + } + if (typeof value === "string") { + const normalized = value.trim().toLowerCase(); + return normalized === "pm" || normalized === "preventivemaintenance" + ? "PM" + : normalized === "emergency" + ? "Emergency" + : normalized === "reactive" + ? "Reactive" + : null; + } + return null; +} + +export function mapService(raw: unknown): Service { + const item = asRecord(raw); + const types = item.supportedWorkOrderTypes ?? item.SupportedWorkOrderTypes; + return { + id: readId(item), + name: readString(item, "name", "Name"), + trade: readString(item, "trade", "Trade"), + iconKey: readString(item, "iconKey", "IconKey"), + requiresCompletionDocument: readBool( + item, + "requiresCompletionDocument", + "RequiresCompletionDocument", + ), + isActive: readBool(item, "isActive", "IsActive"), + supportedWorkOrderTypes: Array.isArray(types) + ? types.flatMap((value) => { + const mapped = mapWorkOrderType(value); + return mapped ? [mapped] : []; + }) + : [], + }; +} + +export function mapServices(raw: unknown): Service[] { + return Array.isArray(raw) ? raw.map(mapService).filter((service) => service.id !== "") : []; +} diff --git a/src/domain/services/types/service.ts b/src/domain/services/types/service.ts new file mode 100644 index 00000000..fa7a87d9 --- /dev/null +++ b/src/domain/services/types/service.ts @@ -0,0 +1,11 @@ +import type { WOType } from "@/domain/work-orders/types/work-order-wizard"; + +export interface Service { + id: string | number; + name: string; + trade: string; + iconKey: string; + requiresCompletionDocument: boolean; + isActive: boolean; + supportedWorkOrderTypes: WOType[]; +} diff --git a/src/domain/services/use-cases/use-services-for-work-order-type.ts b/src/domain/services/use-cases/use-services-for-work-order-type.ts new file mode 100644 index 00000000..6dd8d4ac --- /dev/null +++ b/src/domain/services/use-cases/use-services-for-work-order-type.ts @@ -0,0 +1,25 @@ +import { useQuery, type UseQueryResult } from "@tanstack/react-query"; +import { servicesApi } from "@/domain/services/api/services-api"; +import type { Service } from "@/domain/services/types/service"; +import type { WOType } from "@/domain/work-orders/types/work-order-wizard"; +import { queryKeys } from "@/infra/query-key/query-key"; + +export function useServicesForWorkOrderType( + workOrderType: WOType | "", +): UseQueryResult { + const effectiveType = workOrderType || "PM"; + return useQuery({ + queryKey: queryKeys.services.byWorkOrderType(effectiveType), + queryFn: () => servicesApi.getActiveForWorkOrderType(effectiveType), + enabled: effectiveType !== "Overdue", + staleTime: 60 * 1000, + }); +} + +export function useActiveServices(): UseQueryResult { + return useQuery({ + queryKey: queryKeys.services.active(), + queryFn: () => servicesApi.getActive(), + staleTime: 60 * 1000, + }); +} diff --git a/src/domain/work-orders/mappers/assign-wizard-service-poc-fields.ts b/src/domain/work-orders/mappers/assign-wizard-service-poc-fields.ts index 32e87840..e45381e7 100644 --- a/src/domain/work-orders/mappers/assign-wizard-service-poc-fields.ts +++ b/src/domain/work-orders/mappers/assign-wizard-service-poc-fields.ts @@ -19,6 +19,10 @@ export function assignWizardServicePocFields( payload.primaryService = primaryService; } + if (draft.serviceId != null) { + payload.serviceId = draft.serviceId; + } + const serviceNotes = trimOrUndefined(draft.pmNote); if (serviceNotes) { diff --git a/src/domain/work-orders/mappers/work-order-table-row-mapper.ts b/src/domain/work-orders/mappers/work-order-table-row-mapper.ts index d1d4afc3..2f019989 100644 --- a/src/domain/work-orders/mappers/work-order-table-row-mapper.ts +++ b/src/domain/work-orders/mappers/work-order-table-row-mapper.ts @@ -400,6 +400,7 @@ export function mapWorkOrderTableRow(raw: unknown): WorkOrderTableRow { locationId: typeof locationIdRaw === "string" || typeof locationIdRaw === "number" ? locationIdRaw : "", pm: readString(item, "pm", "PM", "problem", "Problem", "service", "Service", "trade", "Trade"), + serviceId: readNumber(item, "serviceId", "ServiceId"), serviceNotes: readString(item, "serviceNotes", "ServiceNotes", "pmNote", "PmNote"), extraServices: readStringArray(item, "extraServices", "ExtraServices"), vendorId: vendorIdNum != null ? String(vendorIdNum) : "", diff --git a/src/domain/work-orders/types/work-order-board.ts b/src/domain/work-orders/types/work-order-board.ts index 4c5606b4..b7f2693a 100644 --- a/src/domain/work-orders/types/work-order-board.ts +++ b/src/domain/work-orders/types/work-order-board.ts @@ -105,6 +105,8 @@ export interface WorkOrderBoardCreatePayload { trade?: string; /** Primary service label; persisted as board `pm`. */ primaryService?: string; + /** Registry service identity; the backend validates active/type-compatible selections. */ + serviceId?: string | number; /** Services beyond primary; order preserved. */ extraServices?: string[]; /** Issue detail from Service & Notes (`pmNote` in draft). */ @@ -171,6 +173,7 @@ export interface WorkOrderBoardRowApi { originalDate?: string | null; originalWeek?: string | null; workOrderType?: number | null; + serviceId?: number | null; isPastDue?: boolean; siteCode?: string | null; locationName?: string | null; diff --git a/src/domain/work-orders/types/work-order-table-row.ts b/src/domain/work-orders/types/work-order-table-row.ts index d362f931..179f6c95 100644 --- a/src/domain/work-orders/types/work-order-table-row.ts +++ b/src/domain/work-orders/types/work-order-table-row.ts @@ -60,6 +60,7 @@ export interface WorkOrderTableRow { location: string; locationId: string | number; pm: string; + serviceId?: string | number | null; /** Issue detail from Service & Notes. */ serviceNotes: string; /** Additional services beyond primary `pm`. */ diff --git a/src/domain/work-orders/types/work-order-wizard.ts b/src/domain/work-orders/types/work-order-wizard.ts index f13b387e..ca1e94ad 100644 --- a/src/domain/work-orders/types/work-order-wizard.ts +++ b/src/domain/work-orders/types/work-order-wizard.ts @@ -37,6 +37,8 @@ export interface WorkOrderWizardDraft { scheduledOn: string; dueDate: string; pm: string; + /** Registry service selected for this work order; null keeps the optional field empty. */ + serviceId: string | number | null; /** Extra services beyond primary `pm` (Service & Notes). */ extraServices: string[]; /** Free-text note attached to the service (Service & Notes). */ @@ -82,6 +84,7 @@ export const EMPTY_WIZARD_DRAFT: WorkOrderWizardDraft = { scheduledOn: "", dueDate: "", pm: "", + serviceId: null, extraServices: [], pmNote: "", avetaRequired: false, diff --git a/src/infra/query-key/query-key.ts b/src/infra/query-key/query-key.ts index c4a9e284..df63c881 100644 --- a/src/infra/query-key/query-key.ts +++ b/src/infra/query-key/query-key.ts @@ -56,6 +56,12 @@ export const queryKeys = { byCategory: (category: string, parent?: string) => [...queryKeys.dropdownOptions.all, "byCategory", category, parent ?? ""] as const, }, + services: { + all: ["services"] as const, + active: () => [...queryKeys.services.all, "active"] as const, + byWorkOrderType: (workOrderType: string) => + [...queryKeys.services.all, "byWorkOrderType", workOrderType] as const, + }, locations: { all: ["locations"] as const, list: (params?: Record) => diff --git a/src/test/app/(protected)/workorders/wizard-service-clears-vendor.test.tsx b/src/test/app/(protected)/workorders/wizard-service-clears-vendor.test.tsx index 3812fb63..c46f95df 100644 --- a/src/test/app/(protected)/workorders/wizard-service-clears-vendor.test.tsx +++ b/src/test/app/(protected)/workorders/wizard-service-clears-vendor.test.tsx @@ -79,6 +79,7 @@ describe("WizardStepLocationServiceSelect — vendor clear on service change", ( expect(onPatch).toHaveBeenCalledWith({ pm: "Electrical", + serviceId: "3", extraServices: [], vendorId: "", vendorName: "", diff --git a/src/test/domain/services/api/services-api.test.ts b/src/test/domain/services/api/services-api.test.ts new file mode 100644 index 00000000..ab558d03 --- /dev/null +++ b/src/test/domain/services/api/services-api.test.ts @@ -0,0 +1,48 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { API_PATHS } from "@/api/api-paths"; + +const apiGet = vi.fn(); + +vi.mock("@/api/api", () => ({ + apiGet: (...args: unknown[]) => apiGet(...args), +})); + +import { servicesApi } from "@/domain/services/api/services-api"; + +describe("servicesApi", () => { + beforeEach(() => { + apiGet.mockReset(); + }); + + it("loads active services filtered by the selected work-order type", async () => { + apiGet.mockResolvedValue([ + { + id: 7, + name: "HVAC inspection", + trade: "HVAC", + iconKey: "hvac-inspection", + isActive: true, + supportedWorkOrderTypes: [2, "Reactive"], + }, + ]); + + const services = await servicesApi.getActiveForWorkOrderType("PM"); + + expect(apiGet).toHaveBeenCalledWith(API_PATHS.services.list, { + searchParams: { isActive: true, workOrderType: 2 }, + }); + expect(services[0]).toMatchObject({ + id: 7, + name: "HVAC inspection", + trade: "HVAC", + supportedWorkOrderTypes: ["PM", "Reactive"], + }); + }); + + it("does not request services for the unsupported Overdue type", async () => { + const services = await servicesApi.getActiveForWorkOrderType("Overdue"); + + expect(apiGet).not.toHaveBeenCalled(); + expect(services).toEqual([]); + }); +}); 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 75f1e499..c4599641 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 @@ -140,6 +140,25 @@ describe("workOrdersApi.createFromBoard", () => { locationId: 9, }); }); + + it("includes the registry service id when selected in the wizard", async () => { + await workOrdersApi.createFromBoard({ + ...EMPTY_WIZARD_DRAFT, + type: "Reactive", + siteCode: "BK5", + pm: "HVAC inspection", + serviceId: 7, + }); + + expect(apiPost).toHaveBeenCalledWith( + API_PATHS.workOrder.boardCreate, + expect.objectContaining({ + workOrderType: 6, + primaryService: "HVAC inspection", + serviceId: 7, + }), + ); + }); }); describe("workOrdersApi.update", () => { From 71756ca3a54433cce5e99ea35eb9f2aa59aa5193 Mon Sep 17 00:00:00 2001 From: Codex Review Integration Date: Wed, 16 Sep 2026 19:33:08 -0300 Subject: [PATCH 2/3] fix(work-orders): clear registry service when WO type changes Selecting a work-order type on wizard step 1 only patched type and severity, leaving pm, serviceId, extraServices, and the vendor filtered by that service in the draft. Switching type (e.g. PM -> Reactive) then submitted a serviceId that is not in the new type's registry list, which backend ResolveServiceAsync (SH-187/#131) rejects as ServiceInvalid and the create toast renders as the generic "Unable to create the work order". Clear the service and its vendor whenever the type actually changes; re-selecting the same type leaves the selection untouched. --- .../wizard/wizard-step-type-schedule.tsx | 14 ++++ ...step-type-schedule-clears-service.test.tsx | 69 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 src/test/app/(protected)/workorders/wizard-step-type-schedule-clears-service.test.tsx diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-step-type-schedule.tsx b/src/app/(protected)/workorders/_components/wizard/wizard-step-type-schedule.tsx index f4df71a3..96f2a62d 100644 --- a/src/app/(protected)/workorders/_components/wizard/wizard-step-type-schedule.tsx +++ b/src/app/(protected)/workorders/_components/wizard/wizard-step-type-schedule.tsx @@ -262,9 +262,23 @@ export function WizardStepTypeSchedule({ { + const typeChanged = t !== draft.type; onPatch({ type: t, severity: nextSeverityForType(draft.type, t, draft.severity), + // A registry service is scoped to its work-order type, so a service picked + // under the previous type fails backend ResolveServiceAsync (SH-187/#131). + // Clear the service (and its vendor, filtered by the service) on a type change. + ...(typeChanged + ? { + pm: "", + serviceId: null, + extraServices: [], + vendorId: "", + vendorName: "", + techPhone: "", + } + : {}), }); }} /> diff --git a/src/test/app/(protected)/workorders/wizard-step-type-schedule-clears-service.test.tsx b/src/test/app/(protected)/workorders/wizard-step-type-schedule-clears-service.test.tsx new file mode 100644 index 00000000..9c9582ee --- /dev/null +++ b/src/test/app/(protected)/workorders/wizard-step-type-schedule-clears-service.test.tsx @@ -0,0 +1,69 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { WizardStepTypeSchedule } from "@/app/(protected)/workorders/_components/wizard/wizard-step-type-schedule"; +import { + EMPTY_WIZARD_DRAFT, + type WorkOrderWizardDraft, +} from "@/domain/work-orders/types/work-order-wizard"; + +const USERS = [{ id: "sarah", name: "Sarah Chen" }]; + +describe("WizardStepTypeSchedule — service clear on type change", () => { + it("clears the service and its vendor when the work-order type changes", () => { + const onPatch = vi.fn(); + const draft: WorkOrderWizardDraft = { + ...EMPTY_WIZARD_DRAFT, + type: "PM", + pm: "HVAC", + serviceId: "1", + extraServices: ["Plumbing"], + vendorId: "9", + vendorName: "Old Vendor", + techPhone: "555-0100", + }; + + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: /Reactive/ })); + + expect(onPatch).toHaveBeenCalledWith( + expect.objectContaining({ + type: "Reactive", + pm: "", + serviceId: null, + extraServices: [], + vendorId: "", + vendorName: "", + techPhone: "", + }), + ); + }); + + it("keeps the selected service when the same type is re-selected", () => { + const onPatch = vi.fn(); + const draft: WorkOrderWizardDraft = { + ...EMPTY_WIZARD_DRAFT, + type: "PM", + pm: "HVAC", + serviceId: "1", + extraServices: ["Plumbing"], + vendorId: "9", + vendorName: "Old Vendor", + techPhone: "555-0100", + }; + + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: /Scheduled preventive maintenance/ })); + + const patch = onPatch.mock.calls[0][0] as Partial; + expect(patch).not.toHaveProperty("pm"); + expect(patch).not.toHaveProperty("serviceId"); + expect(patch).not.toHaveProperty("extraServices"); + expect(patch).not.toHaveProperty("vendorId"); + }); +}); From 824001dc5ec43797dc079036096211f3a77819b5 Mon Sep 17 00:00:00 2001 From: Codex Review Integration Date: Thu, 17 Sep 2026 01:40:58 -0300 Subject: [PATCH 3/3] fix(work-orders): make service picker search placeholder type-neutral --- .../table/cells/pm-type-picker-popover.tsx | 2 +- .../wizard-step-location-service-select.tsx | 2 +- ...service-picker-search-placeholder.test.tsx | 56 +++++++++++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 src/test/app/(protected)/workorders/service-picker-search-placeholder.test.tsx diff --git a/src/app/(protected)/workorders/_components/list/table/cells/pm-type-picker-popover.tsx b/src/app/(protected)/workorders/_components/list/table/cells/pm-type-picker-popover.tsx index 1df4a262..8f7d75a6 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/pm-type-picker-popover.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/pm-type-picker-popover.tsx @@ -39,7 +39,7 @@ export function PmTypePickerPopover({ autoFocus value={search} onChange={(e) => onSearchChange(e.target.value)} - placeholder="Search PM type…" + placeholder="Search service…" className="w-full rounded border border-border bg-[var(--color-bg-surface)] px-2 py-1 text-xs text-[var(--color-text-primary)] focus:outline-none" /> diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-step-location-service-select.tsx b/src/app/(protected)/workorders/_components/wizard/wizard-step-location-service-select.tsx index 4b9889df..baadd66e 100644 --- a/src/app/(protected)/workorders/_components/wizard/wizard-step-location-service-select.tsx +++ b/src/app/(protected)/workorders/_components/wizard/wizard-step-location-service-select.tsx @@ -50,7 +50,7 @@ export function WizardStepLocationServiceSelect({ icon={ } - searchPlaceholder="Search PM type…" + searchPlaceholder="Search services…" options={services.map((s) => { const Icon = getPmTypeIcon(s.value); return { diff --git a/src/test/app/(protected)/workorders/service-picker-search-placeholder.test.tsx b/src/test/app/(protected)/workorders/service-picker-search-placeholder.test.tsx new file mode 100644 index 00000000..7a2d7fa1 --- /dev/null +++ b/src/test/app/(protected)/workorders/service-picker-search-placeholder.test.tsx @@ -0,0 +1,56 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { WizardStepLocationServiceSelect } from "@/app/(protected)/workorders/_components/wizard/wizard-step-location-service-select"; +import { PmTypePickerPopover } from "@/app/(protected)/workorders/_components/list/table/cells/pm-type-picker-popover"; +import { + EMPTY_WIZARD_DRAFT, + type WorkOrderWizardDraft, +} from "@/domain/work-orders/types/work-order-wizard"; + +describe("service picker search placeholder", () => { + it("uses a type-neutral search placeholder in the wizard service picker", () => { + const draft: WorkOrderWizardDraft = { ...EMPTY_WIZARD_DRAFT, pm: "" }; + + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: /select service/i })); + + expect(screen.getByPlaceholderText("Search services…")).toBeTruthy(); + expect(screen.queryByPlaceholderText("Search PM type…")).toBeNull(); + }); + + it("uses a type-neutral search placeholder in the table service picker popover", () => { + const anchor = document.createElement("button"); + + render( + , + ); + + expect(screen.getByPlaceholderText("Search service…")).toBeTruthy(); + expect(screen.queryByPlaceholderText("Search PM type…")).toBeNull(); + }); +});