mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 06:53:12 +00:00
test(work-orders): cover board filters and wizard mappers
This commit is contained in:
parent
df8f3553e8
commit
e446bbae0e
4 changed files with 383 additions and 0 deletions
|
|
@ -0,0 +1,92 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { mapWizardDraftToFormValues } from "@/domain/work-orders/mappers/wizard-draft-mapper";
|
||||
import {
|
||||
EMPTY_WIZARD_DRAFT,
|
||||
canContinueWizardStep1,
|
||||
canCreateWorkOrderFromWizard,
|
||||
isWizardStepValid,
|
||||
} from "@/domain/work-orders/types/work-order-wizard";
|
||||
|
||||
describe("mapWizardDraftToFormValues", () => {
|
||||
it("maps minimal draft with type and site to valid form values", () => {
|
||||
const draft = {
|
||||
...EMPTY_WIZARD_DRAFT,
|
||||
type: "PM" as const,
|
||||
locationId: "42",
|
||||
locationName: "DFW8 · Dallas, TX",
|
||||
woNumber: "24818",
|
||||
assigneeName: "Sarah Chen",
|
||||
status: "Incomplete" as const,
|
||||
};
|
||||
|
||||
const values = mapWizardDraftToFormValues(draft);
|
||||
|
||||
expect(values.title).toBe("PM — #24818");
|
||||
expect(values.workOrderNumber).toBe("24818");
|
||||
expect(values.locationId).toBe("42");
|
||||
expect(values.assignedTo).toBe("Sarah Chen");
|
||||
expect(values.status).toBe("Open");
|
||||
expect(values.priority).toBe("Sev 3");
|
||||
expect(values.source).toBe("PM");
|
||||
expect(values.createdDate).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
||||
});
|
||||
|
||||
it("maps emergency type to Sev 1 priority", () => {
|
||||
const values = mapWizardDraftToFormValues({
|
||||
...EMPTY_WIZARD_DRAFT,
|
||||
type: "Emergency",
|
||||
locationId: "1",
|
||||
locationName: "Site A",
|
||||
});
|
||||
|
||||
expect(values.priority).toBe("Sev 1");
|
||||
expect(values.title).toBe("Emergency — Site A");
|
||||
});
|
||||
|
||||
it("includes appointment and notes in description", () => {
|
||||
const values = mapWizardDraftToFormValues({
|
||||
...EMPTY_WIZARD_DRAFT,
|
||||
type: "Reactive",
|
||||
locationId: "1",
|
||||
locationName: "Site A",
|
||||
apptStart: "09:00",
|
||||
apptEnd: "11:00",
|
||||
pocNotes: "Call ahead",
|
||||
vendorName: "Roto-Rooter",
|
||||
vendorNotes: "Prefers morning",
|
||||
});
|
||||
|
||||
expect(values.description).toContain("Appt:");
|
||||
expect(values.description).toContain("POC notes: Call ahead");
|
||||
expect(values.description).toContain("Vendor: Roto-Rooter");
|
||||
});
|
||||
});
|
||||
|
||||
describe("wizard step validation", () => {
|
||||
it("blocks step 1 continue without type", () => {
|
||||
expect(canContinueWizardStep1(EMPTY_WIZARD_DRAFT)).toBe(false);
|
||||
expect(isWizardStepValid(1, EMPTY_WIZARD_DRAFT)).toBe(false);
|
||||
});
|
||||
|
||||
it("allows step 1 continue when type is selected", () => {
|
||||
const draft = { ...EMPTY_WIZARD_DRAFT, type: "PM" as const };
|
||||
expect(canContinueWizardStep1(draft)).toBe(true);
|
||||
expect(isWizardStepValid(1, draft)).toBe(true);
|
||||
});
|
||||
|
||||
it("blocks create without location", () => {
|
||||
const draft = { ...EMPTY_WIZARD_DRAFT, type: "PM" as const };
|
||||
expect(canCreateWorkOrderFromWizard(draft)).toBe(false);
|
||||
expect(isWizardStepValid(2, draft)).toBe(false);
|
||||
});
|
||||
|
||||
it("allows create when location is selected", () => {
|
||||
const draft = {
|
||||
...EMPTY_WIZARD_DRAFT,
|
||||
type: "PM" as const,
|
||||
locationId: "99",
|
||||
};
|
||||
expect(canCreateWorkOrderFromWizard(draft)).toBe(true);
|
||||
expect(isWizardStepValid(2, draft)).toBe(true);
|
||||
});
|
||||
});
|
||||
103
src/test/domain/work-orders/utils/advanced-filters-utils.test.ts
Normal file
103
src/test/domain/work-orders/utils/advanced-filters-utils.test.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
ASSIGNEE_FILTER_UNASSIGNED,
|
||||
EMPTY_ADVANCED_FILTERS,
|
||||
type WorkOrderAdvancedFilters,
|
||||
} from "@/domain/work-orders/types/work-order-advanced-filters";
|
||||
import {
|
||||
buildAdvancedFilterChips,
|
||||
clearAdvancedFilterGroup,
|
||||
cloneAdvancedFilters,
|
||||
countAdvancedFilters,
|
||||
} from "@/domain/work-orders/utils/advanced-filters-utils";
|
||||
|
||||
function withSites(...sites: string[]): WorkOrderAdvancedFilters {
|
||||
return {
|
||||
...cloneAdvancedFilters(EMPTY_ADVANCED_FILTERS),
|
||||
sites: new Set(sites),
|
||||
};
|
||||
}
|
||||
|
||||
describe("countAdvancedFilters", () => {
|
||||
it("returns 0 for default empty filters", () => {
|
||||
expect(countAdvancedFilters(EMPTY_ADVANCED_FILTERS)).toBe(0);
|
||||
});
|
||||
|
||||
it("counts non-default date range as one active group", () => {
|
||||
const filters = {
|
||||
...cloneAdvancedFilters(EMPTY_ADVANCED_FILTERS),
|
||||
dateRange: "last-week" as const,
|
||||
};
|
||||
expect(countAdvancedFilters(filters)).toBe(1);
|
||||
});
|
||||
|
||||
it("counts each active filter group once", () => {
|
||||
const filters = {
|
||||
...withSites("1", "2"),
|
||||
types: new Set(["PM" as const]),
|
||||
statuses: new Set(["Pending" as const]),
|
||||
};
|
||||
expect(countAdvancedFilters(filters)).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildAdvancedFilterChips", () => {
|
||||
it("shows site labels when two or fewer sites are selected", () => {
|
||||
const chips = buildAdvancedFilterChips(withSites("1"), {
|
||||
siteNameOf: (id) => (id === "1" ? "DFW8" : id),
|
||||
});
|
||||
|
||||
expect(chips).toHaveLength(1);
|
||||
expect(chips[0]).toMatchObject({
|
||||
key: "sites",
|
||||
group: "Site",
|
||||
value: "DFW8",
|
||||
});
|
||||
});
|
||||
|
||||
it("shows a count badge when more than two sites are selected", () => {
|
||||
const chips = buildAdvancedFilterChips(withSites("1", "2", "3"));
|
||||
|
||||
expect(chips).toHaveLength(1);
|
||||
expect(chips[0]).toMatchObject({
|
||||
key: "sites",
|
||||
group: "Site",
|
||||
count: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it("creates a separate unassigned chip for dispatchers", () => {
|
||||
const filters = {
|
||||
...cloneAdvancedFilters(EMPTY_ADVANCED_FILTERS),
|
||||
dispatchers: new Set([ASSIGNEE_FILTER_UNASSIGNED, "42"]),
|
||||
};
|
||||
|
||||
const chips = buildAdvancedFilterChips(filters, {
|
||||
dispatcherNameOf: (id) => (id === "42" ? "Sarah Chen" : id),
|
||||
});
|
||||
|
||||
expect(
|
||||
chips.some((chip) => chip.key === "disp-unassigned" && chip.value === "Unassigned"),
|
||||
).toBe(true);
|
||||
expect(chips.some((chip) => chip.key === "disp" && chip.value === "Sarah Chen")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearAdvancedFilterGroup", () => {
|
||||
it("removes only the targeted filter group", () => {
|
||||
const filters = {
|
||||
...withSites("1"),
|
||||
types: new Set(["PM" as const]),
|
||||
};
|
||||
|
||||
const next = clearAdvancedFilterGroup(filters, "sites");
|
||||
|
||||
expect(next?.sites.size).toBe(0);
|
||||
expect(next?.types.has("PM")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns null when clearing the last active group", () => {
|
||||
const next = clearAdvancedFilterGroup(withSites("1"), "sites");
|
||||
expect(next).toBeNull();
|
||||
});
|
||||
});
|
||||
56
src/test/domain/work-orders/utils/list-week-utils.test.ts
Normal file
56
src/test/domain/work-orders/utils/list-week-utils.test.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildWeekWindow,
|
||||
compactWeekLabel,
|
||||
currentWeekMonday,
|
||||
isFutureWeek,
|
||||
nextWeekMonday,
|
||||
weekRange,
|
||||
} from "@/domain/work-orders/utils/list-week-utils";
|
||||
import { addDaysIso, mondayOf } from "@/domain/work-orders/utils/wizard-date-utils";
|
||||
|
||||
describe("list-week-utils", () => {
|
||||
it("currentWeekMonday returns a Monday ISO date", () => {
|
||||
const monday = currentWeekMonday();
|
||||
expect(monday).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
||||
expect(mondayOf(monday)).toBe(monday);
|
||||
});
|
||||
|
||||
it("compactWeekLabel formats Mon–Fri range", () => {
|
||||
expect(compactWeekLabel("2026-06-01")).toBe("6/1–6/5");
|
||||
expect(compactWeekLabel("2026-05-25")).toBe("5/25–5/29");
|
||||
});
|
||||
|
||||
it("weekRange returns Monday through Friday", () => {
|
||||
expect(weekRange("2026-06-01")).toEqual(["2026-06-01", "2026-06-05"]);
|
||||
});
|
||||
|
||||
it("buildWeekWindow generates ordered weeks around anchor", () => {
|
||||
const weeks = buildWeekWindow("2026-06-01", 2, 2);
|
||||
expect(weeks.length).toBeGreaterThanOrEqual(3);
|
||||
expect(weeks[0].mondayIso).toBe("2026-05-18");
|
||||
expect(weeks.some((week) => week.mondayIso === "2026-06-01")).toBe(true);
|
||||
expect(weeks.every((week) => week.label.length > 0)).toBe(true);
|
||||
});
|
||||
|
||||
it("buildWeekWindow always includes the next week even when it is in the future", () => {
|
||||
const upcoming = nextWeekMonday();
|
||||
const weeks = buildWeekWindow(currentWeekMonday(), 2, 0);
|
||||
expect(weeks.some((week) => week.mondayIso === upcoming)).toBe(true);
|
||||
if (isFutureWeek(upcoming)) {
|
||||
expect(isFutureWeek(upcoming)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("buildWeekWindow always includes the current live week", () => {
|
||||
const liveMonday = currentWeekMonday();
|
||||
const weeks = buildWeekWindow("2000-01-03", 2, 0);
|
||||
expect(weeks.some((week) => week.mondayIso === liveMonday && week.live)).toBe(true);
|
||||
});
|
||||
|
||||
it("isFutureWeek identifies weeks after the current week", () => {
|
||||
const live = currentWeekMonday();
|
||||
expect(isFutureWeek(live)).toBe(false);
|
||||
expect(isFutureWeek("2099-01-05")).toBe(true);
|
||||
});
|
||||
});
|
||||
132
src/test/domain/work-orders/utils/table-filter-utils.test.ts
Normal file
132
src/test/domain/work-orders/utils/table-filter-utils.test.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||
import { EMPTY_ADVANCED_FILTERS } from "@/domain/work-orders/types/work-order-advanced-filters";
|
||||
import {
|
||||
buildDayGroups,
|
||||
matchesAdvancedFilters,
|
||||
splitScheduledUnscheduled,
|
||||
} from "@/domain/work-orders/utils/table-filter-utils";
|
||||
import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils";
|
||||
import { mapWorkOrderTableRow } from "@/domain/work-orders/mappers/work-order-mapper";
|
||||
|
||||
function makeRow(overrides: Partial<WorkOrderTableRow> = {}): WorkOrderTableRow {
|
||||
return {
|
||||
id: "1",
|
||||
woNumber: "12345678901",
|
||||
site: "SITE01",
|
||||
type: "PM",
|
||||
dispatcherId: "u1",
|
||||
dispatcherName: "Alice",
|
||||
scheduledOn: "2026-06-23",
|
||||
targetWeek: "",
|
||||
dayGroup: "06/23",
|
||||
dayLabel: "TUE · 06/23",
|
||||
location: "Main St",
|
||||
locationId: "10",
|
||||
pm: "HVAC",
|
||||
company: "Acme",
|
||||
tech: "Bob",
|
||||
techPhone: "",
|
||||
apptStart: "",
|
||||
apptEnd: "",
|
||||
apptTime: "",
|
||||
status: "Scheduled",
|
||||
docStatus: "No",
|
||||
pocName: "",
|
||||
pocPhone: "",
|
||||
pocNotes: "",
|
||||
dueDate: "2026-06-30",
|
||||
rescheduleCount: 0,
|
||||
carriedOver: 0,
|
||||
originalWeek: "",
|
||||
originalDate: "",
|
||||
isPastDue: false,
|
||||
scheduleWeekOnly: false,
|
||||
mediaCount: 0,
|
||||
title: "Test WO",
|
||||
priority: "Sev 3",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("deriveDayFields", () => {
|
||||
it("derives MM/DD group and weekday label", () => {
|
||||
expect(deriveDayFields("2026-06-23")).toEqual({
|
||||
dayGroup: "06/23",
|
||||
dayLabel: "TUE · 06/23",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("splitScheduledUnscheduled", () => {
|
||||
it("splits rows without scheduledOn into unscheduled", () => {
|
||||
const scheduled = makeRow({ id: "a", scheduledOn: "2026-06-23" });
|
||||
const unsched = makeRow({ id: "b", scheduledOn: "" });
|
||||
const result = splitScheduledUnscheduled([scheduled, unsched]);
|
||||
expect(result.scheduled).toHaveLength(1);
|
||||
expect(result.unscheduled).toHaveLength(1);
|
||||
expect(result.unscheduled[0].id).toBe("b");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildDayGroups", () => {
|
||||
it("groups by dayGroup and sorts chronologically", () => {
|
||||
const later = makeRow({
|
||||
id: "2",
|
||||
scheduledOn: "2026-06-24",
|
||||
dayGroup: "06/24",
|
||||
dayLabel: "WED · 06/24",
|
||||
});
|
||||
const earlier = makeRow({
|
||||
id: "1",
|
||||
scheduledOn: "2026-06-23",
|
||||
dayGroup: "06/23",
|
||||
dayLabel: "TUE · 06/23",
|
||||
});
|
||||
const groups = buildDayGroups([later, earlier]);
|
||||
expect(groups).toHaveLength(2);
|
||||
expect(groups[0].key).toBe("06/23");
|
||||
expect(groups[1].key).toBe("06/24");
|
||||
});
|
||||
});
|
||||
|
||||
describe("matchesAdvancedFilters", () => {
|
||||
it("filters by site and type", () => {
|
||||
const row = makeRow({ site: "SITE01", type: "PM" });
|
||||
const filters = {
|
||||
...EMPTY_ADVANCED_FILTERS,
|
||||
sites: new Set(["SITE01"]),
|
||||
types: new Set(["PM" as const]),
|
||||
};
|
||||
expect(matchesAdvancedFilters(row, filters)).toBe(true);
|
||||
|
||||
const noMatch = {
|
||||
...filters,
|
||||
types: new Set(["Emergency" as const]),
|
||||
};
|
||||
expect(matchesAdvancedFilters(row, noMatch)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapWorkOrderTableRow", () => {
|
||||
it("maps API list fields to table row", () => {
|
||||
const row = mapWorkOrderTableRow({
|
||||
id: 42,
|
||||
internalWONumber: "12345678901",
|
||||
title: "Leak repair",
|
||||
location: "Building A",
|
||||
status: "Open",
|
||||
scheduledDate: "2026-06-23",
|
||||
dueDate: "2026-06-30",
|
||||
assignedTo: "Alice",
|
||||
problem: "Plumbing",
|
||||
source: "Reactive",
|
||||
});
|
||||
expect(row.id).toBe(42);
|
||||
expect(row.woNumber).toBe("12345678901");
|
||||
expect(row.scheduledOn).toBe("2026-06-23");
|
||||
expect(row.dayGroup).toBe("06/23");
|
||||
expect(row.type).toBe("Reactive");
|
||||
expect(row.status).toBe("Incomplete");
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue