mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 01:12:03 +00:00
fix(work-orders): implement SH-165 Unassigned pinned queue
Replace Unscheduled-focused changes with assignment-based board partition, accessible disclosure, duplicate prevention, and inline unassign support.
This commit is contained in:
parent
f4cfafd7a6
commit
93034f5da2
15 changed files with 396 additions and 305 deletions
|
|
@ -118,15 +118,6 @@ Pending (`No`)=2, Uploaded (`Yes`)=1, N/N (`NN`)=3.
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Buckets
|
|
||||||
|
|
||||||
| Array | Inclusion rule |
|
|
||||||
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `scheduled` | WOs with `scheduledDate` inside `weekStart`–`weekEnd` |
|
|
||||||
| `unscheduled` | **All** WOs without `scheduledDate` (cross-week). Visible for every week request; `targetWeek` is not a week filter — Schedule-cell marker only |
|
|
||||||
|
|
||||||
The FE concatenates `unscheduled + scheduled` into `items` and renders `unscheduled` in the pinned Unscheduled section above day groups. Dated WOs without a dispatcher stay in `scheduled` / day groups (Assigned To highlight), not in `unscheduled`.
|
|
||||||
|
|
||||||
### `/board/search`
|
### `/board/search`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,18 @@ export function AssignedToCell({ dispatcherId, users, highlight, onSave }: Assig
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-52 overflow-y-auto">
|
<div className="max-h-52 overflow-y-auto">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
onSave("", "");
|
||||||
|
setAnchorEl(null);
|
||||||
|
setSearch("");
|
||||||
|
}}
|
||||||
|
className="flex w-full items-center justify-between gap-2 rounded px-1.5 py-1 transition-colors hover:bg-[#F9FAFB]"
|
||||||
|
>
|
||||||
|
<span className="text-[13px] text-[var(--color-text-muted)]">Unassigned</span>
|
||||||
|
{!dispatcherId && <Check size={10} className="shrink-0 text-[var(--color-primary)]" />}
|
||||||
|
</button>
|
||||||
{filtered.map((d) => (
|
{filtered.map((d) => (
|
||||||
<button
|
<button
|
||||||
key={String(d.id)}
|
key={String(d.id)}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { Fragment, type ReactNode } from "react";
|
import { Fragment, type ReactNode } from "react";
|
||||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
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 {
|
||||||
|
UNASSIGNED_DAY_KEY,
|
||||||
|
UNSCHEDULED_DAY_KEY,
|
||||||
|
} from "@/domain/work-orders/types/work-order-table-row";
|
||||||
import type { ReturnTypeOfUseWorkOrdersTableData } from "./wo-table-types";
|
import type { ReturnTypeOfUseWorkOrdersTableData } from "./wo-table-types";
|
||||||
import { WoTableSectionHeader } from "./wo-table-section-header";
|
import { WoTableSectionHeader } from "./wo-table-section-header";
|
||||||
|
|
||||||
|
|
@ -15,11 +18,30 @@ export function WoTableBody({ tableData, renderRow, renderInlineRow }: WoTableBo
|
||||||
return <tbody>{tableData.advResults.map((row) => renderRow(row))}</tbody>;
|
return <tbody>{tableData.advResults.map((row) => renderRow(row))}</tbody>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showUnassigned =
|
||||||
|
tableData.unassigned.length > 0 || tableData.draftDayKey === UNASSIGNED_DAY_KEY;
|
||||||
const showUnscheduled =
|
const showUnscheduled =
|
||||||
tableData.unscheduled.length > 0 || tableData.draftDayKey === UNSCHEDULED_DAY_KEY;
|
tableData.unscheduled.length > 0 || tableData.draftDayKey === UNSCHEDULED_DAY_KEY;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tbody>
|
<tbody>
|
||||||
|
{showUnassigned && (
|
||||||
|
<>
|
||||||
|
<WoTableSectionHeader
|
||||||
|
variant="unassigned"
|
||||||
|
label="Unassigned"
|
||||||
|
count={tableData.unassigned.length}
|
||||||
|
collapsed={!tableData.unassignedOpen}
|
||||||
|
onToggleCollapse={() => tableData.setUnassignedOpen((v) => !v)}
|
||||||
|
/>
|
||||||
|
{tableData.unassignedOpen && (
|
||||||
|
<>
|
||||||
|
{renderInlineRow(UNASSIGNED_DAY_KEY)}
|
||||||
|
{tableData.unassigned.map((row) => renderRow(row))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{showUnscheduled && (
|
{showUnscheduled && (
|
||||||
<>
|
<>
|
||||||
<WoTableSectionHeader
|
<WoTableSectionHeader
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import { ChevronDown, ChevronRight, Plus } from "lucide-react";
|
||||||
|
import { WO_TABLE_COLS } from "./wo-table-cols";
|
||||||
|
|
||||||
|
type CollapsibleSectionStyle = {
|
||||||
|
background: string;
|
||||||
|
borderTop: string;
|
||||||
|
borderBottom: string;
|
||||||
|
accentColor: string;
|
||||||
|
boxShadow: string;
|
||||||
|
labelFontFamily: string;
|
||||||
|
labelLetterSpacing: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SECTION_STYLES: Record<"unassigned" | "unscheduled", CollapsibleSectionStyle> = {
|
||||||
|
unassigned: {
|
||||||
|
background: "rgba(99, 102, 241, 0.08)",
|
||||||
|
borderTop: "1px solid #C7D2FE",
|
||||||
|
borderBottom: "1px solid #C7D2FE",
|
||||||
|
accentColor: "var(--primary)",
|
||||||
|
boxShadow: "inset 3px 0 0 var(--primary)",
|
||||||
|
labelFontFamily: "var(--font-mono)",
|
||||||
|
labelLetterSpacing: "0.08em",
|
||||||
|
},
|
||||||
|
unscheduled: {
|
||||||
|
background: "rgba(217, 119, 6, 0.08)",
|
||||||
|
borderTop: "1px solid #FDE68A",
|
||||||
|
borderBottom: "1px solid #FDE68A",
|
||||||
|
accentColor: "var(--warning)",
|
||||||
|
boxShadow: "inset 3px 0 0 var(--warning)",
|
||||||
|
labelFontFamily: "var(--font-mono)",
|
||||||
|
labelLetterSpacing: "0.08em",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type WoTableCollapsibleSectionHeaderProps = {
|
||||||
|
tone: "unassigned" | "unscheduled";
|
||||||
|
icon: LucideIcon;
|
||||||
|
label: string;
|
||||||
|
count: number;
|
||||||
|
collapsed?: boolean;
|
||||||
|
onToggleCollapse: () => void;
|
||||||
|
onAddWo?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function WoTableCollapsibleSectionHeader({
|
||||||
|
tone,
|
||||||
|
icon: Icon,
|
||||||
|
label,
|
||||||
|
count,
|
||||||
|
collapsed,
|
||||||
|
onToggleCollapse,
|
||||||
|
onAddWo,
|
||||||
|
}: WoTableCollapsibleSectionHeaderProps) {
|
||||||
|
const style = SECTION_STYLES[tone];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr style={{ background: style.background }}>
|
||||||
|
<td
|
||||||
|
colSpan={WO_TABLE_COLS.length}
|
||||||
|
style={{
|
||||||
|
height: 40,
|
||||||
|
padding: "0 14px",
|
||||||
|
boxShadow: style.boxShadow,
|
||||||
|
borderTop: style.borderTop,
|
||||||
|
borderBottom: style.borderBottom,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex flex-1 items-center gap-2.5"
|
||||||
|
aria-expanded={!collapsed}
|
||||||
|
aria-label={`${label}, ${count} work order${count !== 1 ? "s" : ""}${collapsed ? ", collapsed" : ", expanded"}`}
|
||||||
|
onClick={onToggleCollapse}
|
||||||
|
style={{
|
||||||
|
background: "transparent",
|
||||||
|
border: "none",
|
||||||
|
padding: 0,
|
||||||
|
cursor: "pointer",
|
||||||
|
color: "inherit",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon size={14} className="shrink-0" style={{ color: style.accentColor }} />
|
||||||
|
<span
|
||||||
|
className="uppercase"
|
||||||
|
style={{
|
||||||
|
fontFamily: style.labelFontFamily,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 700,
|
||||||
|
letterSpacing: style.labelLetterSpacing,
|
||||||
|
color: style.accentColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<span style={{ fontSize: 12, color: "var(--color-text-muted)" }}>
|
||||||
|
{count} work order{count !== 1 ? "s" : ""}
|
||||||
|
</span>
|
||||||
|
<span className="ml-auto flex items-center" style={{ color: style.accentColor }}>
|
||||||
|
{collapsed ? <ChevronRight size={14} /> : <ChevronDown size={14} />}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{Boolean(onAddWo) && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-1 transition-opacity hover:opacity-70"
|
||||||
|
style={{ fontSize: 12.5, fontWeight: 600, color: "var(--primary)" }}
|
||||||
|
onClick={onAddWo}
|
||||||
|
>
|
||||||
|
<Plus size={13} />
|
||||||
|
Add WO
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,10 @@ import type {
|
||||||
|
|
||||||
import type { WorkOrderTableDraft } from "@/domain/work-orders/types/work-order-table-row";
|
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 {
|
||||||
|
UNASSIGNED_DAY_KEY,
|
||||||
|
UNSCHEDULED_DAY_KEY,
|
||||||
|
} from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
|
||||||
import { WO_TABLE_ROW_HEIGHT } from "./wo-table-cols";
|
import { WO_TABLE_ROW_HEIGHT } from "./wo-table-cols";
|
||||||
import { WoTableInlineRowCells } from "./wo-table-inline-row-cells";
|
import { WoTableInlineRowCells } from "./wo-table-inline-row-cells";
|
||||||
|
|
@ -65,7 +68,7 @@ export function WoTableInlineRow({
|
||||||
|
|
||||||
onCancel,
|
onCancel,
|
||||||
}: WoTableInlineRowProps) {
|
}: WoTableInlineRowProps) {
|
||||||
const weekDefault = draftDayKey === UNSCHEDULED_DAY_KEY;
|
const weekDefault = draftDayKey === UNSCHEDULED_DAY_KEY || draftDayKey === UNASSIGNED_DAY_KEY;
|
||||||
|
|
||||||
const canSave =
|
const canSave =
|
||||||
!saving && (weekDefault ? !!draft.targetWeek || !!draft.scheduledOn : !!draft.scheduledOn);
|
!saving && (weekDefault ? !!draft.targetWeek || !!draft.scheduledOn : !!draft.scheduledOn);
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
import { AlertTriangle, ChevronDown, ChevronRight, Plus } from "lucide-react";
|
import { AlertTriangle, Plus, Users } from "lucide-react";
|
||||||
import { WO_TABLE_COLS } from "./wo-table-cols";
|
import { WO_TABLE_COLS } from "./wo-table-cols";
|
||||||
|
import { WoTableCollapsibleSectionHeader } from "./wo-table-collapsible-section-header";
|
||||||
|
|
||||||
type WoTableSectionHeaderProps = {
|
type WoTableSectionHeaderProps = {
|
||||||
variant: "unscheduled" | "day";
|
variant: "unassigned" | "unscheduled" | "day";
|
||||||
label: string;
|
label: string;
|
||||||
count: number;
|
count: number;
|
||||||
isToday?: boolean;
|
isToday?: boolean;
|
||||||
collapsed?: boolean;
|
collapsed?: boolean;
|
||||||
onToggleCollapse?: () => void;
|
onToggleCollapse?: () => void;
|
||||||
onAddWo: () => void;
|
onAddWo?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function WoTableSectionHeader({
|
export function WoTableSectionHeader({
|
||||||
|
|
@ -20,72 +21,30 @@ export function WoTableSectionHeader({
|
||||||
onToggleCollapse,
|
onToggleCollapse,
|
||||||
onAddWo,
|
onAddWo,
|
||||||
}: WoTableSectionHeaderProps) {
|
}: WoTableSectionHeaderProps) {
|
||||||
const isUnscheduled = variant === "unscheduled";
|
if (variant === "unassigned") {
|
||||||
|
|
||||||
if (isUnscheduled) {
|
|
||||||
const toggleCollapse = () => {
|
|
||||||
onToggleCollapse?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr style={{ background: "rgba(217, 119, 6, 0.08)" }}>
|
<WoTableCollapsibleSectionHeader
|
||||||
<td
|
tone="unassigned"
|
||||||
colSpan={WO_TABLE_COLS.length}
|
icon={Users}
|
||||||
style={{
|
label={label}
|
||||||
height: 40,
|
count={count}
|
||||||
padding: "0 14px",
|
collapsed={collapsed}
|
||||||
boxShadow: "inset 3px 0 0 var(--warning)",
|
onToggleCollapse={() => onToggleCollapse?.()}
|
||||||
borderTop: "1px solid #FDE68A",
|
/>
|
||||||
borderBottom: "1px solid #FDE68A",
|
);
|
||||||
}}
|
}
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2.5">
|
if (variant === "unscheduled") {
|
||||||
<button
|
return (
|
||||||
type="button"
|
<WoTableCollapsibleSectionHeader
|
||||||
className="flex flex-1 items-center gap-2.5"
|
tone="unscheduled"
|
||||||
aria-expanded={!collapsed}
|
icon={AlertTriangle}
|
||||||
aria-label={`${label}, ${count} work order${count !== 1 ? "s" : ""}${collapsed ? ", collapsed" : ", expanded"}`}
|
label={label}
|
||||||
onClick={toggleCollapse}
|
count={count}
|
||||||
style={{
|
collapsed={collapsed}
|
||||||
background: "transparent",
|
onToggleCollapse={() => onToggleCollapse?.()}
|
||||||
border: "none",
|
onAddWo={onAddWo}
|
||||||
padding: 0,
|
/>
|
||||||
cursor: "pointer",
|
|
||||||
color: "inherit",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
<span className="ml-auto flex items-center" style={{ color: "var(--warning)" }}>
|
|
||||||
{collapsed ? <ChevronRight size={14} /> : <ChevronDown size={14} />}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex items-center gap-1 transition-opacity hover:opacity-70"
|
|
||||||
style={{ fontSize: 12.5, fontWeight: 600, color: "var(--primary)" }}
|
|
||||||
onClick={onAddWo}
|
|
||||||
>
|
|
||||||
<Plus size={13} />
|
|
||||||
Add WO
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,15 +98,17 @@ export function WoTableSectionHeader({
|
||||||
Today
|
Today
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<button
|
{Boolean(onAddWo) && (
|
||||||
type="button"
|
<button
|
||||||
className="flex items-center gap-1 transition-opacity hover:opacity-70"
|
type="button"
|
||||||
style={{ fontSize: 12.5, fontWeight: 600, color: "var(--primary)" }}
|
className="flex items-center gap-1 transition-opacity hover:opacity-70"
|
||||||
onClick={onAddWo}
|
style={{ fontSize: 12.5, fontWeight: 600, color: "var(--primary)" }}
|
||||||
>
|
onClick={onAddWo}
|
||||||
<Plus size={13} />
|
>
|
||||||
Add WO
|
<Plus size={13} />
|
||||||
</button>
|
Add WO
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { buildWeekSkeleton, deriveDayFields } from "@/domain/work-orders/utils/t
|
||||||
import {
|
import {
|
||||||
buildDayGroups,
|
buildDayGroups,
|
||||||
filterByBarAndSearch,
|
filterByBarAndSearch,
|
||||||
|
partitionBoardRows,
|
||||||
splitScheduledUnscheduled,
|
splitScheduledUnscheduled,
|
||||||
} from "@/domain/work-orders/utils/table-filter-utils";
|
} from "@/domain/work-orders/utils/table-filter-utils";
|
||||||
|
|
||||||
|
|
@ -38,10 +39,13 @@ export function useWorkOrdersTableData(filters: FiltersReturn) {
|
||||||
return displayRows;
|
return displayRows;
|
||||||
}, [displayRows, filters.barFilters, filters.search, advActive]);
|
}, [displayRows, filters.barFilters, filters.search, advActive]);
|
||||||
|
|
||||||
const { scheduled, unscheduled } = useMemo(
|
const { unassigned, scheduled, unscheduled } = useMemo(() => {
|
||||||
() => (advActive ? splitScheduledUnscheduled(advResults) : splitScheduledUnscheduled(filtered)),
|
if (advActive) {
|
||||||
[advActive, advResults, filtered],
|
const split = splitScheduledUnscheduled(advResults);
|
||||||
);
|
return { unassigned: [], ...split };
|
||||||
|
}
|
||||||
|
return partitionBoardRows(filtered);
|
||||||
|
}, [advActive, advResults, filtered]);
|
||||||
|
|
||||||
const dayGroups = useMemo(() => buildDayGroups(scheduled), [scheduled]);
|
const dayGroups = useMemo(() => buildDayGroups(scheduled), [scheduled]);
|
||||||
const weekSkeleton = useMemo(() => buildWeekSkeleton(filters.weekMonday), [filters.weekMonday]);
|
const weekSkeleton = useMemo(() => buildWeekSkeleton(filters.weekMonday), [filters.weekMonday]);
|
||||||
|
|
@ -49,7 +53,12 @@ export function useWorkOrdersTableData(filters: FiltersReturn) {
|
||||||
const searchActive = filters.search.trim() !== "";
|
const searchActive = filters.search.trim() !== "";
|
||||||
const visibleRows = advActive ? advResults : filtered;
|
const visibleRows = advActive ? advResults : filtered;
|
||||||
const noSearchResults = visibleRows.length === 0 && searchActive;
|
const noSearchResults = visibleRows.length === 0 && searchActive;
|
||||||
const isEmptyWeek = !advActive && scheduled.length === 0 && !searchActive;
|
const isEmptyWeek =
|
||||||
|
!advActive &&
|
||||||
|
scheduled.length === 0 &&
|
||||||
|
unassigned.length === 0 &&
|
||||||
|
unscheduled.length === 0 &&
|
||||||
|
!searchActive;
|
||||||
const displayGroups = isEmptyWeek ? weekSkeleton : dayGroups;
|
const displayGroups = isEmptyWeek ? weekSkeleton : dayGroups;
|
||||||
|
|
||||||
const todayKey =
|
const todayKey =
|
||||||
|
|
@ -70,6 +79,7 @@ export function useWorkOrdersTableData(filters: FiltersReturn) {
|
||||||
refetch,
|
refetch,
|
||||||
advActive,
|
advActive,
|
||||||
advResults,
|
advResults,
|
||||||
|
unassigned,
|
||||||
unscheduled,
|
unscheduled,
|
||||||
displayGroups,
|
displayGroups,
|
||||||
searchActive,
|
searchActive,
|
||||||
|
|
@ -79,6 +89,8 @@ export function useWorkOrdersTableData(filters: FiltersReturn) {
|
||||||
draftDayKey: local.draftDayKey,
|
draftDayKey: local.draftDayKey,
|
||||||
draft: local.draft,
|
draft: local.draft,
|
||||||
setDraft: local.setDraft,
|
setDraft: local.setDraft,
|
||||||
|
unassignedOpen: local.unassignedOpen,
|
||||||
|
setUnassignedOpen: local.setUnassignedOpen,
|
||||||
unschedOpen: local.unschedOpen,
|
unschedOpen: local.unschedOpen,
|
||||||
setUnschedOpen: local.setUnschedOpen,
|
setUnschedOpen: local.setUnschedOpen,
|
||||||
selectedId: local.selectedId,
|
selectedId: local.selectedId,
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,8 @@ export function useWorkOrdersTableLocalState({
|
||||||
setDraft: ui.setDraft,
|
setDraft: ui.setDraft,
|
||||||
startDayDraft: ui.startDayDraft,
|
startDayDraft: ui.startDayDraft,
|
||||||
cancelDraft: ui.cancelDraft,
|
cancelDraft: ui.cancelDraft,
|
||||||
|
unassignedOpen: ui.unassignedOpen,
|
||||||
|
setUnassignedOpen: ui.setUnassignedOpen,
|
||||||
unschedOpen: ui.unschedOpen,
|
unschedOpen: ui.unschedOpen,
|
||||||
setUnschedOpen: ui.setUnschedOpen,
|
setUnschedOpen: ui.setUnschedOpen,
|
||||||
selectedId: ui.selectedId,
|
selectedId: ui.selectedId,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
EMPTY_TABLE_DRAFT,
|
EMPTY_TABLE_DRAFT,
|
||||||
|
UNASSIGNED_DAY_KEY,
|
||||||
UNSCHEDULED_DAY_KEY,
|
UNSCHEDULED_DAY_KEY,
|
||||||
type WorkOrderTableDraft,
|
type WorkOrderTableDraft,
|
||||||
} from "@/domain/work-orders/types/work-order-table-row";
|
} from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
|
@ -11,7 +12,7 @@ export type DragVizState = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function resolveDraftScheduledOn(key: string, weekMonday: string, scheduledIso?: string): string {
|
function resolveDraftScheduledOn(key: string, weekMonday: string, scheduledIso?: string): string {
|
||||||
if (key === UNSCHEDULED_DAY_KEY) {
|
if (key === UNSCHEDULED_DAY_KEY || key === UNASSIGNED_DAY_KEY) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
if (scheduledIso && /^\d{4}-\d{2}-\d{2}$/.test(scheduledIso)) {
|
if (scheduledIso && /^\d{4}-\d{2}-\d{2}$/.test(scheduledIso)) {
|
||||||
|
|
@ -31,10 +32,11 @@ function resolveDraftScheduledOn(key: string, weekMonday: string, scheduledIso?:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Draft, selection, unsched accordion, drag viz and flash scroll helpers. */
|
/** Draft, selection, pinned-section accordions, drag viz and flash scroll helpers. */
|
||||||
export function useWorkOrdersTableUiState(weekMonday: string) {
|
export function useWorkOrdersTableUiState(weekMonday: string) {
|
||||||
const [draftDayKey, setDraftDayKey] = useState<string | null>(null);
|
const [draftDayKey, setDraftDayKey] = useState<string | null>(null);
|
||||||
const [draft, setDraft] = useState<WorkOrderTableDraft>(EMPTY_TABLE_DRAFT);
|
const [draft, setDraft] = useState<WorkOrderTableDraft>(EMPTY_TABLE_DRAFT);
|
||||||
|
const [unassignedOpen, setUnassignedOpen] = useState(true);
|
||||||
const [unschedOpen, setUnschedOpen] = useState(true);
|
const [unschedOpen, setUnschedOpen] = useState(true);
|
||||||
const [selectedId, setSelectedId] = useState<string | number | null>(null);
|
const [selectedId, setSelectedId] = useState<string | number | null>(null);
|
||||||
const [flashId, setFlashId] = useState<string | number | null>(null);
|
const [flashId, setFlashId] = useState<string | number | null>(null);
|
||||||
|
|
@ -81,6 +83,8 @@ export function useWorkOrdersTableUiState(weekMonday: string) {
|
||||||
setDraft,
|
setDraft,
|
||||||
startDayDraft,
|
startDayDraft,
|
||||||
cancelDraft,
|
cancelDraft,
|
||||||
|
unassignedOpen,
|
||||||
|
setUnassignedOpen,
|
||||||
unschedOpen,
|
unschedOpen,
|
||||||
setUnschedOpen,
|
setUnschedOpen,
|
||||||
selectedId,
|
selectedId,
|
||||||
|
|
|
||||||
|
|
@ -115,3 +115,4 @@ export type WorkOrderDayGroup = {
|
||||||
export type WorkOrderTablePatch = Partial<WorkOrderTableRow>;
|
export type WorkOrderTablePatch = Partial<WorkOrderTableRow>;
|
||||||
|
|
||||||
export const UNSCHEDULED_DAY_KEY = "__unscheduled__" as const;
|
export const UNSCHEDULED_DAY_KEY = "__unscheduled__" as const;
|
||||||
|
export const UNASSIGNED_DAY_KEY = "__unassigned__" as const;
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ export type {
|
||||||
|
|
||||||
export {
|
export {
|
||||||
EMPTY_TABLE_DRAFT,
|
EMPTY_TABLE_DRAFT,
|
||||||
|
UNASSIGNED_DAY_KEY,
|
||||||
UNSCHEDULED_DAY_KEY,
|
UNSCHEDULED_DAY_KEY,
|
||||||
WORK_ORDERS_TABLE_PAGE_SIZE,
|
WORK_ORDERS_TABLE_PAGE_SIZE,
|
||||||
} from "./work-order-table-row";
|
} from "./work-order-table-row";
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import type {
|
||||||
WorkOrderTableRow,
|
WorkOrderTableRow,
|
||||||
} from "@/domain/work-orders/types/work-order-table-row";
|
} from "@/domain/work-orders/types/work-order-table-row";
|
||||||
import { ADVANCED_FILTER_PREDICATES } from "@/domain/work-orders/utils/advanced-filter-predicates";
|
import { ADVANCED_FILTER_PREDICATES } from "@/domain/work-orders/utils/advanced-filter-predicates";
|
||||||
|
import { isDateInWeek } from "@/domain/work-orders/utils/table-day-utils";
|
||||||
import { weekRange } from "@/domain/work-orders/utils/list-week-utils";
|
import { weekRange } from "@/domain/work-orders/utils/list-week-utils";
|
||||||
|
|
||||||
const TERMINAL_STATUSES = new Set(["Completed", "Canceled", "Done", "Cancelled"]);
|
const TERMINAL_STATUSES = new Set(["Completed", "Canceled", "Done", "Cancelled"]);
|
||||||
|
|
@ -90,16 +91,47 @@ export function filterByBarAndSearch(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Design weekScoped: undated (!scheduledOn) stay visible every week; targetWeek
|
if (row.scheduledOn) {
|
||||||
// is a Schedule-cell marker only, never a week-inclusion predicate.
|
if (row.scheduledOn < weekFrom || row.scheduledOn > weekTo) {
|
||||||
if (row.scheduledOn && (row.scheduledOn < weekFrom || row.scheduledOn > weekTo)) {
|
return false;
|
||||||
return false;
|
}
|
||||||
|
} else if (row.targetWeek) {
|
||||||
|
if (!isDateInWeek(row.targetWeek, bar.weekMonday)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isUnassignedBoardRow(row: WorkOrderTableRow): boolean {
|
||||||
|
if (TERMINAL_STATUSES.has(row.status)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !row.dispatcherId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function partitionBoardRows(rows: WorkOrderTableRow[]): {
|
||||||
|
unassigned: WorkOrderTableRow[];
|
||||||
|
unscheduled: WorkOrderTableRow[];
|
||||||
|
scheduled: WorkOrderTableRow[];
|
||||||
|
} {
|
||||||
|
const unassigned: WorkOrderTableRow[] = [];
|
||||||
|
const remaining: WorkOrderTableRow[] = [];
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
if (isUnassignedBoardRow(row)) {
|
||||||
|
unassigned.push(row);
|
||||||
|
} else {
|
||||||
|
remaining.push(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { scheduled, unscheduled } = splitScheduledUnscheduled(remaining);
|
||||||
|
return { unassigned, unscheduled, scheduled };
|
||||||
|
}
|
||||||
|
|
||||||
export function splitScheduledUnscheduled(rows: WorkOrderTableRow[]): {
|
export function splitScheduledUnscheduled(rows: WorkOrderTableRow[]): {
|
||||||
scheduled: WorkOrderTableRow[];
|
scheduled: WorkOrderTableRow[];
|
||||||
unscheduled: WorkOrderTableRow[];
|
unscheduled: WorkOrderTableRow[];
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { WoTableBody } from "@/app/(protected)/workorders/_components/list/table/wo-table-body";
|
import { WoTableBody } from "@/app/(protected)/workorders/_components/list/table/wo-table-body";
|
||||||
import type { ReturnTypeOfUseWorkOrdersTableData } from "@/app/(protected)/workorders/_components/list/table/wo-table-types";
|
import type { ReturnTypeOfUseWorkOrdersTableData } from "@/app/(protected)/workorders/_components/list/table/wo-table-types";
|
||||||
|
|
@ -62,6 +63,7 @@ function baseTableData(
|
||||||
refetch: vi.fn(),
|
refetch: vi.fn(),
|
||||||
advActive: false,
|
advActive: false,
|
||||||
advResults: [],
|
advResults: [],
|
||||||
|
unassigned: [],
|
||||||
unscheduled: [],
|
unscheduled: [],
|
||||||
displayGroups: [],
|
displayGroups: [],
|
||||||
searchActive: false,
|
searchActive: false,
|
||||||
|
|
@ -71,6 +73,8 @@ function baseTableData(
|
||||||
draftDayKey: null,
|
draftDayKey: null,
|
||||||
draft: null,
|
draft: null,
|
||||||
setDraft: vi.fn(),
|
setDraft: vi.fn(),
|
||||||
|
unassignedOpen: true,
|
||||||
|
setUnassignedOpen: vi.fn(),
|
||||||
unschedOpen: true,
|
unschedOpen: true,
|
||||||
setUnschedOpen: vi.fn(),
|
setUnschedOpen: vi.fn(),
|
||||||
selectedId: null,
|
selectedId: null,
|
||||||
|
|
@ -95,28 +99,43 @@ function baseTableData(
|
||||||
} as ReturnTypeOfUseWorkOrdersTableData;
|
} as ReturnTypeOfUseWorkOrdersTableData;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("WoTableBody Unscheduled vs day groups", () => {
|
describe("WoTableBody Unassigned queue (SH-165)", () => {
|
||||||
it("renders Unscheduled above day groups and keeps dated unassigned under the day header", () => {
|
it("renders Unassigned above Unscheduled and day groups without duplicate rows", () => {
|
||||||
const datedUnassigned = makeRow({ id: "u-dated", woNumber: "24991" });
|
const datedUnassigned = makeRow({ id: "u-dated", woNumber: "24991" });
|
||||||
const undated = makeRow({
|
const undatedUnassigned = makeRow({
|
||||||
id: "us1",
|
id: "us1",
|
||||||
woNumber: "25021",
|
woNumber: "25021",
|
||||||
scheduledOn: "",
|
scheduledOn: "",
|
||||||
dayGroup: "",
|
dayGroup: "",
|
||||||
dayLabel: "",
|
dayLabel: "",
|
||||||
});
|
});
|
||||||
|
const undatedAssigned = makeRow({
|
||||||
|
id: "us-assigned",
|
||||||
|
woNumber: "25022",
|
||||||
|
dispatcherId: "sarah",
|
||||||
|
dispatcherName: "Sarah",
|
||||||
|
scheduledOn: "",
|
||||||
|
dayGroup: "",
|
||||||
|
dayLabel: "",
|
||||||
|
});
|
||||||
|
const datedAssigned = makeRow({
|
||||||
|
id: "day-assigned",
|
||||||
|
woNumber: "25023",
|
||||||
|
dispatcherId: "sarah",
|
||||||
|
dispatcherName: "Sarah",
|
||||||
|
});
|
||||||
|
|
||||||
const tableData = baseTableData({
|
const tableData = baseTableData({
|
||||||
unscheduled: [undated],
|
unassigned: [datedUnassigned, undatedUnassigned],
|
||||||
|
unscheduled: [undatedAssigned],
|
||||||
displayGroups: [
|
displayGroups: [
|
||||||
{
|
{
|
||||||
key: "06/02",
|
key: "06/02",
|
||||||
label: "TUE · 06/02",
|
label: "TUE · 06/02",
|
||||||
date: "2026-06-02",
|
date: "2026-06-02",
|
||||||
items: [datedUnassigned],
|
items: [datedAssigned],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
unschedOpen: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
render(
|
render(
|
||||||
|
|
@ -133,21 +152,54 @@ describe("WoTableBody Unscheduled vs day groups", () => {
|
||||||
</table>,
|
</table>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByRole("button", { name: "Unscheduled" })).toBeInTheDocument();
|
const unassignedHeader = screen.getByRole("button", {
|
||||||
expect(screen.getByText("TUE · 06/02")).toBeInTheDocument();
|
name: "Unassigned, 2 work orders, expanded",
|
||||||
|
});
|
||||||
const unscheduledHeader = screen.getByRole("button", { name: "Unscheduled" });
|
const unscheduledHeader = screen.getByRole("button", {
|
||||||
|
name: "Unscheduled, 1 work order, expanded",
|
||||||
|
});
|
||||||
const dayLabel = screen.getByText("TUE · 06/02");
|
const dayLabel = screen.getByText("TUE · 06/02");
|
||||||
const undatedRow = screen.getByTestId("row-us1");
|
|
||||||
const datedRow = screen.getByTestId("row-u-dated");
|
|
||||||
|
|
||||||
expect(
|
expect(unassignedHeader.compareDocumentPosition(screen.getByTestId("row-u-dated"))).toBe(
|
||||||
unscheduledHeader.compareDocumentPosition(undatedRow) & Node.DOCUMENT_POSITION_FOLLOWING,
|
Node.DOCUMENT_POSITION_FOLLOWING,
|
||||||
).toBeTruthy();
|
);
|
||||||
expect(
|
expect(unassignedHeader.compareDocumentPosition(unscheduledHeader)).toBe(
|
||||||
dayLabel.compareDocumentPosition(datedRow) & Node.DOCUMENT_POSITION_FOLLOWING,
|
Node.DOCUMENT_POSITION_FOLLOWING,
|
||||||
).toBeTruthy();
|
);
|
||||||
expect(screen.queryByTestId("row-u-dated")).toBeInTheDocument();
|
expect(unscheduledHeader.compareDocumentPosition(dayLabel)).toBe(
|
||||||
expect(screen.getAllByTestId(/row-/)).toHaveLength(2);
|
Node.DOCUMENT_POSITION_FOLLOWING,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByTestId("row-u-dated")).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("row-us1")).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("row-us-assigned")).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("row-day-assigned")).toBeInTheDocument();
|
||||||
|
expect(screen.getAllByTestId(/row-/)).toHaveLength(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("collapses and expands the Unassigned section via the disclosure control", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const setUnassignedOpen = vi.fn();
|
||||||
|
const tableData = baseTableData({
|
||||||
|
unassigned: [makeRow({ id: "u1", woNumber: "24991" })],
|
||||||
|
setUnassignedOpen,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<table>
|
||||||
|
<WoTableBody
|
||||||
|
tableData={tableData}
|
||||||
|
renderRow={(row) => (
|
||||||
|
<tr key={String(row.id)} data-testid={`row-${row.id}`}>
|
||||||
|
<td>{row.woNumber}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
renderInlineRow={() => null}
|
||||||
|
/>
|
||||||
|
</table>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Unassigned, 1 work order, expanded" }));
|
||||||
|
expect(setUnassignedOpen).toHaveBeenCalledWith(expect.any(Function));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -1,165 +0,0 @@
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import type { WorkOrderBarFilters } from "@/domain/work-orders/types/work-order-list-filters";
|
|
||||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
|
||||||
import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils";
|
|
||||||
import {
|
|
||||||
filterByBarAndSearch,
|
|
||||||
splitScheduledUnscheduled,
|
|
||||||
} from "@/domain/work-orders/utils/table-filter-utils";
|
|
||||||
import { weekRange } from "@/domain/work-orders/utils/list-week-utils";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Design oracle — mirrors shoc-desing/src/pages/WorkOrders.tsx:
|
|
||||||
* - weekScoped (~L9734): !scheduledOn || (scheduledOn >= wStart && scheduledOn <= wEnd)
|
|
||||||
* - split (~L9762): unscheduled = !scheduledOn; dated-but-unassigned stay in day groups
|
|
||||||
*/
|
|
||||||
|
|
||||||
type DesignRow = Pick<WorkOrderTableRow, "id" | "scheduledOn" | "dispatcherId" | "targetWeek">;
|
|
||||||
|
|
||||||
function designWeekScoped(rows: DesignRow[], weekStart: string, weekEnd: string): DesignRow[] {
|
|
||||||
return rows.filter(
|
|
||||||
(row) => !row.scheduledOn || (row.scheduledOn >= weekStart && row.scheduledOn <= weekEnd),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function designSplit(rows: DesignRow[]): {
|
|
||||||
scheduled: DesignRow[];
|
|
||||||
unscheduled: DesignRow[];
|
|
||||||
} {
|
|
||||||
const scheduled: DesignRow[] = [];
|
|
||||||
const unscheduled: DesignRow[] = [];
|
|
||||||
for (const row of rows) {
|
|
||||||
if (!row.scheduledOn) {
|
|
||||||
unscheduled.push(row);
|
|
||||||
} else {
|
|
||||||
scheduled.push(row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { scheduled, unscheduled };
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeRow(overrides: Partial<WorkOrderTableRow> = {}): WorkOrderTableRow {
|
|
||||||
return {
|
|
||||||
id: "1",
|
|
||||||
woNumber: "24991",
|
|
||||||
site: "CLV2",
|
|
||||||
type: "PM",
|
|
||||||
dispatcherId: "u1",
|
|
||||||
dispatcherName: "Alice",
|
|
||||||
scheduledOn: "2026-06-02",
|
|
||||||
targetWeek: "",
|
|
||||||
dayGroup: "06/02",
|
|
||||||
dayLabel: "TUE · 06/02",
|
|
||||||
location: "Cleveland, OH",
|
|
||||||
locationId: "",
|
|
||||||
pm: "Backflow",
|
|
||||||
serviceNotes: "",
|
|
||||||
extraServices: [],
|
|
||||||
company: "",
|
|
||||||
tech: "",
|
|
||||||
techPhone: "",
|
|
||||||
apptStart: "",
|
|
||||||
apptEnd: "",
|
|
||||||
apptTime: "—",
|
|
||||||
status: "Incomplete",
|
|
||||||
docStatus: "No",
|
|
||||||
pocName: "",
|
|
||||||
pocPhone: "",
|
|
||||||
pocNotes: "",
|
|
||||||
dueDate: "",
|
|
||||||
rescheduleCount: 0,
|
|
||||||
carriedOver: 0,
|
|
||||||
originalWeek: "",
|
|
||||||
originalDate: "",
|
|
||||||
isPastDue: false,
|
|
||||||
scheduleWeekOnly: false,
|
|
||||||
mediaCount: 0,
|
|
||||||
title: "",
|
|
||||||
priority: "",
|
|
||||||
vendorId: "",
|
|
||||||
primaryDispatchId: null,
|
|
||||||
rowVersion: "",
|
|
||||||
dispatchRowVersion: "",
|
|
||||||
flagColor: null,
|
|
||||||
...overrides,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function idsOf(rows: { id: string | number }[]): string[] {
|
|
||||||
return rows.map((row) => String(row.id)).sort();
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("board Unscheduled design parity", () => {
|
|
||||||
const weekMonday = "2026-06-01";
|
|
||||||
const [weekStart, weekEnd] = weekRange(weekMonday);
|
|
||||||
|
|
||||||
const fixtures: WorkOrderTableRow[] = [
|
|
||||||
makeRow({
|
|
||||||
id: "u-dated",
|
|
||||||
dispatcherId: "",
|
|
||||||
dispatcherName: "",
|
|
||||||
scheduledOn: "2026-06-02",
|
|
||||||
...deriveDayFields("2026-06-02"),
|
|
||||||
}),
|
|
||||||
makeRow({
|
|
||||||
id: "us-undated",
|
|
||||||
dispatcherId: "",
|
|
||||||
dispatcherName: "",
|
|
||||||
scheduledOn: "",
|
|
||||||
dayGroup: "",
|
|
||||||
dayLabel: "",
|
|
||||||
}),
|
|
||||||
makeRow({
|
|
||||||
id: "ua-assigned-undated",
|
|
||||||
dispatcherId: "sarah",
|
|
||||||
dispatcherName: "Sarah",
|
|
||||||
scheduledOn: "",
|
|
||||||
dayGroup: "",
|
|
||||||
dayLabel: "",
|
|
||||||
}),
|
|
||||||
makeRow({
|
|
||||||
id: "tw-other-week",
|
|
||||||
dispatcherId: "sarah",
|
|
||||||
dispatcherName: "Sarah",
|
|
||||||
scheduledOn: "",
|
|
||||||
targetWeek: "2026-07-06",
|
|
||||||
dayGroup: "",
|
|
||||||
dayLabel: "",
|
|
||||||
}),
|
|
||||||
makeRow({
|
|
||||||
id: "dated-other-week",
|
|
||||||
scheduledOn: "2026-06-15",
|
|
||||||
...deriveDayFields("2026-06-15"),
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
const bar: WorkOrderBarFilters = {
|
|
||||||
weekMonday,
|
|
||||||
dispatcherIds: new Set(),
|
|
||||||
type: "All",
|
|
||||||
};
|
|
||||||
|
|
||||||
it("matches design weekScoped ID set via filterByBarAndSearch", () => {
|
|
||||||
const designIds = idsOf(designWeekScoped(fixtures, weekStart, weekEnd));
|
|
||||||
const feIds = idsOf(filterByBarAndSearch(fixtures, bar, ""));
|
|
||||||
expect(feIds).toEqual(designIds);
|
|
||||||
expect(feIds).toContain("tw-other-week");
|
|
||||||
expect(feIds).not.toContain("dated-other-week");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("matches design split: dated unassigned in scheduled, undated in Unscheduled", () => {
|
|
||||||
const scoped = filterByBarAndSearch(fixtures, bar, "");
|
|
||||||
const feSplit = splitScheduledUnscheduled(scoped);
|
|
||||||
const designSplitResult = designSplit(designWeekScoped(fixtures, weekStart, weekEnd));
|
|
||||||
|
|
||||||
expect(idsOf(feSplit.unscheduled)).toEqual(idsOf(designSplitResult.unscheduled));
|
|
||||||
expect(idsOf(feSplit.scheduled)).toEqual(idsOf(designSplitResult.scheduled));
|
|
||||||
|
|
||||||
expect(idsOf(feSplit.scheduled)).toContain("u-dated");
|
|
||||||
expect(idsOf(feSplit.unscheduled)).not.toContain("u-dated");
|
|
||||||
expect(idsOf(feSplit.unscheduled)).toContain("tw-other-week");
|
|
||||||
expect(idsOf(feSplit.unscheduled)).toEqual(
|
|
||||||
expect.arrayContaining(["us-undated", "ua-assigned-undated", "tw-other-week"]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -5,7 +5,9 @@ import type { WorkOrderBarFilters } from "@/domain/work-orders/types/work-order-
|
||||||
import {
|
import {
|
||||||
buildDayGroups,
|
buildDayGroups,
|
||||||
filterByBarAndSearch,
|
filterByBarAndSearch,
|
||||||
|
isUnassignedBoardRow,
|
||||||
matchesAdvancedFilters,
|
matchesAdvancedFilters,
|
||||||
|
partitionBoardRows,
|
||||||
splitScheduledUnscheduled,
|
splitScheduledUnscheduled,
|
||||||
} from "@/domain/work-orders/utils/table-filter-utils";
|
} from "@/domain/work-orders/utils/table-filter-utils";
|
||||||
import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils";
|
import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils";
|
||||||
|
|
@ -83,18 +85,24 @@ describe("deriveDayFields", () => {
|
||||||
describe("filterByBarAndSearch", () => {
|
describe("filterByBarAndSearch", () => {
|
||||||
const weekMonday = "2026-06-01";
|
const weekMonday = "2026-06-01";
|
||||||
|
|
||||||
it("keeps undated rows for any week, including targetWeek outside the active week", () => {
|
it("keeps undated rows without targetWeek for any active week", () => {
|
||||||
const undated = makeRow({ id: "us1", scheduledOn: "", targetWeek: "" });
|
const undated = makeRow({ id: "us1", scheduledOn: "", targetWeek: "" });
|
||||||
|
|
||||||
|
const ids = filterByBarAndSearch([undated], bar(weekMonday), "").map((row) => row.id);
|
||||||
|
expect(ids).toEqual(["us1"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("excludes undated rows whose targetWeek is outside the active week", () => {
|
||||||
const undatedOtherTarget = makeRow({
|
const undatedOtherTarget = makeRow({
|
||||||
id: "tw1",
|
id: "tw1",
|
||||||
scheduledOn: "",
|
scheduledOn: "",
|
||||||
targetWeek: "2026-07-06",
|
targetWeek: "2026-07-06",
|
||||||
});
|
});
|
||||||
|
|
||||||
const ids = filterByBarAndSearch([undated, undatedOtherTarget], bar(weekMonday), "").map(
|
const ids = filterByBarAndSearch([undatedOtherTarget], bar(weekMonday), "").map(
|
||||||
(row) => row.id,
|
(row) => row.id,
|
||||||
);
|
);
|
||||||
expect(ids).toEqual(["us1", "tw1"]);
|
expect(ids).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("excludes dated rows outside the active week", () => {
|
it("excludes dated rows outside the active week", () => {
|
||||||
|
|
@ -114,6 +122,59 @@ describe("filterByBarAndSearch", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("isUnassignedBoardRow", () => {
|
||||||
|
it("treats empty dispatcher as unassigned unless terminal", () => {
|
||||||
|
expect(isUnassignedBoardRow(makeRow({ dispatcherId: "" }))).toBe(true);
|
||||||
|
expect(isUnassignedBoardRow(makeRow({ dispatcherId: "u1" }))).toBe(false);
|
||||||
|
expect(isUnassignedBoardRow(makeRow({ dispatcherId: "", status: "Completed" }))).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("partitionBoardRows", () => {
|
||||||
|
it("pins unassigned rows first and keeps assigned undated rows in Unscheduled", () => {
|
||||||
|
const datedUnassigned = makeRow({
|
||||||
|
id: "u-dated",
|
||||||
|
dispatcherId: "",
|
||||||
|
dispatcherName: "",
|
||||||
|
scheduledOn: "2026-06-02",
|
||||||
|
...deriveDayFields("2026-06-02"),
|
||||||
|
});
|
||||||
|
const undatedUnassigned = makeRow({
|
||||||
|
id: "us-undated",
|
||||||
|
dispatcherId: "",
|
||||||
|
dispatcherName: "",
|
||||||
|
scheduledOn: "",
|
||||||
|
dayGroup: "",
|
||||||
|
dayLabel: "",
|
||||||
|
});
|
||||||
|
const undatedAssigned = makeRow({
|
||||||
|
id: "us-assigned",
|
||||||
|
dispatcherId: "sarah",
|
||||||
|
dispatcherName: "Sarah",
|
||||||
|
scheduledOn: "",
|
||||||
|
dayGroup: "",
|
||||||
|
dayLabel: "",
|
||||||
|
});
|
||||||
|
const datedAssigned = makeRow({ id: "day-assigned", dispatcherId: "sarah" });
|
||||||
|
|
||||||
|
const result = partitionBoardRows([
|
||||||
|
datedUnassigned,
|
||||||
|
undatedUnassigned,
|
||||||
|
undatedAssigned,
|
||||||
|
datedAssigned,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result.unassigned.map((row) => row.id)).toEqual(["u-dated", "us-undated"]);
|
||||||
|
expect(result.unscheduled.map((row) => row.id)).toEqual(["us-assigned"]);
|
||||||
|
expect(result.scheduled.map((row) => row.id)).toEqual(["day-assigned"]);
|
||||||
|
|
||||||
|
const allIds = [...result.unassigned, ...result.unscheduled, ...result.scheduled].map(
|
||||||
|
(row) => row.id,
|
||||||
|
);
|
||||||
|
expect(new Set(allIds).size).toBe(allIds.length);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("splitScheduledUnscheduled", () => {
|
describe("splitScheduledUnscheduled", () => {
|
||||||
it("splits rows without scheduledOn into unscheduled", () => {
|
it("splits rows without scheduledOn into unscheduled", () => {
|
||||||
const scheduled = makeRow({ id: "a", scheduledOn: "2026-06-23" });
|
const scheduled = makeRow({ id: "a", scheduledOn: "2026-06-23" });
|
||||||
|
|
@ -123,23 +184,6 @@ describe("splitScheduledUnscheduled", () => {
|
||||||
expect(result.unscheduled).toHaveLength(1);
|
expect(result.unscheduled).toHaveLength(1);
|
||||||
expect(result.unscheduled[0].id).toBe("b");
|
expect(result.unscheduled[0].id).toBe("b");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps dated unassigned in scheduled and undated in unscheduled without duplicate ids", () => {
|
|
||||||
const datedUnassigned = makeRow({
|
|
||||||
id: "u-dated",
|
|
||||||
dispatcherId: "",
|
|
||||||
dispatcherName: "",
|
|
||||||
scheduledOn: "2026-06-02",
|
|
||||||
...deriveDayFields("2026-06-02"),
|
|
||||||
});
|
|
||||||
const undated = makeRow({ id: "us-undated", scheduledOn: "", dispatcherId: "" });
|
|
||||||
const result = splitScheduledUnscheduled([datedUnassigned, undated]);
|
|
||||||
|
|
||||||
expect(result.scheduled.map((row) => row.id)).toEqual(["u-dated"]);
|
|
||||||
expect(result.unscheduled.map((row) => row.id)).toEqual(["us-undated"]);
|
|
||||||
const allIds = [...result.scheduled, ...result.unscheduled].map((row) => row.id);
|
|
||||||
expect(new Set(allIds).size).toBe(allIds.length);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("buildDayGroups", () => {
|
describe("buildDayGroups", () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue