mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-05 03:42:08 +00:00
feat(work-orders): add board table core and filters
This commit is contained in:
parent
af0e4aa1a6
commit
1cb8362d76
13 changed files with 1523 additions and 0 deletions
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { createElement, type ComponentProps } from "react";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import {
|
||||||
|
AlertTriangle,
|
||||||
|
Droplets,
|
||||||
|
Filter,
|
||||||
|
Gauge,
|
||||||
|
Lightbulb,
|
||||||
|
Lock,
|
||||||
|
Shield,
|
||||||
|
ShieldCheck,
|
||||||
|
Thermometer,
|
||||||
|
Video,
|
||||||
|
Waves,
|
||||||
|
Wind,
|
||||||
|
Wrench,
|
||||||
|
Zap,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
export const PM_TYPE_ICONS: Record<string, LucideIcon> = {
|
||||||
|
"Scrubber Pit": Filter,
|
||||||
|
"Backflow Preventers": Shield,
|
||||||
|
"Domestic Backflow": ShieldCheck,
|
||||||
|
"Tank / Water Heater": Thermometer,
|
||||||
|
"Pressure Reducing Valve": Gauge,
|
||||||
|
"Pumping Systems": Waves,
|
||||||
|
"HVAC Inspection": Wind,
|
||||||
|
Electrical: Zap,
|
||||||
|
Plumbing: Wrench,
|
||||||
|
"Door / Hardware": Lock,
|
||||||
|
"Floor Drain Backing Up": Droplets,
|
||||||
|
"Parking Lot Lighting": Lightbulb,
|
||||||
|
"Mens Restroom Camera Line": Video,
|
||||||
|
"Assess Window Leaks": AlertTriangle,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getPmTypeIcon(pmType: string): LucideIcon {
|
||||||
|
return PM_TYPE_ICONS[pmType] ?? Wrench;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PmTypeIconProps = ComponentProps<LucideIcon> & { pmType: string };
|
||||||
|
|
||||||
|
export function PmTypeIcon({ pmType, ...props }: PmTypeIconProps) {
|
||||||
|
return createElement(getPmTypeIcon(pmType), props);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
export const WO_TABLE_COLS = [
|
||||||
|
{ key: "grip", label: "", w: 28 },
|
||||||
|
{ key: "flag", label: "", w: 28 },
|
||||||
|
{ key: "site", label: "SITE", w: 168 },
|
||||||
|
{ key: "wo", label: "WO", w: 120 },
|
||||||
|
{ key: "type", label: "TYPE OF WO", w: 108 },
|
||||||
|
{ key: "assigned", label: "ASSIGNED TO", w: 136 },
|
||||||
|
{ key: "sched", label: "SCHEDULE ON", w: 112 },
|
||||||
|
{ key: "due", label: "DUE DATE", w: 112 },
|
||||||
|
{ key: "pm", label: "SERVICE", w: 168 },
|
||||||
|
{ key: "vendor", label: "VENDOR", w: 184 },
|
||||||
|
{ key: "appt", label: "APPT TIME", w: 128 },
|
||||||
|
{ key: "status", label: "STATUS", w: 148 },
|
||||||
|
{ key: "doc", label: "COMP DOC", w: 84 },
|
||||||
|
{ key: "actions", label: "", w: 100 },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const WO_TABLE_MIN_WIDTH = 1504;
|
||||||
|
export const WO_TABLE_ROW_HEIGHT = 52;
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { WO_TABLE_COLS } from "./wo-table-cols";
|
||||||
|
|
||||||
|
export function WoTableHead() {
|
||||||
|
return (
|
||||||
|
<thead className="sticky top-0 z-10">
|
||||||
|
<tr>
|
||||||
|
{WO_TABLE_COLS.map((col) => (
|
||||||
|
<th
|
||||||
|
key={col.key}
|
||||||
|
style={{
|
||||||
|
width: col.w,
|
||||||
|
minWidth: col.w,
|
||||||
|
padding: "11px 14px",
|
||||||
|
textAlign: "left",
|
||||||
|
fontSize: 10.5,
|
||||||
|
fontWeight: 700,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: "0.06em",
|
||||||
|
color: "var(--foreground)",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
background: "var(--color-bg-muted)",
|
||||||
|
borderBottom: "1px solid var(--border)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{col.label}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,288 @@
|
||||||
|
import { Check, Flag, X } from "lucide-react";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
LocationOption,
|
||||||
|
VendorDropdownItem,
|
||||||
|
WorkOrderUser,
|
||||||
|
} from "@/domain/work-orders/types/work-order";
|
||||||
|
|
||||||
|
import type { WorkOrderTableDraft } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
|
||||||
|
import { UNSCHEDULED_DAY_KEY } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
|
||||||
|
import { AssignedToCell } from "./cells/assigned-to-cell";
|
||||||
|
|
||||||
|
import { ApptTimeCell } from "./cells/appt-time-cell";
|
||||||
|
|
||||||
|
import { DateCell } from "./cells/date-cell";
|
||||||
|
|
||||||
|
import { EditableWONumberCell } from "./cells/editable-wo-number-cell";
|
||||||
|
|
||||||
|
import { PMTypeCell } from "./cells/pm-type-cell";
|
||||||
|
|
||||||
|
import { SiteCell } from "./cells/site-cell";
|
||||||
|
|
||||||
|
import { StatusCell } from "./cells/status-cell";
|
||||||
|
|
||||||
|
import { TypeCell } from "./cells/type-cell";
|
||||||
|
|
||||||
|
import { VendorCell } from "./cells/vendor-cell";
|
||||||
|
|
||||||
|
import { WO_TABLE_ROW_HEIGHT } from "./wo-table-cols";
|
||||||
|
|
||||||
|
type WoTableInlineRowProps = {
|
||||||
|
draft: WorkOrderTableDraft;
|
||||||
|
|
||||||
|
draftDayKey: string;
|
||||||
|
|
||||||
|
users: WorkOrderUser[];
|
||||||
|
|
||||||
|
sites: LocationOption[];
|
||||||
|
|
||||||
|
vendors: VendorDropdownItem[];
|
||||||
|
|
||||||
|
pmTypes: string[];
|
||||||
|
|
||||||
|
allRows: { id: string | number; woNumber: string }[];
|
||||||
|
|
||||||
|
onChange: (draft: WorkOrderTableDraft) => void;
|
||||||
|
|
||||||
|
onSave: () => void;
|
||||||
|
|
||||||
|
onCancel: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function WoTableInlineRow({
|
||||||
|
draft,
|
||||||
|
|
||||||
|
draftDayKey,
|
||||||
|
|
||||||
|
users,
|
||||||
|
|
||||||
|
sites,
|
||||||
|
|
||||||
|
vendors,
|
||||||
|
|
||||||
|
pmTypes,
|
||||||
|
|
||||||
|
allRows,
|
||||||
|
|
||||||
|
onChange,
|
||||||
|
|
||||||
|
onSave,
|
||||||
|
|
||||||
|
onCancel,
|
||||||
|
}: WoTableInlineRowProps) {
|
||||||
|
const weekDefault = draftDayKey === UNSCHEDULED_DAY_KEY;
|
||||||
|
|
||||||
|
const canSave = weekDefault ? !!draft.targetWeek || !!draft.scheduledOn : !!draft.scheduledOn;
|
||||||
|
|
||||||
|
const patch = (p: Partial<WorkOrderTableDraft>) => onChange({ ...draft, ...p });
|
||||||
|
|
||||||
|
const draftRow = {
|
||||||
|
id: "draft",
|
||||||
|
|
||||||
|
woNumber: draft.woNumber,
|
||||||
|
|
||||||
|
status: draft.status,
|
||||||
|
|
||||||
|
isPastDue: false,
|
||||||
|
|
||||||
|
type: draft.type,
|
||||||
|
|
||||||
|
rescheduleCount: 0,
|
||||||
|
|
||||||
|
carriedOver: 0,
|
||||||
|
|
||||||
|
docStatus: "" as const,
|
||||||
|
|
||||||
|
mediaCount: 0,
|
||||||
|
|
||||||
|
site: draft.site,
|
||||||
|
|
||||||
|
location: draft.location,
|
||||||
|
|
||||||
|
dispatcherId: draft.dispatcherId,
|
||||||
|
|
||||||
|
scheduledOn: draft.scheduledOn,
|
||||||
|
|
||||||
|
dueDate: draft.dueDate,
|
||||||
|
|
||||||
|
pm: draft.pm,
|
||||||
|
|
||||||
|
company: draft.company,
|
||||||
|
|
||||||
|
tech: draft.tech,
|
||||||
|
|
||||||
|
targetWeek: draft.targetWeek,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
style={{
|
||||||
|
height: WO_TABLE_ROW_HEIGHT,
|
||||||
|
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
|
||||||
|
borderLeft: "2px solid var(--color-primary)",
|
||||||
|
|
||||||
|
background: "var(--color-primary-tint)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<td style={{ padding: "0 0 0 6px" }} />
|
||||||
|
|
||||||
|
<td style={{ padding: "0 2px 0 8px" }}>
|
||||||
|
<Flag size={14} strokeWidth={1.75} style={{ color: "var(--color-border)" }} />
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="group/cell" style={{ padding: "0 14px" }}>
|
||||||
|
<SiteCell
|
||||||
|
value={draft.site}
|
||||||
|
location={draft.location}
|
||||||
|
locationId={draft.locationId}
|
||||||
|
pocName={draft.pocName}
|
||||||
|
pocPhone={draft.pocPhone}
|
||||||
|
pocNotes={draft.pocNotes}
|
||||||
|
sites={sites}
|
||||||
|
onSave={(p) =>
|
||||||
|
patch({
|
||||||
|
site: p.site,
|
||||||
|
|
||||||
|
location: p.location,
|
||||||
|
|
||||||
|
locationId: p.locationId,
|
||||||
|
|
||||||
|
pocName: p.pocName,
|
||||||
|
|
||||||
|
pocPhone: p.pocPhone,
|
||||||
|
|
||||||
|
pocNotes: p.pocNotes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td style={{ padding: "0 14px" }}>
|
||||||
|
<EditableWONumberCell
|
||||||
|
value={draft.woNumber}
|
||||||
|
rescheduleCount={0}
|
||||||
|
currentId="draft"
|
||||||
|
allOrders={allRows as never[]}
|
||||||
|
onSave={(v) => patch({ woNumber: v })}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="group/cell" style={{ padding: "0 14px" }}>
|
||||||
|
<TypeCell value={draft.type} onSave={(t) => patch({ type: t })} />
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="group/cell" style={{ padding: "0 14px" }}>
|
||||||
|
<AssignedToCell
|
||||||
|
dispatcherId={draft.dispatcherId}
|
||||||
|
users={users}
|
||||||
|
highlight={!draft.dispatcherId}
|
||||||
|
onSave={(id) => {
|
||||||
|
patch({ dispatcherId: id, status: draft.status || "Incomplete" });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="group/cell" style={{ padding: "0 14px" }}>
|
||||||
|
<DateCell
|
||||||
|
value={draft.scheduledOn}
|
||||||
|
allowWeekMode
|
||||||
|
targetWeek={draft.targetWeek}
|
||||||
|
defaultMode={weekDefault ? "week" : "date"}
|
||||||
|
highlight={!draft.scheduledOn && !draft.targetWeek}
|
||||||
|
onSave={(date) => patch({ scheduledOn: date, scheduleWeekOnly: false, targetWeek: "" })}
|
||||||
|
onSaveWeek={(week) =>
|
||||||
|
patch({ scheduledOn: "", targetWeek: week, scheduleWeekOnly: true })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="group/cell" style={{ padding: "0 14px" }}>
|
||||||
|
<DateCell value={draft.dueDate} onSave={(date) => patch({ dueDate: date })} />
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="group/cell" style={{ padding: "0 14px" }}>
|
||||||
|
<PMTypeCell value={draft.pm} pmTypes={pmTypes} onSave={(pm) => patch({ pm })} />
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="group/cell" style={{ padding: "0 14px" }}>
|
||||||
|
<VendorCell
|
||||||
|
company={draft.company}
|
||||||
|
tech={draft.tech}
|
||||||
|
techPhone={draft.techPhone}
|
||||||
|
vendors={vendors}
|
||||||
|
onSave={(p) => patch({ company: p.company, tech: p.tech, techPhone: p.techPhone })}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="group/cell" style={{ padding: "0 14px" }}>
|
||||||
|
<ApptTimeCell
|
||||||
|
apptTime={
|
||||||
|
draft.apptStart ? `${draft.apptStart}${draft.apptEnd ? ` – ${draft.apptEnd}` : ""}` : ""
|
||||||
|
}
|
||||||
|
onSave={(v) => {
|
||||||
|
const [start, end] = v.split(" – ");
|
||||||
|
|
||||||
|
patch({ apptStart: start ?? "", apptEnd: end ?? "" });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="group/cell" style={{ padding: "0 14px" }}>
|
||||||
|
<StatusCell
|
||||||
|
wo={draftRow as never}
|
||||||
|
onChangeStatus={(status) => patch({ status })}
|
||||||
|
onOpenSlideOver={() => {}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td style={{ padding: "0 14px" }}>
|
||||||
|
<span style={{ fontSize: 11, color: "var(--color-text-muted)" }}>—</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td style={{ padding: "0 4px" }}>
|
||||||
|
<div className="flex items-center gap-0.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onSave}
|
||||||
|
disabled={!canSave}
|
||||||
|
title={canSave ? "Save" : "Schedule On is required"}
|
||||||
|
className="rounded p-1 transition-colors disabled:opacity-30"
|
||||||
|
style={{ color: "var(--color-primary)" }}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (canSave) {
|
||||||
|
(e.currentTarget as HTMLButtonElement).style.background =
|
||||||
|
"var(--color-primary-tint)";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
(e.currentTarget as HTMLButtonElement).style.background = "transparent";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check size={13} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onCancel}
|
||||||
|
title="Cancel"
|
||||||
|
className="rounded p-1 transition-colors"
|
||||||
|
style={{ color: "var(--color-text-muted)" }}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
(e.currentTarget as HTMLButtonElement).style.background = "var(--color-bg-muted)";
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
(e.currentTarget as HTMLButtonElement).style.background = "transparent";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X size={13} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { Eye, Pencil } from "lucide-react";
|
||||||
|
|
||||||
|
type WoTableRowActionsProps = {
|
||||||
|
onView: () => void;
|
||||||
|
|
||||||
|
onEdit: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function WoTableRowActions({ onView, onEdit }: WoTableRowActionsProps) {
|
||||||
|
const mk = (
|
||||||
|
Icon: React.ElementType,
|
||||||
|
|
||||||
|
title: string,
|
||||||
|
|
||||||
|
onClick: () => void,
|
||||||
|
|
||||||
|
hoverBg: string,
|
||||||
|
|
||||||
|
hoverColor: string,
|
||||||
|
) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title={title}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
onClick();
|
||||||
|
}}
|
||||||
|
className="flex items-center justify-center transition-colors"
|
||||||
|
style={{
|
||||||
|
width: 28,
|
||||||
|
|
||||||
|
height: 28,
|
||||||
|
|
||||||
|
borderRadius: 6,
|
||||||
|
|
||||||
|
color: "var(--muted-foreground)",
|
||||||
|
|
||||||
|
background: "transparent",
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
const b = e.currentTarget as HTMLButtonElement;
|
||||||
|
|
||||||
|
b.style.background = hoverBg;
|
||||||
|
|
||||||
|
b.style.color = hoverColor;
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
const b = e.currentTarget as HTMLButtonElement;
|
||||||
|
|
||||||
|
b.style.background = "transparent";
|
||||||
|
|
||||||
|
b.style.color = "var(--muted-foreground)";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon size={15} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-end gap-1 opacity-0 transition-opacity group-hover/row:opacity-100">
|
||||||
|
{mk(Eye, "View details", onView, "#e6f4fb", "var(--primary)")}
|
||||||
|
|
||||||
|
{mk(Pencil, "Edit", onEdit, "#e6f4fb", "var(--primary)")}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,280 @@
|
||||||
|
import { GripVertical } from "lucide-react";
|
||||||
|
import type {
|
||||||
|
LocationOption,
|
||||||
|
VendorDropdownItem,
|
||||||
|
WorkOrderUser,
|
||||||
|
} from "@/domain/work-orders/types/work-order";
|
||||||
|
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
import type { DragVizState } from "@/app/(protected)/workorders/_hooks/use-work-orders-table-data";
|
||||||
|
import {
|
||||||
|
applySchedulePatch,
|
||||||
|
maybeAutoSchedulePatch,
|
||||||
|
} from "@/domain/work-orders/use-cases/use-work-order-table-mutations";
|
||||||
|
import { AssignedToCell } from "./cells/assigned-to-cell";
|
||||||
|
import { ApptTimeCell } from "./cells/appt-time-cell";
|
||||||
|
import { CompDocCell } from "./cells/comp-doc-cell";
|
||||||
|
import { DateCell } from "./cells/date-cell";
|
||||||
|
import { EditableWONumberCell } from "./cells/editable-wo-number-cell";
|
||||||
|
import { FlagCell } from "./cells/flag-cell";
|
||||||
|
import { PMTypeCell } from "./cells/pm-type-cell";
|
||||||
|
import { SiteCell } from "./cells/site-cell";
|
||||||
|
import { StatusCell } from "./cells/status-cell";
|
||||||
|
import { TypeCell } from "./cells/type-cell";
|
||||||
|
import { VendorCell } from "./cells/vendor-cell";
|
||||||
|
import { WO_TABLE_ROW_HEIGHT } from "./wo-table-cols";
|
||||||
|
import { WoTableRowActions } from "./wo-table-row-actions";
|
||||||
|
|
||||||
|
export type WoTableRowHandlers = {
|
||||||
|
onRowClick: (row: WorkOrderTableRow) => void;
|
||||||
|
onView: (row: WorkOrderTableRow) => void;
|
||||||
|
onEdit: (row: WorkOrderTableRow) => void;
|
||||||
|
onOpenCompDoc: (row: WorkOrderTableRow) => void;
|
||||||
|
onOpenMedia: (row: WorkOrderTableRow) => void;
|
||||||
|
onPatch: (id: string | number, patch: Partial<WorkOrderTableRow>) => void;
|
||||||
|
onDuplicateFound?: (existing: WorkOrderTableRow) => void;
|
||||||
|
onOpenSlideOver: (row: WorkOrderTableRow) => void;
|
||||||
|
onPastDueAttempt?: (id: string | number) => void;
|
||||||
|
onReorder: (dragId: string | number, overId: string | number) => void;
|
||||||
|
onDragStart: (id: string | number) => void;
|
||||||
|
onDragOver: (id: string | number) => void;
|
||||||
|
onDrop: () => void;
|
||||||
|
onDragEnd: () => void;
|
||||||
|
setFlag: (id: string | number, color: string | null) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WoTableRowProps = {
|
||||||
|
row: WorkOrderTableRow;
|
||||||
|
allRows: WorkOrderTableRow[];
|
||||||
|
users: WorkOrderUser[];
|
||||||
|
sites: LocationOption[];
|
||||||
|
vendors: VendorDropdownItem[];
|
||||||
|
pmTypes: string[];
|
||||||
|
search?: string;
|
||||||
|
forceScheduleHighlight?: boolean;
|
||||||
|
flagColor?: string;
|
||||||
|
isSelected: boolean;
|
||||||
|
isFlashing: boolean;
|
||||||
|
dragViz: DragVizState;
|
||||||
|
handlers: WoTableRowHandlers;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function WoTableRow({
|
||||||
|
row,
|
||||||
|
allRows,
|
||||||
|
users,
|
||||||
|
sites,
|
||||||
|
vendors,
|
||||||
|
pmTypes,
|
||||||
|
search,
|
||||||
|
forceScheduleHighlight,
|
||||||
|
flagColor,
|
||||||
|
isSelected,
|
||||||
|
isFlashing,
|
||||||
|
dragViz,
|
||||||
|
handlers,
|
||||||
|
}: WoTableRowProps) {
|
||||||
|
const isDragging = dragViz.dragId === row.id;
|
||||||
|
const isDropTarget = dragViz.overId === row.id && dragViz.dragId !== row.id;
|
||||||
|
const isCanceled = (row.status as string) === "Canceled";
|
||||||
|
|
||||||
|
let firstCellShadow = "none";
|
||||||
|
if (row.type === "Emergency") {
|
||||||
|
firstCellShadow = "inset 3px 0 0 var(--destructive)";
|
||||||
|
} else if (row.isPastDue) {
|
||||||
|
firstCellShadow = "inset 3px 0 0 var(--warning)";
|
||||||
|
}
|
||||||
|
|
||||||
|
const patchAndMaybeAuto = (patch: Partial<WorkOrderTableRow>) => {
|
||||||
|
const merged = { ...row, ...patch };
|
||||||
|
const auto = maybeAutoSchedulePatch(merged);
|
||||||
|
handlers.onPatch(row.id, { ...patch, ...auto });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
id={`wo-row-${row.id}`}
|
||||||
|
draggable={!isCanceled}
|
||||||
|
title={isCanceled ? "This work order has been cancelled" : undefined}
|
||||||
|
onClick={() => handlers.onRowClick(row)}
|
||||||
|
onDragStart={() => handlers.onDragStart(row.id)}
|
||||||
|
onDragOver={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handlers.onDragOver(row.id);
|
||||||
|
}}
|
||||||
|
onDrop={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handlers.onDrop();
|
||||||
|
}}
|
||||||
|
onDragEnd={handlers.onDragEnd}
|
||||||
|
className="group/row cursor-pointer transition-colors"
|
||||||
|
style={{
|
||||||
|
height: WO_TABLE_ROW_HEIGHT,
|
||||||
|
borderBottom: "1px solid var(--border)",
|
||||||
|
borderTop: isDropTarget ? "2px solid var(--color-primary)" : undefined,
|
||||||
|
opacity: isCanceled ? 0.45 : isDragging ? 0.4 : 1,
|
||||||
|
pointerEvents: isCanceled ? "none" : undefined,
|
||||||
|
background: isFlashing || isSelected ? "var(--color-primary-tint)" : "var(--card)",
|
||||||
|
outline: isFlashing ? "2px solid var(--color-primary)" : "none",
|
||||||
|
outlineOffset: isFlashing ? "-2px" : undefined,
|
||||||
|
borderRadius: isFlashing ? 4 : undefined,
|
||||||
|
transition: "background 1.8s ease-out, outline 1.8s ease-out",
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (!isSelected && !isFlashing) {
|
||||||
|
(e.currentTarget as HTMLTableRowElement).style.background = "var(--color-primary-tint)";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
if (!isSelected && !isFlashing) {
|
||||||
|
(e.currentTarget as HTMLTableRowElement).style.background = "var(--card)";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
style={{ padding: "0 0 0 6px", boxShadow: firstCellShadow }}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="flex items-center justify-center"
|
||||||
|
style={{
|
||||||
|
cursor: isDragging ? "grabbing" : "grab",
|
||||||
|
color: "var(--color-text-muted)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GripVertical size={14} />
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: "0 2px 0 8px" }} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<FlagCell color={flagColor} onChange={(c) => handlers.setFlag(row.id, c)} />
|
||||||
|
</td>
|
||||||
|
<td className="group/cell" style={{ padding: "0 14px" }} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<SiteCell
|
||||||
|
value={row.site}
|
||||||
|
location={row.location}
|
||||||
|
locationId={row.locationId}
|
||||||
|
pocName={row.pocName}
|
||||||
|
pocPhone={row.pocPhone}
|
||||||
|
pocNotes={row.pocNotes}
|
||||||
|
sites={sites}
|
||||||
|
q={search}
|
||||||
|
onSave={(p) =>
|
||||||
|
handlers.onPatch(row.id, {
|
||||||
|
site: p.site,
|
||||||
|
location: p.location,
|
||||||
|
locationId: p.locationId,
|
||||||
|
pocName: p.pocName,
|
||||||
|
pocPhone: p.pocPhone,
|
||||||
|
pocNotes: p.pocNotes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: "0 14px", whiteSpace: "nowrap" }}>
|
||||||
|
<EditableWONumberCell
|
||||||
|
value={row.woNumber}
|
||||||
|
rescheduleCount={row.rescheduleCount}
|
||||||
|
carriedOver={row.carriedOver}
|
||||||
|
q={search}
|
||||||
|
currentId={row.id}
|
||||||
|
allOrders={allRows}
|
||||||
|
onSave={(v) => handlers.onPatch(row.id, { woNumber: v })}
|
||||||
|
onDuplicateFound={handlers.onDuplicateFound}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="group/cell" style={{ padding: "0 14px" }} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<TypeCell
|
||||||
|
value={row.type}
|
||||||
|
q={search}
|
||||||
|
onSave={(t) => handlers.onPatch(row.id, { type: t })}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="group/cell" style={{ padding: "0 14px" }} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<AssignedToCell
|
||||||
|
dispatcherId={row.dispatcherId}
|
||||||
|
users={users}
|
||||||
|
highlight={!row.dispatcherId}
|
||||||
|
onSave={(id, name) => patchAndMaybeAuto({ dispatcherId: id, dispatcherName: name })}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="group/cell" style={{ padding: "0 14px" }} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<DateCell
|
||||||
|
value={row.scheduledOn}
|
||||||
|
q={search}
|
||||||
|
highlight={forceScheduleHighlight || row.isPastDue}
|
||||||
|
allowWeekMode
|
||||||
|
targetWeek={row.targetWeek}
|
||||||
|
onSave={(date) => patchAndMaybeAuto(applySchedulePatch(date, false))}
|
||||||
|
onSaveWeek={(week) =>
|
||||||
|
patchAndMaybeAuto({
|
||||||
|
scheduledOn: "",
|
||||||
|
targetWeek: week,
|
||||||
|
scheduleWeekOnly: true,
|
||||||
|
dayGroup: "",
|
||||||
|
dayLabel: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="group/cell" style={{ padding: "0 14px" }} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<DateCell
|
||||||
|
value={row.dueDate}
|
||||||
|
q={search}
|
||||||
|
onSave={(date) => handlers.onPatch(row.id, { dueDate: date })}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="group/cell" style={{ padding: "0 14px" }} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<PMTypeCell
|
||||||
|
value={row.pm}
|
||||||
|
pmTypes={pmTypes}
|
||||||
|
q={search}
|
||||||
|
onSave={(pm) => handlers.onPatch(row.id, { pm })}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="group/cell" style={{ padding: "0 14px" }} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<VendorCell
|
||||||
|
company={row.company}
|
||||||
|
tech={row.tech}
|
||||||
|
techPhone={row.techPhone}
|
||||||
|
vendors={vendors}
|
||||||
|
q={search}
|
||||||
|
onSave={(p) =>
|
||||||
|
handlers.onPatch(row.id, {
|
||||||
|
company: p.company,
|
||||||
|
tech: p.tech,
|
||||||
|
techPhone: p.techPhone,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="group/cell" style={{ padding: "0 14px" }} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<ApptTimeCell
|
||||||
|
apptTime={row.apptTime}
|
||||||
|
onSave={(v) => handlers.onPatch(row.id, { apptTime: v })}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="group/cell" style={{ padding: "0 14px" }} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<StatusCell
|
||||||
|
wo={row}
|
||||||
|
onChangeStatus={(status) => handlers.onPatch(row.id, { status })}
|
||||||
|
onOpenSlideOver={() => handlers.onOpenSlideOver(row)}
|
||||||
|
onPastDueAttempt={() => handlers.onPastDueAttempt?.(row.id)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: "0 14px" }} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<CompDocCell
|
||||||
|
status={row.docStatus}
|
||||||
|
woType={row.type}
|
||||||
|
mediaCount={row.mediaCount}
|
||||||
|
onOpen={() => handlers.onOpenCompDoc(row)}
|
||||||
|
onOpenMedia={() => handlers.onOpenMedia(row)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: "0 8px" }} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<WoTableRowActions
|
||||||
|
onView={() => handlers.onView(row)}
|
||||||
|
onEdit={() => handlers.onEdit(row)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
import { AlertTriangle, ChevronDown, ChevronRight, Plus } from "lucide-react";
|
||||||
|
import { WO_TABLE_COLS } from "./wo-table-cols";
|
||||||
|
|
||||||
|
type WoTableSectionHeaderProps = {
|
||||||
|
variant: "unscheduled" | "day";
|
||||||
|
label: string;
|
||||||
|
count: number;
|
||||||
|
isToday?: boolean;
|
||||||
|
collapsed?: boolean;
|
||||||
|
onToggleCollapse?: () => void;
|
||||||
|
onAddWo: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function WoTableSectionHeader({
|
||||||
|
variant,
|
||||||
|
label,
|
||||||
|
count,
|
||||||
|
isToday,
|
||||||
|
collapsed,
|
||||||
|
onToggleCollapse,
|
||||||
|
onAddWo,
|
||||||
|
}: WoTableSectionHeaderProps) {
|
||||||
|
const isUnscheduled = variant === "unscheduled";
|
||||||
|
|
||||||
|
if (isUnscheduled) {
|
||||||
|
return (
|
||||||
|
<tr style={{ background: "rgba(217, 119, 6, 0.08)" }}>
|
||||||
|
<td
|
||||||
|
colSpan={WO_TABLE_COLS.length}
|
||||||
|
onClick={onToggleCollapse}
|
||||||
|
style={{
|
||||||
|
height: 40,
|
||||||
|
padding: "0 14px",
|
||||||
|
cursor: "pointer",
|
||||||
|
boxShadow: "inset 3px 0 0 var(--warning)",
|
||||||
|
borderTop: "1px solid #FDE68A",
|
||||||
|
borderBottom: "1px solid #FDE68A",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<AlertTriangle size={14} className="shrink-0" style={{ color: "var(--warning)" }} />
|
||||||
|
<span
|
||||||
|
className="uppercase"
|
||||||
|
style={{
|
||||||
|
fontFamily: "var(--font-mono)",
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 700,
|
||||||
|
letterSpacing: "0.08em",
|
||||||
|
color: "var(--warning)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<span style={{ fontSize: 12, color: "var(--color-text-muted)" }}>
|
||||||
|
{count} work order{count !== 1 ? "s" : ""}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-1 transition-opacity hover:opacity-70"
|
||||||
|
style={{ fontSize: 12.5, fontWeight: 600, color: "var(--primary)" }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onAddWo();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus size={13} />
|
||||||
|
Add WO
|
||||||
|
</button>
|
||||||
|
<span className="ml-auto flex items-center" style={{ color: "var(--warning)" }}>
|
||||||
|
{collapsed ? <ChevronRight size={14} /> : <ChevronDown size={14} />}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr style={{ background: "#eef1f6" }}>
|
||||||
|
<td
|
||||||
|
colSpan={WO_TABLE_COLS.length}
|
||||||
|
style={{
|
||||||
|
height: 40,
|
||||||
|
padding: "0 14px",
|
||||||
|
borderTop: "1px solid var(--border)",
|
||||||
|
borderBottom: "1px solid var(--border)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<span
|
||||||
|
className="uppercase"
|
||||||
|
style={{
|
||||||
|
fontFamily: "var(--font-sans)",
|
||||||
|
fontSize: 12.5,
|
||||||
|
fontWeight: 500,
|
||||||
|
letterSpacing: "0.02em",
|
||||||
|
color: "var(--muted-foreground)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: 500,
|
||||||
|
color: "var(--muted-foreground)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{count} WO{count !== 1 ? "s" : ""}
|
||||||
|
</span>
|
||||||
|
{isToday && (
|
||||||
|
<span
|
||||||
|
className="uppercase"
|
||||||
|
style={{
|
||||||
|
fontFamily: "var(--font-mono)",
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: "#ffffff",
|
||||||
|
background: "var(--success)",
|
||||||
|
borderRadius: 9999,
|
||||||
|
padding: "1px 5px",
|
||||||
|
letterSpacing: "0.04em",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Today
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-1 transition-opacity hover:opacity-70"
|
||||||
|
style={{ fontSize: 12.5, fontWeight: 600, color: "var(--primary)" }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onAddWo();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus size={13} />
|
||||||
|
Add WO
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import type { StatusVariant } from "@/components/domain/status-badge";
|
||||||
|
import { resolveStatusVariant } from "@/components/domain/status-badge-utils";
|
||||||
|
|
||||||
|
export type WoTableStatusStyle = { text: string; dot: string };
|
||||||
|
|
||||||
|
const STATUS_TABLE_STYLE: Record<StatusVariant, WoTableStatusStyle> = {
|
||||||
|
incomplete: { text: "var(--status-incomplete-text)", dot: "#9CA3AF" },
|
||||||
|
pending: { text: "var(--status-pending-text)", dot: "#D97706" },
|
||||||
|
scheduled: { text: "var(--status-scheduled-text)", dot: "#3B82F6" },
|
||||||
|
inroute: { text: "var(--status-inroute-text)", dot: "#0EA5E9" },
|
||||||
|
onsite: { text: "var(--status-onsite-text)", dot: "#10B981" },
|
||||||
|
inprogress: { text: "var(--status-inprogress-text)", dot: "#2563EB" },
|
||||||
|
completed: { text: "var(--status-completed-text)", dot: "#16A34A" },
|
||||||
|
rescheduled: { text: "var(--status-rescheduled-text)", dot: "#F59E0B" },
|
||||||
|
canceled: { text: "var(--status-canceled-text)", dot: "#9CA3AF" },
|
||||||
|
pendingquote: { text: "var(--status-pendingquote-text)", dot: "#9333EA" },
|
||||||
|
pastdue: { text: "var(--status-pastdue-text)", dot: "#BE123C" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getWoTableStatusStyle(status: string): WoTableStatusStyle {
|
||||||
|
const variant = resolveStatusVariant(status) ?? "incomplete";
|
||||||
|
return STATUS_TABLE_STYLE[variant];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import type { WOType } from "@/domain/work-orders/types/work-order-wizard";
|
||||||
|
|
||||||
|
export const WO_TABLE_TYPE_TEXT: Record<WOType, string> = {
|
||||||
|
Emergency: "var(--destructive)",
|
||||||
|
Reactive: "#e0701a",
|
||||||
|
PM: "var(--primary)",
|
||||||
|
"Add-On": "#7c3aed",
|
||||||
|
Overdue: "#9F1239",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getWoTableTypeColor(type: WOType | ""): string {
|
||||||
|
if (!type) return "var(--color-text-muted)";
|
||||||
|
return WO_TABLE_TYPE_TEXT[type];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import type { useWorkOrdersTableData } from "@/app/(protected)/workorders/_hooks/use-work-orders-table-data";
|
||||||
|
|
||||||
|
export type ReturnTypeOfUseWorkOrdersTableData = ReturnType<typeof useWorkOrdersTableData>;
|
||||||
|
|
@ -0,0 +1,329 @@
|
||||||
|
import { Fragment, useMemo, useState } from "react";
|
||||||
|
import { CircularProgress } from "@mui/material";
|
||||||
|
import { SearchX } from "lucide-react";
|
||||||
|
import type {
|
||||||
|
LocationOption,
|
||||||
|
VendorDropdownItem,
|
||||||
|
WorkOrderUser,
|
||||||
|
} from "@/domain/work-orders/types/work-order";
|
||||||
|
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
import { UNSCHEDULED_DAY_KEY } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
import { mapWizardDraftToFormValues } from "@/domain/work-orders/mappers/wizard-draft-mapper";
|
||||||
|
import { useCreateWorkOrderFromWizard } from "@/domain/work-orders/use-cases/use-create-work-order-from-wizard";
|
||||||
|
import { useWorkOrderTableMutations } from "@/domain/work-orders/use-cases/use-work-order-table-mutations";
|
||||||
|
import type { ReturnTypeOfUseWorkOrdersTableData } from "./wo-table-types";
|
||||||
|
import { WoTableHead } from "./wo-table-head";
|
||||||
|
import { WoTableInlineRow } from "./wo-table-inline-row";
|
||||||
|
import { WoTableRow } from "./wo-table-row";
|
||||||
|
import { WoTableSectionHeader } from "./wo-table-section-header";
|
||||||
|
import { WO_TABLE_MIN_WIDTH } from "./wo-table-cols";
|
||||||
|
|
||||||
|
const CARD_STYLE: React.CSSProperties = {
|
||||||
|
background: "var(--card)",
|
||||||
|
border: "1px solid var(--border)",
|
||||||
|
borderRadius: 8,
|
||||||
|
boxShadow: "0 1px 3px rgba(38,34,98,.05)",
|
||||||
|
};
|
||||||
|
|
||||||
|
type WoTableProps = {
|
||||||
|
tableData: ReturnTypeOfUseWorkOrdersTableData;
|
||||||
|
users: WorkOrderUser[];
|
||||||
|
sites: LocationOption[];
|
||||||
|
vendors: VendorDropdownItem[];
|
||||||
|
pmTypes: string[];
|
||||||
|
onOpenSlideOver: (row: WorkOrderTableRow, edit?: boolean) => void;
|
||||||
|
onOpenCompDoc: (row: WorkOrderTableRow) => void;
|
||||||
|
onOpenMedia: (row: WorkOrderTableRow) => void;
|
||||||
|
onClearSearch?: () => void;
|
||||||
|
onOpenAdvancedFilters?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function WoTableShell({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-0 flex-1 overflow-hidden px-2 py-4">
|
||||||
|
<div className="relative h-full overflow-auto" style={CARD_STYLE}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function WoTableEmptyState({
|
||||||
|
message,
|
||||||
|
actionLabel,
|
||||||
|
onAction,
|
||||||
|
}: {
|
||||||
|
message: string;
|
||||||
|
actionLabel: string;
|
||||||
|
onAction?: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex h-full flex-col items-center justify-center text-center"
|
||||||
|
style={{ gap: 14, padding: 48 }}
|
||||||
|
>
|
||||||
|
<SearchX size={34} style={{ color: "var(--color-text-muted)" }} />
|
||||||
|
<p style={{ fontSize: 14, color: "var(--muted-foreground)" }}>{message}</p>
|
||||||
|
{onAction && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onAction}
|
||||||
|
className="transition-opacity hover:opacity-80"
|
||||||
|
style={{ fontSize: 13, fontWeight: 500, color: "var(--color-accent)" }}
|
||||||
|
>
|
||||||
|
{actionLabel}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WoTable({
|
||||||
|
tableData,
|
||||||
|
users,
|
||||||
|
sites,
|
||||||
|
vendors,
|
||||||
|
pmTypes,
|
||||||
|
onOpenSlideOver,
|
||||||
|
onOpenCompDoc,
|
||||||
|
onOpenMedia,
|
||||||
|
onClearSearch,
|
||||||
|
onOpenAdvancedFilters,
|
||||||
|
}: WoTableProps) {
|
||||||
|
const [scheduleHighlightId, setScheduleHighlightId] = useState<string | number | null>(null);
|
||||||
|
const { patchField } = useWorkOrderTableMutations(tableData.patchRow);
|
||||||
|
const createMutation = useCreateWorkOrderFromWizard(() => {
|
||||||
|
tableData.cancelDraft();
|
||||||
|
});
|
||||||
|
const { dragState: dragStateRef, setDragViz, reorderRows, endDrag, setFlag } = tableData;
|
||||||
|
|
||||||
|
const handlers = useMemo(
|
||||||
|
() => ({
|
||||||
|
onRowClick: (row: WorkOrderTableRow) => {
|
||||||
|
tableData.setSelectedId(row.id);
|
||||||
|
onOpenSlideOver(row, false);
|
||||||
|
},
|
||||||
|
onView: (row: WorkOrderTableRow) => {
|
||||||
|
tableData.setSelectedId(row.id);
|
||||||
|
onOpenSlideOver(row, false);
|
||||||
|
},
|
||||||
|
onEdit: (row: WorkOrderTableRow) => {
|
||||||
|
tableData.setSelectedId(row.id);
|
||||||
|
onOpenSlideOver(row, true);
|
||||||
|
},
|
||||||
|
onOpenCompDoc: (row: WorkOrderTableRow) => {
|
||||||
|
tableData.setSelectedId(row.id);
|
||||||
|
onOpenCompDoc(row);
|
||||||
|
},
|
||||||
|
onOpenMedia: (row: WorkOrderTableRow) => {
|
||||||
|
tableData.setSelectedId(row.id);
|
||||||
|
onOpenMedia(row);
|
||||||
|
},
|
||||||
|
onPatch: (id: string | number, patch: Partial<WorkOrderTableRow>) => {
|
||||||
|
patchField(id, patch);
|
||||||
|
},
|
||||||
|
onDuplicateFound: (existing: WorkOrderTableRow) => {
|
||||||
|
tableData.setSelectedId(existing.id);
|
||||||
|
onOpenSlideOver(existing, false);
|
||||||
|
},
|
||||||
|
onOpenSlideOver: (row: WorkOrderTableRow) => onOpenSlideOver(row, false),
|
||||||
|
onPastDueAttempt: (id: string | number) => setScheduleHighlightId(id),
|
||||||
|
onReorder: reorderRows,
|
||||||
|
onDragStart: (id: string | number) => {
|
||||||
|
dragStateRef.current = { dragId: id, overId: null };
|
||||||
|
setDragViz({ dragId: id, overId: null });
|
||||||
|
},
|
||||||
|
onDragOver: (id: string | number) => {
|
||||||
|
const did = dragStateRef.current.dragId;
|
||||||
|
if (did && did !== id && dragStateRef.current.overId !== id) {
|
||||||
|
dragStateRef.current.overId = id;
|
||||||
|
setDragViz((v) => ({ ...v, overId: id }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDrop: () => {
|
||||||
|
const { dragId, overId } = dragStateRef.current;
|
||||||
|
if (dragId && overId) {
|
||||||
|
reorderRows(dragId, overId);
|
||||||
|
}
|
||||||
|
endDrag();
|
||||||
|
},
|
||||||
|
onDragEnd: endDrag,
|
||||||
|
setFlag,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
tableData,
|
||||||
|
dragStateRef,
|
||||||
|
setDragViz,
|
||||||
|
reorderRows,
|
||||||
|
endDrag,
|
||||||
|
setFlag,
|
||||||
|
onOpenSlideOver,
|
||||||
|
onOpenCompDoc,
|
||||||
|
onOpenMedia,
|
||||||
|
patchField,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderRow = (row: WorkOrderTableRow, forceScheduleHighlight = false) => (
|
||||||
|
<WoTableRow
|
||||||
|
key={String(row.id)}
|
||||||
|
row={row}
|
||||||
|
allRows={tableData.rows}
|
||||||
|
users={users}
|
||||||
|
sites={sites}
|
||||||
|
vendors={vendors}
|
||||||
|
pmTypes={pmTypes}
|
||||||
|
search={tableData.search}
|
||||||
|
forceScheduleHighlight={forceScheduleHighlight || scheduleHighlightId === row.id}
|
||||||
|
flagColor={tableData.flags[String(row.id)]}
|
||||||
|
isSelected={tableData.selectedId === row.id}
|
||||||
|
isFlashing={tableData.flashId === row.id}
|
||||||
|
dragViz={tableData.dragViz}
|
||||||
|
handlers={handlers}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSaveDraft = () => {
|
||||||
|
const d = tableData.draft;
|
||||||
|
const assignee = users.find((u) => String(u.id) === d.dispatcherId);
|
||||||
|
const formValues = mapWizardDraftToFormValues({
|
||||||
|
locationId: String(d.locationId || ""),
|
||||||
|
locationName: d.location || d.site,
|
||||||
|
woNumber: d.woNumber,
|
||||||
|
type: d.type,
|
||||||
|
assigneeId: d.dispatcherId,
|
||||||
|
assigneeName: assignee?.name ?? "",
|
||||||
|
scheduledOn: d.scheduledOn,
|
||||||
|
dueDate: d.dueDate,
|
||||||
|
pm: d.pm,
|
||||||
|
vendorId: "",
|
||||||
|
vendorName: d.company,
|
||||||
|
techPhone: d.techPhone,
|
||||||
|
vendorNotes: "",
|
||||||
|
apptStart: d.apptStart,
|
||||||
|
apptEnd: d.apptEnd,
|
||||||
|
status: d.status || "Incomplete",
|
||||||
|
pocName: d.pocName,
|
||||||
|
pocPhone: d.pocPhone,
|
||||||
|
pocNotes: d.pocNotes,
|
||||||
|
scheduleWeekOnly: d.scheduleWeekOnly,
|
||||||
|
});
|
||||||
|
createMutation.mutate(formValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tableData.isLoading) {
|
||||||
|
return (
|
||||||
|
<WoTableShell>
|
||||||
|
<div className="flex h-full items-center justify-center p-12">
|
||||||
|
<CircularProgress />
|
||||||
|
</div>
|
||||||
|
</WoTableShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyAdv = tableData.advActive && tableData.advResults.length === 0;
|
||||||
|
|
||||||
|
if (emptyAdv) {
|
||||||
|
return (
|
||||||
|
<WoTableShell>
|
||||||
|
<WoTableEmptyState
|
||||||
|
message="No work orders match these filters"
|
||||||
|
actionLabel="Adjust filters"
|
||||||
|
onAction={onOpenAdvancedFilters}
|
||||||
|
/>
|
||||||
|
</WoTableShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tableData.advActive && tableData.noSearchResults) {
|
||||||
|
return (
|
||||||
|
<WoTableShell>
|
||||||
|
<WoTableEmptyState
|
||||||
|
message="No work orders match your search"
|
||||||
|
actionLabel="Clear search"
|
||||||
|
onAction={onClearSearch}
|
||||||
|
/>
|
||||||
|
</WoTableShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WoTableShell>
|
||||||
|
<table
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
minWidth: WO_TABLE_MIN_WIDTH,
|
||||||
|
borderCollapse: "collapse",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<WoTableHead />
|
||||||
|
<tbody>
|
||||||
|
{tableData.advActive ? (
|
||||||
|
tableData.advResults.map((row) => renderRow(row))
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{(tableData.unscheduled.length > 0 ||
|
||||||
|
tableData.draftDayKey === UNSCHEDULED_DAY_KEY) && (
|
||||||
|
<>
|
||||||
|
<WoTableSectionHeader
|
||||||
|
variant="unscheduled"
|
||||||
|
label="Unscheduled"
|
||||||
|
count={tableData.unscheduled.length}
|
||||||
|
collapsed={!tableData.unschedOpen}
|
||||||
|
onToggleCollapse={() => tableData.setUnschedOpen((v) => !v)}
|
||||||
|
onAddWo={() => tableData.startDayDraft(UNSCHEDULED_DAY_KEY)}
|
||||||
|
/>
|
||||||
|
{tableData.unschedOpen && (
|
||||||
|
<>
|
||||||
|
{tableData.draftDayKey === UNSCHEDULED_DAY_KEY && (
|
||||||
|
<WoTableInlineRow
|
||||||
|
draft={tableData.draft}
|
||||||
|
draftDayKey={tableData.draftDayKey}
|
||||||
|
users={users}
|
||||||
|
sites={sites}
|
||||||
|
vendors={vendors}
|
||||||
|
pmTypes={pmTypes}
|
||||||
|
allRows={tableData.rows}
|
||||||
|
onChange={tableData.setDraft}
|
||||||
|
onSave={handleSaveDraft}
|
||||||
|
onCancel={tableData.cancelDraft}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{tableData.unscheduled.map((row) => renderRow(row, true))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{tableData.displayGroups.map((group) => (
|
||||||
|
<Fragment key={group.key}>
|
||||||
|
<WoTableSectionHeader
|
||||||
|
variant="day"
|
||||||
|
label={group.label}
|
||||||
|
count={group.items.length}
|
||||||
|
isToday={group.key === tableData.todayKey}
|
||||||
|
onAddWo={() => tableData.startDayDraft(group.key)}
|
||||||
|
/>
|
||||||
|
{tableData.draftDayKey === group.key && (
|
||||||
|
<WoTableInlineRow
|
||||||
|
draft={tableData.draft}
|
||||||
|
draftDayKey={tableData.draftDayKey}
|
||||||
|
users={users}
|
||||||
|
sites={sites}
|
||||||
|
vendors={vendors}
|
||||||
|
pmTypes={pmTypes}
|
||||||
|
allRows={tableData.rows}
|
||||||
|
onChange={tableData.setDraft}
|
||||||
|
onSave={handleSaveDraft}
|
||||||
|
onCancel={tableData.cancelDraft}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{group.items.map((row) => renderRow(row))}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</WoTableShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
src/app/(protected)/workorders/_hooks/use-table-sort.ts
Normal file
58
src/app/(protected)/workorders/_hooks/use-table-sort.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import type {
|
||||||
|
SortDirection,
|
||||||
|
WorkOrdersSortableColumn,
|
||||||
|
} from "@/domain/work-orders/types/work-order-list";
|
||||||
|
|
||||||
|
type TableSortState = {
|
||||||
|
column: WorkOrdersSortableColumn | "";
|
||||||
|
direction: SortDirection;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useTableSort(initialColumn: WorkOrdersSortableColumn | "" = "") {
|
||||||
|
const [sortBy, setSortBy] = useState<WorkOrdersSortableColumn | "">(initialColumn);
|
||||||
|
const [sortDir, setSortDir] = useState<SortDirection>("");
|
||||||
|
|
||||||
|
const handleSort = useCallback((column: WorkOrdersSortableColumn | string) => {
|
||||||
|
const col = column as WorkOrdersSortableColumn;
|
||||||
|
setSortBy((currentColumn) => {
|
||||||
|
if (currentColumn !== col) {
|
||||||
|
setSortDir("asc");
|
||||||
|
return col;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSortDir((currentDir) => {
|
||||||
|
if (currentDir === "asc") {
|
||||||
|
return "desc";
|
||||||
|
}
|
||||||
|
if (currentDir === "desc") {
|
||||||
|
setSortBy("");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return "asc";
|
||||||
|
});
|
||||||
|
|
||||||
|
return currentColumn;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const sortIndicator = useCallback(
|
||||||
|
(column: string) => {
|
||||||
|
if (sortBy !== column) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return sortDir === "desc" ? " ↓" : sortDir === "asc" ? " ↑" : "";
|
||||||
|
},
|
||||||
|
[sortBy, sortDir],
|
||||||
|
);
|
||||||
|
|
||||||
|
const sort: TableSortState = { column: sortBy, direction: sortDir };
|
||||||
|
|
||||||
|
return {
|
||||||
|
sortBy,
|
||||||
|
sortDir,
|
||||||
|
sort,
|
||||||
|
handleSort,
|
||||||
|
sortIndicator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,222 @@
|
||||||
|
import { useCallback, useMemo, useRef, useState } from "react";
|
||||||
|
import type { useWorkOrdersListFilters } from "@/app/(protected)/workorders/_hooks/use-work-orders-list-filters";
|
||||||
|
import { useWorkOrdersList } from "@/domain/work-orders/use-cases/use-work-orders-list";
|
||||||
|
import {
|
||||||
|
EMPTY_TABLE_DRAFT,
|
||||||
|
UNSCHEDULED_DAY_KEY,
|
||||||
|
type WorkOrderTableDraft,
|
||||||
|
type WorkOrderTableRow,
|
||||||
|
} from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
import { currentWeekMonday } from "@/domain/work-orders/utils/list-week-utils";
|
||||||
|
import { buildWeekSkeleton, deriveDayFields } from "@/domain/work-orders/utils/table-day-utils";
|
||||||
|
import {
|
||||||
|
buildDayGroups,
|
||||||
|
enrichTableRow,
|
||||||
|
filterByBarAndSearch,
|
||||||
|
matchesAdvancedFilters,
|
||||||
|
splitScheduledUnscheduled,
|
||||||
|
} from "@/domain/work-orders/utils/table-filter-utils";
|
||||||
|
|
||||||
|
type FiltersReturn = ReturnType<typeof useWorkOrdersListFilters>;
|
||||||
|
|
||||||
|
export type DragVizState = {
|
||||||
|
dragId: string | number | null;
|
||||||
|
overId: string | number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useWorkOrdersTableData(filters: FiltersReturn) {
|
||||||
|
const { data, isLoading, isFetching } = useWorkOrdersList(filters.listParams);
|
||||||
|
const { data: scopeData } = useWorkOrdersList(filters.scopeListParams);
|
||||||
|
|
||||||
|
const [patches, setPatches] = useState<Record<string, Partial<WorkOrderTableRow>>>({});
|
||||||
|
const [localRows, setLocalRows] = useState<WorkOrderTableRow[]>([]);
|
||||||
|
const [rowOrder, setRowOrder] = useState<string[] | null>(null);
|
||||||
|
const [flags, setFlagsState] = useState<Record<string, string>>({});
|
||||||
|
const [draftDayKey, setDraftDayKey] = useState<string | null>(null);
|
||||||
|
const [draft, setDraft] = useState<WorkOrderTableDraft>(EMPTY_TABLE_DRAFT);
|
||||||
|
const [unschedOpen, setUnschedOpen] = useState(true);
|
||||||
|
const [selectedId, setSelectedId] = useState<string | number | null>(null);
|
||||||
|
const [flashId, setFlashId] = useState<string | number | null>(null);
|
||||||
|
const [dragViz, setDragViz] = useState<DragVizState>({ dragId: null, overId: null });
|
||||||
|
const dragState = useRef<DragVizState>({ dragId: null, overId: null });
|
||||||
|
|
||||||
|
const apiRows = useMemo(
|
||||||
|
() => (data?.items ?? []).map((row) => enrichTableRow({ ...row, ...patches[String(row.id)] })),
|
||||||
|
[data?.items, patches],
|
||||||
|
);
|
||||||
|
|
||||||
|
const displayRows = useMemo(() => {
|
||||||
|
const apiIds = new Set(apiRows.map((r) => String(r.id)));
|
||||||
|
const extra = localRows.filter((r) => !apiIds.has(String(r.id)));
|
||||||
|
let merged = [...apiRows, ...extra.map(enrichTableRow)];
|
||||||
|
if (rowOrder?.length) {
|
||||||
|
const orderMap = new Map(rowOrder.map((id, idx) => [id, idx]));
|
||||||
|
merged = [...merged].sort((a, b) => {
|
||||||
|
const ai = orderMap.get(String(a.id)) ?? 9999;
|
||||||
|
const bi = orderMap.get(String(b.id)) ?? 9999;
|
||||||
|
return ai - bi;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}, [apiRows, localRows, rowOrder]);
|
||||||
|
|
||||||
|
const advActive = filters.advApplied !== null;
|
||||||
|
const advResults = useMemo(() => {
|
||||||
|
if (!filters.advApplied) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return displayRows.filter((row) => matchesAdvancedFilters(row, filters.advApplied!));
|
||||||
|
}, [displayRows, filters.advApplied]);
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
if (advActive) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return filterByBarAndSearch(displayRows, filters.barFilters, filters.search);
|
||||||
|
}, [displayRows, filters.barFilters, filters.search, advActive]);
|
||||||
|
|
||||||
|
const { scheduled, unscheduled } = useMemo(() => splitScheduledUnscheduled(filtered), [filtered]);
|
||||||
|
|
||||||
|
const dayGroups = useMemo(() => buildDayGroups(scheduled), [scheduled]);
|
||||||
|
const weekSkeleton = useMemo(() => buildWeekSkeleton(filters.weekMonday), [filters.weekMonday]);
|
||||||
|
|
||||||
|
const searchActive = filters.search.trim() !== "";
|
||||||
|
const noSearchResults = filtered.length === 0 && searchActive;
|
||||||
|
const isEmptyWeek = scheduled.length === 0 && !searchActive;
|
||||||
|
const displayGroups = isEmptyWeek ? weekSkeleton : dayGroups;
|
||||||
|
|
||||||
|
const todayKey =
|
||||||
|
filters.weekMonday === currentWeekMonday()
|
||||||
|
? deriveDayFields(new Date().toISOString().slice(0, 10)).dayGroup
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const selectedRow = useMemo(
|
||||||
|
() => displayRows.find((row) => String(row.id) === String(selectedId)) ?? null,
|
||||||
|
[displayRows, selectedId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setFlag = useCallback((id: string | number, color: string | null) => {
|
||||||
|
setFlagsState((prev) => {
|
||||||
|
const key = String(id);
|
||||||
|
if (!color) {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[key];
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
return { ...prev, [key]: color };
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const patchRow = useCallback((id: string | number, patch: Partial<WorkOrderTableRow>) => {
|
||||||
|
const key = String(id);
|
||||||
|
setPatches((prev) => {
|
||||||
|
const merged = { ...prev[key], ...patch };
|
||||||
|
if (patch.scheduledOn !== undefined) {
|
||||||
|
const dayFields = deriveDayFields(patch.scheduledOn);
|
||||||
|
merged.dayGroup = dayFields.dayGroup;
|
||||||
|
merged.dayLabel = dayFields.dayLabel;
|
||||||
|
}
|
||||||
|
return { ...prev, [key]: merged };
|
||||||
|
});
|
||||||
|
setLocalRows((prev) =>
|
||||||
|
prev.map((row) => (String(row.id) === key ? enrichTableRow({ ...row, ...patch }) : row)),
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const reorderRows = useCallback(
|
||||||
|
(dragId: string | number, overId: string | number) => {
|
||||||
|
const dragKey = String(dragId);
|
||||||
|
const overKey = String(overId);
|
||||||
|
const dragRow = displayRows.find((r) => String(r.id) === dragKey);
|
||||||
|
const overRow = displayRows.find((r) => String(r.id) === overKey);
|
||||||
|
if (!dragRow || !overRow || dragRow.dayGroup !== overRow.dayGroup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ids = displayRows.map((r) => String(r.id));
|
||||||
|
const dragIdx = ids.indexOf(dragKey);
|
||||||
|
const overIdx = ids.indexOf(overKey);
|
||||||
|
if (dragIdx < 0 || overIdx < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const next = [...ids];
|
||||||
|
next.splice(dragIdx, 1);
|
||||||
|
next.splice(overIdx, 0, dragKey);
|
||||||
|
setRowOrder(next);
|
||||||
|
},
|
||||||
|
[displayRows],
|
||||||
|
);
|
||||||
|
|
||||||
|
const endDrag = useCallback(() => {
|
||||||
|
dragState.current = { dragId: null, overId: null };
|
||||||
|
setDragViz({ dragId: null, overId: null });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const startDayDraft = useCallback(
|
||||||
|
(key: string) => {
|
||||||
|
setDraftDayKey(key);
|
||||||
|
const scheduledOn =
|
||||||
|
key === UNSCHEDULED_DAY_KEY
|
||||||
|
? ""
|
||||||
|
: (() => {
|
||||||
|
const [mm, dd] = key.split("/").map(Number);
|
||||||
|
const year = filters.weekMonday.slice(0, 4);
|
||||||
|
return `${year}-${String(mm).padStart(2, "0")}-${String(dd).padStart(2, "0")}`;
|
||||||
|
})();
|
||||||
|
setDraft({
|
||||||
|
...EMPTY_TABLE_DRAFT,
|
||||||
|
scheduledOn,
|
||||||
|
scheduleWeekOnly: key === UNSCHEDULED_DAY_KEY,
|
||||||
|
targetWeek: key === UNSCHEDULED_DAY_KEY ? filters.weekMonday : "",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[filters.weekMonday],
|
||||||
|
);
|
||||||
|
|
||||||
|
const cancelDraft = useCallback(() => {
|
||||||
|
setDraftDayKey(null);
|
||||||
|
setDraft(EMPTY_TABLE_DRAFT);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const addRow = useCallback((row: WorkOrderTableRow) => {
|
||||||
|
setLocalRows((prev) => [...prev, enrichTableRow(row)]);
|
||||||
|
setFlashId(row.id);
|
||||||
|
setTimeout(() => setFlashId(null), 2000);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
rows: displayRows,
|
||||||
|
isLoading,
|
||||||
|
isFetching,
|
||||||
|
advActive,
|
||||||
|
advResults,
|
||||||
|
unscheduled,
|
||||||
|
displayGroups,
|
||||||
|
searchActive,
|
||||||
|
noSearchResults,
|
||||||
|
isEmptyWeek,
|
||||||
|
todayKey,
|
||||||
|
flags,
|
||||||
|
setFlag,
|
||||||
|
draftDayKey,
|
||||||
|
draft,
|
||||||
|
setDraft,
|
||||||
|
unschedOpen,
|
||||||
|
setUnschedOpen,
|
||||||
|
selectedId,
|
||||||
|
setSelectedId,
|
||||||
|
selectedRow,
|
||||||
|
flashId,
|
||||||
|
dragViz,
|
||||||
|
setDragViz,
|
||||||
|
dragState,
|
||||||
|
patchRow,
|
||||||
|
reorderRows,
|
||||||
|
endDrag,
|
||||||
|
startDayDraft,
|
||||||
|
cancelDraft,
|
||||||
|
addRow,
|
||||||
|
scopeTotalCount: scopeData?.totalCount ?? 0,
|
||||||
|
totalCount: data?.totalCount ?? displayRows.length,
|
||||||
|
search: filters.search,
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue