From 3a85c8c9e334e00ca3a81742f923fe7ef87d4113 Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Tue, 25 Aug 2026 22:51:29 -0300 Subject: [PATCH] fix(work-orders): require type and 11-digit WO number on create Wizard step 1 cannot continue without type and a numeric WO number. Remove the Mark as Add-On checkbox from the create wizard. --- .../_components/wizard/wizard-footer.tsx | 10 +-- .../wizard/wizard-step-type-schedule.tsx | 66 ++++++++----------- .../work-orders/types/work-order-wizard.ts | 30 +++++++-- .../new-wo-wizard-duplicate.test.tsx | 4 +- ...ork-orders-board-core-affordances.test.tsx | 15 +++-- .../mappers/wizard-draft-mapper.test.ts | 61 ++++++++++------- 6 files changed, 106 insertions(+), 80 deletions(-) diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-footer.tsx b/src/app/(protected)/workorders/_components/wizard/wizard-footer.tsx index 36a4e4ba..12ea5609 100644 --- a/src/app/(protected)/workorders/_components/wizard/wizard-footer.tsx +++ b/src/app/(protected)/workorders/_components/wizard/wizard-footer.tsx @@ -2,10 +2,9 @@ import { Check, ChevronLeft, ChevronRight, Lightbulb } from "lucide-react"; import { Button } from "@/components/ui/button"; import { WIZARD_STEPS, - canContinueWizardStep1, canCreateWorkOrderFromWizard, isWizardStepValid, - isWizardWoNumberValid, + wizardStep1ContinueHint, wizardStep2ContinueHint, type WorkOrderWizardDraft, } from "@/domain/work-orders/types/work-order-wizard"; @@ -22,11 +21,8 @@ type WizardFooterProps = { }; function stepHint(step: number, draft: WorkOrderWizardDraft): string | null { - if (step === 1 && !canContinueWizardStep1(draft)) { - if (!isWizardWoNumberValid(draft.woNumber)) { - return "Enter an alphanumeric WO number, or leave blank for SH generation"; - } - return "Select severity to continue"; + if (step === 1) { + return wizardStep1ContinueHint(draft); } if (step === 2) { return wizardStep2ContinueHint(draft); 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 2b8f36b7..9150a4fa 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 @@ -4,6 +4,7 @@ import type { WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order import { WO_TYPE_META, WO_TYPES, + WIZARD_WO_NUMBER_MAX_LENGTH, isWizardWoNumberValid, type WOType, } from "@/domain/work-orders/types/work-order-wizard"; @@ -231,28 +232,6 @@ function clearSchedulePatch(): Partial { }; } -function WizardAddOnOverrideField({ - checked, - onChange, -}: { - checked: boolean; - onChange: (value: boolean) => void; -}) { - return ( - - ); -} - export function WizardStepTypeSchedule({ draft, users, @@ -262,13 +241,14 @@ 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 { @@ -278,6 +258,14 @@ export function WizardStepTypeSchedule({ }); }} /> + + Type of WO is required +
{Boolean(needsSeverity) && ( @@ -289,24 +277,31 @@ export function WizardStepTypeSchedule({ )}
- WO Number + WO Number - onUpdate("woNumber", e.target.value.replace(/[^a-zA-Z0-9]/g, "").slice(0, 20)) + onUpdate( + "woNumber", + e.target.value.replace(/\D/g, "").slice(0, WIZARD_WO_NUMBER_MAX_LENGTH), + ) } onBlur={() => onDuplicateWoNumber?.(draft.woNumber)} - placeholder="Leave blank for SH provisional number" - maxLength={20} + placeholder="Enter WO number" + maxLength={WIZARD_WO_NUMBER_MAX_LENGTH} className={DLG_INPUT_CLS} style={DLG_INPUT_STYLE} /> - {Boolean(woNumberError) && ( - - WO number must be alphanumeric. - - )} + + {draft.woNumber.trim() ? "WO number must be up to 11 digits" : "WO number is required"} +
@@ -338,13 +333,6 @@ export function WizardStepTypeSchedule({
- - {Boolean(!hasWizardSchedule(draft)) && ( - onUpdate("isAddOnOverride", value)} - /> - )}
); } diff --git a/src/domain/work-orders/types/work-order-wizard.ts b/src/domain/work-orders/types/work-order-wizard.ts index 7b89687d..d7ccdb40 100644 --- a/src/domain/work-orders/types/work-order-wizard.ts +++ b/src/domain/work-orders/types/work-order-wizard.ts @@ -103,13 +103,14 @@ export const EMPTY_WIZARD_DRAFT: WorkOrderWizardDraft = { isAddOnOverride: false, }; -/** Blank numbers receive an SH provisional number from the backend; nonblank values are alphanumeric. */ +export const WIZARD_WO_NUMBER_MAX_LENGTH = 11; + +/** Create requires a WO number of 1–11 digits. */ export function isWizardWoNumberValid(woNumber: string): boolean { const trimmed = woNumber.trim(); - if (!trimmed) { - return true; - } - return trimmed.length <= 20 && /^[a-z0-9]+$/i.test(trimmed); + return ( + trimmed.length > 0 && trimmed.length <= WIZARD_WO_NUMBER_MAX_LENGTH && /^\d+$/.test(trimmed) + ); } /** Type is optional at creation; empty draft type defaults to PM. */ @@ -185,6 +186,9 @@ export const TIME_SLOTS: string[] = (() => { })(); export function canContinueWizardStep1(draft: WorkOrderWizardDraft): boolean { + if (!draft.type) { + return false; + } if (!isWizardWoNumberValid(draft.woNumber)) { return false; } @@ -194,6 +198,22 @@ export function canContinueWizardStep1(draft: WorkOrderWizardDraft): boolean { return true; } +export function wizardStep1ContinueHint(draft: WorkOrderWizardDraft): string | null { + 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 "Select severity to continue"; +} + export function canContinueWizardStep2(draft: WorkOrderWizardDraft): boolean { const hasSite = Boolean(draft.locationId && (draft.siteCode.trim() || draft.locationName.trim())); return Boolean(hasSite && draft.pocName.trim() && draft.pocPhone.trim()); 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 02a1fa6c..fd6b1212 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("Leave blank for SH provisional number"); + const woNumberInput = screen.getByPlaceholderText("Enter WO number"); fireEvent.change(woNumberInput, { target: { value: "12345678901" }, }); @@ -77,7 +77,7 @@ describe("NewWoWizard duplicate wo number", () => { />, ); - const woNumberInput = screen.getByPlaceholderText("Leave blank for SH provisional number"); + const woNumberInput = screen.getByPlaceholderText("Enter WO 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 2dbdc94e..3ec402b5 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,12 +227,17 @@ describe("WorkOrdersListPage board-core public interface", () => { expect(screen.getByRole("dialog")).toBeInTheDocument(); expect(screen.getByRole("heading", { name: /type & schedule/i })).toBeInTheDocument(); - expect( - screen.getByPlaceholderText(/leave blank for sh provisional number/i), - ).toBeInTheDocument(); - expect(screen.getByRole("button", { name: /continue/i })).toBeEnabled(); + 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" }, + }); fireEvent.click(screen.getByRole("button", { name: /continue/i })); - expect(screen.queryByText(/wo number is required/i)).not.toBeInTheDocument(); expect(screen.getByRole("heading", { name: /location & service/i })).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: /^back$/i })); 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 ca9bf56e..cbcf9be7 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 @@ -7,6 +7,7 @@ import { canCreateWorkOrderFromWizard, isWizardStepValid, isWizardWoNumberValid, + wizardStep1ContinueHint, wizardStep2ContinueHint, } from "@/domain/work-orders/types/work-order-wizard"; @@ -66,21 +67,21 @@ describe("mapWizardDraftToFormValues", () => { }); describe("wizard step validation", () => { - it("allows step 1 continue without a 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 a woNumber", () => { + expect(canContinueWizardStep1(EMPTY_WIZARD_DRAFT)).toBe(false); + expect(isWizardStepValid(1, EMPTY_WIZARD_DRAFT)).toBe(false); }); - it("allows step 1 without type when woNumber is set (type defaults to PM)", () => { + it("blocks step 1 without type even when woNumber is set", () => { const draft = { ...EMPTY_WIZARD_DRAFT, woNumber: "12345" }; - expect(canContinueWizardStep1(draft)).toBe(true); - expect(isWizardStepValid(1, draft)).toBe(true); + expect(canContinueWizardStep1(draft)).toBe(false); + expect(isWizardStepValid(1, draft)).toBe(false); }); - it("allows step 1 when woNumber is missing even with type", () => { + it("blocks step 1 without woNumber even when type is set", () => { const draft = { ...EMPTY_WIZARD_DRAFT, type: "PM" as const }; - expect(canContinueWizardStep1(draft)).toBe(true); - expect(isWizardStepValid(1, draft)).toBe(true); + expect(canContinueWizardStep1(draft)).toBe(false); + expect(isWizardStepValid(1, draft)).toBe(false); }); it("allows step 1 when type and woNumber are set (schedule optional)", () => { @@ -183,7 +184,7 @@ describe("wizard step validation", () => { const draft = { ...EMPTY_WIZARD_DRAFT, type: "PM" as const, - woNumber: "12345678901234567890", + woNumber: "12345678901", locationId: "99", siteCode: "BK5", pocName: "Jordan", @@ -193,40 +194,56 @@ describe("wizard step validation", () => { expect(isWizardStepValid(2, draft)).toBe(true); }); - it("allows provisional SH-prefixed woNumber", () => { + it("blocks SH-prefixed woNumber on create", () => { const draft = { ...EMPTY_WIZARD_DRAFT, + type: "PM" as const, woNumber: "SH12345", }; - expect(canContinueWizardStep1(draft)).toBe(true); + expect(canContinueWizardStep1(draft)).toBe(false); }); - it("accepts blank woNumber for backend SH generation", () => { + it("rejects blank woNumber", () => { const draft = { ...EMPTY_WIZARD_DRAFT, type: "PM" as const, woNumber: "", }; - expect(isWizardWoNumberValid(draft.woNumber)).toBe(true); - expect(canContinueWizardStep1(draft)).toBe(true); + expect(isWizardWoNumberValid(draft.woNumber)).toBe(false); + expect(canContinueWizardStep1(draft)).toBe(false); }); - it("accepts whitespace-only woNumber for backend SH generation", () => { + it("rejects whitespace-only woNumber", () => { const draft = { ...EMPTY_WIZARD_DRAFT, type: "PM" as const, woNumber: " ", }; - expect(isWizardWoNumberValid(draft.woNumber)).toBe(true); - expect(canContinueWizardStep1(draft)).toBe(true); + expect(isWizardWoNumberValid(draft.woNumber)).toBe(false); + expect(canContinueWizardStep1(draft)).toBe(false); }); - it("rejects non-alphanumeric woNumber", () => { + it("rejects non-digit woNumber", () => { expect(isWizardWoNumberValid("!!!")).toBe(false); - expect(canContinueWizardStep1({ ...EMPTY_WIZARD_DRAFT, woNumber: "!!!" })).toBe(false); + expect(canContinueWizardStep1({ ...EMPTY_WIZARD_DRAFT, type: "PM", woNumber: "!!!" })).toBe( + false, + ); }); - it("accepts alphanumeric woNumber", () => { - expect(isWizardWoNumberValid("ABC123")).toBe(true); + it("rejects woNumber longer than 11 digits", () => { + expect(isWizardWoNumberValid("123456789012")).toBe(false); + }); + + 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", + ); + expect(wizardStep1ContinueHint({ ...EMPTY_WIZARD_DRAFT, type: "PM" })).toBe( + "Enter a WO number to continue", + ); }); });