diff --git a/e2e/__screenshots__/work-orders/work-orders.visual.spec.ts/wo-new.png b/e2e/__screenshots__/work-orders/work-orders.visual.spec.ts/wo-new.png index d5d7f798..9edaccd5 100644 Binary files a/e2e/__screenshots__/work-orders/work-orders.visual.spec.ts/wo-new.png and b/e2e/__screenshots__/work-orders/work-orders.visual.spec.ts/wo-new.png differ 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 9150a4fa..47cd6ee3 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 @@ -241,14 +241,13 @@ export function WizardStepTypeSchedule({ onDuplicateWoNumber, }: WizardStepTypeScheduleProps) { const needsSeverity = typeNeedsSeverity(draft.type); - const typeError = showErrors && !draft.type; const woNumberError = showErrors && !isWizardWoNumberValid(draft.woNumber); const selectedUser = users.find((u) => String(u.id) === draft.assigneeId); return (
- Type of WO + Type of WO { @@ -258,14 +257,6 @@ export function WizardStepTypeSchedule({ }); }} /> - - Type of WO is required -
{Boolean(needsSeverity) && ( @@ -277,19 +268,18 @@ export function WizardStepTypeSchedule({ )}
- WO Number + WO Number onUpdate( "woNumber", - e.target.value.replace(/\D/g, "").slice(0, WIZARD_WO_NUMBER_MAX_LENGTH), + e.target.value.replace(/[^a-zA-Z0-9]/g, "").slice(0, WIZARD_WO_NUMBER_MAX_LENGTH), ) } onBlur={() => onDuplicateWoNumber?.(draft.woNumber)} - placeholder="Enter WO number" + placeholder="Leave blank for SH provisional number" maxLength={WIZARD_WO_NUMBER_MAX_LENGTH} className={DLG_INPUT_CLS} style={DLG_INPUT_STYLE} @@ -300,7 +290,7 @@ export function WizardStepTypeSchedule({ when={Boolean(woNumberError)} sx={{ fontSize: 11, marginTop: "6px" }} > - {draft.woNumber.trim() ? "WO number must be up to 11 digits" : "WO number is required"} + WO number must be alphanumeric.
diff --git a/src/domain/work-orders/types/work-order-wizard.ts b/src/domain/work-orders/types/work-order-wizard.ts index d7ccdb40..f13b387e 100644 --- a/src/domain/work-orders/types/work-order-wizard.ts +++ b/src/domain/work-orders/types/work-order-wizard.ts @@ -103,14 +103,15 @@ export const EMPTY_WIZARD_DRAFT: WorkOrderWizardDraft = { isAddOnOverride: false, }; -export const WIZARD_WO_NUMBER_MAX_LENGTH = 11; +export const WIZARD_WO_NUMBER_MAX_LENGTH = 20; -/** Create requires a WO number of 1–11 digits. */ +/** Blank is valid (backend generates an SH provisional number). Otherwise alphanumeric, max 20. */ export function isWizardWoNumberValid(woNumber: string): boolean { const trimmed = woNumber.trim(); - return ( - trimmed.length > 0 && trimmed.length <= WIZARD_WO_NUMBER_MAX_LENGTH && /^\d+$/.test(trimmed) - ); + if (!trimmed) { + return true; + } + return trimmed.length <= WIZARD_WO_NUMBER_MAX_LENGTH && /^[a-z0-9]+$/i.test(trimmed); } /** Type is optional at creation; empty draft type defaults to PM. */ @@ -186,9 +187,6 @@ export const TIME_SLOTS: string[] = (() => { })(); export function canContinueWizardStep1(draft: WorkOrderWizardDraft): boolean { - if (!draft.type) { - return false; - } if (!isWizardWoNumberValid(draft.woNumber)) { return false; } @@ -202,14 +200,8 @@ export function wizardStep1ContinueHint(draft: WorkOrderWizardDraft): string | n if (canContinueWizardStep1(draft)) { return null; } - if (!draft.type) { - return "Select a work order type to continue"; - } - if (!draft.woNumber.trim()) { - return "Enter a WO number to continue"; - } if (!isWizardWoNumberValid(draft.woNumber)) { - return "WO number must be up to 11 digits"; + return "WO number must be alphanumeric"; } return "Select severity to continue"; } diff --git a/src/test/app/(protected)/workorders/new-wo-wizard-duplicate.test.tsx b/src/test/app/(protected)/workorders/new-wo-wizard-duplicate.test.tsx index fd6b1212..02a1fa6c 100644 --- a/src/test/app/(protected)/workorders/new-wo-wizard-duplicate.test.tsx +++ b/src/test/app/(protected)/workorders/new-wo-wizard-duplicate.test.tsx @@ -48,7 +48,7 @@ describe("NewWoWizard duplicate wo number", () => { />, ); - const woNumberInput = screen.getByPlaceholderText("Enter WO number"); + const woNumberInput = screen.getByPlaceholderText("Leave blank for SH provisional number"); fireEvent.change(woNumberInput, { target: { value: "12345678901" }, }); @@ -77,7 +77,7 @@ describe("NewWoWizard duplicate wo number", () => { />, ); - const woNumberInput = screen.getByPlaceholderText("Enter WO number"); + const woNumberInput = screen.getByPlaceholderText("Leave blank for SH provisional number"); fireEvent.change(woNumberInput, { target: { value: "98765432109" }, }); diff --git a/src/test/app/(protected)/workorders/work-orders-board-core-affordances.test.tsx b/src/test/app/(protected)/workorders/work-orders-board-core-affordances.test.tsx index 3ec402b5..e273324c 100644 --- a/src/test/app/(protected)/workorders/work-orders-board-core-affordances.test.tsx +++ b/src/test/app/(protected)/workorders/work-orders-board-core-affordances.test.tsx @@ -227,16 +227,9 @@ describe("WorkOrdersListPage board-core public interface", () => { expect(screen.getByRole("dialog")).toBeInTheDocument(); expect(screen.getByRole("heading", { name: /type & schedule/i })).toBeInTheDocument(); - expect(screen.getByPlaceholderText(/enter wo number/i)).toBeInTheDocument(); - fireEvent.click(screen.getByRole("button", { name: /continue/i })); - expect(screen.getByText(/type of wo is required/i)).toBeInTheDocument(); - expect(screen.getByText(/wo number is required/i)).toBeInTheDocument(); - expect(screen.getByRole("heading", { name: /type & schedule/i })).toBeInTheDocument(); - - fireEvent.click(screen.getByRole("button", { name: /scheduled preventive maintenance/i })); - fireEvent.change(screen.getByPlaceholderText(/enter wo number/i), { - target: { value: "12345678901" }, - }); + expect( + screen.getByPlaceholderText(/leave blank for sh provisional number/i), + ).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: /continue/i })); expect(screen.getByRole("heading", { name: /location & service/i })).toBeInTheDocument(); 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 cbcf9be7..b1124749 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 @@ -67,21 +67,21 @@ describe("mapWizardDraftToFormValues", () => { }); describe("wizard step validation", () => { - it("blocks step 1 without a woNumber", () => { - expect(canContinueWizardStep1(EMPTY_WIZARD_DRAFT)).toBe(false); - expect(isWizardStepValid(1, EMPTY_WIZARD_DRAFT)).toBe(false); + it("allows step 1 with a blank woNumber for backend SH generation", () => { + expect(canContinueWizardStep1(EMPTY_WIZARD_DRAFT)).toBe(true); + expect(isWizardStepValid(1, EMPTY_WIZARD_DRAFT)).toBe(true); }); - it("blocks step 1 without type even when woNumber is set", () => { + it("allows step 1 without type when woNumber is set", () => { const draft = { ...EMPTY_WIZARD_DRAFT, woNumber: "12345" }; - expect(canContinueWizardStep1(draft)).toBe(false); - expect(isWizardStepValid(1, draft)).toBe(false); + expect(canContinueWizardStep1(draft)).toBe(true); + expect(isWizardStepValid(1, draft)).toBe(true); }); - it("blocks step 1 without woNumber even when type is set", () => { + it("allows step 1 without woNumber when type is set", () => { const draft = { ...EMPTY_WIZARD_DRAFT, type: "PM" as const }; - expect(canContinueWizardStep1(draft)).toBe(false); - expect(isWizardStepValid(1, draft)).toBe(false); + expect(canContinueWizardStep1(draft)).toBe(true); + expect(isWizardStepValid(1, draft)).toBe(true); }); it("allows step 1 when type and woNumber are set (schedule optional)", () => { @@ -194,56 +194,44 @@ describe("wizard step validation", () => { expect(isWizardStepValid(2, draft)).toBe(true); }); - it("blocks SH-prefixed woNumber on create", () => { + it("allows provisional SH-prefixed woNumber", () => { const draft = { ...EMPTY_WIZARD_DRAFT, type: "PM" as const, woNumber: "SH12345", }; - expect(canContinueWizardStep1(draft)).toBe(false); + expect(isWizardWoNumberValid(draft.woNumber)).toBe(true); + expect(canContinueWizardStep1(draft)).toBe(true); }); - it("rejects blank woNumber", () => { - const draft = { - ...EMPTY_WIZARD_DRAFT, - type: "PM" as const, - woNumber: "", - }; - expect(isWizardWoNumberValid(draft.woNumber)).toBe(false); - expect(canContinueWizardStep1(draft)).toBe(false); - }); - - it("rejects whitespace-only woNumber", () => { + it("accepts whitespace-only woNumber for backend SH generation", () => { const draft = { ...EMPTY_WIZARD_DRAFT, type: "PM" as const, woNumber: " ", }; - expect(isWizardWoNumberValid(draft.woNumber)).toBe(false); - expect(canContinueWizardStep1(draft)).toBe(false); + expect(isWizardWoNumberValid(draft.woNumber)).toBe(true); + expect(canContinueWizardStep1(draft)).toBe(true); }); - it("rejects non-digit woNumber", () => { + it("rejects non-alphanumeric woNumber", () => { expect(isWizardWoNumberValid("!!!")).toBe(false); expect(canContinueWizardStep1({ ...EMPTY_WIZARD_DRAFT, type: "PM", woNumber: "!!!" })).toBe( false, ); }); - it("rejects woNumber longer than 11 digits", () => { - expect(isWizardWoNumberValid("123456789012")).toBe(false); + it("accepts alphanumeric woNumber", () => { + expect(isWizardWoNumberValid("ABC123")).toBe(true); }); - it("accepts an 11-digit woNumber", () => { - expect(isWizardWoNumberValid("12345678901")).toBe(true); - }); - - it("hints type then WO number when step 1 is incomplete", () => { - expect(wizardStep1ContinueHint(EMPTY_WIZARD_DRAFT)).toBe( - "Select a work order type to continue", + it("hints alphanumeric WO number or severity when step 1 is incomplete", () => { + expect(wizardStep1ContinueHint(EMPTY_WIZARD_DRAFT)).toBeNull(); + expect(wizardStep1ContinueHint({ ...EMPTY_WIZARD_DRAFT, type: "PM", woNumber: "!!!" })).toBe( + "WO number must be alphanumeric", ); - expect(wizardStep1ContinueHint({ ...EMPTY_WIZARD_DRAFT, type: "PM" })).toBe( - "Enter a WO number to continue", + expect(wizardStep1ContinueHint({ ...EMPTY_WIZARD_DRAFT, type: "Emergency" })).toBe( + "Select severity to continue", ); }); });