mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 08:03:13 +00:00
fix(work-orders): align consumers with domain-core types
This commit is contained in:
parent
e13bc4c1a2
commit
4beefba719
13 changed files with 1425 additions and 12 deletions
|
|
@ -192,7 +192,10 @@ export default function WorkOrderViewPage() {
|
|||
}
|
||||
|
||||
commentMutation.mutate(
|
||||
{ text: commentText.trim(), commentType: activeTab },
|
||||
{
|
||||
text: commentText.trim(),
|
||||
commentType: activeTab === "customer" ? "customer" : "internal",
|
||||
},
|
||||
{ onSuccess: () => setCommentText("") },
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export function DispatchCreateModal({
|
|||
.filter(
|
||||
(item) =>
|
||||
!woSearch ||
|
||||
item.number.includes(woSearch) ||
|
||||
item.woNumber.includes(woSearch) ||
|
||||
item.title.toLowerCase().includes(woSearch.toLowerCase()),
|
||||
)
|
||||
.slice(0, 10),
|
||||
|
|
@ -180,13 +180,13 @@ export function DispatchCreateModal({
|
|||
onClick={() => {
|
||||
setSelectedWOs((prev) => [
|
||||
...prev,
|
||||
{ id: item.id, number: item.number, title: item.title },
|
||||
{ id: item.id, number: item.woNumber, title: item.title },
|
||||
]);
|
||||
setWoSearch("");
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2">
|
||||
<strong>{item.number}</strong> — {item.title}
|
||||
<strong>{item.woNumber}</strong> — {item.title}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import {
|
|||
Typography,
|
||||
} from "@mui/material";
|
||||
import { useDebounce } from "@/hooks/use-debounce";
|
||||
import type { WorkOrderListItem } from "@/domain/work-orders/types/work-order";
|
||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||
import {
|
||||
useWorkOrderUsers,
|
||||
useWorkOrdersList,
|
||||
|
|
@ -271,7 +271,7 @@ export default function WorkOrdersListPage() {
|
|||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
rows.map((row: WorkOrderListItem) => (
|
||||
rows.map((row: WorkOrderTableRow) => (
|
||||
<TableRow key={String(row.id)} hover>
|
||||
<TableCell>
|
||||
<Button
|
||||
|
|
@ -280,7 +280,7 @@ export default function WorkOrdersListPage() {
|
|||
className="font-semibold"
|
||||
onClick={() => navigate(`/workorders/${row.id}`)}
|
||||
>
|
||||
{row.number || row.id}
|
||||
{row.woNumber || row.id}
|
||||
</Button>
|
||||
</TableCell>
|
||||
<TableCell>{row.title}</TableCell>
|
||||
|
|
@ -290,12 +290,10 @@ export default function WorkOrdersListPage() {
|
|||
<WorkOrderStatusBadge status={row.status || "Open"} />
|
||||
</TableCell>
|
||||
<TableCell className="whitespace-nowrap">
|
||||
{formatDateTime(row.createdDate)}
|
||||
</TableCell>
|
||||
<TableCell>{row.assignedTo || "Unassigned"}</TableCell>
|
||||
<TableCell className="whitespace-nowrap">
|
||||
{formatDateTime(row.lastUpdated)}
|
||||
{formatDateTime(row.scheduledOn)}
|
||||
</TableCell>
|
||||
<TableCell>{row.dispatcherName || "Unassigned"}</TableCell>
|
||||
<TableCell className="whitespace-nowrap">{formatDateTime(row.dueDate)}</TableCell>
|
||||
<TableCell align="right">
|
||||
<Tooltip title="View">
|
||||
<IconButton
|
||||
|
|
|
|||
45
src/domain/work-orders/mocks/dispatcher-filter-mock-data.ts
Normal file
45
src/domain/work-orders/mocks/dispatcher-filter-mock-data.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import type { WorkOrderUser } from "@/domain/work-orders/types/work-order";
|
||||
|
||||
export type MockDispatcherUser = WorkOrderUser & { color: string };
|
||||
|
||||
/** UI-only dispatchers until the list filter is wired to the API. */
|
||||
export const MOCK_DISPATCHER_USERS: MockDispatcherUser[] = [
|
||||
{ id: "mock-dispatcher-sarah", name: "Sarah Chen", color: "#F59E0B" },
|
||||
{ id: "mock-dispatcher-alyssa", name: "Alyssa Reeves", color: "#0D9488" },
|
||||
{ id: "mock-dispatcher-derek", name: "Derek Walsh", color: "#6366F1" },
|
||||
{ id: "mock-dispatcher-morgan", name: "Morgan Blake", color: "#F43F5E" },
|
||||
{ id: "mock-dispatcher-james", name: "James Torres", color: "#7C3AED" },
|
||||
{ id: "mock-dispatcher-rachel", name: "Rachel Kim", color: "#0EA5E9" },
|
||||
{ id: "mock-dispatcher-tiffany", name: "Tiffany Moore", color: "#16A34A" },
|
||||
{ id: "mock-dispatcher-brandon", name: "Brandon Lee", color: "#EA580C" },
|
||||
];
|
||||
|
||||
const MOCK_DISPATCHER_COLOR_BY_ID = Object.fromEntries(
|
||||
MOCK_DISPATCHER_USERS.map((dispatcher) => [dispatcher.id, dispatcher.color]),
|
||||
) as Record<string, string>;
|
||||
|
||||
const FALLBACK_AVATAR_COLORS = [
|
||||
"#F59E0B",
|
||||
"#0D9488",
|
||||
"#6366F1",
|
||||
"#F43F5E",
|
||||
"#7C3AED",
|
||||
"#0EA5E9",
|
||||
"#16A34A",
|
||||
"#EA580C",
|
||||
];
|
||||
|
||||
function paletteColorFromKey(key: string): string {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < key.length; i += 1) {
|
||||
hash = key.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
return FALLBACK_AVATAR_COLORS[Math.abs(hash) % FALLBACK_AVATAR_COLORS.length];
|
||||
}
|
||||
|
||||
export function getDispatcherAvatarColor(user: WorkOrderUser): string {
|
||||
return (
|
||||
MOCK_DISPATCHER_COLOR_BY_ID[String(user.id)] ??
|
||||
paletteColorFromKey(String(user.id || user.name))
|
||||
);
|
||||
}
|
||||
10
src/domain/work-orders/mocks/is-wo-table-mock-enabled.ts
Normal file
10
src/domain/work-orders/mocks/is-wo-table-mock-enabled.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { isAuthBypassEnabled } from "@/lib/auth/auth-bypass";
|
||||
|
||||
/** Use seeded WO table rows instead of GetWorkOrderList (dev / auth bypass). */
|
||||
export function isWoTableMockEnabled(): boolean {
|
||||
return isAuthBypassEnabled() || import.meta.env.VITE_WO_TABLE_MOCK === "true";
|
||||
}
|
||||
|
||||
export function shouldUseMockWorkOrderDetail(id: string | number): boolean {
|
||||
return isWoTableMockEnabled() && String(id).startsWith("mock-wo-");
|
||||
}
|
||||
10
src/domain/work-orders/mocks/is-wo-wizard-mock-enabled.ts
Normal file
10
src/domain/work-orders/mocks/is-wo-wizard-mock-enabled.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { env } from "@/lib/env";
|
||||
import { isAuthBypassEnabled } from "@/lib/auth/auth-bypass";
|
||||
|
||||
export function isWoWizardMockEnabled(): boolean {
|
||||
return env.isDev;
|
||||
}
|
||||
|
||||
export function shouldUseMockWoWizardCreate(locationId: string): boolean {
|
||||
return isAuthBypassEnabled() || locationId.startsWith("mock-");
|
||||
}
|
||||
521
src/domain/work-orders/mocks/wo-table-mock-data.ts
Normal file
521
src/domain/work-orders/mocks/wo-table-mock-data.ts
Normal file
|
|
@ -0,0 +1,521 @@
|
|||
import type {
|
||||
LocationOption,
|
||||
VendorDropdownItem,
|
||||
WorkOrderDetail,
|
||||
WorkOrderUser,
|
||||
} from "@/domain/work-orders/types/work-order";
|
||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||
import type { WorkOrdersListParams } from "@/domain/work-orders/types/work-order-list";
|
||||
import { MOCK_DISPATCHER_USERS } from "@/domain/work-orders/mocks/dispatcher-filter-mock-data";
|
||||
import {
|
||||
MOCK_WO_WIZARD_LOCATIONS,
|
||||
MOCK_WO_WIZARD_SERVICES,
|
||||
MOCK_WO_WIZARD_VENDORS,
|
||||
} from "@/domain/work-orders/mocks/wo-wizard-mock-data";
|
||||
import { currentWeekMonday } from "@/domain/work-orders/utils/list-week-utils";
|
||||
import { deriveDayFields, isDateInWeek } from "@/domain/work-orders/utils/table-day-utils";
|
||||
import { enrichTableRow } from "@/domain/work-orders/utils/table-filter-utils";
|
||||
import { addDaysIso, todayIso } from "@/domain/work-orders/utils/wizard-date-utils";
|
||||
|
||||
export const MOCK_WO_TABLE_USERS: WorkOrderUser[] = MOCK_DISPATCHER_USERS.map(({ id, name }) => ({
|
||||
id,
|
||||
name,
|
||||
}));
|
||||
|
||||
function sched(iso: string): Pick<WorkOrderTableRow, "scheduledOn" | "dayGroup" | "dayLabel"> {
|
||||
const { dayGroup, dayLabel } = deriveDayFields(iso);
|
||||
return { scheduledOn: iso, dayGroup, dayLabel };
|
||||
}
|
||||
|
||||
type RowSeed = {
|
||||
id: string | number;
|
||||
woNumber: string;
|
||||
site: string;
|
||||
location: string;
|
||||
locationId: string | number;
|
||||
type: WorkOrderTableRow["type"];
|
||||
dispatcherId: string;
|
||||
dispatcherName: string;
|
||||
scheduledOn?: string;
|
||||
targetWeek?: string;
|
||||
dueDate: string;
|
||||
pm: string;
|
||||
company: string;
|
||||
tech: string;
|
||||
techPhone: string;
|
||||
apptStart: string;
|
||||
apptEnd: string;
|
||||
apptTime: string;
|
||||
status: WorkOrderTableRow["status"];
|
||||
docStatus: WorkOrderTableRow["docStatus"];
|
||||
pocName: string;
|
||||
pocPhone: string;
|
||||
pocNotes: string;
|
||||
rescheduleCount: number;
|
||||
carriedOver: number;
|
||||
originalWeek: string;
|
||||
originalDate: string;
|
||||
scheduleWeekOnly: boolean;
|
||||
mediaCount: number;
|
||||
title?: string;
|
||||
priority?: string;
|
||||
};
|
||||
|
||||
function buildRow(seed: RowSeed): WorkOrderTableRow {
|
||||
const scheduledOn = seed.scheduledOn ?? "";
|
||||
const dayFields = scheduledOn ? sched(scheduledOn) : deriveDayFields("");
|
||||
return enrichTableRow({
|
||||
title: seed.title ?? `${seed.type || "Work Order"} — ${seed.site}`,
|
||||
priority: seed.priority ?? "Sev 3",
|
||||
...seed,
|
||||
...dayFields,
|
||||
scheduledOn,
|
||||
targetWeek: seed.targetWeek ?? "",
|
||||
isPastDue: false,
|
||||
});
|
||||
}
|
||||
|
||||
function buildMockRows(): WorkOrderTableRow[] {
|
||||
const mon = currentWeekMonday();
|
||||
const tue = addDaysIso(mon, 1);
|
||||
const wed = addDaysIso(mon, 2);
|
||||
const thu = addDaysIso(mon, 3);
|
||||
const fri = addDaysIso(mon, 4);
|
||||
const yesterday = addDaysIso(todayIso(), -1);
|
||||
|
||||
const sarah = MOCK_DISPATCHER_USERS[0];
|
||||
const alyssa = MOCK_DISPATCHER_USERS[1];
|
||||
const derek = MOCK_DISPATCHER_USERS[2];
|
||||
const morgan = MOCK_DISPATCHER_USERS[3];
|
||||
|
||||
return [
|
||||
buildRow({
|
||||
id: "mock-wo-1",
|
||||
woNumber: "20260623001",
|
||||
site: "CHI-1042",
|
||||
location: "Chicago Loop",
|
||||
locationId: "mock-loc-1",
|
||||
type: "PM",
|
||||
dispatcherId: String(sarah.id),
|
||||
dispatcherName: sarah.name,
|
||||
...sched(mon),
|
||||
dueDate: fri,
|
||||
pm: "HVAC — Quarterly PM",
|
||||
company: "Midwest HVAC Co.",
|
||||
tech: "Mike Torres",
|
||||
techPhone: "(312) 555-0198",
|
||||
apptStart: "09:00",
|
||||
apptEnd: "11:00",
|
||||
apptTime: "09:00 – 11:00",
|
||||
status: "Scheduled",
|
||||
docStatus: "No",
|
||||
pocName: "Maria Gonzalez",
|
||||
pocPhone: "(312) 555-0142",
|
||||
pocNotes: "Loading dock B",
|
||||
rescheduleCount: 0,
|
||||
carriedOver: 0,
|
||||
originalWeek: "",
|
||||
originalDate: "",
|
||||
scheduleWeekOnly: false,
|
||||
mediaCount: 0,
|
||||
}),
|
||||
buildRow({
|
||||
id: "mock-wo-2",
|
||||
woNumber: "20260623002",
|
||||
site: "IND-2208",
|
||||
location: "Indianapolis North",
|
||||
locationId: "mock-loc-2",
|
||||
type: "Emergency",
|
||||
dispatcherId: String(alyssa.id),
|
||||
dispatcherName: alyssa.name,
|
||||
...sched(tue),
|
||||
dueDate: tue,
|
||||
pm: "Plumbing — Leak repair",
|
||||
company: "Great Lakes Plumbing",
|
||||
tech: "Jen Walsh",
|
||||
techPhone: "(317) 555-4412",
|
||||
apptStart: "08:00",
|
||||
apptEnd: "",
|
||||
apptTime: "08:00",
|
||||
status: "On Site",
|
||||
docStatus: "No",
|
||||
pocName: "Chris Bennett",
|
||||
pocPhone: "(317) 555-2208",
|
||||
pocNotes: "",
|
||||
rescheduleCount: 1,
|
||||
carriedOver: 0,
|
||||
originalWeek: "",
|
||||
originalDate: "",
|
||||
scheduleWeekOnly: false,
|
||||
mediaCount: 3,
|
||||
}),
|
||||
buildRow({
|
||||
id: "mock-wo-3",
|
||||
woNumber: "20260623003",
|
||||
site: "MIL-3315",
|
||||
location: "Milwaukee West",
|
||||
locationId: "mock-loc-3",
|
||||
type: "Reactive",
|
||||
dispatcherId: String(derek.id),
|
||||
dispatcherName: derek.name,
|
||||
...sched(wed),
|
||||
dueDate: thu,
|
||||
pm: "Electrical — Panel inspection",
|
||||
company: "SparkLine Electric",
|
||||
tech: "Pat Nguyen",
|
||||
techPhone: "",
|
||||
apptStart: "13:00",
|
||||
apptEnd: "15:00",
|
||||
apptTime: "13:00 – 15:00",
|
||||
status: "In Progress",
|
||||
docStatus: "Yes",
|
||||
pocName: "Taylor Brooks",
|
||||
pocPhone: "(414) 555-3315",
|
||||
pocNotes: "Badge required at front desk",
|
||||
rescheduleCount: 0,
|
||||
carriedOver: 0,
|
||||
originalWeek: "",
|
||||
originalDate: "",
|
||||
scheduleWeekOnly: false,
|
||||
mediaCount: 0,
|
||||
}),
|
||||
buildRow({
|
||||
id: "mock-wo-4",
|
||||
woNumber: "20260623004",
|
||||
site: "CHI-1042",
|
||||
location: "Chicago Loop",
|
||||
locationId: "mock-loc-1",
|
||||
type: "Add-On",
|
||||
dispatcherId: String(morgan.id),
|
||||
dispatcherName: morgan.name,
|
||||
...sched(thu),
|
||||
dueDate: fri,
|
||||
pm: "Roof — Membrane patch",
|
||||
company: "Midwest HVAC Co.",
|
||||
tech: "Mike Torres",
|
||||
techPhone: "(312) 555-0198",
|
||||
apptStart: "10:30",
|
||||
apptEnd: "12:00",
|
||||
apptTime: "10:30 – 12:00",
|
||||
status: "Pending",
|
||||
docStatus: "NN",
|
||||
pocName: "Maria Gonzalez",
|
||||
pocPhone: "(312) 555-0142",
|
||||
pocNotes: "",
|
||||
rescheduleCount: 0,
|
||||
carriedOver: 0,
|
||||
originalWeek: "",
|
||||
originalDate: "",
|
||||
scheduleWeekOnly: false,
|
||||
mediaCount: 0,
|
||||
}),
|
||||
buildRow({
|
||||
id: "mock-wo-5",
|
||||
woNumber: "20260623005",
|
||||
site: "IND-2208",
|
||||
location: "Indianapolis North",
|
||||
locationId: "mock-loc-2",
|
||||
type: "PM",
|
||||
dispatcherId: "",
|
||||
dispatcherName: "",
|
||||
...sched(fri),
|
||||
dueDate: fri,
|
||||
pm: "HVAC — Quarterly PM",
|
||||
company: "",
|
||||
tech: "",
|
||||
techPhone: "",
|
||||
apptStart: "",
|
||||
apptEnd: "",
|
||||
apptTime: "",
|
||||
status: "Incomplete",
|
||||
docStatus: "No",
|
||||
pocName: "",
|
||||
pocPhone: "",
|
||||
pocNotes: "",
|
||||
rescheduleCount: 0,
|
||||
carriedOver: 0,
|
||||
originalWeek: "",
|
||||
originalDate: "",
|
||||
scheduleWeekOnly: false,
|
||||
mediaCount: 0,
|
||||
}),
|
||||
buildRow({
|
||||
id: "mock-wo-6",
|
||||
woNumber: "20260623006",
|
||||
site: "MIL-3315",
|
||||
location: "Milwaukee West",
|
||||
locationId: "mock-loc-3",
|
||||
type: "Overdue",
|
||||
dispatcherId: String(sarah.id),
|
||||
dispatcherName: sarah.name,
|
||||
...sched(mon),
|
||||
dueDate: yesterday,
|
||||
pm: "Plumbing — Leak repair",
|
||||
company: "Great Lakes Plumbing",
|
||||
tech: "Jen Walsh",
|
||||
techPhone: "(317) 555-4412",
|
||||
apptStart: "07:00",
|
||||
apptEnd: "09:00",
|
||||
apptTime: "07:00 – 09:00",
|
||||
status: "Scheduled",
|
||||
docStatus: "No",
|
||||
pocName: "Taylor Brooks",
|
||||
pocPhone: "(414) 555-3315",
|
||||
pocNotes: "",
|
||||
rescheduleCount: 2,
|
||||
carriedOver: 1,
|
||||
originalWeek: "",
|
||||
originalDate: yesterday,
|
||||
scheduleWeekOnly: false,
|
||||
mediaCount: 0,
|
||||
}),
|
||||
buildRow({
|
||||
id: "mock-wo-7",
|
||||
woNumber: "20260623007",
|
||||
site: "CHI-1042",
|
||||
location: "Chicago Loop",
|
||||
locationId: "mock-loc-1",
|
||||
type: "Reactive",
|
||||
dispatcherId: String(alyssa.id),
|
||||
dispatcherName: alyssa.name,
|
||||
scheduledOn: "",
|
||||
targetWeek: mon,
|
||||
dueDate: wed,
|
||||
pm: "Electrical — Panel inspection",
|
||||
company: "SparkLine Electric",
|
||||
tech: "Pat Nguyen",
|
||||
techPhone: "",
|
||||
apptStart: "",
|
||||
apptEnd: "",
|
||||
apptTime: "",
|
||||
status: "Incomplete",
|
||||
docStatus: "No",
|
||||
pocName: "Maria Gonzalez",
|
||||
pocPhone: "(312) 555-0142",
|
||||
pocNotes: "Week-only target",
|
||||
rescheduleCount: 0,
|
||||
carriedOver: 0,
|
||||
originalWeek: mon,
|
||||
originalDate: "",
|
||||
scheduleWeekOnly: true,
|
||||
mediaCount: 0,
|
||||
}),
|
||||
buildRow({
|
||||
id: "mock-wo-8",
|
||||
woNumber: "20260623008",
|
||||
site: "IND-2208",
|
||||
location: "Indianapolis North",
|
||||
locationId: "mock-loc-2",
|
||||
type: "PM",
|
||||
dispatcherId: String(derek.id),
|
||||
dispatcherName: derek.name,
|
||||
scheduledOn: "",
|
||||
targetWeek: "",
|
||||
dueDate: thu,
|
||||
pm: "HVAC — Quarterly PM",
|
||||
company: "Midwest HVAC Co.",
|
||||
tech: "Mike Torres",
|
||||
techPhone: "(312) 555-0198",
|
||||
apptStart: "",
|
||||
apptEnd: "",
|
||||
apptTime: "",
|
||||
status: "Pending",
|
||||
docStatus: "No",
|
||||
pocName: "Chris Bennett",
|
||||
pocPhone: "(317) 555-2208",
|
||||
pocNotes: "",
|
||||
rescheduleCount: 0,
|
||||
carriedOver: 0,
|
||||
originalWeek: "",
|
||||
originalDate: "",
|
||||
scheduleWeekOnly: false,
|
||||
mediaCount: 0,
|
||||
}),
|
||||
buildRow({
|
||||
id: "mock-wo-9",
|
||||
woNumber: "20260623009",
|
||||
site: "MIL-3315",
|
||||
location: "Milwaukee West",
|
||||
locationId: "mock-loc-3",
|
||||
type: "PM",
|
||||
dispatcherId: String(morgan.id),
|
||||
dispatcherName: morgan.name,
|
||||
...sched(wed),
|
||||
dueDate: fri,
|
||||
pm: "Roof — Membrane patch",
|
||||
company: "SparkLine Electric",
|
||||
tech: "Pat Nguyen",
|
||||
techPhone: "",
|
||||
apptStart: "14:00",
|
||||
apptEnd: "16:00",
|
||||
apptTime: "14:00 – 16:00",
|
||||
status: "En Route",
|
||||
docStatus: "No",
|
||||
pocName: "Taylor Brooks",
|
||||
pocPhone: "(414) 555-3315",
|
||||
pocNotes: "",
|
||||
rescheduleCount: 0,
|
||||
carriedOver: 0,
|
||||
originalWeek: "",
|
||||
originalDate: "",
|
||||
scheduleWeekOnly: false,
|
||||
mediaCount: 0,
|
||||
}),
|
||||
buildRow({
|
||||
id: "mock-wo-10",
|
||||
woNumber: "20260623010",
|
||||
site: "CHI-1042",
|
||||
location: "Chicago Loop",
|
||||
locationId: "mock-loc-1",
|
||||
type: "Reactive",
|
||||
dispatcherId: String(sarah.id),
|
||||
dispatcherName: sarah.name,
|
||||
...sched(tue),
|
||||
dueDate: wed,
|
||||
pm: "Plumbing — Leak repair",
|
||||
company: "Great Lakes Plumbing",
|
||||
tech: "Jen Walsh",
|
||||
techPhone: "(317) 555-4412",
|
||||
apptStart: "11:00",
|
||||
apptEnd: "12:30",
|
||||
apptTime: "11:00 – 12:30",
|
||||
status: "Completed",
|
||||
docStatus: "Yes",
|
||||
pocName: "Maria Gonzalez",
|
||||
pocPhone: "(312) 555-0142",
|
||||
pocNotes: "",
|
||||
rescheduleCount: 0,
|
||||
carriedOver: 0,
|
||||
originalWeek: "",
|
||||
originalDate: "",
|
||||
scheduleWeekOnly: false,
|
||||
mediaCount: 2,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
let cachedRows: WorkOrderTableRow[] | null = null;
|
||||
|
||||
export function getMockWorkOrderTableRows(): WorkOrderTableRow[] {
|
||||
if (!cachedRows) {
|
||||
cachedRows = buildMockRows();
|
||||
}
|
||||
return cachedRows;
|
||||
}
|
||||
|
||||
function rowMatchesSearch(row: WorkOrderTableRow, search: string): boolean {
|
||||
const q = search.trim().toLowerCase();
|
||||
if (!q) {
|
||||
return true;
|
||||
}
|
||||
const haystack = [
|
||||
row.site,
|
||||
row.location,
|
||||
row.woNumber,
|
||||
row.type,
|
||||
row.dispatcherName,
|
||||
row.pm,
|
||||
row.company,
|
||||
row.tech,
|
||||
row.status,
|
||||
row.title,
|
||||
]
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
return haystack.includes(q);
|
||||
}
|
||||
|
||||
export function getMockWorkOrdersList(params: WorkOrdersListParams = {}): {
|
||||
items: WorkOrderTableRow[];
|
||||
totalCount: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
} {
|
||||
let items = getMockWorkOrderTableRows();
|
||||
|
||||
if (params.search) {
|
||||
items = items.filter((row) => rowMatchesSearch(row, params.search!));
|
||||
}
|
||||
|
||||
if (params.type) {
|
||||
items = items.filter((row) => row.type === params.type);
|
||||
}
|
||||
|
||||
if (params.dateFrom && params.dateTo) {
|
||||
const weekMonday = params.dateFrom;
|
||||
items = items.filter((row) => {
|
||||
if (row.scheduledOn) {
|
||||
return row.scheduledOn >= params.dateFrom! && row.scheduledOn <= params.dateTo!;
|
||||
}
|
||||
if (row.targetWeek) {
|
||||
return isDateInWeek(row.targetWeek, weekMonday);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
const page = params.page ?? 1;
|
||||
const pageSize = params.pageSize ?? items.length;
|
||||
|
||||
return {
|
||||
items,
|
||||
totalCount: items.length,
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
export function getMockWorkOrderTableRow(id: string | number): WorkOrderTableRow | undefined {
|
||||
return getMockWorkOrderTableRows().find((row) => String(row.id) === String(id));
|
||||
}
|
||||
|
||||
export function buildMockWorkOrderDetailFromTableRow(row: WorkOrderTableRow): WorkOrderDetail {
|
||||
return {
|
||||
id: row.id,
|
||||
internalWONumber: row.woNumber,
|
||||
workOrderNumber: row.woNumber,
|
||||
title: row.title,
|
||||
location: row.location,
|
||||
locationId: String(row.locationId),
|
||||
locationZip: "",
|
||||
assignedTo: row.dispatcherName,
|
||||
dueDate: row.dueDate,
|
||||
createdDate: addDaysIso(todayIso(), -7),
|
||||
createdDateFull: new Date().toISOString(),
|
||||
description: [
|
||||
row.pocNotes && `POC notes: ${row.pocNotes}`,
|
||||
row.pocName && `POC: ${row.pocName} · ${row.pocPhone}`,
|
||||
row.apptTime && row.apptTime !== "—" && `Appt: ${row.apptTime}`,
|
||||
row.company && `Vendor: ${row.company}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n"),
|
||||
priority: row.priority,
|
||||
status:
|
||||
row.status === "Completed" ? "Done" : row.status === "Incomplete" ? "Open" : "In Progress",
|
||||
po: "",
|
||||
tt: "",
|
||||
problem: row.pm,
|
||||
trade: "",
|
||||
subTrade: "",
|
||||
vendorNTE: "",
|
||||
scheduledDate: row.scheduledOn,
|
||||
completedDate: row.status === "Completed" ? todayIso() : "",
|
||||
source: row.type,
|
||||
comments: [],
|
||||
auditLog: [],
|
||||
dispatches: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function getMockWorkOrderLocations(): LocationOption[] {
|
||||
return MOCK_WO_WIZARD_LOCATIONS;
|
||||
}
|
||||
|
||||
export function getMockWorkOrderVendors(): VendorDropdownItem[] {
|
||||
return MOCK_WO_WIZARD_VENDORS;
|
||||
}
|
||||
|
||||
export function getMockPmTypes(): string[] {
|
||||
return MOCK_WO_WIZARD_SERVICES.map((s) => s.value).filter(Boolean);
|
||||
}
|
||||
158
src/domain/work-orders/mocks/wo-wizard-mock-data.ts
Normal file
158
src/domain/work-orders/mocks/wo-wizard-mock-data.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import type { Location } from "@/domain/locations/types/location";
|
||||
import type { DropdownOption } from "@/domain/settings/dropdown-options/types/dropdown-option";
|
||||
import type {
|
||||
LocationOption,
|
||||
VendorDropdownItem,
|
||||
WorkOrderDetail,
|
||||
WorkOrderFormValues,
|
||||
WorkOrderUser,
|
||||
} from "@/domain/work-orders/types/work-order";
|
||||
|
||||
export const MOCK_WO_WIZARD_USERS: WorkOrderUser[] = [
|
||||
{ id: "mock-user-1", name: "Alex Rivera" },
|
||||
{ id: "mock-user-2", name: "Jordan Kim" },
|
||||
{ id: "mock-user-3", name: "Sam Patel" },
|
||||
];
|
||||
|
||||
export const MOCK_WO_WIZARD_LOCATIONS: LocationOption[] = [
|
||||
{ id: "mock-loc-1", name: "CHI-1042 — Chicago Loop" },
|
||||
{ id: "mock-loc-2", name: "IND-2208 — Indianapolis North" },
|
||||
{ id: "mock-loc-3", name: "MIL-3315 — Milwaukee West" },
|
||||
];
|
||||
|
||||
const MOCK_LOCATION_DETAILS: Record<string, Location> = {
|
||||
"mock-loc-1": {
|
||||
id: "mock-loc-1",
|
||||
name: "CHI-1042 — Chicago Loop",
|
||||
address: "233 S Wacker Dr",
|
||||
city: "Chicago",
|
||||
state: "IL",
|
||||
zipCode: "60606",
|
||||
phone: "(312) 555-0142",
|
||||
contact: "Maria Gonzalez",
|
||||
contactEmail: "maria.gonzalez@example.com",
|
||||
status: "Active",
|
||||
},
|
||||
"mock-loc-2": {
|
||||
id: "mock-loc-2",
|
||||
name: "IND-2208 — Indianapolis North",
|
||||
address: "8702 Keystone Crossing",
|
||||
city: "Indianapolis",
|
||||
state: "IN",
|
||||
zipCode: "46240",
|
||||
phone: "(317) 555-2208",
|
||||
contact: "Chris Bennett",
|
||||
contactEmail: "chris.bennett@example.com",
|
||||
status: "Active",
|
||||
},
|
||||
"mock-loc-3": {
|
||||
id: "mock-loc-3",
|
||||
name: "MIL-3315 — Milwaukee West",
|
||||
address: "411 E Wisconsin Ave",
|
||||
city: "Milwaukee",
|
||||
state: "WI",
|
||||
zipCode: "53202",
|
||||
phone: "(414) 555-3315",
|
||||
contact: "Taylor Brooks",
|
||||
contactEmail: "taylor.brooks@example.com",
|
||||
status: "Active",
|
||||
},
|
||||
};
|
||||
|
||||
export const MOCK_WO_WIZARD_SERVICES: DropdownOption[] = [
|
||||
{
|
||||
id: "mock-svc-1",
|
||||
category: "Problem",
|
||||
value: "HVAC — Quarterly PM",
|
||||
parentValue: null,
|
||||
sortOrder: 1,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: "mock-svc-2",
|
||||
category: "Problem",
|
||||
value: "Plumbing — Leak repair",
|
||||
parentValue: null,
|
||||
sortOrder: 2,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: "mock-svc-3",
|
||||
category: "Problem",
|
||||
value: "Electrical — Panel inspection",
|
||||
parentValue: null,
|
||||
sortOrder: 3,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: "mock-svc-4",
|
||||
category: "Problem",
|
||||
value: "Roof — Membrane patch",
|
||||
parentValue: null,
|
||||
sortOrder: 4,
|
||||
isActive: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const MOCK_WO_WIZARD_VENDORS: VendorDropdownItem[] = [
|
||||
{
|
||||
id: "mock-vendor-1",
|
||||
companyName: "Midwest HVAC Co.",
|
||||
tradeSpecialties: "HVAC",
|
||||
address: "1200 Industrial Pkwy, Chicago, IL 60632",
|
||||
distanceMiles: 4.2,
|
||||
},
|
||||
{
|
||||
id: "mock-vendor-2",
|
||||
companyName: "Great Lakes Plumbing",
|
||||
tradeSpecialties: "Plumbing",
|
||||
address: "455 Water St, Milwaukee, WI 53202",
|
||||
distanceMiles: 8.7,
|
||||
},
|
||||
{
|
||||
id: "mock-vendor-3",
|
||||
companyName: "SparkLine Electric",
|
||||
tradeSpecialties: "Electrical",
|
||||
address: "88 Volt Ave, Indianapolis, IN 46225",
|
||||
distanceMiles: 12.1,
|
||||
},
|
||||
];
|
||||
|
||||
export function getMockWoWizardLocationDetail(locationId: string): Location | undefined {
|
||||
return MOCK_LOCATION_DETAILS[locationId];
|
||||
}
|
||||
|
||||
export function buildMockWorkOrderDetail(values: WorkOrderFormValues): WorkOrderDetail {
|
||||
const id = `mock-wo-${Date.now()}`;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
return {
|
||||
id,
|
||||
internalWONumber: values.internalWONumber || id,
|
||||
workOrderNumber: values.workOrderNumber || "MOCK-0001",
|
||||
title: values.title || "Mock Work Order",
|
||||
location:
|
||||
MOCK_WO_WIZARD_LOCATIONS.find((l) => String(l.id) === values.locationId)?.name ?? "Mock Site",
|
||||
locationId: values.locationId,
|
||||
locationZip: getMockWoWizardLocationDetail(values.locationId)?.zipCode ?? "",
|
||||
assignedTo: values.assignedTo,
|
||||
dueDate: values.dueDate,
|
||||
createdDate: values.createdDate,
|
||||
createdDateFull: now,
|
||||
description: values.description,
|
||||
priority: values.priority,
|
||||
status: values.status,
|
||||
po: values.po,
|
||||
tt: values.tt,
|
||||
problem: values.problem,
|
||||
trade: values.trade,
|
||||
subTrade: values.subTrade,
|
||||
vendorNTE: values.vendorNTE,
|
||||
scheduledDate: values.scheduledDate,
|
||||
completedDate: values.completedDate,
|
||||
source: values.source,
|
||||
comments: [],
|
||||
auditLog: [],
|
||||
dispatches: [],
|
||||
};
|
||||
}
|
||||
240
src/domain/work-orders/utils/advanced-filters-utils.ts
Normal file
240
src/domain/work-orders/utils/advanced-filters-utils.ts
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
import {
|
||||
ASSIGNEE_FILTER_UNASSIGNED,
|
||||
DATE_RANGE_OPTIONS,
|
||||
DOC_FILTER_LABELS,
|
||||
EMPTY_ADVANCED_FILTERS,
|
||||
type CompletionDocFilter,
|
||||
type DateRangeKey,
|
||||
type WorkOrderAdvancedFilters,
|
||||
} from "@/domain/work-orders/types/work-order-advanced-filters";
|
||||
import {
|
||||
MONTH_NAMES,
|
||||
addDaysIso,
|
||||
mondayOf,
|
||||
pad2,
|
||||
todayIso,
|
||||
} from "@/domain/work-orders/utils/wizard-date-utils";
|
||||
|
||||
export interface AdvancedFilterChip {
|
||||
key: string;
|
||||
group: string;
|
||||
value?: string;
|
||||
count?: number;
|
||||
accent?: boolean;
|
||||
clear: (filters: WorkOrderAdvancedFilters) => WorkOrderAdvancedFilters;
|
||||
}
|
||||
|
||||
function cloneFilters(filters: WorkOrderAdvancedFilters): WorkOrderAdvancedFilters {
|
||||
return {
|
||||
...filters,
|
||||
sites: new Set(filters.sites),
|
||||
types: new Set(filters.types),
|
||||
dispatchers: new Set(filters.dispatchers),
|
||||
statuses: new Set(filters.statuses),
|
||||
pmTypes: new Set(filters.pmTypes),
|
||||
vendorTechs: new Set(filters.vendorTechs),
|
||||
docs: new Set(filters.docs),
|
||||
};
|
||||
}
|
||||
|
||||
export function countAdvancedFilters(filters: WorkOrderAdvancedFilters): number {
|
||||
let count = 0;
|
||||
if (filters.dateRange !== "this-week") count += 1;
|
||||
if (filters.sites.size) count += 1;
|
||||
if (filters.types.size) count += 1;
|
||||
if (filters.dispatchers.size) count += 1;
|
||||
if (filters.statuses.size) count += 1;
|
||||
if (filters.pmTypes.size) count += 1;
|
||||
if (filters.vendorTechs.size) count += 1;
|
||||
if (filters.docs.size) count += 1;
|
||||
return count;
|
||||
}
|
||||
|
||||
function fmtChipDate(iso: string): string {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(iso)) return "";
|
||||
const [, m, d] = iso.split("-").map(Number);
|
||||
return `${MONTH_NAMES[m - 1].slice(0, 3)} ${d}`;
|
||||
}
|
||||
|
||||
function monthStart(y: number, m: number): string {
|
||||
return `${y}-${pad2(m + 1)}-01`;
|
||||
}
|
||||
|
||||
function monthEnd(y: number, m: number): string {
|
||||
return `${y}-${pad2(m + 1)}-${pad2(new Date(y, m + 1, 0).getDate())}`;
|
||||
}
|
||||
|
||||
export function dateRangeFor(filters: WorkOrderAdvancedFilters): [string, string] {
|
||||
const today = todayIso();
|
||||
const ty = Number(today.slice(0, 4));
|
||||
const tm = Number(today.slice(5, 7)) - 1;
|
||||
const thisWeekMonday = mondayOf(today);
|
||||
const lastWeekMonday = addDaysIso(thisWeekMonday, -7);
|
||||
|
||||
switch (filters.dateRange) {
|
||||
case "this-week":
|
||||
return [thisWeekMonday, addDaysIso(thisWeekMonday, 4)];
|
||||
case "last-week":
|
||||
return [lastWeekMonday, addDaysIso(lastWeekMonday, 4)];
|
||||
case "this-month":
|
||||
return [monthStart(ty, tm), monthEnd(ty, tm)];
|
||||
case "last-3-months": {
|
||||
const d = new Date(ty, tm - 2, 1);
|
||||
return [monthStart(d.getFullYear(), d.getMonth()), monthEnd(ty, tm)];
|
||||
}
|
||||
case "next-week":
|
||||
return [today, addDaysIso(today, 7)];
|
||||
case "next-month":
|
||||
return [today, addDaysIso(today, 30)];
|
||||
case "custom":
|
||||
return [filters.customFrom || "0000-01-01", filters.customTo || "9999-12-31"];
|
||||
}
|
||||
}
|
||||
|
||||
function setChip(
|
||||
chips: AdvancedFilterChip[],
|
||||
key: string,
|
||||
group: string,
|
||||
set: Set<string>,
|
||||
nameOf: (value: string) => string,
|
||||
clear: (filters: WorkOrderAdvancedFilters) => WorkOrderAdvancedFilters,
|
||||
) {
|
||||
if (!set.size) return;
|
||||
if (set.size <= 2) {
|
||||
chips.push({ key, group, value: [...set].map(nameOf).join(", "), clear });
|
||||
return;
|
||||
}
|
||||
chips.push({ key, group, count: set.size, clear });
|
||||
}
|
||||
|
||||
export function buildAdvancedFilterChips(
|
||||
filters: WorkOrderAdvancedFilters,
|
||||
options?: {
|
||||
siteNameOf?: (id: string) => string;
|
||||
dispatcherNameOf?: (id: string) => string;
|
||||
},
|
||||
): AdvancedFilterChip[] {
|
||||
const chips: AdvancedFilterChip[] = [];
|
||||
const siteNameOf = options?.siteNameOf ?? ((v) => v);
|
||||
const dispatcherNameOf = options?.dispatcherNameOf ?? ((v) => v);
|
||||
|
||||
if (filters.dateRange !== "this-week") {
|
||||
const value =
|
||||
filters.dateRange === "custom"
|
||||
? `${fmtChipDate(filters.customFrom) || "Any"} – ${fmtChipDate(filters.customTo) || "Any"}`
|
||||
: (DATE_RANGE_OPTIONS.find((o) => o.key === filters.dateRange)?.label ?? "Custom");
|
||||
chips.push({
|
||||
key: "date",
|
||||
group: "Date",
|
||||
value,
|
||||
clear: (current) => ({
|
||||
...current,
|
||||
dateRange: "this-week",
|
||||
customFrom: "",
|
||||
customTo: "",
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
setChip(chips, "sites", "Site", filters.sites, siteNameOf, (current) => ({
|
||||
...current,
|
||||
sites: new Set(),
|
||||
}));
|
||||
|
||||
setChip(
|
||||
chips,
|
||||
"types",
|
||||
"Type of WO",
|
||||
filters.types as Set<string>,
|
||||
(v) => v,
|
||||
(current) => ({ ...current, types: new Set() }),
|
||||
);
|
||||
|
||||
const realDispatchers = new Set(
|
||||
[...filters.dispatchers].filter((id) => id !== ASSIGNEE_FILTER_UNASSIGNED),
|
||||
);
|
||||
setChip(chips, "disp", "Assigned to", realDispatchers, dispatcherNameOf, (current) => ({
|
||||
...current,
|
||||
dispatchers: new Set(
|
||||
[...current.dispatchers].filter((id) => id === ASSIGNEE_FILTER_UNASSIGNED),
|
||||
),
|
||||
}));
|
||||
|
||||
if (filters.dispatchers.has(ASSIGNEE_FILTER_UNASSIGNED)) {
|
||||
chips.push({
|
||||
key: "disp-unassigned",
|
||||
group: "Assigned to",
|
||||
value: "Unassigned",
|
||||
accent: true,
|
||||
clear: (current) => {
|
||||
const next = new Set(current.dispatchers);
|
||||
next.delete(ASSIGNEE_FILTER_UNASSIGNED);
|
||||
return { ...current, dispatchers: next };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setChip(
|
||||
chips,
|
||||
"status",
|
||||
"Status",
|
||||
filters.statuses as Set<string>,
|
||||
(v) => v,
|
||||
(current) => ({ ...current, statuses: new Set() }),
|
||||
);
|
||||
|
||||
setChip(
|
||||
chips,
|
||||
"pm",
|
||||
"PM type",
|
||||
filters.pmTypes,
|
||||
(v) => v,
|
||||
(current) => ({ ...current, pmTypes: new Set() }),
|
||||
);
|
||||
|
||||
setChip(
|
||||
chips,
|
||||
"vendor",
|
||||
"Vendor / tech",
|
||||
filters.vendorTechs,
|
||||
(v) => v,
|
||||
(current) => ({ ...current, vendorTechs: new Set() }),
|
||||
);
|
||||
|
||||
setChip(
|
||||
chips,
|
||||
"doc",
|
||||
"Comp doc",
|
||||
filters.docs as Set<string>,
|
||||
(v) => DOC_FILTER_LABELS[v as CompletionDocFilter] ?? v,
|
||||
(current) => ({ ...current, docs: new Set() }),
|
||||
);
|
||||
|
||||
return chips;
|
||||
}
|
||||
|
||||
export function clearAdvancedFilterGroup(
|
||||
filters: WorkOrderAdvancedFilters,
|
||||
chipKey: string,
|
||||
): WorkOrderAdvancedFilters | null {
|
||||
const chips = buildAdvancedFilterChips(filters);
|
||||
const chip = chips.find((c) => c.key === chipKey);
|
||||
if (!chip) return filters;
|
||||
const next = chip.clear(cloneFilters(filters));
|
||||
return countAdvancedFilters(next) > 0 ? next : null;
|
||||
}
|
||||
|
||||
export function getInitialAdvancedFilters(
|
||||
applied: WorkOrderAdvancedFilters | null,
|
||||
): WorkOrderAdvancedFilters {
|
||||
if (!applied) return cloneFilters(EMPTY_ADVANCED_FILTERS);
|
||||
return cloneFilters(applied);
|
||||
}
|
||||
|
||||
export function cloneAdvancedFilters(filters: WorkOrderAdvancedFilters): WorkOrderAdvancedFilters {
|
||||
return cloneFilters(filters);
|
||||
}
|
||||
|
||||
export function isDefaultDateRange(dateRange: DateRangeKey): boolean {
|
||||
return dateRange === "this-week";
|
||||
}
|
||||
68
src/domain/work-orders/utils/list-week-utils.ts
Normal file
68
src/domain/work-orders/utils/list-week-utils.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { addDaysIso, mondayOf, todayIso } from "@/domain/work-orders/utils/wizard-date-utils";
|
||||
|
||||
export interface WorkWeek {
|
||||
mondayIso: string;
|
||||
label: string;
|
||||
live: boolean;
|
||||
}
|
||||
|
||||
export function currentWeekMonday(): string {
|
||||
return mondayOf(todayIso());
|
||||
}
|
||||
|
||||
export function weekRange(mondayIso: string): [string, string] {
|
||||
return [mondayIso, addDaysIso(mondayIso, 4)];
|
||||
}
|
||||
|
||||
export function compactWeekLabel(mondayIso: string): string {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(mondayIso)) {
|
||||
return "";
|
||||
}
|
||||
const fridayIso = addDaysIso(mondayIso, 4);
|
||||
const [, sm, sd] = mondayIso.split("-").map(Number);
|
||||
const [, em, ed] = fridayIso.split("-").map(Number);
|
||||
return `${sm}/${sd}–${em}/${ed}`;
|
||||
}
|
||||
|
||||
export function isLiveWeek(mondayIso: string): boolean {
|
||||
return mondayIso === currentWeekMonday();
|
||||
}
|
||||
|
||||
export function isFutureWeek(mondayIso: string): boolean {
|
||||
return mondayIso > currentWeekMonday();
|
||||
}
|
||||
|
||||
export function nextWeekMonday(): string {
|
||||
return addDaysIso(currentWeekMonday(), 7);
|
||||
}
|
||||
|
||||
function toWorkWeek(mondayIso: string): WorkWeek {
|
||||
return {
|
||||
mondayIso,
|
||||
label: compactWeekLabel(mondayIso),
|
||||
live: isLiveWeek(mondayIso),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildWeekWindow(anchorMonday: string, before = 4, after = 4): WorkWeek[] {
|
||||
const weeks: WorkWeek[] = [];
|
||||
const liveMonday = currentWeekMonday();
|
||||
const upcomingMonday = nextWeekMonday();
|
||||
|
||||
for (let i = -before; i <= after; i += 1) {
|
||||
const mondayIso = addDaysIso(anchorMonday, i * 7);
|
||||
if (isFutureWeek(mondayIso) && mondayIso !== upcomingMonday) {
|
||||
continue;
|
||||
}
|
||||
weeks.push(toWorkWeek(mondayIso));
|
||||
}
|
||||
|
||||
for (const requiredMonday of [liveMonday, upcomingMonday]) {
|
||||
if (!weeks.some((week) => week.mondayIso === requiredMonday)) {
|
||||
weeks.push(toWorkWeek(requiredMonday));
|
||||
}
|
||||
}
|
||||
|
||||
weeks.sort((a, b) => a.mondayIso.localeCompare(b.mondayIso));
|
||||
return weeks;
|
||||
}
|
||||
46
src/domain/work-orders/utils/table-day-utils.ts
Normal file
46
src/domain/work-orders/utils/table-day-utils.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { addDaysIso, pad2 } from "@/domain/work-orders/utils/wizard-date-utils";
|
||||
|
||||
export const WEEKDAY_LABELS = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"] as const;
|
||||
|
||||
export function deriveDayFields(rawDate: string): {
|
||||
dayGroup: string;
|
||||
dayLabel: string;
|
||||
} {
|
||||
const dayGroup = rawDate ? rawDate.replace(/^\d{4}-(\d{2})-(\d{2})$/, "$1/$2") : "";
|
||||
const dayLabel = rawDate
|
||||
? (() => {
|
||||
const d = new Date(`${rawDate}T12:00:00`);
|
||||
return `${WEEKDAY_LABELS[d.getDay()]} · ${dayGroup}`;
|
||||
})()
|
||||
: "";
|
||||
return { dayGroup, dayLabel };
|
||||
}
|
||||
|
||||
export function buildWeekSkeleton(weekMonday: string): {
|
||||
key: string;
|
||||
label: string;
|
||||
date: string;
|
||||
items: never[];
|
||||
}[] {
|
||||
const base = new Date(`${weekMonday}T12:00:00`);
|
||||
return Array.from({ length: 5 }, (_, i) => {
|
||||
const d = new Date(base);
|
||||
d.setDate(base.getDate() + i);
|
||||
const key = `${pad2(d.getMonth() + 1)}/${pad2(d.getDate())}`;
|
||||
const iso = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
||||
return {
|
||||
key,
|
||||
label: `${WEEKDAY_LABELS[d.getDay()]} · ${key}`,
|
||||
date: iso,
|
||||
items: [],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function isDateInWeek(iso: string, weekMonday: string): boolean {
|
||||
if (!iso || !/^\d{4}-\d{2}-\d{2}$/.test(iso)) {
|
||||
return false;
|
||||
}
|
||||
const friday = addDaysIso(weekMonday, 4);
|
||||
return iso >= weekMonday && iso <= friday;
|
||||
}
|
||||
166
src/domain/work-orders/utils/table-filter-utils.ts
Normal file
166
src/domain/work-orders/utils/table-filter-utils.ts
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
import type { WorkOrderAdvancedFilters } from "@/domain/work-orders/types/work-order-advanced-filters";
|
||||
import { ASSIGNEE_FILTER_UNASSIGNED } from "@/domain/work-orders/types/work-order-list";
|
||||
import type { WorkOrderBarFilters } from "@/domain/work-orders/types/work-order-list-filters";
|
||||
import type {
|
||||
WorkOrderDayGroup,
|
||||
WorkOrderTableRow,
|
||||
} from "@/domain/work-orders/types/work-order-table-row";
|
||||
import { dateRangeFor } from "@/domain/work-orders/utils/advanced-filters-utils";
|
||||
import { isDateInWeek } from "@/domain/work-orders/utils/table-day-utils";
|
||||
import { weekRange } from "@/domain/work-orders/utils/list-week-utils";
|
||||
|
||||
const TERMINAL_STATUSES = new Set(["Completed", "Canceled", "Done", "Cancelled"]);
|
||||
|
||||
export function computeIsPastDue(row: WorkOrderTableRow): boolean {
|
||||
if (!row.dueDate || TERMINAL_STATUSES.has(row.status)) {
|
||||
return false;
|
||||
}
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
return row.dueDate < today;
|
||||
}
|
||||
|
||||
export function enrichTableRow(row: WorkOrderTableRow): WorkOrderTableRow {
|
||||
return {
|
||||
...row,
|
||||
isPastDue: computeIsPastDue(row),
|
||||
};
|
||||
}
|
||||
|
||||
export function matchesAdvancedFilters(
|
||||
row: WorkOrderTableRow,
|
||||
filters: WorkOrderAdvancedFilters,
|
||||
): boolean {
|
||||
const [from, to] = dateRangeFor(filters);
|
||||
if (row.scheduledOn && (row.scheduledOn < from || row.scheduledOn > to)) {
|
||||
return false;
|
||||
}
|
||||
if (filters.sites.size && !filters.sites.has(row.site)) {
|
||||
return false;
|
||||
}
|
||||
if (filters.types.size && !filters.types.has(row.type as never)) {
|
||||
return false;
|
||||
}
|
||||
if (filters.dispatchers.size) {
|
||||
const unassignedMatch =
|
||||
filters.dispatchers.has(ASSIGNEE_FILTER_UNASSIGNED) && !row.dispatcherId;
|
||||
const idMatch = !!row.dispatcherId && filters.dispatchers.has(row.dispatcherId);
|
||||
if (!unassignedMatch && !idMatch) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (filters.statuses.size && (!row.status || !filters.statuses.has(row.status as never))) {
|
||||
return false;
|
||||
}
|
||||
if (filters.pmTypes.size && !filters.pmTypes.has(row.pm)) {
|
||||
return false;
|
||||
}
|
||||
if (filters.vendorTechs.size && !filters.vendorTechs.has(row.tech)) {
|
||||
return false;
|
||||
}
|
||||
if (filters.docs.size && !filters.docs.has(row.docStatus as never)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function rowMatchesSearch(row: WorkOrderTableRow, query: string): boolean {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) {
|
||||
return true;
|
||||
}
|
||||
const haystack = [
|
||||
row.site,
|
||||
row.location,
|
||||
row.woNumber,
|
||||
row.type,
|
||||
row.dispatcherName,
|
||||
row.scheduledOn,
|
||||
row.dueDate,
|
||||
row.pm,
|
||||
row.company,
|
||||
row.tech,
|
||||
row.status,
|
||||
row.title,
|
||||
]
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
return haystack.includes(q);
|
||||
}
|
||||
|
||||
export function filterByBarAndSearch(
|
||||
rows: WorkOrderTableRow[],
|
||||
bar: WorkOrderBarFilters,
|
||||
search: string,
|
||||
): WorkOrderTableRow[] {
|
||||
const [weekFrom, weekTo] = weekRange(bar.weekMonday);
|
||||
|
||||
return rows.filter((row) => {
|
||||
if (!rowMatchesSearch(row, search)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bar.type !== "All" && row.type !== bar.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bar.dispatcherIds.size > 0) {
|
||||
const unassignedMatch =
|
||||
bar.dispatcherIds.has(ASSIGNEE_FILTER_UNASSIGNED) && !row.dispatcherId;
|
||||
const idMatch = !!row.dispatcherId && bar.dispatcherIds.has(String(row.dispatcherId));
|
||||
if (!unassignedMatch && !idMatch) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (row.scheduledOn) {
|
||||
if (row.scheduledOn < weekFrom || row.scheduledOn > weekTo) {
|
||||
return false;
|
||||
}
|
||||
} else if (row.targetWeek) {
|
||||
if (!isDateInWeek(row.targetWeek, bar.weekMonday)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export function splitScheduledUnscheduled(rows: WorkOrderTableRow[]): {
|
||||
scheduled: WorkOrderTableRow[];
|
||||
unscheduled: WorkOrderTableRow[];
|
||||
} {
|
||||
const scheduled: WorkOrderTableRow[] = [];
|
||||
const unscheduled: WorkOrderTableRow[] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
if (!row.scheduledOn) {
|
||||
unscheduled.push(row);
|
||||
} else {
|
||||
scheduled.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
return { scheduled, unscheduled };
|
||||
}
|
||||
|
||||
export function buildDayGroups(scheduled: WorkOrderTableRow[]): WorkOrderDayGroup[] {
|
||||
const groups: WorkOrderDayGroup[] = [];
|
||||
|
||||
for (const row of scheduled) {
|
||||
const existing = groups.find((group) => group.key === row.dayGroup);
|
||||
if (existing) {
|
||||
existing.items.push(row);
|
||||
} else {
|
||||
groups.push({
|
||||
key: row.dayGroup,
|
||||
label: row.dayLabel,
|
||||
date: row.scheduledOn,
|
||||
items: [row],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
groups.sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));
|
||||
return groups;
|
||||
}
|
||||
148
src/domain/work-orders/utils/wizard-date-utils.ts
Normal file
148
src/domain/work-orders/utils/wizard-date-utils.ts
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
export const MONTH_NAMES = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
|
||||
export function pad2(n: number): string {
|
||||
return String(n).padStart(2, "0");
|
||||
}
|
||||
|
||||
export function todayIso(): string {
|
||||
const d = new Date();
|
||||
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
||||
}
|
||||
|
||||
export function fmtDate(iso: string): string {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(iso)) {
|
||||
return "";
|
||||
}
|
||||
const [y, m, d] = iso.split("-").map(Number);
|
||||
return `${MONTH_NAMES[m - 1].slice(0, 3)} ${d}, ${y}`;
|
||||
}
|
||||
|
||||
export function addDaysIso(iso: string, days: number): string {
|
||||
const d = new Date(`${iso}T12:00:00`);
|
||||
d.setDate(d.getDate() + days);
|
||||
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
||||
}
|
||||
|
||||
export function mondayOf(iso: string): string {
|
||||
const d = new Date(`${iso}T12:00:00`);
|
||||
const dow = d.getDay();
|
||||
d.setDate(d.getDate() + (dow === 0 ? -6 : 1 - dow));
|
||||
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
||||
}
|
||||
|
||||
export function weeksOfMonth(y: number, m: number): string[] {
|
||||
const firstIso = `${y}-${pad2(m)}-01`;
|
||||
const lastIso = `${y}-${pad2(m)}-${pad2(new Date(y, m, 0).getDate())}`;
|
||||
const out: string[] = [];
|
||||
for (let mon = mondayOf(firstIso); mon <= lastIso; mon = addDaysIso(mon, 7)) {
|
||||
if (addDaysIso(mon, 4) >= firstIso) {
|
||||
out.push(mon);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function weekRangeLabel(mondayIso: string): string {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(mondayIso)) {
|
||||
return "";
|
||||
}
|
||||
const fri = addDaysIso(mondayIso, 4);
|
||||
const [, sm, sd] = mondayIso.split("-").map(Number);
|
||||
const [, em, ed] = fri.split("-").map(Number);
|
||||
return `${MONTH_NAMES[sm - 1].slice(0, 3)} ${sd} – ${MONTH_NAMES[em - 1].slice(0, 3)} ${ed}`;
|
||||
}
|
||||
|
||||
export function fmtWeekRange(startIso: string): string {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(startIso)) {
|
||||
return fmtDate(startIso);
|
||||
}
|
||||
return weekRangeLabel(startIso);
|
||||
}
|
||||
|
||||
export function fmt12(t: string): string {
|
||||
const [h, m] = t.split(":").map(Number);
|
||||
const ampm = h < 12 ? "AM" : "PM";
|
||||
const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
|
||||
return `${h12}:${String(m).padStart(2, "0")} ${ampm}`;
|
||||
}
|
||||
|
||||
export function calcDuration(s: string, e: string): string {
|
||||
const [sh, sm] = s.split(":").map(Number);
|
||||
const [eh, em] = e.split(":").map(Number);
|
||||
const mins = eh * 60 + em - (sh * 60 + sm);
|
||||
if (mins <= 0) {
|
||||
return "";
|
||||
}
|
||||
const h = Math.floor(mins / 60);
|
||||
const m = mins % 60;
|
||||
if (h && m) {
|
||||
return `${h}h ${m}m`;
|
||||
}
|
||||
if (h) {
|
||||
return `${h}h`;
|
||||
}
|
||||
return `${m}m`;
|
||||
}
|
||||
|
||||
export function formatApptRange(start: string, end: string): string {
|
||||
if (!start) {
|
||||
return "—";
|
||||
}
|
||||
return end ? `${fmt12(start)} – ${fmt12(end)}` : fmt12(start);
|
||||
}
|
||||
|
||||
export const TIME_SLOTS: string[] = (() => {
|
||||
const out: string[] = [];
|
||||
for (let m = 5 * 60; m <= 22 * 60; m += 30) {
|
||||
const h = Math.floor(m / 60);
|
||||
const mm = m % 60;
|
||||
out.push(`${String(h).padStart(2, "0")}:${String(mm).padStart(2, "0")}`);
|
||||
}
|
||||
return out;
|
||||
})();
|
||||
|
||||
export function parseApptTime(appt: string): { start: string; end: string } {
|
||||
const m = appt.match(/(\d{1,2}):(\d{2})\s*[–-]\s*(\d{1,2}):(\d{2})/);
|
||||
if (m) {
|
||||
return {
|
||||
start: `${m[1].padStart(2, "0")}:${m[2]}`,
|
||||
end: `${m[3].padStart(2, "0")}:${m[4]}`,
|
||||
};
|
||||
}
|
||||
const single = appt.match(/(\d{1,2}):(\d{2})\s*(AM|PM)/i);
|
||||
if (single) {
|
||||
let h = Number(single[1]) % 12;
|
||||
if (/pm/i.test(single[3])) {
|
||||
h += 12;
|
||||
}
|
||||
return { start: `${pad2(h)}:${single[2]}`, end: "" };
|
||||
}
|
||||
return { start: "", end: "" };
|
||||
}
|
||||
|
||||
export function formatApptTimeValue(start: string, end: string): string {
|
||||
if (!start) {
|
||||
return "";
|
||||
}
|
||||
return end ? `${start} – ${end}` : start;
|
||||
}
|
||||
|
||||
export function fmtWeekOf(mondayIso: string): string {
|
||||
if (!mondayIso) {
|
||||
return "";
|
||||
}
|
||||
return `Week of ${weekRangeLabel(mondayIso)}`;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue