mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 06:53:12 +00:00
Merge pull request #269 from Sea-Haven-Industries/feat/ab/sh-322-overdue-type
feat(work-orders): send Overdue as its own work order type
This commit is contained in:
commit
73f639c271
17 changed files with 263 additions and 73 deletions
|
|
@ -109,7 +109,11 @@ export function AdvancedSearchFilterSections({
|
||||||
/>
|
/>
|
||||||
</AdvSection>
|
</AdvSection>
|
||||||
|
|
||||||
<AdvSection icon={CheckCircle2} label="Status" count={draft.statuses.size}>
|
<AdvSection
|
||||||
|
icon={CheckCircle2}
|
||||||
|
label="Status"
|
||||||
|
count={draft.statuses.size + (draft.pastDue ? 1 : 0)}
|
||||||
|
>
|
||||||
{selectAllRow("statuses", [...ALL_WIZARD_STATUSES], draft.statuses.size)}
|
{selectAllRow("statuses", [...ALL_WIZARD_STATUSES], draft.statuses.size)}
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{ALL_WIZARD_STATUSES.map((status) => (
|
{ALL_WIZARD_STATUSES.map((status) => (
|
||||||
|
|
@ -122,6 +126,14 @@ export function AdvancedSearchFilterSections({
|
||||||
onClick={() => toggleIn("statuses", status)}
|
onClick={() => toggleIn("statuses", status)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
<AdvStatusPill
|
||||||
|
label="Past Due"
|
||||||
|
dotColor="var(--status-pastdue-text)"
|
||||||
|
selected={draft.pastDue}
|
||||||
|
selectedBg="var(--status-pastdue-bg)"
|
||||||
|
selectedText="var(--status-pastdue-text)"
|
||||||
|
onClick={() => setDraft((prev) => ({ ...prev, pastDue: !prev.pastDue }))}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AdvSection>
|
</AdvSection>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,13 @@ const SERVICE_WORK_ORDER_TYPE_API = {
|
||||||
Reactive: 6,
|
Reactive: 6,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const TYPE_TO_API: Record<Exclude<WOType, "Overdue">, number> = {
|
// The registry is keyed on PM, Reactive and Emergency; Overdue is PM work the
|
||||||
|
// client never confirmed, so it lists the PM catalog.
|
||||||
|
const TYPE_TO_API: Record<WOType, number> = {
|
||||||
PM: WORK_ORDER_TYPE_API.PM,
|
PM: WORK_ORDER_TYPE_API.PM,
|
||||||
Reactive: WORK_ORDER_TYPE_API.Reactive,
|
Reactive: WORK_ORDER_TYPE_API.Reactive,
|
||||||
Emergency: WORK_ORDER_TYPE_API.Emergency,
|
Emergency: WORK_ORDER_TYPE_API.Emergency,
|
||||||
|
Overdue: WORK_ORDER_TYPE_API.PM,
|
||||||
};
|
};
|
||||||
|
|
||||||
function toApiInput(input: ServiceInput) {
|
function toApiInput(input: ServiceInput) {
|
||||||
|
|
@ -51,7 +54,6 @@ export const servicesApi = {
|
||||||
},
|
},
|
||||||
|
|
||||||
getActiveForWorkOrderType: async (workOrderType: WOType): Promise<Service[]> => {
|
getActiveForWorkOrderType: async (workOrderType: WOType): Promise<Service[]> => {
|
||||||
if (workOrderType === "Overdue") return [];
|
|
||||||
const data = await apiGet<unknown>(API_PATHS.services.list, {
|
const data = await apiGet<unknown>(API_PATHS.services.list, {
|
||||||
searchParams: { isActive: true, workOrderType: TYPE_TO_API[workOrderType] },
|
searchParams: { isActive: true, workOrderType: TYPE_TO_API[workOrderType] },
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ export function useServicesForWorkOrderType(
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: queryKeys.services.byWorkOrderType(effectiveType),
|
queryKey: queryKeys.services.byWorkOrderType(effectiveType),
|
||||||
queryFn: () => servicesApi.getActiveForWorkOrderType(effectiveType),
|
queryFn: () => servicesApi.getActiveForWorkOrderType(effectiveType),
|
||||||
enabled: effectiveType !== "Overdue",
|
|
||||||
staleTime: 60 * 1000,
|
staleTime: 60 * 1000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { WorkOrderBoardCreatePayload } from "@/domain/work-orders/types/work-order-board";
|
import type { WorkOrderBoardCreatePayload } from "@/domain/work-orders/types/work-order-board";
|
||||||
|
import { typeNeedsSeverity } from "@/domain/work-orders/types/work-order-severity";
|
||||||
import type { WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order-wizard";
|
import type { WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order-wizard";
|
||||||
import { normalizeAdditionalContacts } from "@/domain/work-orders/mappers/additional-contacts-mapper";
|
import { normalizeAdditionalContacts } from "@/domain/work-orders/mappers/additional-contacts-mapper";
|
||||||
|
|
||||||
|
|
@ -64,7 +65,7 @@ export function assignWizardServicePocFields(
|
||||||
payload.vendorNotes = vendorNotes;
|
payload.vendorNotes = vendorNotes;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (draft.severity != null) {
|
if (draft.severity != null && typeNeedsSeverity(draft.type)) {
|
||||||
payload.severity = draft.severity;
|
payload.severity = draft.severity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ const WO_TYPE_TO_API: Record<WOType, WorkOrderTypeApi> = {
|
||||||
|
|
||||||
Reactive: WORK_ORDER_TYPE_API.Reactive,
|
Reactive: WORK_ORDER_TYPE_API.Reactive,
|
||||||
|
|
||||||
Overdue: WORK_ORDER_TYPE_API.Other,
|
Overdue: WORK_ORDER_TYPE_API.Overdue,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Leading token of a location label, e.g. `BK5` from `BK5 · Dallas` or `CHI-1042` from `CHI-1042 — Chicago`. */
|
/** Leading token of a location label, e.g. `BK5` from `BK5 · Dallas` or `CHI-1042` from `CHI-1042 — Chicago`. */
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ export interface WorkOrderAdvancedFilters {
|
||||||
dispatchers: Set<string>;
|
dispatchers: Set<string>;
|
||||||
regions: Set<string>;
|
regions: Set<string>;
|
||||||
statuses: Set<WizardWOStatus>;
|
statuses: Set<WizardWOStatus>;
|
||||||
|
/** Past Due overlay (any type); narrows the other filters, independent of the Overdue type. */
|
||||||
|
pastDue: boolean;
|
||||||
rescheduled: boolean;
|
rescheduled: boolean;
|
||||||
carriedOver: boolean;
|
carriedOver: boolean;
|
||||||
addOn: boolean;
|
addOn: boolean;
|
||||||
|
|
@ -61,6 +63,7 @@ export const EMPTY_ADVANCED_FILTERS: WorkOrderAdvancedFilters = {
|
||||||
dispatchers: new Set(),
|
dispatchers: new Set(),
|
||||||
regions: new Set(),
|
regions: new Set(),
|
||||||
statuses: new Set(),
|
statuses: new Set(),
|
||||||
|
pastDue: false,
|
||||||
rescheduled: false,
|
rescheduled: false,
|
||||||
carriedOver: false,
|
carriedOver: false,
|
||||||
addOn: false,
|
addOn: false,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ export const WORK_ORDER_TYPE_API = {
|
||||||
Inspection: 5,
|
Inspection: 5,
|
||||||
Reactive: 6,
|
Reactive: 6,
|
||||||
AddOn: 7,
|
AddOn: 7,
|
||||||
|
Overdue: 8,
|
||||||
Other: 99,
|
Other: 99,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
@ -64,6 +65,7 @@ export const WORK_ORDER_TYPE_API_TO_FE: Record<number, WOType | ""> = {
|
||||||
[WORK_ORDER_TYPE_API.PM]: "PM",
|
[WORK_ORDER_TYPE_API.PM]: "PM",
|
||||||
[WORK_ORDER_TYPE_API.Reactive]: "Reactive",
|
[WORK_ORDER_TYPE_API.Reactive]: "Reactive",
|
||||||
[WORK_ORDER_TYPE_API.Emergency]: "Emergency",
|
[WORK_ORDER_TYPE_API.Emergency]: "Emergency",
|
||||||
|
[WORK_ORDER_TYPE_API.Overdue]: "Overdue",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LIFECYCLE_STATUS_API_TO_FE: Record<number, WizardWOStatus | "Canceled"> = {
|
export const LIFECYCLE_STATUS_API_TO_FE: Record<number, WizardWOStatus | "Canceled"> = {
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,15 @@ export function appendStatusPmVendorDocChips(
|
||||||
clear: (current) => ({ ...current, statuses: new Set() }),
|
clear: (current) => ({ ...current, statuses: new Set() }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (filters.pastDue) {
|
||||||
|
chips.push({
|
||||||
|
key: "pastdue",
|
||||||
|
group: "Status",
|
||||||
|
value: "Past Due",
|
||||||
|
clear: (current) => ({ ...current, pastDue: false }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pushSetChip({
|
pushSetChip({
|
||||||
chips,
|
chips,
|
||||||
key: "pm",
|
key: "pm",
|
||||||
|
|
|
||||||
|
|
@ -45,12 +45,10 @@ const matchesDateRange: AdvancedFilterPredicate = (row, filters) => {
|
||||||
const matchesSite: AdvancedFilterPredicate = (row, filters) =>
|
const matchesSite: AdvancedFilterPredicate = (row, filters) =>
|
||||||
!filters.sites.size || filters.sites.has(row.site);
|
!filters.sites.size || filters.sites.has(row.site);
|
||||||
|
|
||||||
const matchesType: AdvancedFilterPredicate = (row, filters) => {
|
const matchesType: AdvancedFilterPredicate = (row, filters) =>
|
||||||
if (!filters.types.size) return true;
|
!filters.types.size || filters.types.has(row.type as never);
|
||||||
const overdueMatch = filters.types.has("Overdue") && row.isPastDue;
|
|
||||||
const typeMatch = filters.types.has(row.type as never);
|
const matchesPastDue: AdvancedFilterPredicate = (row, filters) => !filters.pastDue || row.isPastDue;
|
||||||
return overdueMatch || typeMatch;
|
|
||||||
};
|
|
||||||
|
|
||||||
const matchesDispatcher: AdvancedFilterPredicate = (row, filters) => {
|
const matchesDispatcher: AdvancedFilterPredicate = (row, filters) => {
|
||||||
if (!filters.dispatchers.size) return true;
|
if (!filters.dispatchers.size) return true;
|
||||||
|
|
@ -95,6 +93,7 @@ export const ADVANCED_FILTER_PREDICATES: AdvancedFilterPredicate[] = [
|
||||||
matchesType,
|
matchesType,
|
||||||
matchesDispatcher,
|
matchesDispatcher,
|
||||||
matchesStatus,
|
matchesStatus,
|
||||||
|
matchesPastDue,
|
||||||
matchesPmType,
|
matchesPmType,
|
||||||
matchesVendorTech,
|
matchesVendorTech,
|
||||||
matchesDoc,
|
matchesDoc,
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ function cloneFilters(filters: WorkOrderAdvancedFilters): WorkOrderAdvancedFilte
|
||||||
upliftStatuses: new Set(filters.upliftStatuses ?? []),
|
upliftStatuses: new Set(filters.upliftStatuses ?? []),
|
||||||
workOrderIds: new Set(filters.workOrderIds ?? []),
|
workOrderIds: new Set(filters.workOrderIds ?? []),
|
||||||
unscheduledOnly: Boolean(filters.unscheduledOnly),
|
unscheduledOnly: Boolean(filters.unscheduledOnly),
|
||||||
|
pastDue: Boolean(filters.pastDue),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,6 +48,7 @@ const ADVANCED_FILTER_ACTIVE_SIGNALS = (filters: WorkOrderAdvancedFilters): bool
|
||||||
Boolean(filters.dispatchers?.size),
|
Boolean(filters.dispatchers?.size),
|
||||||
Boolean(filters.regions?.size),
|
Boolean(filters.regions?.size),
|
||||||
Boolean(filters.statuses?.size),
|
Boolean(filters.statuses?.size),
|
||||||
|
Boolean(filters.pastDue),
|
||||||
filters.rescheduled,
|
filters.rescheduled,
|
||||||
filters.carriedOver,
|
filters.carriedOver,
|
||||||
filters.addOn,
|
filters.addOn,
|
||||||
|
|
|
||||||
|
|
@ -42,30 +42,19 @@ function mapDispatchers(ids: Iterable<string>): string[] | undefined {
|
||||||
return list.length > 0 ? list : undefined;
|
return list.length > 0 ? list : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Maps UI type filters to API `types` + separate `overdue` flag. Overdue is never sent as types=99. */
|
/** Maps UI type filters to API `types`. The Overdue type is sent as its own enum; past due uses `overdue`. */
|
||||||
function mapWoTypeFilters(types: Iterable<WOType | string>): {
|
function mapWoTypeFilters(types: Iterable<WOType | string>): number[] | undefined {
|
||||||
types?: number[];
|
|
||||||
overdue?: boolean;
|
|
||||||
} {
|
|
||||||
const list: number[] = [];
|
const list: number[] = [];
|
||||||
let overdue = false;
|
|
||||||
for (const type of types) {
|
for (const type of types) {
|
||||||
if (type === "All") {
|
if (type === "All") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (type === "Overdue") {
|
|
||||||
overdue = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const api = mapWizardTypeToApi(type as WOType);
|
const api = mapWizardTypeToApi(type as WOType);
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
list.push(api);
|
list.push(api);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return list.length > 0 ? list : undefined;
|
||||||
types: list.length > 0 ? list : undefined,
|
|
||||||
overdue: overdue || undefined,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStatuses(statuses: Iterable<WizardWOStatus | string>): number[] | undefined {
|
function mapStatuses(statuses: Iterable<WizardWOStatus | string>): number[] | undefined {
|
||||||
|
|
@ -147,14 +136,11 @@ export function toBoardQueryParams(
|
||||||
search: string,
|
search: string,
|
||||||
): WorkOrderBoardQueryParams {
|
): WorkOrderBoardQueryParams {
|
||||||
const [, weekTo] = weekRange(bar.weekMonday);
|
const [, weekTo] = weekRange(bar.weekMonday);
|
||||||
const typeFilters = bar.types.size > 0 ? mapWoTypeFilters(bar.types) : {};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
weekStart: bar.weekMonday,
|
weekStart: bar.weekMonday,
|
||||||
weekEnd: weekTo,
|
weekEnd: weekTo,
|
||||||
dispatchers: mapDispatchers(bar.dispatcherIds),
|
dispatchers: mapDispatchers(bar.dispatcherIds),
|
||||||
types: typeFilters.types,
|
types: mapWoTypeFilters(bar.types),
|
||||||
overdue: typeFilters.overdue,
|
|
||||||
search: search.trim().length >= 2 ? search.trim() : undefined,
|
search: search.trim().length >= 2 ? search.trim() : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -211,7 +197,6 @@ export function toBoardSearchQueryParams(input: {
|
||||||
BOARD_SEARCH_MAX_PAGE_SIZE,
|
BOARD_SEARCH_MAX_PAGE_SIZE,
|
||||||
);
|
);
|
||||||
|
|
||||||
const typeFilters = mapWoTypeFilters(filters.types);
|
|
||||||
const trimmedSearch = input.search?.trim();
|
const trimmedSearch = input.search?.trim();
|
||||||
const search = trimmedSearch && trimmedSearch.length >= 2 ? trimmedSearch : undefined;
|
const search = trimmedSearch && trimmedSearch.length >= 2 ? trimmedSearch : undefined;
|
||||||
|
|
||||||
|
|
@ -224,8 +209,8 @@ export function toBoardSearchQueryParams(input: {
|
||||||
dateTo: date.dateTo,
|
dateTo: date.dateTo,
|
||||||
includeDateless: date.includeDateless,
|
includeDateless: date.includeDateless,
|
||||||
sites: filters.sites.size > 0 ? [...filters.sites] : undefined,
|
sites: filters.sites.size > 0 ? [...filters.sites] : undefined,
|
||||||
types: typeFilters.types,
|
types: mapWoTypeFilters(filters.types),
|
||||||
overdue: typeFilters.overdue,
|
overdue: filters.pastDue || undefined,
|
||||||
dispatchers: mapDispatchers(filters.dispatchers),
|
dispatchers: mapDispatchers(filters.dispatchers),
|
||||||
regions: filters.regions.size > 0 ? [...filters.regions] : undefined,
|
regions: filters.regions.size > 0 ? [...filters.regions] : undefined,
|
||||||
statuses: mapStatuses(filters.statuses),
|
statuses: mapStatuses(filters.statuses),
|
||||||
|
|
@ -261,10 +246,6 @@ export function buildBoardSearchParamsRecord(
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function woTypeFilterIsOverdue(type: string): boolean {
|
|
||||||
return type === "Overdue";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Week-independent GET /board/search params for the pinned Unassigned queue. */
|
/** Week-independent GET /board/search params for the pinned Unassigned queue. */
|
||||||
export function toUnassignedQueueQueryParams(
|
export function toUnassignedQueueQueryParams(
|
||||||
page = 0,
|
page = 0,
|
||||||
|
|
|
||||||
|
|
@ -65,12 +65,7 @@ function rowMatchesSearch(row: WorkOrderTableRow, query: string): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
function rowMatchesBarTypes(row: WorkOrderTableRow, types: Set<WOType>): boolean {
|
function rowMatchesBarTypes(row: WorkOrderTableRow, types: Set<WOType>): boolean {
|
||||||
if (types.size === 0) {
|
return types.size === 0 || types.has(row.type as WOType);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const overdueMatch = types.has("Overdue") && row.isPastDue;
|
|
||||||
const typeMatch = types.has(row.type as WOType);
|
|
||||||
return overdueMatch || typeMatch;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function filterByBarAndSearch(
|
export function filterByBarAndSearch(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { SlideOverCompletionTab } from "@/app/(protected)/workorders/_components/detail/tabs/slide-over-completion-tab";
|
||||||
|
import { AdvancedSearchFilterSections } from "@/app/(protected)/workorders/_components/list/advanced-search/adv-filter-sections";
|
||||||
|
import { TypeCell } from "@/app/(protected)/workorders/_components/list/table/cells/type-cell";
|
||||||
|
import { WizardStepTypeSchedule } from "@/app/(protected)/workorders/_components/wizard/wizard-step-type-schedule";
|
||||||
|
import { mapWorkOrderTableRow } from "@/domain/work-orders/mappers/work-order-table-row-mapper";
|
||||||
|
import { mapWizardDraftToBoardCreatePayload } from "@/domain/work-orders/mappers/work-order-board-mapper";
|
||||||
|
import {
|
||||||
|
EMPTY_ADVANCED_FILTERS,
|
||||||
|
type WorkOrderAdvancedFilters,
|
||||||
|
} from "@/domain/work-orders/types/work-order-advanced-filters";
|
||||||
|
import { WORK_ORDER_TYPE_API } from "@/domain/work-orders/types/work-order-board";
|
||||||
|
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
import {
|
||||||
|
canContinueWizardStep1,
|
||||||
|
EMPTY_WIZARD_DRAFT,
|
||||||
|
type WorkOrderWizardDraft,
|
||||||
|
} from "@/domain/work-orders/types/work-order-wizard";
|
||||||
|
import { buildAdvancedFilterChips } from "@/domain/work-orders/utils/advanced-filters-utils";
|
||||||
|
import { getClosabilityGaps } from "@/domain/work-orders/utils/wo-closability";
|
||||||
|
|
||||||
|
const USERS = [{ id: "sarah", name: "Sarah Chen" }];
|
||||||
|
|
||||||
|
describe("Overdue work order type", () => {
|
||||||
|
it("offers Overdue in the Create WO wizard and never asks for a severity", () => {
|
||||||
|
const onPatch = vi.fn();
|
||||||
|
const { rerender } = render(
|
||||||
|
<WizardStepTypeSchedule
|
||||||
|
draft={{ ...EMPTY_WIZARD_DRAFT, type: "PM" }}
|
||||||
|
users={USERS}
|
||||||
|
onUpdate={vi.fn()}
|
||||||
|
onPatch={onPatch}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /No client response — due date passed/ }));
|
||||||
|
expect(onPatch).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ type: "Overdue", severity: null }),
|
||||||
|
);
|
||||||
|
|
||||||
|
const overdueDraft: WorkOrderWizardDraft = { ...EMPTY_WIZARD_DRAFT, type: "Overdue" };
|
||||||
|
rerender(
|
||||||
|
<WizardStepTypeSchedule
|
||||||
|
draft={overdueDraft}
|
||||||
|
users={USERS}
|
||||||
|
onUpdate={vi.fn()}
|
||||||
|
onPatch={onPatch}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.queryByText(/Severity \(from APM\)/)).not.toBeInTheDocument();
|
||||||
|
expect(canContinueWizardStep1(overdueDraft)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates the work order with the Overdue enum and no severity", () => {
|
||||||
|
const payload = mapWizardDraftToBoardCreatePayload({
|
||||||
|
...EMPTY_WIZARD_DRAFT,
|
||||||
|
type: "Overdue",
|
||||||
|
severity: 2,
|
||||||
|
locationId: "1",
|
||||||
|
siteCode: "BK5",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(payload.workOrderType).toBe(WORK_ORDER_TYPE_API.Overdue);
|
||||||
|
expect(payload.severity ?? null).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("corrects a type inline to Overdue without opening the severity dialog", async () => {
|
||||||
|
const onSave = vi.fn();
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<TypeCell value="Reactive" severity={3} onSave={onSave} />);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Type of WO, Reactive" }));
|
||||||
|
await user.click(screen.getByRole("button", { name: /^Overdue$/ }));
|
||||||
|
|
||||||
|
expect(onSave).toHaveBeenCalledWith("Overdue", null);
|
||||||
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reads the Overdue enum back from the board as the Overdue type", () => {
|
||||||
|
const row = mapWorkOrderTableRow({ id: 1, workOrderType: WORK_ORDER_TYPE_API.Overdue });
|
||||||
|
expect(row.type).toBe("Overdue");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("closes with the completion document, not photos", () => {
|
||||||
|
const base = {
|
||||||
|
id: "1",
|
||||||
|
dispatcherId: "sarah",
|
||||||
|
dispatcherName: "Sarah Chen",
|
||||||
|
company: "Gateway Plumbing",
|
||||||
|
vendorId: "",
|
||||||
|
tech: "",
|
||||||
|
completedDate: "2026-09-24",
|
||||||
|
woNumber: "24971",
|
||||||
|
pm: "Backflow Preventers",
|
||||||
|
mediaCount: 0,
|
||||||
|
};
|
||||||
|
expect(
|
||||||
|
getClosabilityGaps({ ...base, type: "Overdue" as const, docStatus: "No" as const }),
|
||||||
|
).toEqual(["Completion document"]);
|
||||||
|
expect(
|
||||||
|
getClosabilityGaps({ ...base, type: "Overdue" as const, docStatus: "Yes" as const }),
|
||||||
|
).toEqual([]);
|
||||||
|
|
||||||
|
const row = {
|
||||||
|
id: "1",
|
||||||
|
type: "Overdue",
|
||||||
|
pm: "Backflow Preventers",
|
||||||
|
docStatus: "No",
|
||||||
|
rowVersion: "1",
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<SlideOverCompletionTab
|
||||||
|
row={row as WorkOrderTableRow}
|
||||||
|
isReadOnly={false}
|
||||||
|
media={[]}
|
||||||
|
onOpenCompletionDoc={vi.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText("Completion Document")).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("button", { name: /open completion document/i })).toBeEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a separate Past Due status toggle in the advanced filters", () => {
|
||||||
|
let draft: WorkOrderAdvancedFilters = { ...EMPTY_ADVANCED_FILTERS };
|
||||||
|
const setDraft = vi.fn(
|
||||||
|
(update: (prev: WorkOrderAdvancedFilters) => WorkOrderAdvancedFilters) => {
|
||||||
|
draft = update(draft);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
render(
|
||||||
|
<AdvancedSearchFilterSections
|
||||||
|
draft={draft}
|
||||||
|
setDraft={setDraft as never}
|
||||||
|
toggleIn={vi.fn()}
|
||||||
|
setMany={vi.fn()}
|
||||||
|
sites={[]}
|
||||||
|
users={[]}
|
||||||
|
pmTypes={[]}
|
||||||
|
vendorTechs={[]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /^Status/ }));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /Past Due/ }));
|
||||||
|
|
||||||
|
expect(draft.pastDue).toBe(true);
|
||||||
|
expect(draft.types.size).toBe(0);
|
||||||
|
expect(buildAdvancedFilterChips(draft)).toContainEqual(
|
||||||
|
expect.objectContaining({ group: "Status", value: "Past Due" }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -78,11 +78,14 @@ describe("servicesApi", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not request services for the unsupported Overdue type", async () => {
|
it("lists the PM catalog for the Overdue type", async () => {
|
||||||
const services = await servicesApi.getActiveForWorkOrderType("Overdue");
|
vi.mocked(apiGet).mockResolvedValue([]);
|
||||||
|
|
||||||
expect(apiGet).not.toHaveBeenCalled();
|
await servicesApi.getActiveForWorkOrderType("Overdue");
|
||||||
expect(services).toEqual([]);
|
|
||||||
|
expect(apiGet).toHaveBeenCalledWith(API_PATHS.services.list, {
|
||||||
|
searchParams: { isActive: true, workOrderType: 2 },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates, updates, and deactivates through the registry routes", async () => {
|
it("creates, updates, and deactivates through the registry routes", async () => {
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,9 @@ describe("mapWizardTypeToApi", () => {
|
||||||
|
|
||||||
expect(mapWizardTypeToApi("Reactive")).toBe(WORK_ORDER_TYPE_API.Reactive);
|
expect(mapWizardTypeToApi("Reactive")).toBe(WORK_ORDER_TYPE_API.Reactive);
|
||||||
|
|
||||||
expect(mapWizardTypeToApi("Overdue")).toBe(WORK_ORDER_TYPE_API.Other);
|
expect(mapWizardTypeToApi("Overdue")).toBe(WORK_ORDER_TYPE_API.Overdue);
|
||||||
|
|
||||||
|
expect(WORK_ORDER_TYPE_API.Overdue).toBe(8);
|
||||||
|
|
||||||
expect(mapWizardTypeToApi("")).toBeNull();
|
expect(mapWizardTypeToApi("")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -60,16 +60,16 @@ describe("toBoardQueryParams", () => {
|
||||||
expect(params.overdue).toBeUndefined();
|
expect(params.overdue).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("maps Overdue bar filter to overdue=true without types", () => {
|
it("maps the Overdue bar filter to the Overdue type, not the past-due flag", () => {
|
||||||
const params = toBoardQueryParams({ ...baseBar, types: new Set(["Overdue"]) }, "");
|
const params = toBoardQueryParams({ ...baseBar, types: new Set(["Overdue"]) }, "");
|
||||||
expect(params.overdue).toBe(true);
|
expect(params.types).toEqual([WORK_ORDER_TYPE_API.Overdue]);
|
||||||
expect(params.types).toBeUndefined();
|
expect(params.overdue).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("maps Overdue with real types as OR (types + overdue)", () => {
|
it("maps Overdue with other types as a plain type union", () => {
|
||||||
const params = toBoardQueryParams({ ...baseBar, types: new Set(["PM", "Overdue"]) }, "");
|
const params = toBoardQueryParams({ ...baseBar, types: new Set(["PM", "Overdue"]) }, "");
|
||||||
expect(params.types).toEqual([WORK_ORDER_TYPE_API.PM]);
|
expect(params.types).toEqual([WORK_ORDER_TYPE_API.PM, WORK_ORDER_TYPE_API.Overdue]);
|
||||||
expect(params.overdue).toBe(true);
|
expect(params.overdue).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("omits types when the set is empty", () => {
|
it("omits types when the set is empty", () => {
|
||||||
|
|
@ -222,22 +222,23 @@ describe("toBoardSearchQueryParams", () => {
|
||||||
expect(buildBoardSearchParamsRecord(params)).not.toHaveProperty("unscheduledOnly");
|
expect(buildBoardSearchParamsRecord(params)).not.toHaveProperty("unscheduledOnly");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("maps Overdue-only advanced filter to overdue=true without types", () => {
|
it("maps the Overdue type advanced filter to types without the past-due flag", () => {
|
||||||
const params = toBoardSearchQueryParams({
|
const params = toBoardSearchQueryParams({
|
||||||
filters: {
|
filters: {
|
||||||
...EMPTY_ADVANCED_FILTERS,
|
...EMPTY_ADVANCED_FILTERS,
|
||||||
types: new Set(["Overdue"]),
|
types: new Set(["Overdue"]),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(params.overdue).toBe(true);
|
expect(params.types).toEqual([WORK_ORDER_TYPE_API.Overdue]);
|
||||||
expect(params.types).toBeUndefined();
|
expect(params.overdue).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("maps Overdue with real types as OR (types + overdue)", () => {
|
it("maps the Past Due status toggle to overdue=true alongside types", () => {
|
||||||
const params = toBoardSearchQueryParams({
|
const params = toBoardSearchQueryParams({
|
||||||
filters: {
|
filters: {
|
||||||
...EMPTY_ADVANCED_FILTERS,
|
...EMPTY_ADVANCED_FILTERS,
|
||||||
types: new Set(["PM", "Overdue"]),
|
types: new Set(["PM"]),
|
||||||
|
pastDue: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(params.types).toEqual([WORK_ORDER_TYPE_API.PM]);
|
expect(params.types).toEqual([WORK_ORDER_TYPE_API.PM]);
|
||||||
|
|
|
||||||
|
|
@ -172,26 +172,26 @@ describe("filterByBarAndSearch", () => {
|
||||||
expect(ids).toEqual(["em", "rx"]);
|
expect(ids).toEqual(["em", "rx"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("matches Overdue via isPastDue even when row.type is not Overdue", () => {
|
it("matches the Overdue type only, never a past-due row of another type", () => {
|
||||||
const pastDuePm = makeRow({
|
const pastDuePm = makeRow({
|
||||||
id: "pd",
|
id: "pd",
|
||||||
type: "PM",
|
type: "PM",
|
||||||
isPastDue: true,
|
isPastDue: true,
|
||||||
scheduledOn: "2026-06-03",
|
scheduledOn: "2026-06-03",
|
||||||
});
|
});
|
||||||
const currentPm = makeRow({
|
const overdue = makeRow({
|
||||||
id: "now",
|
id: "ov",
|
||||||
type: "PM",
|
type: "Overdue",
|
||||||
isPastDue: false,
|
isPastDue: false,
|
||||||
scheduledOn: "2026-06-03",
|
scheduledOn: "2026-06-03",
|
||||||
});
|
});
|
||||||
|
|
||||||
const ids = filterByBarAndSearch(
|
const ids = filterByBarAndSearch(
|
||||||
[pastDuePm, currentPm],
|
[pastDuePm, overdue],
|
||||||
bar(weekMonday, { types: new Set(["Overdue"]) }),
|
bar(weekMonday, { types: new Set(["Overdue"]) }),
|
||||||
"",
|
"",
|
||||||
).map((row) => row.id);
|
).map((row) => row.id);
|
||||||
expect(ids).toEqual(["pd"]);
|
expect(ids).toEqual(["ov"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("hides a row after type correction when the bar filter no longer matches", () => {
|
it("hides a row after type correction when the bar filter no longer matches", () => {
|
||||||
|
|
@ -395,13 +395,8 @@ describe("matchesAdvancedFilters", () => {
|
||||||
expect(matchesAdvancedFilters(row, noMatch)).toBe(false);
|
expect(matchesAdvancedFilters(row, noMatch)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("matches Overdue when isPastDue is true", () => {
|
it("matches the Overdue type filter by type, independent of isPastDue", () => {
|
||||||
const weekMonday = mondayOf(todayIso());
|
const weekMonday = mondayOf(todayIso());
|
||||||
const pastDue = makeRow({
|
|
||||||
scheduledOn: weekMonday,
|
|
||||||
...deriveDayFields(weekMonday),
|
|
||||||
isPastDue: true,
|
|
||||||
});
|
|
||||||
const filters = {
|
const filters = {
|
||||||
...EMPTY_ADVANCED_FILTERS,
|
...EMPTY_ADVANCED_FILTERS,
|
||||||
dateRange: "custom" as const,
|
dateRange: "custom" as const,
|
||||||
|
|
@ -409,9 +404,39 @@ describe("matchesAdvancedFilters", () => {
|
||||||
customTo: "9999-12-31",
|
customTo: "9999-12-31",
|
||||||
types: new Set(["Overdue" as const]),
|
types: new Set(["Overdue" as const]),
|
||||||
};
|
};
|
||||||
expect(matchesAdvancedFilters(pastDue, filters)).toBe(true);
|
const overdue = makeRow({ type: "Overdue", isPastDue: false, scheduledOn: weekMonday });
|
||||||
|
const pastDuePm = makeRow({ type: "PM", isPastDue: true, scheduledOn: weekMonday });
|
||||||
|
expect(matchesAdvancedFilters(overdue, filters)).toBe(true);
|
||||||
|
expect(matchesAdvancedFilters(pastDuePm, filters)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("narrows to past-due rows with the Past Due status toggle", () => {
|
||||||
|
const weekMonday = mondayOf(todayIso());
|
||||||
|
const filters = {
|
||||||
|
...EMPTY_ADVANCED_FILTERS,
|
||||||
|
dateRange: "custom" as const,
|
||||||
|
customFrom: "0000-01-01",
|
||||||
|
customTo: "9999-12-31",
|
||||||
|
types: new Set(["PM" as const]),
|
||||||
|
pastDue: true,
|
||||||
|
};
|
||||||
expect(
|
expect(
|
||||||
matchesAdvancedFilters(makeRow({ isPastDue: false, scheduledOn: weekMonday }), filters),
|
matchesAdvancedFilters(
|
||||||
|
makeRow({ type: "PM", isPastDue: true, scheduledOn: weekMonday }),
|
||||||
|
filters,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
matchesAdvancedFilters(
|
||||||
|
makeRow({ type: "PM", isPastDue: false, scheduledOn: weekMonday }),
|
||||||
|
filters,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
matchesAdvancedFilters(
|
||||||
|
makeRow({ type: "Overdue", isPastDue: true, scheduledOn: weekMonday }),
|
||||||
|
filters,
|
||||||
|
),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue