Merge pull request #289 from Sea-Haven-Industries/fix/ab/sh-357-incomplete-empty-schedule
Some checks are pending
Frontend checks / static (push) Waiting to run
Frontend checks / build (push) Waiting to run
Frontend checks / unit (push) Waiting to run
Frontend checks / Visual regression (push) Waiting to run
Frontend checks / browser-smoke (push) Waiting to run
Frontend checks / governance (push) Waiting to run
Frontend checks / ci-complete (push) Blocked by required conditions
Deploy Web / Resolve target (push) Waiting to run
Deploy Web / Deploy SPA to (push) Blocked by required conditions

SH-357: disable Incomplete creation without Schedule On
This commit is contained in:
Alexandre Brandizzi 2026-10-01 19:46:17 +00:00 • committed by GitHub
commit 84cfc90282
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 63 additions and 38 deletions

View file

@ -181,8 +181,9 @@ test.describe("existing work order status menu", () => {
.click();
await page.getByRole("button", { name: "Continue" }).click();
await page.getByRole("button", { name: "Incomplete", exact: true }).last().click();
await page.getByRole("button", { name: "Pending", exact: true }).last().click();
const menu = page.locator(".MuiPopover-paper").last();
await expect(menu.getByRole("button", { name: "Incomplete", exact: true })).toBeDisabled();
await expect(menu.getByRole("button", { name: "Pending", exact: true })).toBeEnabled();
await menu.getByRole("button", { name: "Pending", exact: true }).click();
await expect(
@ -228,8 +229,9 @@ test.describe("existing work order status menu", () => {
.click();
await page.getByRole("button", { name: "Continue" }).click();
await page.getByRole("button", { name: "Incomplete", exact: true }).last().click();
await page.getByRole("button", { name: "Pending", exact: true }).last().click();
const menu = page.locator(".MuiPopover-paper").last();
await expect(menu.getByRole("button", { name: "Incomplete", exact: true })).toBeDisabled();
await menu.getByRole("button", { name: "Pending", exact: true }).click();
await expect(
page.getByRole("dialog").getByRole("button", { name: "Pending", exact: true }),

View file

@ -19,7 +19,7 @@ function resolveInlineDraftSchedule(draft: WorkOrderTableDraft) {
return {
scheduleWeekOnly,
status: resolveCreationStatus({
status: draft.status || "Incomplete",
status: draft.status || "Pending",
scheduledOn: draft.scheduledOn,
scheduleWeekOnly,
}),

View file

@ -230,7 +230,7 @@ export function WoTableInlineRowCells({
dispatcherId={draft.dispatcherId}
users={users}
onSave={(id) => {
onPatch({ dispatcherId: id, status: draft.status || "Incomplete" });
onPatch({ dispatcherId: id, status: draft.status || "Pending" });
}}
/>
</InlineFieldCell>

View file

@ -9,7 +9,6 @@ export function useNewWoWizardDraft(open: boolean) {
const [step, setStep] = useState(1);
const [draft, setDraft] = useState<WorkOrderWizardDraft>({
...EMPTY_WIZARD_DRAFT,
status: "Incomplete",
});
const [svcOpen, setSvcOpen] = useState(false);
const [svcSearch, setSvcSearch] = useState("");
@ -18,7 +17,7 @@ export function useNewWoWizardDraft(open: boolean) {
useEffect(() => {
if (open) {
setStep(1);
setDraft({ ...EMPTY_WIZARD_DRAFT, status: "Incomplete" });
setDraft({ ...EMPTY_WIZARD_DRAFT });
setSvcOpen(false);
setSvcSearch("");
setShowErrors(false);

View file

@ -40,7 +40,6 @@ export function useNewWoWizardState({
const [showErrors, setShowErrors] = useState(false);
const [draft, setDraft] = useState<WorkOrderWizardDraft>({
...EMPTY_WIZARD_DRAFT,
status: "Incomplete",
});
const [pocAutoFilledFor, setPocAutoFilledFor] = useState("");
@ -83,7 +82,7 @@ export function useNewWoWizardState({
if (open) {
setStep(1);
setShowErrors(false);
setDraft({ ...EMPTY_WIZARD_DRAFT, status: "Incomplete" });
setDraft({ ...EMPTY_WIZARD_DRAFT });
setPocAutoFilledFor("");
setDuplicateRow(null);
}

View file

@ -53,6 +53,6 @@ export function buildWizardSidebarSummaryRows(draft: WorkOrderWizardDraft): [str
["Technician", draft.vendorName || "—"],
["Company", draft.vendorName || "—"],
["Appt time", draft.apptStart ? formatApptRange(draft.apptStart, draft.apptEnd) : "—"],
["Status", draft.status || "Incomplete"],
["Status", draft.status || "Pending"],
];
}

View file

@ -8,9 +8,9 @@ import { ColorDot } from "@/app/(protected)/workorders/_components/wizard/wizard
export function WizardSidebarSummary({ draft }: { draft: WorkOrderWizardDraft }) {
const summary = buildWizardSidebarSummaryRows(draft);
const statusLabel = draft.status || "Incomplete";
const statusLabel = draft.status || "Pending";
const statusValue = statusLabel as WizardWOStatus;
const statusDotColor = WIZARD_STATUS_DOTS[statusValue] ?? WIZARD_STATUS_DOTS.Incomplete;
const statusDotColor = WIZARD_STATUS_DOTS[statusValue] ?? WIZARD_STATUS_DOTS.Pending;
return (
<div style={{ marginTop: "auto", paddingTop: 44 }}>

View file

@ -194,8 +194,8 @@ export function WizardStepVendorTime({
draft.technicianAssigned === false
? undefined
: vendors.find((v) => String(v.id) === draft.vendorId);
const statusValue = (draft.status || "Incomplete") as WizardWOStatus;
const statusDotColor = WIZARD_STATUS_DOTS[statusValue] ?? WIZARD_STATUS_DOTS.Incomplete;
const statusValue = (draft.status || "Pending") as WizardWOStatus;
const statusDotColor = WIZARD_STATUS_DOTS[statusValue] ?? WIZARD_STATUS_DOTS.Pending;
const selectedDistance = formatVendorDistance(selectedVendor?.distanceMiles);
const closabilityGaps = getClosabilityGaps(wizardDraftToClosabilityInput(draft));

View file

@ -60,7 +60,7 @@ export function useWorkOrdersTableUiState(weekMonday: string) {
setDraft({
...EMPTY_TABLE_DRAFT,
scheduledOn,
status: scheduledOn ? "Scheduled" : "Incomplete",
status: scheduledOn ? "Scheduled" : "Pending",
});
},
[weekMonday],

View file

@ -8,11 +8,7 @@ import {
WORK_ORDER_TYPE_API,
} from "@/domain/work-orders/types/work-order-board";
import type {
WizardWOStatus,
WOType,
WorkOrderWizardDraft,
} from "@/domain/work-orders/types/work-order-wizard";
import type { WOType, WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order-wizard";
import { fmtDateRange } from "@/domain/work-orders/utils/wizard-date-utils";
@ -20,6 +16,7 @@ import { assignWizardScheduleFields } from "@/domain/work-orders/mappers/assign-
import { assignWizardVendorApptFields } from "@/domain/work-orders/mappers/assign-wizard-vendor-appt-fields";
import { assignWizardServicePocFields } from "@/domain/work-orders/mappers/assign-wizard-service-poc-fields";
import { resolveIsAddOnForCreate } from "@/domain/work-orders/utils/wo-addon-cutoff";
import { resolveCreationStatus } from "@/domain/work-orders/utils/wo-lifecycle-transitions";
const WO_TYPE_TO_API: Record<WOType, WorkOrderTypeApi> = {
PM: WORK_ORDER_TYPE_API.PM,
@ -114,7 +111,7 @@ export function mapWizardDraftToBoardCreatePayload(
};
if (draft.status) {
payload.lifecycleStatus = LIFECYCLE_STATUS_API[draft.status as WizardWOStatus];
payload.lifecycleStatus = LIFECYCLE_STATUS_API[resolveCreationStatus(draft)];
}
if (draft.woNumber.trim()) {

View file

@ -175,7 +175,7 @@ export const EMPTY_TABLE_DRAFT: WorkOrderTableDraft = {
techPhone: "",
apptStart: "",
apptEnd: "",
status: "Incomplete",
status: "Pending",
pocName: "",
pocPhone: "",
pocNotes: "",

View file

@ -109,7 +109,7 @@ export const EMPTY_WIZARD_DRAFT: WorkOrderWizardDraft = {
vendorNotes: "",
apptStart: "",
apptEnd: "",
status: "Incomplete",
status: "Pending",
pocName: "",
pocPhone: "",
pocNotes: "",

View file

@ -38,6 +38,8 @@ export const CREATION_INLINE_STATUSES: readonly WizardWOStatus[] = [
export const SCHEDULED_NEEDS_DATE_MSG =
"Set a Schedule On date before marking this work order as Scheduled.";
export const INCOMPLETE_NEEDS_DATE_MSG =
"Set a Schedule On date before marking this work order as Incomplete.";
export const PENDING_NEEDS_NO_DATE_MSG =
"Clear the Schedule On date before marking this work order as Pending.";
@ -134,6 +136,9 @@ export function getStatusOptionState(
status: WizardWOStatus,
closabilityGaps: string[],
): StatusOptionState {
if (status === "Incomplete" && !hasConcreteScheduleDate(row)) {
return { disabled: true, title: INCOMPLETE_NEEDS_DATE_MSG };
}
if (status === "Pending" && hasConcreteScheduleDate(row)) {
return { disabled: true, title: PENDING_NEEDS_NO_DATE_MSG };
}
@ -157,7 +162,7 @@ export function maybeAutoSchedulePatch(
if (hasDate && isPreScheduleStatus(row.status ?? "")) {
return { status: "Scheduled" };
}
if (!hasDate && row.status === "Scheduled") {
if (!hasDate && (row.status === "Scheduled" || row.status === "Incomplete")) {
return { status: "Pending" };
}
return {};
@ -211,5 +216,8 @@ export function resolveCreationStatus(row: ScheduleDateInput & { status: string
if (autoStatus === "Scheduled" || autoStatus === "Pending") {
return autoStatus;
}
return (row.status || "Incomplete") as WizardWOStatus;
if (!row.status || (row.status === "Incomplete" && !hasConcreteScheduleDate(row))) {
return "Pending";
}
return row.status as WizardWOStatus;
}

View file

@ -134,7 +134,7 @@ describe("workOrdersApi.createFromBoard", () => {
expect(apiPost).toHaveBeenCalledWith(API_PATHS.workOrder.boardCreate, {
workOrderType: 2,
siteCode: "BK5",
lifecycleStatus: 1,
lifecycleStatus: 2,
woNumber: "12345",
isAddOn: false,
locationId: 9,

View file

@ -61,7 +61,7 @@ describe("mapWizardDraftToBoardCreatePayload", () => {
isAddOn: false,
lifecycleStatus: 1,
lifecycleStatus: 2,
});
});
@ -162,6 +162,7 @@ describe("mapWizardDraftToBoardCreatePayload", () => {
type: "PM",
siteCode: "BK5",
status: "Scheduled",
scheduledOn: "2026-06-01",
});
expect(payload.lifecycleStatus).toBe(3);
@ -283,7 +284,7 @@ describe("mapWizardDraftToBoardCreatePayload", () => {
expect(payload.scheduledDate).toBeUndefined();
});
it("keeps Incomplete on week-only create and omits scheduledDate", () => {
it("normalizes Incomplete to Pending on week-only create and omits scheduledDate", () => {
const payload = mapWizardDraftToBoardCreatePayload({
...EMPTY_WIZARD_DRAFT,
type: "PM",
@ -293,7 +294,7 @@ describe("mapWizardDraftToBoardCreatePayload", () => {
targetWeek: "2026-06-22",
});
expect(payload.lifecycleStatus).toBe(LIFECYCLE_STATUS_API.Incomplete);
expect(payload.lifecycleStatus).toBe(LIFECYCLE_STATUS_API.Pending);
expect(payload.scheduleWeekOnly).toBe(true);
expect(payload.targetWeek).toBe("2026-06-22");
expect(payload.scheduledDate).toBeUndefined();

View file

@ -6,6 +6,7 @@ import {
COMPLETED_DOC_BLOCK_MSG,
COMPLETED_MEDIA_BLOCK_MSG,
CREATION_INLINE_STATUSES,
INCOMPLETE_NEEDS_DATE_MSG,
PENDING_NEEDS_NO_DATE_MSG,
SCHEDULED_NEEDS_DATE_MSG,
formatAapCompletedBlockTitle,
@ -59,7 +60,17 @@ describe("wo-lifecycle-transitions", () => {
expect(hasConcreteScheduleDate({ scheduledOn: "2026-06-01" })).toBe(true);
});
it("disables Scheduled without a concrete date", () => {
it("disables Incomplete and Scheduled without a concrete date", () => {
expect(getStatusOptionState({ scheduledOn: "" }, "Incomplete", [])).toEqual({
disabled: true,
title: INCOMPLETE_NEEDS_DATE_MSG,
});
expect(
getStatusOptionState({ scheduledOn: "2026-06-01", scheduleWeekOnly: true }, "Incomplete", []),
).toEqual({ disabled: true, title: INCOMPLETE_NEEDS_DATE_MSG });
expect(getStatusOptionState({ scheduledOn: "2026-06-01" }, "Incomplete", [])).toEqual({
disabled: false,
});
expect(getStatusOptionState({ scheduledOn: "" }, "Scheduled", [])).toEqual({
disabled: true,
title: SCHEDULED_NEEDS_DATE_MSG,
@ -102,14 +113,15 @@ describe("wo-lifecycle-transitions", () => {
).toEqual({ status: "Scheduled" });
});
it("reverts Scheduled without a concrete date to Pending", () => {
expect(
maybeAutoSchedulePatch({
status: "Scheduled",
scheduledOn: "",
scheduleWeekOnly: false,
}),
).toEqual({ status: "Pending" });
it("reverts Scheduled or Incomplete without a concrete date to Pending", () => {
for (const status of ["Scheduled", "Incomplete"]) {
expect(maybeAutoSchedulePatch({ status, scheduledOn: "", scheduleWeekOnly: false })).toEqual({
status: "Pending",
});
expect(
maybeAutoSchedulePatch({ status, scheduledOn: "2026-06-01", scheduleWeekOnly: true }),
).toEqual({ status: "Pending" });
}
});
it("does not auto-change In Progress when a date exists", () => {
@ -180,7 +192,7 @@ describe("wo-lifecycle-transitions", () => {
});
});
it("still demotes Scheduled when the schedule is cleared", () => {
it("demotes Scheduled or Incomplete when the schedule is cleared", () => {
const row = {
status: "Scheduled",
scheduledOn: "2026-06-01",
@ -191,6 +203,13 @@ describe("wo-lifecycle-transitions", () => {
scheduleWeekOnly: false,
status: "Pending",
});
expect(withAutoScheduleStatus({ ...row, status: "Incomplete" }, { scheduledOn: "" })).toEqual({
scheduledOn: "",
status: "Pending",
});
expect(
withAutoScheduleStatus({ ...row, status: "Incomplete" }, { scheduleWeekOnly: true }),
).toEqual({ scheduleWeekOnly: true, status: "Pending" });
});
it("promotes a Pending create pick to Scheduled when the draft is dated", () => {