feat(work-orders): add domain core types, api, mappers and use-cases

This commit is contained in:
Arthur Bassi 2026-07-17 12:01:45 -03:00
parent a3e8a533b1
commit e13bc4c1a2
21 changed files with 1333 additions and 303 deletions

View file

@ -1,6 +1,18 @@
import { API_PATHS } from "@/api/api-paths";
import { apiDelete, apiGet, apiPost } from "@/api/api";
import { handleApiResponse, handlePaginatedResponse } from "@/api/handle-api-response";
import {
isWoTableMockEnabled,
shouldUseMockWorkOrderDetail,
} from "@/domain/work-orders/mocks/is-wo-table-mock-enabled";
import {
buildMockWorkOrderDetailFromTableRow,
getMockWorkOrderLocations,
getMockWorkOrderTableRow,
getMockWorkOrderVendors,
getMockWorkOrdersList,
MOCK_WO_TABLE_USERS,
} from "@/domain/work-orders/mocks/wo-table-mock-data";
import {
mapDispatchComment,
mapDispatchDetail,
@ -15,7 +27,7 @@ import {
mapWorkOrderDetail,
mapWorkOrderFormToCreatePayload,
mapWorkOrderFormToUpdatePayload,
mapWorkOrderListItems,
mapWorkOrderTableRows,
} from "@/domain/work-orders/mappers/work-order-mapper";
import type { WorkOrderFormSchemaValues } from "@/domain/work-orders/schemas/work-order-schema";
import type {
@ -30,9 +42,12 @@ import type {
WorkOrderDetail,
WorkOrderFormValues,
WorkOrderUser,
WorkOrdersListParams,
WorkOrdersListResult,
} from "@/domain/work-orders/types/work-order";
import {
DISPATCH_LOCATION_WO_PAGE_SIZE,
type WorkOrdersListParams,
type WorkOrdersListResult,
} from "@/domain/work-orders/types/work-order-list";
function buildSearchParams(params: WorkOrdersListParams): Record<string, string | number> {
const searchParams: Record<string, string | number> = {
@ -56,6 +71,18 @@ function buildSearchParams(params: WorkOrdersListParams): Record<string, string
if (params.locationId) {
searchParams.locationId = params.locationId;
}
if (params.dateFrom) {
searchParams.dateFrom = params.dateFrom;
}
if (params.dateTo) {
searchParams.dateTo = params.dateTo;
}
if (params.type) {
searchParams.type = params.type;
}
if (params.assigneeIds) {
searchParams.assigneeIds = params.assigneeIds;
}
return searchParams;
}
@ -75,8 +102,17 @@ function mapUser(raw: unknown): WorkOrderUser {
};
}
async function mockNetworkDelay(): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, 250));
}
export const workOrdersApi = {
getList: async (params: WorkOrdersListParams = {}): Promise<WorkOrdersListResult> => {
if (isWoTableMockEnabled()) {
await mockNetworkDelay();
return getMockWorkOrdersList(params);
}
const searchParams = buildSearchParams(params);
const data = await apiGet<unknown>(API_PATHS.workOrder.getList, { searchParams });
const paginated = handlePaginatedResponse<unknown>(data, {
@ -85,20 +121,45 @@ export const workOrdersApi = {
});
return {
items: mapWorkOrderListItems(paginated.items),
items: mapWorkOrderTableRows(paginated.items),
totalCount: paginated.totalCount,
page: paginated.page,
pageSize: paginated.pageSize,
};
},
getListByLocation: async (
locationId: string | number,
search = "",
): Promise<WorkOrdersListResult> => {
return workOrdersApi.getList({
locationId,
search,
page: 1,
pageSize: DISPATCH_LOCATION_WO_PAGE_SIZE,
sortBy: "number",
sortDir: "asc",
});
},
fetchUsers: async (): Promise<WorkOrderUser[]> => {
if (isWoTableMockEnabled()) {
return MOCK_WO_TABLE_USERS;
}
const data = await apiGet<unknown>(API_PATHS.user.list);
const users = handleApiResponse<unknown[]>(data);
return Array.isArray(users) ? users.map(mapUser) : [];
},
getById: async (id: string | number): Promise<WorkOrderDetail> => {
if (shouldUseMockWorkOrderDetail(id)) {
await mockNetworkDelay();
const row = getMockWorkOrderTableRow(id);
if (!row) {
throw new Error("Mock work order not found");
}
return buildMockWorkOrderDetailFromTableRow(row);
}
const data = await apiGet<unknown>(API_PATHS.workOrder.getById, {
searchParams: { id },
});
@ -117,6 +178,22 @@ export const workOrdersApi = {
id: string | number,
values: WorkOrderFormSchemaValues | WorkOrderFormValues,
): Promise<WorkOrderDetail> => {
if (shouldUseMockWorkOrderDetail(id)) {
await mockNetworkDelay();
const row = getMockWorkOrderTableRow(id);
if (!row) {
throw new Error("Mock work order not found");
}
return buildMockWorkOrderDetailFromTableRow({
...row,
woNumber: values.workOrderNumber || row.woNumber,
dueDate: values.dueDate || row.dueDate,
scheduledOn: values.scheduledDate || row.scheduledOn,
dispatcherName: values.assignedTo || row.dispatcherName,
pm: values.problem || row.pm,
type: (values.source || row.type) as typeof row.type,
});
}
const payload = mapWorkOrderFormToUpdatePayload(id, values);
const data = await apiPost<unknown>(API_PATHS.workOrder.editWorkorder, payload);
return mapWorkOrderDetail(handleApiResponse(data));
@ -142,50 +219,44 @@ export const workOrdersApi = {
},
changeStatus: async (id: string | number, status: string): Promise<void> => {
if (shouldUseMockWorkOrderDetail(id)) {
await mockNetworkDelay();
return;
}
await apiPost<unknown>(API_PATHS.workOrder.changeStatus, undefined, {
searchParams: { id, status },
});
},
changeAssignment: async (id: string | number, assignTo: string | number): Promise<void> => {
if (shouldUseMockWorkOrderDetail(id)) {
await mockNetworkDelay();
return;
}
await apiPost<unknown>(API_PATHS.workOrder.changeAssignment, undefined, {
searchParams: { id, assignTo: assignTo || "" },
});
},
fetchLocations: async (): Promise<LocationOption[]> => {
if (isWoTableMockEnabled()) {
return getMockWorkOrderLocations();
}
const data = await apiGet<unknown>(API_PATHS.contact.getLocations);
const items = handleApiResponse<unknown[]>(data);
return Array.isArray(items) ? mapLocationOptions(items) : [];
},
fetchEmployeesDropdown: async (): Promise<EmployeeOption[]> => {
try {
const data = await apiGet<unknown>(API_PATHS.employee.dropdown);
const items = handleApiResponse<unknown[]>(data);
return Array.isArray(items) ? mapEmployeeOptions(items) : [];
} catch {
const data = await apiGet<unknown>(API_PATHS.employee.getList, {
searchParams: { page: 1, pageSize: 1000 },
});
const paginated = handlePaginatedResponse<unknown>(data);
const items = paginated.items.map((item) => {
const record = (typeof item === "object" && item !== null ? item : {}) as Record<
string,
unknown
>;
const firstName = String(record.firstName ?? record.FirstName ?? "");
const lastName = String(record.lastName ?? record.LastName ?? "");
return {
id: record.id ?? record.Id,
name: `${firstName} ${lastName}`.trim(),
};
});
return mapEmployeeOptions(items);
}
const data = await apiGet<unknown>(API_PATHS.employee.dropdown);
const items = handleApiResponse<unknown[]>(data);
return Array.isArray(items) ? mapEmployeeOptions(items) : [];
},
fetchVendorsDropdown: async (trade?: string, siteZip?: string): Promise<VendorDropdownItem[]> => {
if (isWoTableMockEnabled()) {
return getMockWorkOrderVendors();
}
const searchParams: Record<string, string> = {};
if (trade) {
searchParams.trade = trade;

View file

@ -0,0 +1,122 @@
import type { WorkOrderFormValues } from "@/domain/work-orders/types/work-order-detail";
import type {
WOType,
WizardWOStatus,
WorkOrderWizardDraft,
} from "@/domain/work-orders/types/work-order-wizard";
import type { WorkOrderStatus } from "@/domain/work-orders/types/work-order-common";
import { formatApptRange, todayIso } from "@/domain/work-orders/utils/wizard-date-utils";
const WIZARD_TO_API_STATUS: Record<WizardWOStatus, WorkOrderStatus> = {
Incomplete: "Open",
Pending: "Open",
Scheduled: "Open",
"En Route": "In Progress",
"On Site": "In Progress",
"In Progress": "In Progress",
Completed: "Done",
Rescheduled: "On Hold",
"Pending Quote": "On Hold",
};
const API_TO_WIZARD_STATUS: Partial<Record<WorkOrderStatus, WizardWOStatus>> = {
Open: "Incomplete",
"In Progress": "In Progress",
"On Hold": "Pending",
Done: "Completed",
};
const TYPE_TO_PRIORITY: Record<WOType, string> = {
Emergency: "Sev 1",
Reactive: "Sev 2",
Overdue: "Sev 2",
"Add-On": "Sev 3",
PM: "Sev 3",
};
export function mapWizardStatusToApi(status: WizardWOStatus | ""): WorkOrderStatus {
if (!status) {
return "Open";
}
return WIZARD_TO_API_STATUS[status] ?? "Open";
}
export function mapApiStatusToWizard(status: string): WizardWOStatus | "" {
if (!status) {
return "";
}
if (API_TO_WIZARD_STATUS[status as WorkOrderStatus]) {
return API_TO_WIZARD_STATUS[status as WorkOrderStatus]!;
}
const wizardStatuses = Object.keys(WIZARD_TO_API_STATUS) as WizardWOStatus[];
if (wizardStatuses.includes(status as WizardWOStatus)) {
return status as WizardWOStatus;
}
if (status === "Canceled" || status === "Cancelled") {
return "Canceled" as WizardWOStatus;
}
return "Incomplete";
}
export function mapTypeToPriority(type: WOType | ""): string {
if (!type) {
return "Sev 3";
}
return TYPE_TO_PRIORITY[type];
}
function buildTitle(draft: WorkOrderWizardDraft): string {
if (draft.woNumber) {
return `${draft.type || "Work Order"} — #${draft.woNumber}`;
}
if (draft.locationName) {
return `${draft.type || "Work Order"} — ${draft.locationName}`;
}
return draft.type || "Work Order";
}
function buildDescription(draft: WorkOrderWizardDraft): string {
const parts: string[] = [];
if (draft.pocNotes) {
parts.push(`POC notes: ${draft.pocNotes}`);
}
if (draft.pocName || draft.pocPhone) {
parts.push(`POC: ${[draft.pocName, draft.pocPhone].filter(Boolean).join(" · ")}`);
}
if (draft.apptStart) {
parts.push(`Appt: ${formatApptRange(draft.apptStart, draft.apptEnd)}`);
}
if (draft.vendorNotes) {
parts.push(`Vendor notes: ${draft.vendorNotes}`);
}
if (draft.vendorName) {
parts.push(`Vendor: ${draft.vendorName}`);
}
return parts.join("\n");
}
export function mapWizardDraftToFormValues(draft: WorkOrderWizardDraft): WorkOrderFormValues {
return {
internalWONumber: "",
workOrderNumber: draft.woNumber,
title: buildTitle(draft),
locationId: draft.locationId,
priority: mapTypeToPriority(draft.type),
status: mapWizardStatusToApi(draft.status),
createdDate: todayIso(),
dueDate: draft.dueDate,
scheduledDate: draft.scheduleWeekOnly ? "" : draft.scheduledOn,
completedDate: "",
assignedTo: draft.assigneeName,
description: buildDescription(draft),
po: "",
tt: "",
problem: draft.pm,
trade: "",
subTrade: "",
vendorNTE: "",
source: draft.type,
};
}

View file

@ -1,3 +1,4 @@
import { mapApiStatusToWizard } from "@/domain/work-orders/mappers/wizard-draft-mapper";
import type {
DispatchChecklistItem,
DispatchComment,
@ -9,7 +10,6 @@ import type {
LocationOption,
TaskListTemplate,
UpdateDispatchPayload,
UpliftRequest,
VendorDropdownItem,
WorkOrderAuditEntry,
WorkOrderComment,
@ -18,12 +18,29 @@ import type {
WorkOrderFormValues,
WorkOrderListItem,
} from "@/domain/work-orders/types/work-order";
import type {
CompletionDocStatus,
WorkOrderTableRow,
} from "@/domain/work-orders/types/work-order-table-row";
import type { WOType } from "@/domain/work-orders/types/work-order-wizard";
import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils";
import { formatApptRange } from "@/domain/work-orders/utils/wizard-date-utils";
import type { UpliftRequest } from "@/domain/uplifts/types/uplift";
import type { WorkOrderStatus } from "@/domain/work-orders/types/work-order-common";
import { WORK_ORDER_STATUS_OPTIONS } from "@/domain/work-orders/types/work-order-common";
import { toDateInputValue } from "@/lib/time-utils";
function asRecord(raw: unknown): Record<string, unknown> {
return typeof raw === "object" && raw !== null ? (raw as Record<string, unknown>) : {};
}
function toWorkOrderStatus(value: string): WorkOrderStatus {
if (WORK_ORDER_STATUS_OPTIONS.includes(value as WorkOrderStatus)) {
return value as WorkOrderStatus;
}
return "Open";
}
function readString(record: Record<string, unknown>, ...keys: string[]): string {
for (const key of keys) {
const value = record[key];
@ -92,6 +109,114 @@ export function mapWorkOrderListItems(rawItems: unknown[]): WorkOrderListItem[]
return rawItems.map(mapWorkOrderListItem);
}
const WO_TYPES_SET = new Set(["PM", "Reactive", "Emergency", "Add-On", "Overdue"]);
function readWoType(record: Record<string, unknown>): WOType | "" {
const raw = readString(record, "type", "Type", "source", "Source", "woType", "WoType");
if (WO_TYPES_SET.has(raw)) {
return raw as WOType;
}
return "";
}
function readDocStatus(record: Record<string, unknown>): CompletionDocStatus {
const raw = readString(record, "docStatus", "DocStatus", "completionDoc", "CompletionDoc");
if (raw === "Yes" || raw === "No" || raw === "NN") {
return raw;
}
return "";
}
export function mapWorkOrderTableRow(raw: unknown): WorkOrderTableRow {
const item = asRecord(raw);
const scheduledOn = toDateInputValue(
readString(item, "scheduledOn", "scheduledDate", "ScheduledDate", "ScheduledOn"),
);
const { dayGroup, dayLabel } = deriveDayFields(scheduledOn);
const woNumber = readString(
item,
"woNumber",
"internalWONumber",
"InternalWONumber",
"number",
"workOrderNumber",
"WorkerOrderNumber",
);
const site = readString(item, "site", "Site", "locationCode", "LocationCode");
const location = readString(item, "location", "Location", "locationName", "LocationName");
const dispatcherId = readString(
item,
"dispatcherId",
"DispatcherId",
"assigneeId",
"AssigneeId",
"employeeId",
"EmployeeId",
);
const dispatcherName = readString(
item,
"dispatcherName",
"DispatcherName",
"assignedTo",
"AssignedTo",
"employeeName",
"EmployeeName",
"assignee",
"Assignee",
);
const apptStart = readString(item, "apptStart", "ApptStart", "appointmentStart");
const apptEnd = readString(item, "apptEnd", "ApptEnd", "appointmentEnd");
const apptTime =
readString(item, "apptTime", "ApptTime") ||
(apptStart ? formatApptRange(apptStart, apptEnd) : "");
const apiStatus = readString(item, "status", "Status");
const locationIdRaw = item.locationId ?? item.LocationId;
const row: WorkOrderTableRow = {
id: readId(item),
woNumber,
site: site || location.slice(0, 8),
type: readWoType(item),
dispatcherId,
dispatcherName,
scheduledOn,
targetWeek: readString(item, "targetWeek", "TargetWeek"),
dayGroup,
dayLabel,
location,
locationId:
typeof locationIdRaw === "string" || typeof locationIdRaw === "number" ? locationIdRaw : "",
pm: readString(item, "pm", "PM", "problem", "Problem", "service", "Service"),
company: readString(item, "company", "Company", "vendorName", "VendorName"),
tech: readString(item, "tech", "Tech", "technician", "Technician"),
techPhone: readString(item, "techPhone", "TechPhone"),
apptStart,
apptEnd,
apptTime,
status: mapApiStatusToWizard(apiStatus),
docStatus: readDocStatus(item),
pocName: readString(item, "pocName", "PocName", "POCName"),
pocPhone: readString(item, "pocPhone", "PocPhone", "POCPhone"),
pocNotes: readString(item, "pocNotes", "PocNotes", "POCNotes"),
dueDate: toDateInputValue(readString(item, "dueDate", "DueDate")),
rescheduleCount: readNumber(item, "rescheduleCount", "RescheduleCount") ?? 0,
carriedOver: readNumber(item, "carriedOver", "CarriedOver") ?? 0,
originalWeek: readString(item, "originalWeek", "OriginalWeek"),
originalDate: toDateInputValue(readString(item, "originalDate", "OriginalDate")),
isPastDue: readBool(item, "isPastDue", "IsPastDue"),
scheduleWeekOnly: readBool(item, "scheduleWeekOnly", "ScheduleWeekOnly"),
mediaCount: readNumber(item, "mediaCount", "MediaCount") ?? 0,
title: readString(item, "title", "Title", "workerOrderTitle", "WorkerOrderTitle"),
priority: readString(item, "priority", "Priority"),
};
return row;
}
export function mapWorkOrderTableRows(rawItems: unknown[]): WorkOrderTableRow[] {
return rawItems.map(mapWorkOrderTableRow);
}
export function mapWorkOrderComment(raw: unknown): WorkOrderComment {
const item = asRecord(raw);
@ -187,7 +312,7 @@ export function mapWorkOrderDetailToFormValues(detail: WorkOrderDetail): WorkOrd
title: detail.title,
locationId: detail.locationId,
priority: detail.priority,
status: detail.status,
status: toWorkOrderStatus(detail.status),
createdDate: detail.createdDate,
dueDate: detail.dueDate,
scheduledDate: detail.scheduledDate,

View file

@ -0,0 +1,42 @@
import type { ElementType } from "react";
import type { CompletionDocStatus } from "@/domain/work-orders/types/work-order-table-row";
export interface CompDocData {
notes: string;
generatedAt?: string;
}
export interface DocFields {
street: string;
city: string;
state: string;
zip: string;
dateCompleted: string;
amazonRep: string;
seaRep: string;
}
export type DocState = "draft" | "generated" | "not-required";
export interface CompletionDocStateConfig {
pillLabel: string;
pillBg: string;
pillText: string;
cardBg: string;
cardBorder: string;
cardIconBg: string;
iconColor: string;
cardTitle: string;
icon: ElementType;
}
export interface CompDocSiteInfo {
street: string;
city: string;
state: string;
zip: string;
label: string;
fullAddress: string;
}
export type CompDocSaveStatus = Exclude<CompletionDocStatus, "">;

View file

@ -0,0 +1,80 @@
export interface WorkOrderDispatchSummary {
id: string | number;
dispatchNumber: string;
poNumber: string;
vendorName: string;
nteAmount: number | null;
status: string;
dispatchedAt: string;
}
export interface DispatchChecklistItem {
id: string | number;
itemText: string;
isCompleted: boolean;
completedBy: string | null;
completedAt: string | null;
}
export interface DispatchSignoff {
id: string | number;
signoffType: string;
name: string;
signature: string;
signatureMethod: string;
signedAt: string;
}
export interface DispatchComment {
id: string | number;
createdDate: string;
commenttext: string;
userName: string;
commenter: string | null;
}
export interface DispatchWorkOrderRef {
id: string | number;
woNumber: string;
}
export interface DispatchDetail {
id: string | number;
dispatchNumber: string;
poNumber: string;
vendorName: string;
vendorEmail: string;
woNumber: string;
status: string;
nteAmount: number | null;
scheduledDate: string;
completedDate: string;
description: string;
dispatchedAt: string;
acknowledgedAt: string;
verifiedAt: string;
verifiedBy: string;
checklistItems: DispatchChecklistItem[];
signoffs: DispatchSignoff[];
comments: DispatchComment[];
workOrders: DispatchWorkOrderRef[];
}
export interface DispatchToVendorPayload {
workOrderIds: (string | number)[];
vendorIds: (string | number)[];
nteAmount: number | null;
scheduledDate: string | null;
description: string;
taskListTemplateId: number | null;
customChecklistItems: string[] | null;
}
export interface UpdateDispatchPayload {
id: string | number;
status: string;
nteAmount: number | null;
scheduledDate: string | null;
completedDate: string | null;
description: string;
}

View file

@ -0,0 +1,74 @@
import { ASSIGNEE_FILTER_UNASSIGNED } from "./work-order-list";
import {
ALL_WIZARD_STATUSES,
WO_TYPE_META,
WO_TYPES,
type WizardWOStatus,
type WOType,
} from "./work-order-wizard";
export { ASSIGNEE_FILTER_UNASSIGNED };
export type DateRangeKey =
| "this-week"
| "last-week"
| "this-month"
| "last-3-months"
| "next-week"
| "next-month"
| "custom";
export type CompletionDocFilter = "No" | "Yes" | "NN";
export interface WorkOrderAdvancedFilters {
dateRange: DateRangeKey;
customFrom: string;
customTo: string;
sites: Set<string>;
types: Set<WOType>;
dispatchers: Set<string>;
statuses: Set<WizardWOStatus>;
pmTypes: Set<string>;
vendorTechs: Set<string>;
docs: Set<CompletionDocFilter>;
}
export const EMPTY_ADVANCED_FILTERS: WorkOrderAdvancedFilters = {
dateRange: "this-week",
customFrom: "",
customTo: "",
sites: new Set(),
types: new Set(),
dispatchers: new Set(),
statuses: new Set(),
pmTypes: new Set(),
vendorTechs: new Set(),
docs: new Set(),
};
export const DATE_RANGE_OPTIONS: { key: DateRangeKey; label: string }[] = [
{ key: "this-week", label: "This week" },
{ key: "last-week", label: "Last week" },
{ key: "this-month", label: "This month" },
{ key: "last-3-months", label: "Last 3 months" },
{ key: "next-week", label: "Next week" },
{ key: "next-month", label: "Next month" },
{ key: "custom", label: "Custom" },
];
export const DOC_FILTER_LABELS: Record<CompletionDocFilter, string> = {
No: "Pending",
Yes: "Uploaded",
NN: "N/N",
};
export const COMPLETION_DOC_OPTIONS: CompletionDocFilter[] = ["No", "Yes", "NN"];
export const WO_TYPE_DESCRIPTIONS: Record<WOType, string> = Object.fromEntries(
WO_TYPES.map((type) => [type, WO_TYPE_META[type].desc]),
) as Record<WOType, string>;
export { ALL_WIZARD_STATUSES, WO_TYPES };
export type AdvancedFilterSetKey =
"sites" | "types" | "dispatchers" | "statuses" | "pmTypes" | "vendorTechs" | "docs";

View file

@ -0,0 +1,48 @@
export const WORK_ORDER_STATUS_OPTIONS = [
"Open",
"In Progress",
"On Hold",
"Done",
"Cancelled",
] as const;
export const WORK_ORDER_PRIORITY_OPTIONS = ["Sev 1", "Sev 2", "Sev 3", "Sev 4", "Sev 5"] as const;
export const DISPATCH_STATUS_OPTIONS = [
"Sent",
"Acknowledged",
"In Progress",
"Completed",
"Verified",
"Cancelled",
] as const;
export type WorkOrderStatus = (typeof WORK_ORDER_STATUS_OPTIONS)[number];
export type WorkOrderPriority = (typeof WORK_ORDER_PRIORITY_OPTIONS)[number];
export type DispatchStatus = (typeof DISPATCH_STATUS_OPTIONS)[number];
export type WorkOrderCommentType = "customer" | "internal";
export interface LocationOption {
id: string | number;
name: string;
}
export interface EmployeeOption {
id: string | number;
name: string;
}
export interface VendorDropdownItem {
id: string | number;
companyName: string;
tradeSpecialties: string;
address: string;
distanceMiles: number | null;
}
export interface TaskListTemplate {
id: string | number;
name: string;
items: { id?: string | number; itemText?: string }[];
}

View file

@ -0,0 +1,97 @@
import type { WorkOrderCommentType, WorkOrderPriority, WorkOrderStatus } from "./work-order-common";
import type { WorkOrderDispatchSummary } from "./dispatch";
export interface WorkOrderComment {
id: string | number;
createdDate: string;
commenttext: string;
commentType: WorkOrderCommentType | string;
userName: string;
commenter: string | null;
recordType: string | null;
}
export interface WorkOrderAuditEntry {
id: string | number;
fieldName: string;
oldValue: string;
newValue: string;
action: string;
createdAt: string;
userName: string;
}
export interface WorkOrderDetail {
id: string | number;
internalWONumber: string;
workOrderNumber: string;
title: string;
location: string;
locationId: string;
locationZip: string;
assignedTo: string;
dueDate: string;
createdDate: string;
createdDateFull: string;
description: string;
priority: WorkOrderPriority | string;
status: WorkOrderStatus | string;
po: string;
tt: string;
problem: string;
trade: string;
subTrade: string;
vendorNTE: string;
scheduledDate: string;
completedDate: string;
source: string;
comments: WorkOrderComment[];
auditLog: WorkOrderAuditEntry[];
dispatches: WorkOrderDispatchSummary[];
}
export interface WorkOrderFormValues {
internalWONumber: string;
workOrderNumber: string;
title: string;
locationId: string;
priority: string;
status: WorkOrderStatus;
createdDate: string;
dueDate: string;
scheduledDate: string;
completedDate: string;
assignedTo: string;
description: string;
po: string;
tt: string;
problem: string;
trade: string;
subTrade: string;
vendorNTE: string;
source: string;
}
export function createDefaultWorkOrderFormValues(): WorkOrderFormValues {
return {
internalWONumber: "",
workOrderNumber: "",
title: "",
locationId: "",
priority: "",
status: "Open",
createdDate: new Date().toISOString().split("T")[0] ?? "",
dueDate: "",
scheduledDate: "",
completedDate: "",
assignedTo: "",
description: "",
po: "",
tt: "",
problem: "",
trade: "",
subTrade: "",
vendorNTE: "",
source: "",
};
}

View file

@ -0,0 +1,9 @@
import type { WOType } from "./work-order-wizard";
export type WorkOrderTypeFilter = WOType | "All";
export interface WorkOrderBarFilters {
weekMonday: string;
dispatcherIds: Set<string>;
type: WorkOrderTypeFilter;
}

View file

@ -0,0 +1,61 @@
import type { WorkOrderPriority, WorkOrderStatus } from "./work-order-common";
import type { WorkOrderTableRow } from "./work-order-table-row";
export type SortDirection = "asc" | "desc" | "";
export type WorkOrdersSortableColumn = "number" | "title";
export const ASSIGNEE_FILTER_UNASSIGNED = "__unassigned" as const;
export interface WorkOrderListItem {
id: string | number;
number: string;
title: string;
location: string;
priority: WorkOrderPriority | string;
status: WorkOrderStatus | string;
createdDate: string;
dueDate: string;
assignedTo: string;
lastUpdated: string;
}
export interface WorkOrdersListParams {
search?: string;
page?: number;
pageSize?: number;
sortBy?: WorkOrdersSortableColumn | string;
sortDir?: SortDirection;
status?: string;
assignee?: string;
locationId?: string | number;
/** Phase 2: filter bar week range (Monday ISO). */
dateFrom?: string;
/** Phase 2: filter bar week range (Friday ISO). */
dateTo?: string;
/** Phase 2: filter bar WO type. */
type?: string;
/** Phase 2: filter bar dispatcher ids (format TBD by API contract). */
assigneeIds?: string;
}
export interface WorkOrderUser {
id: string | number;
name: string;
}
export interface WorkOrdersListResult {
items: WorkOrderTableRow[];
totalCount: number;
page: number;
pageSize: number;
}
export interface SelectedWorkOrderRef {
id: string | number;
number: string;
title: string;
}
/** Max page size when loading WOs at a location for dispatch multi-select. */
export const DISPATCH_LOCATION_WO_PAGE_SIZE = 50;

View file

@ -0,0 +1,24 @@
export type FileCategory = "image" | "video" | "pdf" | "doc" | "other";
export type MediaCategory =
"Photo" | "Video" | "Receipt" | "Quote" | "Invoice" | "Report" | "Other";
export interface MediaFile {
id: string;
name: string;
fileType: FileCategory;
url: string;
category: MediaCategory;
}
export const MEDIA_CATEGORIES: MediaCategory[] = [
"Photo",
"Video",
"Receipt",
"Quote",
"Invoice",
"Report",
"Other",
];
export const EMPTY_MEDIA: MediaFile[] = [];

View file

@ -0,0 +1,99 @@
import type { WizardWOStatus, WOType } from "./work-order-wizard";
export type CompletionDocStatus = "Yes" | "No" | "NN" | "";
export const WORK_ORDERS_TABLE_PAGE_SIZE = 500;
export interface WorkOrderTableRow {
id: string | number;
woNumber: string;
site: string;
type: WOType | "";
dispatcherId: string;
dispatcherName: string;
scheduledOn: string;
targetWeek: string;
dayGroup: string;
dayLabel: string;
location: string;
locationId: string | number;
pm: string;
company: string;
tech: string;
techPhone: string;
apptStart: string;
apptEnd: string;
apptTime: string;
status: WizardWOStatus | "";
docStatus: CompletionDocStatus;
pocName: string;
pocPhone: string;
pocNotes: string;
dueDate: string;
rescheduleCount: number;
carriedOver: number;
originalWeek: string;
originalDate: string;
isPastDue: boolean;
scheduleWeekOnly: boolean;
mediaCount: number;
title: string;
priority: string;
}
export interface WorkOrderTableDraft {
site: string;
woNumber: string;
type: WOType | "";
dispatcherId: string;
scheduledOn: string;
schedNotes: string;
dueDate: string;
location: string;
locationId: string | number;
pm: string;
company: string;
tech: string;
techPhone: string;
apptStart: string;
apptEnd: string;
status: WizardWOStatus | "";
pocName: string;
pocPhone: string;
pocNotes: string;
scheduleWeekOnly: boolean;
targetWeek: string;
}
export const EMPTY_TABLE_DRAFT: WorkOrderTableDraft = {
site: "",
woNumber: "",
type: "",
dispatcherId: "",
scheduledOn: "",
schedNotes: "",
dueDate: "",
location: "",
locationId: "",
pm: "",
company: "",
tech: "",
techPhone: "",
apptStart: "",
apptEnd: "",
status: "Incomplete",
pocName: "",
pocPhone: "",
pocNotes: "",
scheduleWeekOnly: false,
targetWeek: "",
};
export interface WorkOrderDayGroup {
key: string;
label: string;
date: string;
items: WorkOrderTableRow[];
}
export const UNSCHEDULED_DAY_KEY = "__unscheduled__" as const;

View file

@ -0,0 +1,149 @@
export const WO_TYPES = ["PM", "Reactive", "Emergency", "Add-On", "Overdue"] as const;
export type WOType = (typeof WO_TYPES)[number];
export const ALL_WIZARD_STATUSES = [
"Incomplete",
"Pending",
"Scheduled",
"En Route",
"On Site",
"In Progress",
"Completed",
"Rescheduled",
"Pending Quote",
] as const;
export type WizardWOStatus = (typeof ALL_WIZARD_STATUSES)[number];
export interface WorkOrderWizardDraft {
locationId: string;
locationName: string;
woNumber: string;
type: WOType | "";
assigneeId: string;
assigneeName: string;
scheduledOn: string;
dueDate: string;
pm: string;
vendorId: string;
vendorName: string;
techPhone: string;
vendorNotes: string;
apptStart: string;
apptEnd: string;
status: WizardWOStatus | "";
pocName: string;
pocPhone: string;
pocNotes: string;
scheduleWeekOnly: boolean;
}
export const EMPTY_WIZARD_DRAFT: WorkOrderWizardDraft = {
locationId: "",
locationName: "",
woNumber: "",
type: "",
assigneeId: "",
assigneeName: "",
scheduledOn: "",
dueDate: "",
pm: "",
vendorId: "",
vendorName: "",
techPhone: "",
vendorNotes: "",
apptStart: "",
apptEnd: "",
status: "Incomplete",
pocName: "",
pocPhone: "",
pocNotes: "",
scheduleWeekOnly: false,
};
export const WIZARD_STEPS: { title: string; nav: string; subtitle: string }[] = [
{
title: "Type & schedule",
nav: "What and when",
subtitle: "Set the work-order type and when it should happen.",
},
{
title: "Location & service",
nav: "Where and what work",
subtitle: "Pick the site and the service to be performed.",
},
{
title: "Vendor & time",
nav: "Who and the window",
subtitle: "Assign a vendor crew and the appointment window.",
},
];
export const WO_TYPE_META: Record<WOType, { desc: string; bg: string; fg: string }> = {
PM: {
desc: "Scheduled preventive maintenance",
bg: "var(--type-pm-bg)",
fg: "var(--type-pm-text)",
},
Reactive: {
desc: "Tenant-reported repair",
bg: "var(--type-reactive-bg)",
fg: "var(--type-reactive-text)",
},
Emergency: {
desc: "Urgent same-day dispatch",
bg: "var(--type-emergency-bg)",
fg: "var(--type-emergency-text)",
},
"Add-On": {
desc: "Extra scope on an existing visit",
bg: "var(--type-addon-bg)",
fg: "var(--type-addon-text)",
},
Overdue: {
desc: "No client response — due date passed",
bg: "var(--type-overdue-bg)",
fg: "var(--type-overdue-text)",
},
};
export const WIZARD_STATUS_DOTS: Record<WizardWOStatus, string> = {
Incomplete: "#9CA3AF",
Pending: "#D97706",
Scheduled: "#3B82F6",
"En Route": "#0EA5E9",
"On Site": "#10B981",
"In Progress": "#2563EB",
Completed: "#16A34A",
Rescheduled: "#F59E0B",
"Pending Quote": "#9333EA",
};
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 canContinueWizardStep1(draft: WorkOrderWizardDraft): boolean {
return Boolean(draft.type);
}
export function canCreateWorkOrderFromWizard(draft: WorkOrderWizardDraft): boolean {
return Boolean(draft.locationId);
}
export function isWizardStepValid(step: number, draft: WorkOrderWizardDraft): boolean {
if (step === 1) {
return canContinueWizardStep1(draft);
}
if (step === 2) {
return Boolean(draft.locationId);
}
return true;
}

View file

@ -1,270 +1,61 @@
export interface WorkOrderListItem {
id: string | number;
number: string;
title: string;
location: string;
priority: string;
status: string;
createdDate: string;
dueDate: string;
assignedTo: string;
lastUpdated: string;
}
export type {
DispatchChecklistItem,
DispatchComment,
DispatchDetail,
DispatchSignoff,
DispatchToVendorPayload,
DispatchWorkOrderRef,
UpdateDispatchPayload,
WorkOrderDispatchSummary,
} from "./dispatch";
export interface WorkOrdersListParams {
search?: string;
page?: number;
pageSize?: number;
sortBy?: string;
sortDir?: string;
status?: string;
assignee?: string;
locationId?: string | number;
}
export type {
DispatchStatus,
EmployeeOption,
LocationOption,
TaskListTemplate,
VendorDropdownItem,
WorkOrderCommentType,
WorkOrderPriority,
WorkOrderStatus,
} from "./work-order-common";
export interface WorkOrderUser {
id: string | number;
name: string;
}
export {
DISPATCH_STATUS_OPTIONS,
WORK_ORDER_PRIORITY_OPTIONS,
WORK_ORDER_STATUS_OPTIONS,
} from "./work-order-common";
export interface WorkOrdersListResult {
items: WorkOrderListItem[];
totalCount: number;
page: number;
pageSize: number;
}
export type {
WorkOrderAuditEntry,
WorkOrderComment,
WorkOrderDetail,
WorkOrderFormValues,
} from "./work-order-detail";
export interface WorkOrderComment {
id: string | number;
createdDate: string;
commenttext: string;
commentType: string;
userName: string;
commenter: string | null;
recordType: string | null;
}
export { createDefaultWorkOrderFormValues } from "./work-order-detail";
export interface WorkOrderAuditEntry {
id: string | number;
fieldName: string;
oldValue: string;
newValue: string;
action: string;
createdAt: string;
userName: string;
}
export type {
SelectedWorkOrderRef,
SortDirection,
WorkOrderListItem,
WorkOrdersListParams,
WorkOrdersListResult,
WorkOrderUser,
WorkOrdersSortableColumn,
} from "./work-order-list";
export interface WorkOrderDispatchSummary {
id: string | number;
dispatchNumber: string;
poNumber: string;
vendorName: string;
nteAmount: number | null;
status: string;
dispatchedAt: string;
}
export { ASSIGNEE_FILTER_UNASSIGNED, DISPATCH_LOCATION_WO_PAGE_SIZE } from "./work-order-list";
export interface WorkOrderDetail {
id: string | number;
internalWONumber: string;
workOrderNumber: string;
title: string;
location: string;
locationId: string;
locationZip: string;
assignedTo: string;
dueDate: string;
createdDate: string;
createdDateFull: string;
description: string;
priority: string;
status: string;
po: string;
tt: string;
problem: string;
trade: string;
subTrade: string;
vendorNTE: string;
scheduledDate: string;
completedDate: string;
source: string;
comments: WorkOrderComment[];
auditLog: WorkOrderAuditEntry[];
dispatches: WorkOrderDispatchSummary[];
}
export type {
CompletionDocStatus,
WorkOrderDayGroup,
WorkOrderTableDraft,
WorkOrderTableRow,
} from "./work-order-table-row";
export interface WorkOrderFormValues {
internalWONumber: string;
workOrderNumber: string;
title: string;
locationId: string;
priority: string;
status: string;
createdDate: string;
dueDate: string;
scheduledDate: string;
completedDate: string;
assignedTo: string;
description: string;
po: string;
tt: string;
problem: string;
trade: string;
subTrade: string;
vendorNTE: string;
source: string;
}
export interface LocationOption {
id: string | number;
name: string;
}
export interface EmployeeOption {
id: string | number;
name: string;
}
export interface VendorDropdownItem {
id: string | number;
companyName: string;
tradeSpecialties: string;
address: string;
distanceMiles: number | null;
}
export interface TaskListTemplate {
id: string | number;
name: string;
items: { id?: string | number; itemText?: string }[];
}
export interface DispatchChecklistItem {
id: string | number;
itemText: string;
isCompleted: boolean;
completedBy: string | null;
completedAt: string | null;
}
export interface DispatchSignoff {
id: string | number;
signoffType: string;
name: string;
signature: string;
signatureMethod: string;
signedAt: string;
}
export interface DispatchComment {
id: string | number;
createdDate: string;
commenttext: string;
userName: string;
commenter: string | null;
}
export interface DispatchWorkOrderRef {
id: string | number;
woNumber: string;
}
export interface DispatchDetail {
id: string | number;
dispatchNumber: string;
poNumber: string;
vendorName: string;
vendorEmail: string;
woNumber: string;
status: string;
nteAmount: number | null;
scheduledDate: string;
completedDate: string;
description: string;
dispatchedAt: string;
acknowledgedAt: string;
verifiedAt: string;
verifiedBy: string;
checklistItems: DispatchChecklistItem[];
signoffs: DispatchSignoff[];
comments: DispatchComment[];
workOrders: DispatchWorkOrderRef[];
}
export interface DispatchToVendorPayload {
workOrderIds: (string | number)[];
vendorIds: (string | number)[];
nteAmount: number | null;
scheduledDate: string | null;
description: string;
taskListTemplateId: number | null;
customChecklistItems: string[] | null;
}
export interface UpdateDispatchPayload {
id: string | number;
status: string;
nteAmount: number | null;
scheduledDate: string | null;
completedDate: string | null;
description: string;
}
export interface UpliftRequest {
id: string | number;
status: string;
currentNTE: number | null;
requestedNTE: number;
delta: number;
vendorReason: string;
requestedAt: string;
requestedByVendorName: string;
decidedAt: string;
decidedByName: string;
decisionNote: string;
requiredTier: number;
canDecide: boolean;
}
export const WORK_ORDER_STATUS_OPTIONS = [
"Open",
"In Progress",
"On Hold",
"Done",
"Cancelled",
] as const;
export const WORK_ORDER_PRIORITY_OPTIONS = ["Sev 1", "Sev 2", "Sev 3", "Sev 4", "Sev 5"] as const;
export const DISPATCH_STATUS_OPTIONS = [
"Sent",
"Acknowledged",
"In Progress",
"Completed",
"Verified",
"Cancelled",
] as const;
export function createDefaultWorkOrderFormValues(): WorkOrderFormValues {
return {
internalWONumber: "",
workOrderNumber: "",
title: "",
locationId: "",
priority: "",
status: "Open",
createdDate: new Date().toISOString().split("T")[0] ?? "",
dueDate: "",
scheduledDate: "",
completedDate: "",
assignedTo: "",
description: "",
po: "",
tt: "",
problem: "",
trade: "",
subTrade: "",
vendorNTE: "",
source: "",
};
}
export {
EMPTY_TABLE_DRAFT,
UNSCHEDULED_DAY_KEY,
WORK_ORDERS_TABLE_PAGE_SIZE,
} from "./work-order-table-row";

View file

@ -0,0 +1,35 @@
import { API_PATHS } from "@/api/api-paths";
import { apiGet } from "@/api/api";
import { handleApiResponse, handlePaginatedResponse } from "@/api/handle-api-response";
import { mapEmployeeOptions } from "@/domain/work-orders/mappers/work-order-mapper";
import type { EmployeeOption } from "@/domain/work-orders/types/work-order";
async function fetchEmployeesFromList(): Promise<EmployeeOption[]> {
const data = await apiGet<unknown>(API_PATHS.employee.getList, {
searchParams: { page: 1, pageSize: 1000 },
});
const paginated = handlePaginatedResponse<unknown>(data);
const items = paginated.items.map((item) => {
const record = (typeof item === "object" && item !== null ? item : {}) as Record<
string,
unknown
>;
const firstName = String(record.firstName ?? record.FirstName ?? "");
const lastName = String(record.lastName ?? record.LastName ?? "");
return {
id: record.id ?? record.Id,
name: `${firstName} ${lastName}`.trim(),
};
});
return mapEmployeeOptions(items);
}
export async function fetchWorkOrderEmployees(): Promise<EmployeeOption[]> {
try {
const data = await apiGet<unknown>(API_PATHS.employee.dropdown);
const items = handleApiResponse<unknown[]>(data);
return Array.isArray(items) ? mapEmployeeOptions(items) : [];
} catch {
return fetchEmployeesFromList();
}
}

View file

@ -0,0 +1,35 @@
import { useMutation, useQueryClient, type UseMutationResult } from "@tanstack/react-query";
import { toast } from "react-toastify";
import { workOrdersApi } from "@/domain/work-orders/api/work-orders-api";
import { shouldUseMockWoWizardCreate } from "@/domain/work-orders/mocks/is-wo-wizard-mock-enabled";
import { buildMockWorkOrderDetail } from "@/domain/work-orders/mocks/wo-wizard-mock-data";
import type { WorkOrderFormValues } from "@/domain/work-orders/types/work-order-detail";
import type { WorkOrderDetail } from "@/domain/work-orders/types/work-order";
import { queryKeys } from "@/infra/query-key/query-key";
async function createWorkOrderFromWizard(values: WorkOrderFormValues): Promise<WorkOrderDetail> {
if (shouldUseMockWoWizardCreate(values.locationId)) {
await new Promise((resolve) => setTimeout(resolve, 400));
return buildMockWorkOrderDetail(values);
}
return workOrdersApi.create(values);
}
export function useCreateWorkOrderFromWizard(
onSuccess?: () => void,
): UseMutationResult<WorkOrderDetail, Error, WorkOrderFormValues> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createWorkOrderFromWizard,
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.all });
toast.success("Work Order created successfully!");
onSuccess?.();
},
onError: (error: Error) => {
toast.error(error.message || "Failed to create work order");
},
});
}

View file

@ -1,5 +1,6 @@
import { useQuery, type UseQueryResult } from "@tanstack/react-query";
import { workOrdersApi } from "@/domain/work-orders/api/work-orders-api";
import { fetchWorkOrderEmployees } from "@/domain/work-orders/use-cases/fetch-work-order-employees";
import type {
DispatchDetail,
EmployeeOption,
@ -32,7 +33,7 @@ export function useWorkOrderLocations(): UseQueryResult<LocationOption[], Error>
export function useWorkOrderEmployeesDropdown(): UseQueryResult<EmployeeOption[], Error> {
return useQuery({
queryKey: [...queryKeys.employees.all, "dropdown"] as const,
queryFn: () => workOrdersApi.fetchEmployeesDropdown(),
queryFn: () => fetchWorkOrderEmployees(),
staleTime: 5 * 60 * 1000,
});
}

View file

@ -20,32 +20,32 @@ function invalidateWorkOrders(queryClient: ReturnType<typeof useQueryClient>) {
interface AddWorkOrderCommentInput {
text: string;
commentType: string;
commentType: "customer" | "internal";
}
interface ChangeWorkOrderAssignmentInput {
userId: string | number;
userName: string;
}
interface AddDispatchCommentInput {
export interface AddDispatchCommentInput {
text: string;
sendEmail: boolean;
}
interface UpdateChecklistItemInput {
export interface UpdateChecklistItemInput {
itemId: string | number;
isCompleted: boolean;
completedBy: string;
}
interface AddDispatchSignoffInput {
export interface AddDispatchSignoffInput {
signoffType: string;
name: string;
signature: string;
signatureMethod: string;
}
interface ChangeWorkOrderAssignmentInput {
userId: string | number;
userName: string;
}
export function useCreateWorkOrder(): UseMutationResult<
WorkOrderDetail,
Error,

View file

@ -0,0 +1,154 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "react-toastify";
import { workOrdersApi } from "@/domain/work-orders/api/work-orders-api";
import {
mapApiStatusToWizard,
mapWizardStatusToApi,
} from "@/domain/work-orders/mappers/wizard-draft-mapper";
import {
mapWorkOrderDetailToFormValues,
mapWorkOrderTableRow,
} from "@/domain/work-orders/mappers/work-order-mapper";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import type { WizardWOStatus } from "@/domain/work-orders/types/work-order-wizard";
import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils";
import { enrichTableRow } from "@/domain/work-orders/utils/table-filter-utils";
import { queryKeys } from "@/infra/query-key/query-key";
export type WorkOrderTablePatch = Partial<WorkOrderTableRow>;
async function loadFormAndUpdate(
id: string | number,
patch: (
values: ReturnType<typeof mapWorkOrderDetailToFormValues>,
) => ReturnType<typeof mapWorkOrderDetailToFormValues>,
) {
const detail = await workOrdersApi.getById(id);
const values = mapWorkOrderDetailToFormValues(detail);
const next = patch(values);
await workOrdersApi.update(id, next);
return detail;
}
export function useWorkOrderTableMutations(
onPatch: (id: string | number, patch: WorkOrderTablePatch) => void,
) {
const queryClient = useQueryClient();
const invalidate = () => {
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.all });
};
const patchMutation = useMutation({
mutationFn: async ({
id,
patch,
useFullUpdate,
}: {
id: string | number;
patch: WorkOrderTablePatch;
useFullUpdate?: boolean;
}) => {
if (patch.status !== undefined && !useFullUpdate) {
const apiStatus = mapWizardStatusToApi(patch.status as WizardWOStatus);
await workOrdersApi.changeStatus(id, apiStatus);
return patch;
}
if (patch.dispatcherId !== undefined && !useFullUpdate) {
await workOrdersApi.changeAssignment(id, patch.dispatcherId || "");
return {
...patch,
dispatcherName: patch.dispatcherName ?? "",
};
}
await loadFormAndUpdate(id, (values) => ({
...values,
workOrderNumber: patch.woNumber ?? values.workOrderNumber,
internalWONumber: patch.woNumber ?? values.internalWONumber,
locationId: patch.locationId != null ? String(patch.locationId) : values.locationId,
dueDate: patch.dueDate ?? values.dueDate,
scheduledDate: patch.scheduledOn ?? values.scheduledDate,
assignedTo: patch.dispatcherName ?? values.assignedTo,
problem: patch.pm ?? values.problem,
source: patch.type ?? values.source,
priority: values.priority,
status:
patch.status !== undefined
? mapWizardStatusToApi(patch.status as WizardWOStatus)
: values.status,
description: [
values.description,
patch.pocNotes ? `POC notes: ${patch.pocNotes}` : "",
patch.pocName || patch.pocPhone
? `POC: ${[patch.pocName, patch.pocPhone].filter(Boolean).join(" · ")}`
: "",
]
.filter(Boolean)
.join("\n"),
}));
return patch;
},
onMutate: async ({ id, patch }) => {
onPatch(id, patch);
},
onSuccess: () => {
invalidate();
},
onError: (error: Error) => {
toast.error(error.message || "Failed to update work order");
invalidate();
},
});
const patchField = (id: string | number, patch: WorkOrderTablePatch, useFullUpdate = false) => {
patchMutation.mutate({ id, patch, useFullUpdate });
};
return {
patchField,
isPending: patchMutation.isPending,
};
}
export function tableRowFromDetail(detail: Awaited<ReturnType<typeof workOrdersApi.getById>>) {
return enrichTableRow(
mapWorkOrderTableRow({
...detail,
status: mapApiStatusToWizard(detail.status),
scheduledOn: detail.scheduledDate,
woNumber: detail.internalWONumber || detail.workOrderNumber,
dispatcherName: detail.assignedTo,
pm: detail.problem,
type: detail.source,
}),
);
}
export function maybeAutoSchedulePatch(row: WorkOrderTableRow): WorkOrderTablePatch {
const hasDate = !!row.scheduledOn;
const hasDispatcher = !!row.dispatcherId;
if (hasDate && hasDispatcher && row.status === "Incomplete") {
return { status: "Scheduled" };
}
return {};
}
export function applySchedulePatch(
scheduledOn: string,
scheduleWeekOnly: boolean,
): WorkOrderTablePatch {
if (scheduleWeekOnly) {
return {
scheduledOn: "",
targetWeek: scheduledOn,
scheduleWeekOnly: true,
...deriveDayFields(""),
};
}
return {
scheduledOn,
scheduleWeekOnly: false,
targetWeek: "",
...deriveDayFields(scheduledOn),
};
}

View file

@ -11,7 +11,7 @@ export function useWorkOrdersList(
params: WorkOrdersListParams,
): UseQueryResult<WorkOrdersListResult, Error> {
return useQuery({
queryKey: queryKeys.workOrders.list(params as Record<string, unknown>),
queryKey: queryKeys.workOrders.list(params),
queryFn: () => workOrdersApi.getList(params),
placeholderData: keepPreviousData,
});
@ -24,3 +24,16 @@ export function useWorkOrderUsers(): UseQueryResult<WorkOrderUser[], Error> {
staleTime: 5 * 60 * 1000,
});
}
export function useWorkOrdersByLocation(
locationId: string | number | undefined,
search = "",
enabled = true,
): UseQueryResult<WorkOrdersListResult, Error> {
return useQuery({
queryKey: queryKeys.workOrders.byLocation(locationId ?? "", search),
queryFn: () => workOrdersApi.getListByLocation(locationId!, search),
enabled: enabled && locationId !== undefined && locationId !== "",
staleTime: 30 * 1000,
});
}

View file

@ -1,4 +1,4 @@
type WorkOrdersListParams = Record<string, unknown>;
import type { WorkOrdersListParams } from "@/domain/work-orders/types/work-order-list";
export const queryKeys = {
auth: {