mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 08:03:13 +00:00
Merge pull request #169 from Sea-Haven-Industries/feat/SH-310-schedule-on-range
fix(work-orders): restore Range tab on Schedule On for inline and slide-over
This commit is contained in:
commit
a766dc9c50
23 changed files with 702 additions and 101 deletions
|
|
@ -16,6 +16,7 @@ import type {
|
|||
} from "@/domain/work-orders/types/work-order";
|
||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||
import type { WizardWOStatus } from "@/domain/work-orders/types/work-order-wizard";
|
||||
import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils";
|
||||
import { toVendorTablePatch } from "@/domain/work-orders/utils/vendor-assignment-patch";
|
||||
import { DocBadge } from "./slide-over-doc-badge";
|
||||
|
||||
|
|
@ -31,6 +32,58 @@ type SlideOverInfoTabEditViewProps = {
|
|||
onRequestComplete?: () => void;
|
||||
};
|
||||
|
||||
function SlideOverScheduleOnField({
|
||||
draft,
|
||||
onDraftChange,
|
||||
}: {
|
||||
draft: WorkOrderTableRow;
|
||||
onDraftChange: (patch: Partial<WorkOrderTableRow>) => void;
|
||||
}) {
|
||||
return (
|
||||
<DateCell
|
||||
value={draft.scheduledOn}
|
||||
allowWeekMode
|
||||
targetWeek={draft.targetWeek}
|
||||
defaultMode={draft.scheduleWeekOnly ? "week" : "date"}
|
||||
scheduleEnd={draft.scheduleEnd}
|
||||
schedMode={draft.schedMode}
|
||||
onSave={(scheduledOn) =>
|
||||
onDraftChange({
|
||||
scheduledOn,
|
||||
scheduleWeekOnly: false,
|
||||
targetWeek: scheduledOn ? "" : draft.targetWeek,
|
||||
scheduleEnd: "",
|
||||
schedMode: "date",
|
||||
isPastDue: false,
|
||||
...deriveDayFields(scheduledOn),
|
||||
})
|
||||
}
|
||||
onSaveWeek={(weekStart) =>
|
||||
onDraftChange({
|
||||
scheduledOn: "",
|
||||
targetWeek: weekStart,
|
||||
scheduleWeekOnly: true,
|
||||
scheduleEnd: "",
|
||||
schedMode: "week",
|
||||
isPastDue: false,
|
||||
...deriveDayFields(""),
|
||||
})
|
||||
}
|
||||
onSaveRange={(start, end) =>
|
||||
onDraftChange({
|
||||
scheduledOn: start,
|
||||
scheduleEnd: end,
|
||||
schedMode: "range",
|
||||
scheduleWeekOnly: false,
|
||||
targetWeek: "",
|
||||
isPastDue: false,
|
||||
...deriveDayFields(start),
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SlideOverInfoTabEditView({
|
||||
draft,
|
||||
users,
|
||||
|
|
@ -99,23 +152,7 @@ export function SlideOverInfoTabEditView({
|
|||
/>
|
||||
</SlideOverInfoRow>
|
||||
<SlideOverInfoRow label="Schedule On">
|
||||
<DateCell
|
||||
value={draft.scheduledOn}
|
||||
allowWeekMode
|
||||
targetWeek={draft.targetWeek}
|
||||
defaultMode={draft.scheduleWeekOnly ? "week" : "date"}
|
||||
onSave={(scheduledOn) =>
|
||||
onDraftChange({ scheduledOn, scheduleWeekOnly: false, isPastDue: false })
|
||||
}
|
||||
onSaveWeek={(weekStart) =>
|
||||
onDraftChange({
|
||||
scheduledOn: "",
|
||||
targetWeek: weekStart,
|
||||
scheduleWeekOnly: true,
|
||||
isPastDue: false,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<SlideOverScheduleOnField draft={draft} onDraftChange={onDraftChange} />
|
||||
</SlideOverInfoRow>
|
||||
<SlideOverInfoRow label="Due Date">
|
||||
<DueDateCell
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ export function buildInlineDraftCreateInput(
|
|||
additionalContacts: draft.additionalContacts ?? [],
|
||||
scheduleWeekOnly,
|
||||
targetWeek: draft.targetWeek || "",
|
||||
scheduleEnd: draft.scheduleEnd || "",
|
||||
schedMode: draft.schedMode,
|
||||
isAddOnOverride: draft.isAddOnOverride,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
import { CalendarDays, Calendar as CalendarIcon, CalendarRange } from "lucide-react";
|
||||
import { fmtDateRange, fmtWeekOf } from "@/domain/work-orders/utils/wizard-date-utils";
|
||||
import { HighlightText } from "./highlight-text";
|
||||
|
||||
type DateCellValueArgs = {
|
||||
showRange: boolean;
|
||||
showWeek: boolean;
|
||||
value: string;
|
||||
scheduleEnd: string;
|
||||
targetWeek: string;
|
||||
q?: string;
|
||||
highlight?: boolean;
|
||||
};
|
||||
|
||||
export function DateCellValue({
|
||||
showRange,
|
||||
showWeek,
|
||||
value,
|
||||
scheduleEnd,
|
||||
targetWeek,
|
||||
q,
|
||||
highlight,
|
||||
}: DateCellValueArgs) {
|
||||
if (showRange) {
|
||||
return (
|
||||
<>
|
||||
<CalendarRange size={11} className="shrink-0 text-[var(--color-text-muted)]" />
|
||||
<span className="truncate text-[13px] text-[var(--color-text-secondary)]">
|
||||
{fmtDateRange(value, scheduleEnd)}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (showWeek) {
|
||||
return (
|
||||
<>
|
||||
<CalendarDays size={10} className="shrink-0 text-[var(--color-text-muted)]" />
|
||||
<span className="truncate text-[11px] font-medium text-[var(--color-text-muted)]">
|
||||
{fmtWeekOf(targetWeek)}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<CalendarIcon
|
||||
size={11}
|
||||
className="shrink-0"
|
||||
style={{ color: highlight ? "var(--primary)" : "var(--color-text-muted)" }}
|
||||
/>
|
||||
{value ? (
|
||||
<span className="truncate text-[13px] text-[var(--color-text-secondary)]">
|
||||
<HighlightText text={value.replace(/^\d{4}-/, "")} q={q} />
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-[13px] text-[var(--color-text-muted)]">—</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
import { useState } from "react";
|
||||
import { CalendarDays, Calendar as CalendarIcon, ChevronDown } from "lucide-react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { Popover } from "@mui/material";
|
||||
import { DateCellValue } from "@/app/(protected)/workorders/_components/list/table/cells/date-cell-value";
|
||||
import { MiniCalendar } from "@/app/(protected)/workorders/_components/wizard/mini-calendar";
|
||||
import { WeekPicker } from "@/app/(protected)/workorders/_components/wizard/week-picker";
|
||||
import { fmtWeekOf } from "@/domain/work-orders/utils/wizard-date-utils";
|
||||
import { HighlightText } from "./highlight-text";
|
||||
import { useWizardDateFieldState } from "@/app/(protected)/workorders/_components/wizard/use-wizard-date-field-state";
|
||||
import { WizardDateModeTabs } from "@/app/(protected)/workorders/_components/wizard/wizard-date-mode-tabs";
|
||||
import { WizardDateRangePanel } from "@/app/(protected)/workorders/_components/wizard/wizard-date-range-panel";
|
||||
import type { WizardSchedMode } from "@/domain/work-orders/types/work-order-wizard";
|
||||
|
||||
type DateCellProps = {
|
||||
value: string;
|
||||
|
|
@ -15,6 +18,9 @@ type DateCellProps = {
|
|||
targetWeek?: string;
|
||||
onSaveWeek?: (weekStart: string) => void;
|
||||
defaultMode?: "week" | "date";
|
||||
scheduleEnd?: string;
|
||||
schedMode?: WizardSchedMode;
|
||||
onSaveRange?: (start: string, end: string) => void;
|
||||
/** When set, shows Clear in the popover so schedule can be removed (e.g. day-group → Unscheduled). */
|
||||
onClear?: () => void;
|
||||
/** Screen-reader-only status appended inside the button so the accessible name keeps the visible date and adds durable state. */
|
||||
|
|
@ -30,14 +36,34 @@ export function DateCell({
|
|||
targetWeek = "",
|
||||
onSaveWeek,
|
||||
defaultMode,
|
||||
scheduleEnd = "",
|
||||
schedMode,
|
||||
onSaveRange,
|
||||
onClear,
|
||||
srStatusSuffix,
|
||||
}: DateCellProps) {
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||
const [cellMode, setCellMode] = useState<"week" | "date">(
|
||||
allowWeekMode ? (defaultMode ?? (!value && targetWeek ? "week" : "date")) : "date",
|
||||
const open = Boolean(anchorEl);
|
||||
const allowRange = Boolean(allowWeekMode && onSaveRange);
|
||||
const { cellMode, setCellMode, rangeStart, rangeEnd, handleRangeClick } = useWizardDateFieldState(
|
||||
{
|
||||
open,
|
||||
value,
|
||||
scheduleEnd,
|
||||
allowRange,
|
||||
allowWeekMode,
|
||||
targetWeek,
|
||||
schedMode,
|
||||
defaultMode,
|
||||
},
|
||||
);
|
||||
const showWeek = allowWeekMode && !value && !!targetWeek;
|
||||
const showWeek = Boolean(allowWeekMode && !value && targetWeek);
|
||||
const showRange = Boolean(allowRange && schedMode === "range" && value && scheduleEnd);
|
||||
const modes: WizardSchedMode[] = allowRange
|
||||
? ["week", "date", "range"]
|
||||
: allowWeekMode
|
||||
? ["week", "date"]
|
||||
: [];
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -60,29 +86,15 @@ export function DateCell({
|
|||
}
|
||||
>
|
||||
<span className="flex min-w-0 flex-1 items-center gap-1.5 overflow-hidden">
|
||||
{showWeek ? (
|
||||
<>
|
||||
<CalendarDays size={10} className="shrink-0 text-[var(--color-text-muted)]" />
|
||||
<span className="truncate text-[11px] font-medium text-[var(--color-text-muted)]">
|
||||
{fmtWeekOf(targetWeek)}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CalendarIcon
|
||||
size={11}
|
||||
className="shrink-0"
|
||||
style={{ color: highlight ? "var(--primary)" : "var(--color-text-muted)" }}
|
||||
/>
|
||||
{value ? (
|
||||
<span className="truncate text-[13px] text-[var(--color-text-secondary)]">
|
||||
<HighlightText text={value.replace(/^\d{4}-/, "")} q={q} />
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-[13px] text-[var(--color-text-muted)]">—</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<DateCellValue
|
||||
showRange={showRange}
|
||||
showWeek={showWeek}
|
||||
value={value}
|
||||
scheduleEnd={scheduleEnd}
|
||||
targetWeek={targetWeek}
|
||||
q={q}
|
||||
highlight={highlight}
|
||||
/>
|
||||
</span>
|
||||
{Boolean(srStatusSuffix) && <span className="sr-only">{srStatusSuffix}</span>}
|
||||
<ChevronDown
|
||||
|
|
@ -92,36 +104,19 @@ export function DateCell({
|
|||
/>
|
||||
</button>
|
||||
<Popover
|
||||
open={Boolean(anchorEl)}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
slotProps={{ paper: { className: "rounded-lg border border-border p-3" } }}
|
||||
>
|
||||
{Boolean(allowWeekMode) && (
|
||||
<div className="mb-2.5 flex items-center gap-1.5">
|
||||
{(["week", "date"] as const).map((m) => {
|
||||
const active = cellMode === m;
|
||||
return (
|
||||
<button
|
||||
key={m}
|
||||
type="button"
|
||||
onClick={() => setCellMode(m)}
|
||||
className="rounded-full border px-3 text-[11.5px] font-medium"
|
||||
style={{
|
||||
height: 26,
|
||||
background: active ? "var(--color-primary-tint)" : "var(--color-bg-surface)",
|
||||
borderColor: active ? "var(--color-primary)" : "var(--color-border)",
|
||||
color: active ? "var(--color-primary)" : "var(--color-text-secondary)",
|
||||
}}
|
||||
>
|
||||
{m === "week" ? "Week" : "Specific date"}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<WizardDateModeTabs
|
||||
modes={modes}
|
||||
cellMode={cellMode}
|
||||
allowRange={allowRange}
|
||||
onSelect={setCellMode}
|
||||
/>
|
||||
{allowWeekMode && cellMode === "week" ? (
|
||||
<WeekPicker
|
||||
selected={!value ? targetWeek : ""}
|
||||
|
|
@ -130,6 +125,16 @@ export function DateCell({
|
|||
setAnchorEl(null);
|
||||
}}
|
||||
/>
|
||||
) : allowRange && cellMode === "range" ? (
|
||||
<WizardDateRangePanel
|
||||
rangeStart={rangeStart}
|
||||
rangeEnd={rangeEnd}
|
||||
onRangeClick={handleRangeClick}
|
||||
onDone={(start, end) => {
|
||||
onSaveRange?.(start, end);
|
||||
setAnchorEl(null);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<MiniCalendar
|
||||
value={value}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ export function toInlineDraftRow(draft: WorkOrderTableDraft) {
|
|||
dispatcherName: "",
|
||||
scheduledOn: draft.scheduledOn,
|
||||
scheduleWeekOnly: draft.scheduleWeekOnly,
|
||||
scheduleEnd: draft.scheduleEnd,
|
||||
schedMode: draft.schedMode,
|
||||
vendorId: draft.vendorId,
|
||||
completedDate: "",
|
||||
dueDate: draft.dueDate,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useMemo } from "react";
|
|||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||
import {
|
||||
applySchedulePatch,
|
||||
applyScheduleRangePatch,
|
||||
type WorkOrderTablePatchFn,
|
||||
} from "@/domain/work-orders/use-cases/use-work-order-table-mutations";
|
||||
import { shouldNotifyCrossWeek } from "@/domain/work-orders/utils/should-notify-cross-week";
|
||||
|
|
@ -9,6 +10,27 @@ import type { ReturnTypeOfUseWorkOrdersTableData } from "./wo-table-types";
|
|||
import { rescheduleNotify, weekMoveNotify } from "./reschedule-notify";
|
||||
import type { WoTableRowHandlers } from "./wo-table-row";
|
||||
|
||||
function revealOrNotifyScheduledDay(args: {
|
||||
id: string | number;
|
||||
date: string;
|
||||
weekMonday: string;
|
||||
onNavigateToDay: (dateIso: string) => void;
|
||||
revealScheduledRow: (id: string | number) => void;
|
||||
}) {
|
||||
const { id, date, weekMonday, onNavigateToDay, revealScheduledRow } = args;
|
||||
if (shouldNotifyCrossWeek(date, weekMonday)) {
|
||||
rescheduleNotify(date, {
|
||||
onNavigate: () => {
|
||||
onNavigateToDay(date);
|
||||
revealScheduledRow(id);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
onNavigateToDay(date);
|
||||
revealScheduledRow(id);
|
||||
}
|
||||
|
||||
type UseWoTableRowHandlersOptions = {
|
||||
tableData: ReturnTypeOfUseWorkOrdersTableData;
|
||||
weekMonday: string;
|
||||
|
|
@ -81,23 +103,31 @@ export function useWoTableRowHandlers({
|
|||
},
|
||||
onScheduleDate: (id: string | number, date: string) => {
|
||||
onPatch(id, applySchedulePatch(date, false));
|
||||
if (shouldNotifyCrossWeek(date, weekMonday)) {
|
||||
rescheduleNotify(date, {
|
||||
onNavigate: () => {
|
||||
onNavigateToDay(date);
|
||||
revealScheduledRow(id);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
onNavigateToDay(date);
|
||||
revealScheduledRow(id);
|
||||
revealOrNotifyScheduledDay({
|
||||
id,
|
||||
date,
|
||||
weekMonday,
|
||||
onNavigateToDay,
|
||||
revealScheduledRow,
|
||||
});
|
||||
},
|
||||
onScheduleRange: (id: string | number, start: string, end: string) => {
|
||||
onPatch(id, applyScheduleRangePatch(start, end));
|
||||
revealOrNotifyScheduledDay({
|
||||
id,
|
||||
date: start,
|
||||
weekMonday,
|
||||
onNavigateToDay,
|
||||
revealScheduledRow,
|
||||
});
|
||||
},
|
||||
onScheduleWeek: (id: string | number, week: string) => {
|
||||
onPatch(id, {
|
||||
scheduledOn: "",
|
||||
targetWeek: week,
|
||||
scheduleWeekOnly: true,
|
||||
scheduleEnd: "",
|
||||
schedMode: "week",
|
||||
dayGroup: "",
|
||||
dayLabel: "",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -78,7 +78,8 @@ function InlineScheduleOnCell({
|
|||
weekDefault: boolean;
|
||||
onPatch: (patch: Partial<WorkOrderTableDraft>) => void;
|
||||
}) {
|
||||
const hasSchedule = Boolean(draft.scheduledOn) || Boolean(draft.targetWeek);
|
||||
const hasSchedule =
|
||||
Boolean(draft.scheduledOn) || Boolean(draft.targetWeek) || Boolean(draft.scheduleEnd);
|
||||
return (
|
||||
<td className="group/cell" style={{ padding: "0 14px" }}>
|
||||
<DateCell
|
||||
|
|
@ -87,12 +88,16 @@ function InlineScheduleOnCell({
|
|||
targetWeek={draft.targetWeek}
|
||||
defaultMode={weekDefault ? "week" : "date"}
|
||||
highlight={!draft.scheduledOn && !draft.targetWeek}
|
||||
scheduleEnd={draft.scheduleEnd}
|
||||
schedMode={draft.schedMode}
|
||||
onSave={(date) =>
|
||||
onPatch(
|
||||
withAutoScheduleStatus(draft, {
|
||||
scheduledOn: date,
|
||||
scheduleWeekOnly: false,
|
||||
targetWeek: "",
|
||||
scheduleEnd: "",
|
||||
schedMode: "date",
|
||||
isAddOnOverride: false,
|
||||
}),
|
||||
)
|
||||
|
|
@ -103,6 +108,20 @@ function InlineScheduleOnCell({
|
|||
scheduledOn: "",
|
||||
targetWeek: week,
|
||||
scheduleWeekOnly: true,
|
||||
scheduleEnd: "",
|
||||
schedMode: "week",
|
||||
isAddOnOverride: false,
|
||||
}),
|
||||
)
|
||||
}
|
||||
onSaveRange={(start, end) =>
|
||||
onPatch(
|
||||
withAutoScheduleStatus(draft, {
|
||||
scheduledOn: start,
|
||||
scheduleEnd: end,
|
||||
schedMode: "range",
|
||||
scheduleWeekOnly: false,
|
||||
targetWeek: "",
|
||||
isAddOnOverride: false,
|
||||
}),
|
||||
)
|
||||
|
|
@ -115,6 +134,8 @@ function InlineScheduleOnCell({
|
|||
scheduledOn: "",
|
||||
targetWeek: "",
|
||||
scheduleWeekOnly: false,
|
||||
scheduleEnd: "",
|
||||
schedMode: undefined,
|
||||
isAddOnOverride: false,
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -129,8 +129,11 @@ export function WoTableRowCells({
|
|||
highlight={forceScheduleHighlight}
|
||||
allowWeekMode
|
||||
targetWeek={row.targetWeek}
|
||||
scheduleEnd={row.scheduleEnd}
|
||||
schedMode={row.schedMode}
|
||||
onSave={(date) => handlers.onScheduleDate(row.id, date)}
|
||||
onSaveWeek={(week) => handlers.onScheduleWeek(row.id, week)}
|
||||
onSaveRange={(start, end) => handlers.onScheduleRange(row.id, start, end)}
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export type WoTableRowHandlers = {
|
|||
) => void;
|
||||
onScheduleDate: (id: string | number, date: string) => void;
|
||||
onScheduleWeek: (id: string | number, week: string) => void;
|
||||
onScheduleRange: (id: string | number, start: string, end: string) => void;
|
||||
onDuplicateFound?: (existing: WorkOrderTableRow) => void;
|
||||
onOpenSlideOver: (row: WorkOrderTableRow) => void;
|
||||
onRequestComplete?: (row: WorkOrderTableRow) => void;
|
||||
|
|
@ -66,6 +67,31 @@ type WoTableRowProps = {
|
|||
handlers: WoTableRowHandlers;
|
||||
};
|
||||
|
||||
function guardScheduleHandlers(
|
||||
row: WorkOrderTableRow,
|
||||
handlers: WoTableRowHandlers,
|
||||
): WoTableRowHandlers {
|
||||
return {
|
||||
...handlers,
|
||||
onPatch: (id, patch, options) => {
|
||||
if (!canApplyWorkOrderTablePatch(row.status, patch)) return;
|
||||
handlers.onPatch(id, patch, options);
|
||||
},
|
||||
onScheduleDate: (id, date) => {
|
||||
if (!canApplyWorkOrderTablePatch(row.status, { scheduledOn: date })) return;
|
||||
handlers.onScheduleDate(id, date);
|
||||
},
|
||||
onScheduleWeek: (id, week) => {
|
||||
if (!canApplyWorkOrderTablePatch(row.status, { targetWeek: week })) return;
|
||||
handlers.onScheduleWeek(id, week);
|
||||
},
|
||||
onScheduleRange: (id, start, end) => {
|
||||
if (!canApplyWorkOrderTablePatch(row.status, { scheduledOn: start })) return;
|
||||
handlers.onScheduleRange(id, start, end);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function WoTableRow({
|
||||
row,
|
||||
allRows,
|
||||
|
|
@ -88,21 +114,7 @@ export function WoTableRow({
|
|||
const isCoreLocked = isWorkOrderCoreLocked(row.status);
|
||||
const closedForEdit = isCanceled || isCoreLocked;
|
||||
|
||||
const guardedHandlers: WoTableRowHandlers = {
|
||||
...handlers,
|
||||
onPatch: (id, patch, options) => {
|
||||
if (!canApplyWorkOrderTablePatch(row.status, patch)) return;
|
||||
handlers.onPatch(id, patch, options);
|
||||
},
|
||||
onScheduleDate: (id, date) => {
|
||||
if (!canApplyWorkOrderTablePatch(row.status, { scheduledOn: date })) return;
|
||||
handlers.onScheduleDate(id, date);
|
||||
},
|
||||
onScheduleWeek: (id, week) => {
|
||||
if (!canApplyWorkOrderTablePatch(row.status, { targetWeek: week })) return;
|
||||
handlers.onScheduleWeek(id, week);
|
||||
},
|
||||
};
|
||||
const guardedHandlers = guardScheduleHandlers(row, handlers);
|
||||
|
||||
const patchRow = (patch: Partial<WorkOrderTableRow>) => {
|
||||
if (!canApplyWorkOrderTablePatch(row.status, patch)) return;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ type UseWizardDateFieldStateArgs = {
|
|||
allowWeekMode?: boolean;
|
||||
targetWeek: string;
|
||||
schedMode?: WizardSchedMode;
|
||||
defaultMode?: "week" | "date";
|
||||
};
|
||||
|
||||
function resolveInitialMode({
|
||||
|
|
@ -17,9 +18,10 @@ function resolveInitialMode({
|
|||
allowWeekMode,
|
||||
value,
|
||||
targetWeek,
|
||||
defaultMode,
|
||||
}: Pick<
|
||||
UseWizardDateFieldStateArgs,
|
||||
"allowRange" | "schedMode" | "allowWeekMode" | "value" | "targetWeek"
|
||||
"allowRange" | "schedMode" | "allowWeekMode" | "value" | "targetWeek" | "defaultMode"
|
||||
>): WizardSchedMode {
|
||||
if (allowRange && schedMode === "range") {
|
||||
return "range";
|
||||
|
|
@ -27,6 +29,9 @@ function resolveInitialMode({
|
|||
if (allowWeekMode && !value && targetWeek) {
|
||||
return "week";
|
||||
}
|
||||
if (allowWeekMode && defaultMode) {
|
||||
return defaultMode;
|
||||
}
|
||||
return "date";
|
||||
}
|
||||
|
||||
|
|
@ -39,6 +44,7 @@ export function useWizardDateFieldState({
|
|||
allowWeekMode,
|
||||
targetWeek,
|
||||
schedMode,
|
||||
defaultMode,
|
||||
}: UseWizardDateFieldStateArgs) {
|
||||
const initialMode = resolveInitialMode({
|
||||
allowRange,
|
||||
|
|
@ -46,6 +52,7 @@ export function useWizardDateFieldState({
|
|||
allowWeekMode,
|
||||
value,
|
||||
targetWeek,
|
||||
defaultMode,
|
||||
});
|
||||
|
||||
const [cellMode, setCellMode] = useState<WizardSchedMode>(initialMode);
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ const LOCAL_ONLY_KEYS = new Set<keyof WorkOrderTablePatch>([
|
|||
/** Optimistic overlay only — backend owns isAddOn after schedule ops (SH-184). */
|
||||
"isAddOn",
|
||||
"primaryDispatchStatus",
|
||||
/** Client-only Schedule On range overlay — API persists range start as scheduledDate. */
|
||||
"scheduleEnd",
|
||||
"schedMode",
|
||||
]);
|
||||
|
||||
export function isLocalOnlyTablePatch(patch: WorkOrderTablePatch): boolean {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { WorkOrderAdditionalContact } from "./work-order-additional-contact";
|
||||
import type { WizardWOStatus, WOType } from "./work-order-wizard";
|
||||
import type { WizardSchedMode, WizardWOStatus, WOType } from "./work-order-wizard";
|
||||
import type { WorkOrderSeverity } from "./work-order-severity";
|
||||
import type { UpliftSummary } from "./work-order-uplift";
|
||||
|
||||
|
|
@ -65,6 +65,10 @@ export interface WorkOrderTableRow {
|
|||
originalDate: string;
|
||||
isPastDue: boolean;
|
||||
scheduleWeekOnly: boolean;
|
||||
/** Optimistic range end; API persists only `scheduledOn` (range start). */
|
||||
scheduleEnd?: string;
|
||||
/** Last Schedule On mode used in this session. */
|
||||
schedMode?: WizardSchedMode;
|
||||
mediaCount: number;
|
||||
title: string;
|
||||
priority: string;
|
||||
|
|
@ -105,6 +109,8 @@ export interface WorkOrderTableDraft {
|
|||
additionalContacts: WorkOrderAdditionalContact[];
|
||||
scheduleWeekOnly: boolean;
|
||||
targetWeek: string;
|
||||
scheduleEnd?: string;
|
||||
schedMode?: WizardSchedMode;
|
||||
/** Manual Add-On override when schedule is empty at inline create. */
|
||||
isAddOnOverride: boolean;
|
||||
}
|
||||
|
|
@ -137,6 +143,8 @@ export const EMPTY_TABLE_DRAFT: WorkOrderTableDraft = {
|
|||
additionalContacts: [],
|
||||
scheduleWeekOnly: false,
|
||||
targetWeek: "",
|
||||
scheduleEnd: "",
|
||||
schedMode: undefined,
|
||||
isAddOnOverride: false,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -220,6 +220,8 @@ export function applySchedulePatch(
|
|||
scheduledOn: "",
|
||||
targetWeek: scheduledOn,
|
||||
scheduleWeekOnly: true,
|
||||
scheduleEnd: "",
|
||||
schedMode: "week",
|
||||
...deriveDayFields(""),
|
||||
};
|
||||
}
|
||||
|
|
@ -229,6 +231,8 @@ export function applySchedulePatch(
|
|||
scheduledOn: "",
|
||||
scheduleWeekOnly: false,
|
||||
targetWeek: "",
|
||||
scheduleEnd: "",
|
||||
schedMode: undefined,
|
||||
isAddOn: false,
|
||||
...deriveDayFields(""),
|
||||
};
|
||||
|
|
@ -237,6 +241,17 @@ export function applySchedulePatch(
|
|||
scheduledOn: trimmed,
|
||||
scheduleWeekOnly: false,
|
||||
targetWeek: "",
|
||||
scheduleEnd: "",
|
||||
schedMode: "date",
|
||||
...deriveDayFields(trimmed),
|
||||
};
|
||||
}
|
||||
|
||||
/** Range lands on the first day; end/mode stay optimistic (no board API field). */
|
||||
export function applyScheduleRangePatch(start: string, end: string): WorkOrderTablePatch {
|
||||
return {
|
||||
...applySchedulePatch(start, false),
|
||||
scheduleEnd: end,
|
||||
schedMode: "range",
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,6 +93,31 @@ describe("buildInlineDraftCreateInput", () => {
|
|||
expect(payload).not.toHaveProperty("assignTo");
|
||||
});
|
||||
|
||||
it("maps range mode so create persists the start as scheduledDate", () => {
|
||||
const draft = {
|
||||
...EMPTY_TABLE_DRAFT,
|
||||
site: "BK5",
|
||||
locationId: "9",
|
||||
woNumber: "25007",
|
||||
pocName: "Jane",
|
||||
pocPhone: "555-0100",
|
||||
scheduledOn: "2026-06-08",
|
||||
scheduleEnd: "2026-06-12",
|
||||
schedMode: "range" as const,
|
||||
scheduleWeekOnly: false,
|
||||
};
|
||||
|
||||
const input = buildInlineDraftCreateInput(draft, [], [{ id: 9, name: "BK5 · Dallas, TX" }]);
|
||||
const payload = mapWizardDraftToBoardCreatePayload(input);
|
||||
|
||||
expect(input.schedMode).toBe("range");
|
||||
expect(input.scheduleEnd).toBe("2026-06-12");
|
||||
expect(payload.scheduledDate).toBe("2026-06-08");
|
||||
expect(payload.scheduleWeekOnly).toBeUndefined();
|
||||
expect(payload.targetWeek).toBeUndefined();
|
||||
expect(payload).not.toHaveProperty("scheduleEnd");
|
||||
});
|
||||
|
||||
it("promotes Pending to Scheduled when create has a concrete date", () => {
|
||||
const draft = {
|
||||
...EMPTY_TABLE_DRAFT,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
import { fireEvent, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { DateCell } from "@/app/(protected)/workorders/_components/list/table/cells/date-cell";
|
||||
import { DueDateCell } from "@/app/(protected)/workorders/_components/list/table/cells/due-date-cell";
|
||||
import { pad2, todayIso } from "@/domain/work-orders/utils/wizard-date-utils";
|
||||
import { renderWithProviders } from "@/test/test-utils";
|
||||
|
||||
function currentMonthIso(day: number): string {
|
||||
const [y, m] = todayIso().split("-").map(Number);
|
||||
return `${y}-${pad2(m)}-${pad2(day)}`;
|
||||
}
|
||||
|
||||
async function openRangeTab(triggerName: RegExp | string) {
|
||||
fireEvent.click(screen.getByRole("button", { name: triggerName }));
|
||||
const rangeTab = await screen.findByRole("button", { name: "Range" });
|
||||
fireEvent.click(rangeTab);
|
||||
}
|
||||
|
||||
describe("DateCell — Schedule On range", () => {
|
||||
it("shows Week, Date, and Range when onSaveRange is provided", async () => {
|
||||
renderWithProviders(
|
||||
<DateCell
|
||||
value=""
|
||||
allowWeekMode
|
||||
onSave={vi.fn()}
|
||||
onSaveWeek={vi.fn()}
|
||||
onSaveRange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "—" }));
|
||||
|
||||
expect(await screen.findByRole("button", { name: "Week" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Date" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Range" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Specific date" })).not.toBeInTheDocument();
|
||||
}, 10_000);
|
||||
|
||||
it("keeps Week / Specific date and hides Range without onSaveRange", async () => {
|
||||
renderWithProviders(<DateCell value="" allowWeekMode onSave={vi.fn()} onSaveWeek={vi.fn()} />);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "—" }));
|
||||
|
||||
expect(await screen.findByRole("button", { name: "Week" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Specific date" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Range" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("submits start and end via Done, including inverted clicks", async () => {
|
||||
const onSaveRange = vi.fn();
|
||||
renderWithProviders(
|
||||
<DateCell
|
||||
value=""
|
||||
allowWeekMode
|
||||
onSave={vi.fn()}
|
||||
onSaveWeek={vi.fn()}
|
||||
onSaveRange={onSaveRange}
|
||||
/>,
|
||||
);
|
||||
|
||||
await openRangeTab("—");
|
||||
fireEvent.click(screen.getByRole("button", { name: "20" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "5" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: /^done$/i }));
|
||||
|
||||
expect(onSaveRange).toHaveBeenCalledWith(currentMonthIso(5), currentMonthIso(20));
|
||||
});
|
||||
|
||||
it("does not submit until both ends are picked", async () => {
|
||||
const onSaveRange = vi.fn();
|
||||
renderWithProviders(
|
||||
<DateCell
|
||||
value=""
|
||||
allowWeekMode
|
||||
onSave={vi.fn()}
|
||||
onSaveWeek={vi.fn()}
|
||||
onSaveRange={onSaveRange}
|
||||
/>,
|
||||
);
|
||||
|
||||
await openRangeTab("—");
|
||||
fireEvent.click(screen.getByRole("button", { name: "5" }));
|
||||
|
||||
expect(screen.getByRole("button", { name: /^done$/i })).toBeDisabled();
|
||||
expect(onSaveRange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not persist when switching tabs without confirming", async () => {
|
||||
const onSave = vi.fn();
|
||||
const onSaveRange = vi.fn();
|
||||
renderWithProviders(
|
||||
<DateCell
|
||||
value=""
|
||||
allowWeekMode
|
||||
onSave={onSave}
|
||||
onSaveWeek={vi.fn()}
|
||||
onSaveRange={onSaveRange}
|
||||
/>,
|
||||
);
|
||||
|
||||
await openRangeTab("—");
|
||||
fireEvent.click(screen.getByRole("button", { name: "5" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Date" }));
|
||||
|
||||
expect(onSave).not.toHaveBeenCalled();
|
||||
expect(onSaveRange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("DueDateCell — no Schedule On range tabs", () => {
|
||||
it("does not offer Week / Date / Range on a PM due date", () => {
|
||||
renderWithProviders(<DueDateCell type="PM" value="2026-07-15" onSave={vi.fn()} />);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /07-15/ }));
|
||||
|
||||
expect(screen.queryByRole("button", { name: "Week" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Range" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Specific date" })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
import { fireEvent, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { SlideOverInfoTabEditView } from "@/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view";
|
||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||
import { pad2, todayIso } from "@/domain/work-orders/utils/wizard-date-utils";
|
||||
import { renderWithProviders } from "@/test/test-utils";
|
||||
|
||||
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/site-cell", () => ({
|
||||
SiteCell: () => null,
|
||||
}));
|
||||
vi.mock(
|
||||
"@/app/(protected)/workorders/_components/list/table/cells/editable-wo-number-cell",
|
||||
() => ({
|
||||
EditableWONumberCell: () => null,
|
||||
}),
|
||||
);
|
||||
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/type-cell", () => ({
|
||||
TypeCell: () => null,
|
||||
}));
|
||||
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/assigned-to-cell", () => ({
|
||||
AssignedToCell: () => null,
|
||||
}));
|
||||
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/appt-time-cell", () => ({
|
||||
ApptTimeCell: () => null,
|
||||
}));
|
||||
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/pm-type-cell", () => ({
|
||||
PMTypeCell: () => null,
|
||||
}));
|
||||
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/vendor-cell", () => ({
|
||||
VendorCell: () => null,
|
||||
}));
|
||||
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/status-cell", () => ({
|
||||
StatusCell: () => null,
|
||||
}));
|
||||
|
||||
function currentMonthIso(day: number): string {
|
||||
const [y, m] = todayIso().split("-").map(Number);
|
||||
return `${y}-${pad2(m)}-${pad2(day)}`;
|
||||
}
|
||||
|
||||
function baseRow(overrides: Partial<WorkOrderTableRow> = {}): WorkOrderTableRow {
|
||||
return {
|
||||
id: 42,
|
||||
woNumber: "WO-42",
|
||||
site: "SEA1",
|
||||
type: "PM",
|
||||
severity: null,
|
||||
dispatcherId: "u1",
|
||||
dispatcherName: "Dispatcher",
|
||||
scheduledOn: "",
|
||||
completedDate: "",
|
||||
targetWeek: "",
|
||||
dayGroup: "",
|
||||
dayLabel: "",
|
||||
location: "Seattle",
|
||||
locationId: "loc-1",
|
||||
pm: "HVAC",
|
||||
serviceNotes: "",
|
||||
extraServices: [],
|
||||
company: "Vendor Co",
|
||||
tech: "Tech",
|
||||
techPhone: "",
|
||||
vendorId: "v1",
|
||||
apptStart: "",
|
||||
apptEnd: "",
|
||||
apptTime: "",
|
||||
status: "Incomplete",
|
||||
docStatus: "No",
|
||||
pocName: "",
|
||||
pocPhone: "",
|
||||
pocNotes: "",
|
||||
additionalContacts: [],
|
||||
dueDate: "",
|
||||
rescheduleCount: 0,
|
||||
carriedOver: 0,
|
||||
isAddOn: false,
|
||||
originalWeek: "",
|
||||
originalDate: "",
|
||||
isPastDue: false,
|
||||
scheduleWeekOnly: false,
|
||||
mediaCount: 0,
|
||||
title: "",
|
||||
priority: "",
|
||||
primaryDispatchId: null,
|
||||
rowVersion: "v1",
|
||||
dispatchRowVersion: "",
|
||||
flagColor: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("SlideOverInfoTabEditView — Schedule On range", () => {
|
||||
it("patches range start/end and schedMode from the DateCell", async () => {
|
||||
const onDraftChange = vi.fn();
|
||||
renderWithProviders(
|
||||
<SlideOverInfoTabEditView
|
||||
draft={baseRow()}
|
||||
users={[]}
|
||||
sites={[]}
|
||||
vendors={[]}
|
||||
pmTypes={[]}
|
||||
allOrders={[]}
|
||||
onDraftChange={onDraftChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const scheduleRow = screen.getByText("Schedule On").closest("div");
|
||||
fireEvent.click(scheduleRow!.querySelector("button")!);
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Range" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "5" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "20" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: /^done$/i }));
|
||||
|
||||
expect(onDraftChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
scheduledOn: currentMonthIso(5),
|
||||
scheduleEnd: currentMonthIso(20),
|
||||
schedMode: "range",
|
||||
scheduleWeekOnly: false,
|
||||
targetWeek: "",
|
||||
}),
|
||||
);
|
||||
}, 10_000);
|
||||
});
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
import { renderHook } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useWoTableRowHandlers } from "@/app/(protected)/workorders/_components/list/table/use-wo-table-row-handlers";
|
||||
import { applySchedulePatch } from "@/domain/work-orders/use-cases/use-work-order-table-mutations";
|
||||
import {
|
||||
applySchedulePatch,
|
||||
applyScheduleRangePatch,
|
||||
} from "@/domain/work-orders/use-cases/use-work-order-table-mutations";
|
||||
|
||||
const { rescheduleNotify, weekMoveNotify } = vi.hoisted(() => ({
|
||||
rescheduleNotify: vi.fn(),
|
||||
|
|
@ -25,6 +28,11 @@ function buildTableData(revealScheduledRow = vi.fn()) {
|
|||
}
|
||||
|
||||
describe("useWoTableRowHandlers schedule navigation (SH-240)", () => {
|
||||
beforeEach(() => {
|
||||
rescheduleNotify.mockClear();
|
||||
weekMoveNotify.mockClear();
|
||||
});
|
||||
|
||||
it("selects the scheduled weekday in the same week so the row stays visible", () => {
|
||||
const onNavigateToDay = vi.fn();
|
||||
const onPatch = vi.fn();
|
||||
|
|
@ -80,6 +88,34 @@ describe("useWoTableRowHandlers schedule navigation (SH-240)", () => {
|
|||
expect(revealScheduledRow).toHaveBeenCalledWith("wo-1");
|
||||
});
|
||||
|
||||
it("lands a range on the start day and reveals the row", () => {
|
||||
const onNavigateToDay = vi.fn();
|
||||
const onPatch = vi.fn();
|
||||
const revealScheduledRow = vi.fn();
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useWoTableRowHandlers({
|
||||
tableData: buildTableData(revealScheduledRow),
|
||||
weekMonday: "2026-05-18",
|
||||
onNavigateToDay,
|
||||
onOpenSlideOver: vi.fn(),
|
||||
onOpenMedia: vi.fn(),
|
||||
onPatch,
|
||||
onScheduleHighlight: vi.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.onScheduleRange("wo-1", "2026-05-21", "2026-05-23");
|
||||
|
||||
expect(onPatch).toHaveBeenCalledWith(
|
||||
"wo-1",
|
||||
applyScheduleRangePatch("2026-05-21", "2026-05-23"),
|
||||
);
|
||||
expect(onNavigateToDay).toHaveBeenCalledWith("2026-05-21");
|
||||
expect(revealScheduledRow).toHaveBeenCalledWith("wo-1");
|
||||
expect(rescheduleNotify).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("opens Advanced Filters for week-only moves instead of the removed Unscheduled section", () => {
|
||||
const onNavigateToDay = vi.fn();
|
||||
const onOpenAdvancedFilters = vi.fn();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
import { createRef } from "react";
|
||||
import { fireEvent, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { WoTableInlineRowCells } from "@/app/(protected)/workorders/_components/list/table/wo-table-inline-row-cells";
|
||||
import { EMPTY_TABLE_DRAFT } from "@/domain/work-orders/types/work-order-table-row";
|
||||
import { pad2, todayIso } from "@/domain/work-orders/utils/wizard-date-utils";
|
||||
import { renderWithProviders } from "@/test/test-utils";
|
||||
|
||||
function currentMonthIso(day: number): string {
|
||||
const [y, m] = todayIso().split("-").map(Number);
|
||||
return `${y}-${pad2(m)}-${pad2(day)}`;
|
||||
}
|
||||
|
||||
describe("WoTableInlineRowCells — Schedule On range", () => {
|
||||
it("patches schedMode range with start and end", async () => {
|
||||
const onPatch = vi.fn();
|
||||
renderWithProviders(
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<WoTableInlineRowCells
|
||||
draft={EMPTY_TABLE_DRAFT}
|
||||
weekDefault={false}
|
||||
users={[]}
|
||||
sites={[]}
|
||||
vendors={[]}
|
||||
pmTypes={[]}
|
||||
allRows={[]}
|
||||
siteCellRef={createRef()}
|
||||
woCellRef={createRef()}
|
||||
siteShowError={false}
|
||||
siteError=""
|
||||
woShowError={false}
|
||||
onPatch={onPatch}
|
||||
/>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>,
|
||||
);
|
||||
|
||||
const scheduleButtons = screen.getAllByRole("button", { name: "—" });
|
||||
fireEvent.click(scheduleButtons[0]);
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Range" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "5" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "20" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: /^done$/i }));
|
||||
|
||||
expect(onPatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
scheduledOn: currentMonthIso(5),
|
||||
scheduleEnd: currentMonthIso(20),
|
||||
schedMode: "range",
|
||||
scheduleWeekOnly: false,
|
||||
targetWeek: "",
|
||||
}),
|
||||
);
|
||||
}, 10_000);
|
||||
});
|
||||
|
|
@ -65,6 +65,7 @@ const handlers: WoTableRowHandlers = {
|
|||
onPatch: vi.fn(),
|
||||
onScheduleDate: vi.fn(),
|
||||
onScheduleWeek: vi.fn(),
|
||||
onScheduleRange: vi.fn(),
|
||||
onOpenSlideOver: vi.fn(),
|
||||
onOpenCompDoc: vi.fn(),
|
||||
onOpenUplifts: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ function handlers(overrides: Partial<WoTableRowHandlers> = {}): WoTableRowHandle
|
|||
onPatch: vi.fn(),
|
||||
onScheduleDate: vi.fn(),
|
||||
onScheduleWeek: vi.fn(),
|
||||
onScheduleRange: vi.fn(),
|
||||
onOpenSlideOver: vi.fn(),
|
||||
onReorder: vi.fn(),
|
||||
onDragStart: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ const handlers: WoTableRowHandlers = {
|
|||
onPatch: vi.fn(),
|
||||
onScheduleDate: vi.fn(),
|
||||
onScheduleWeek: vi.fn(),
|
||||
onScheduleRange: vi.fn(),
|
||||
onOpenSlideOver: vi.fn(),
|
||||
onOpenCompDoc: vi.fn(),
|
||||
onOpenUplifts: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ describe("isLocalOnlyTablePatch", () => {
|
|||
it("treats primaryDispatchStatus overlay as local-only", () => {
|
||||
expect(isLocalOnlyTablePatch({ primaryDispatchStatus: "" })).toBe(true);
|
||||
});
|
||||
|
||||
it("treats scheduleEnd and schedMode as local-only overlays", () => {
|
||||
expect(isLocalOnlyTablePatch({ scheduleEnd: "2026-06-12", schedMode: "range" })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("expandTablePatchToBoardFieldOps", () => {
|
||||
|
|
@ -123,6 +127,21 @@ describe("expandTablePatchToBoardFieldOps", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it("does not emit scheduleEnd or schedMode as board fields", () => {
|
||||
const ops = expandTablePatchToBoardFieldOps({
|
||||
scheduleWeekOnly: false,
|
||||
scheduledOn: "2026-06-08",
|
||||
targetWeek: "",
|
||||
scheduleEnd: "2026-06-12",
|
||||
schedMode: "range",
|
||||
});
|
||||
expect(ops).toEqual([
|
||||
{ field: "scheduleWeekOnly", value: "false" },
|
||||
{ field: "scheduledDate", value: "2026-06-08" },
|
||||
{ field: "targetWeek", value: "" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("emits additionalContacts as serialized JSON for board PATCH", () => {
|
||||
expect(
|
||||
expandTablePatchToBoardFieldOps({
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ vi.mock("react-toastify", () => ({
|
|||
import { mapWorkOrderTableRow } from "@/domain/work-orders/mappers/work-order-table-row-mapper";
|
||||
import {
|
||||
applySchedulePatch,
|
||||
applyScheduleRangePatch,
|
||||
useWorkOrderTableMutations,
|
||||
} from "@/domain/work-orders/use-cases/use-work-order-table-mutations";
|
||||
import { queryKeys } from "@/infra/query-key/query-key";
|
||||
|
|
@ -275,6 +276,16 @@ describe("applySchedulePatch isAddOn optimistic clear", () => {
|
|||
expect(applySchedulePatch("2026-07-15", false).isAddOn).toBeUndefined();
|
||||
expect(applySchedulePatch("2026-07-13", true).isAddOn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps range end/mode optimistic while landing on the start day", () => {
|
||||
expect(applyScheduleRangePatch("2026-06-08", "2026-06-12")).toMatchObject({
|
||||
scheduledOn: "2026-06-08",
|
||||
scheduleEnd: "2026-06-12",
|
||||
schedMode: "range",
|
||||
scheduleWeekOnly: false,
|
||||
targetWeek: "",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("useWorkOrderTableMutations schedule isAddOn from BE", () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue