mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 17:02:05 +00:00
Merge pull request #163 from Sea-Haven-Industries/feat/sh-167-wo-board-carry-over
feat(work-orders): show carry-over pill and map day-tab for prior-week WOs
This commit is contained in:
commit
e3ffa5a94b
14 changed files with 408 additions and 13 deletions
|
|
@ -50,6 +50,9 @@ export function SlideOverInfoTabEditView({
|
||||||
rescheduleCount={draft.rescheduleCount}
|
rescheduleCount={draft.rescheduleCount}
|
||||||
carriedOver={draft.carriedOver}
|
carriedOver={draft.carriedOver}
|
||||||
originalDate={draft.originalDate}
|
originalDate={draft.originalDate}
|
||||||
|
originalWeek={draft.originalWeek}
|
||||||
|
scheduledOn={draft.scheduledOn}
|
||||||
|
status={draft.status}
|
||||||
currentId={draft.id}
|
currentId={draft.id}
|
||||||
allOrders={allOrders}
|
allOrders={allOrders}
|
||||||
onSave={(woNumber) => onDraftChange({ woNumber })}
|
onSave={(woNumber) => onDraftChange({ woNumber })}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ type EditableWONumberCellProps = {
|
||||||
carriedOver?: number;
|
carriedOver?: number;
|
||||||
isAddOn?: boolean;
|
isAddOn?: boolean;
|
||||||
originalDate?: string;
|
originalDate?: string;
|
||||||
|
originalWeek?: string;
|
||||||
|
scheduledOn?: string;
|
||||||
|
status?: string;
|
||||||
q?: string;
|
q?: string;
|
||||||
currentId: string | number;
|
currentId: string | number;
|
||||||
allOrders: WorkOrderTableRow[];
|
allOrders: WorkOrderTableRow[];
|
||||||
|
|
@ -37,6 +40,9 @@ export function EditableWONumberCell({
|
||||||
carriedOver = 0,
|
carriedOver = 0,
|
||||||
isAddOn = false,
|
isAddOn = false,
|
||||||
originalDate,
|
originalDate,
|
||||||
|
originalWeek,
|
||||||
|
scheduledOn,
|
||||||
|
status,
|
||||||
q,
|
q,
|
||||||
currentId,
|
currentId,
|
||||||
allOrders,
|
allOrders,
|
||||||
|
|
@ -118,7 +124,13 @@ export function EditableWONumberCell({
|
||||||
</span>
|
</span>
|
||||||
<AddOnPill when={isAddOn} />
|
<AddOnPill when={isAddOn} />
|
||||||
<RescheduleCounterPill count={rescheduleCount} originalDate={originalDate} />
|
<RescheduleCounterPill count={rescheduleCount} originalDate={originalDate} />
|
||||||
<CarriedOverCounterPill count={carriedOver} />
|
<CarriedOverCounterPill
|
||||||
|
count={carriedOver}
|
||||||
|
originalWeek={originalWeek}
|
||||||
|
originalDate={originalDate}
|
||||||
|
scheduledOn={scheduledOn}
|
||||||
|
status={status}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -155,7 +167,13 @@ export function EditableWONumberCell({
|
||||||
)}
|
)}
|
||||||
<AddOnPill when={isAddOn} />
|
<AddOnPill when={isAddOn} />
|
||||||
<RescheduleCounterPill count={rescheduleCount} originalDate={originalDate} />
|
<RescheduleCounterPill count={rescheduleCount} originalDate={originalDate} />
|
||||||
<CarriedOverCounterPill count={carriedOver} />
|
<CarriedOverCounterPill
|
||||||
|
count={carriedOver}
|
||||||
|
originalWeek={originalWeek}
|
||||||
|
originalDate={originalDate}
|
||||||
|
scheduledOn={scheduledOn}
|
||||||
|
status={status}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
import { RefreshCw } from "lucide-react";
|
import { RefreshCw } from "lucide-react";
|
||||||
import { Tooltip } from "@mui/material";
|
import { Tooltip } from "@mui/material";
|
||||||
|
import {
|
||||||
|
formatCarriedOverHover,
|
||||||
|
resolveCarryOverSourceWeek,
|
||||||
|
shouldShowCarriedOverPill,
|
||||||
|
} from "@/domain/work-orders/utils/carry-over-display";
|
||||||
import { formatRescheduleHover } from "@/domain/work-orders/utils/reschedule-indicator";
|
import { formatRescheduleHover } from "@/domain/work-orders/utils/reschedule-indicator";
|
||||||
|
|
||||||
const RESCHEDULE_PILL_STYLE = {
|
const RESCHEDULE_PILL_STYLE = {
|
||||||
|
|
@ -46,17 +51,30 @@ export function RescheduleCounterPill({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CarriedOverCounterPill({ count }: { count: number }) {
|
export function CarriedOverCounterPill({
|
||||||
if (count < 2) {
|
count,
|
||||||
|
originalWeek,
|
||||||
|
originalDate,
|
||||||
|
scheduledOn,
|
||||||
|
status,
|
||||||
|
}: {
|
||||||
|
count: number;
|
||||||
|
originalWeek?: string;
|
||||||
|
originalDate?: string;
|
||||||
|
scheduledOn?: string;
|
||||||
|
status?: string;
|
||||||
|
}) {
|
||||||
|
if (!shouldShowCarriedOverPill(count, status)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const label = `Carried over ${count} times`;
|
const sourceWeek = resolveCarryOverSourceWeek({ originalWeek, originalDate, scheduledOn });
|
||||||
|
const hover = formatCarriedOverHover(count, sourceWeek);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title="Carried over" arrow placement="top">
|
<Tooltip title={hover} arrow placement="top">
|
||||||
<span
|
<span
|
||||||
aria-label={label}
|
aria-label={hover}
|
||||||
className="inline-flex h-[18px] items-center rounded px-1.5 text-[10px] font-semibold"
|
className="inline-flex h-[18px] items-center rounded px-1.5 text-[10px] font-semibold"
|
||||||
style={MUTED_PILL_STYLE}
|
style={MUTED_PILL_STYLE}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,9 @@ export function WoTableRowCells({
|
||||||
carriedOver={row.carriedOver}
|
carriedOver={row.carriedOver}
|
||||||
isAddOn={row.isAddOn}
|
isAddOn={row.isAddOn}
|
||||||
originalDate={row.originalDate}
|
originalDate={row.originalDate}
|
||||||
|
originalWeek={row.originalWeek}
|
||||||
|
scheduledOn={row.scheduledOn}
|
||||||
|
status={row.status}
|
||||||
q={search}
|
q={search}
|
||||||
currentId={row.id}
|
currentId={row.id}
|
||||||
allOrders={allRows}
|
allOrders={allRows}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } 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 { applyCarryOverDisplayDayFields } from "@/domain/work-orders/utils/carry-over-display";
|
||||||
import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils";
|
import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils";
|
||||||
import { enrichTableRow } from "@/domain/work-orders/utils/table-filter-utils";
|
import { enrichTableRow } from "@/domain/work-orders/utils/table-filter-utils";
|
||||||
import {
|
import {
|
||||||
|
|
@ -62,8 +63,11 @@ export function useWorkOrdersTableLocalState({
|
||||||
const displayRows = useMemo(() => {
|
const displayRows = useMemo(() => {
|
||||||
const apiIds = new Set(apiRows.map((r) => String(r.id)));
|
const apiIds = new Set(apiRows.map((r) => String(r.id)));
|
||||||
const extra = localRows.filter((r) => !apiIds.has(String(r.id)));
|
const extra = localRows.filter((r) => !apiIds.has(String(r.id)));
|
||||||
return applyRowOrderByDay([...apiRows, ...extra.map(enrichTableRow)], rowOrderByDay);
|
const merged = [...apiRows, ...extra.map(enrichTableRow)].map((row) =>
|
||||||
}, [apiRows, localRows, rowOrderByDay]);
|
applyCarryOverDisplayDayFields(row, weekMonday),
|
||||||
|
);
|
||||||
|
return applyRowOrderByDay(merged, rowOrderByDay);
|
||||||
|
}, [apiRows, localRows, rowOrderByDay, weekMonday]);
|
||||||
|
|
||||||
const clearRowPatch = useCallback((id: string | number) => {
|
const clearRowPatch = useCallback((id: string | number) => {
|
||||||
const key = String(id);
|
const key = String(id);
|
||||||
|
|
|
||||||
109
src/domain/work-orders/utils/carry-over-display.ts
Normal file
109
src/domain/work-orders/utils/carry-over-display.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { compactWeekLabel } from "@/domain/work-orders/utils/list-week-utils";
|
||||||
|
import { formatRescheduleCountLabel } from "@/domain/work-orders/utils/reschedule-indicator";
|
||||||
|
import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils";
|
||||||
|
import { addDaysIso, mondayOf } from "@/domain/work-orders/utils/wizard-date-utils";
|
||||||
|
|
||||||
|
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
||||||
|
const WEEKDAY_COUNT = 7;
|
||||||
|
const FRIDAY_OFFSET = 4;
|
||||||
|
|
||||||
|
export type CarryOverRow = {
|
||||||
|
scheduledOn: string;
|
||||||
|
status?: string;
|
||||||
|
carriedOver?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isTerminalCarryOverStatus(status?: string): boolean {
|
||||||
|
return status === "Completed" || status === "Canceled";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldShowCarriedOverPill(count: number, status?: string): boolean {
|
||||||
|
return count >= 2 && !isTerminalCarryOverStatus(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toIsoDate(value?: string): string {
|
||||||
|
if (!value) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const iso = value.slice(0, 10);
|
||||||
|
return ISO_DATE.test(iso) ? iso : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveCarryOverSourceWeek(input: {
|
||||||
|
originalWeek?: string;
|
||||||
|
originalDate?: string;
|
||||||
|
scheduledOn?: string;
|
||||||
|
}): string {
|
||||||
|
const originalWeek = toIsoDate(input.originalWeek);
|
||||||
|
if (originalWeek) {
|
||||||
|
return mondayOf(originalWeek);
|
||||||
|
}
|
||||||
|
const originalDate = toIsoDate(input.originalDate);
|
||||||
|
if (originalDate) {
|
||||||
|
return mondayOf(originalDate);
|
||||||
|
}
|
||||||
|
const scheduledOn = toIsoDate(input.scheduledOn);
|
||||||
|
return scheduledOn ? mondayOf(scheduledOn) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatCarriedOverHover(count: number, sourceWeekIso?: string): string {
|
||||||
|
const times = formatRescheduleCountLabel(count);
|
||||||
|
const base = `Carried over ${times} — this WO was not completed and moved to the next period`;
|
||||||
|
const sourceLabel = sourceWeekIso ? compactWeekLabel(sourceWeekIso) : "";
|
||||||
|
if (!sourceLabel) {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
return `${base}. Source week: ${sourceLabel}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Non-terminal WOs scheduled before the viewed week are display carry-overs. */
|
||||||
|
export function isCarryOverDisplayRow(row: CarryOverRow, weekMonday: string): boolean {
|
||||||
|
if (isTerminalCarryOverStatus(row.status)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const scheduled = toIsoDate(row.scheduledOn);
|
||||||
|
return Boolean(scheduled) && scheduled < weekMonday;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapCarryOverDisplayDay(scheduledOn: string, weekMonday: string): string {
|
||||||
|
const scheduled = toIsoDate(scheduledOn);
|
||||||
|
if (!scheduled || !ISO_DATE.test(weekMonday)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const sourceMonday = mondayOf(scheduled);
|
||||||
|
const offsetMs =
|
||||||
|
new Date(`${scheduled}T12:00:00`).getTime() - new Date(`${sourceMonday}T12:00:00`).getTime();
|
||||||
|
const offset = Math.round(offsetMs / 86_400_000);
|
||||||
|
if (offset < 0 || offset >= WEEKDAY_COUNT) {
|
||||||
|
return weekMonday;
|
||||||
|
}
|
||||||
|
if (offset > FRIDAY_OFFSET) {
|
||||||
|
return weekMonday;
|
||||||
|
}
|
||||||
|
return addDaysIso(weekMonday, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rowMatchesCarryOverDay(
|
||||||
|
row: CarryOverRow,
|
||||||
|
selectedDayIso: string,
|
||||||
|
weekMonday: string,
|
||||||
|
): boolean {
|
||||||
|
if (!isCarryOverDisplayRow(row, weekMonday)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return mapCarryOverDisplayDay(row.scheduledOn, weekMonday) === selectedDayIso;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Aligns UI day keys with the mapped weekday so reorder stays on the displayed tab. */
|
||||||
|
export function applyCarryOverDisplayDayFields<
|
||||||
|
T extends CarryOverRow & { dayGroup: string; dayLabel: string },
|
||||||
|
>(row: T, weekMonday: string): T {
|
||||||
|
if (!isCarryOverDisplayRow(row, weekMonday)) {
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
const displayDay = mapCarryOverDisplayDay(row.scheduledOn, weekMonday);
|
||||||
|
if (!displayDay) {
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
return { ...row, ...deriveDayFields(displayDay) };
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,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 { isCarryOverDisplayRow } from "@/domain/work-orders/utils/carry-over-display";
|
||||||
import { isDateInWeek } from "@/domain/work-orders/utils/table-day-utils";
|
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";
|
||||||
|
|
||||||
|
|
@ -96,7 +97,7 @@ export function filterByBarAndSearch(
|
||||||
|
|
||||||
if (row.scheduledOn) {
|
if (row.scheduledOn) {
|
||||||
if (row.scheduledOn < weekFrom || row.scheduledOn > weekTo) {
|
if (row.scheduledOn < weekFrom || row.scheduledOn > weekTo) {
|
||||||
return false;
|
return isCarryOverDisplayRow(row, bar.weekMonday);
|
||||||
}
|
}
|
||||||
} else if (row.targetWeek) {
|
} else if (row.targetWeek) {
|
||||||
if (!isDateInWeek(row.targetWeek, bar.weekMonday)) {
|
if (!isDateInWeek(row.targetWeek, bar.weekMonday)) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import type {
|
||||||
WorkOrderDayGroup,
|
WorkOrderDayGroup,
|
||||||
WorkOrderTableRow,
|
WorkOrderTableRow,
|
||||||
} from "@/domain/work-orders/types/work-order-table-row";
|
} from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
import { rowMatchesCarryOverDay } from "@/domain/work-orders/utils/carry-over-display";
|
||||||
import { isLiveWeek } from "@/domain/work-orders/utils/list-week-utils";
|
import { isLiveWeek } from "@/domain/work-orders/utils/list-week-utils";
|
||||||
import { isDateInWeek } from "@/domain/work-orders/utils/table-day-utils";
|
import { isDateInWeek } from "@/domain/work-orders/utils/table-day-utils";
|
||||||
import { addDaysIso, mondayOf, todayIso } from "@/domain/work-orders/utils/wizard-date-utils";
|
import { addDaysIso, mondayOf, todayIso } from "@/domain/work-orders/utils/wizard-date-utils";
|
||||||
|
|
@ -65,10 +66,17 @@ export function formatDayTabLabel(iso: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function rowMatchesSelectedDay(
|
export function rowMatchesSelectedDay(
|
||||||
row: Pick<WorkOrderTableRow, "scheduledOn">,
|
row: Pick<WorkOrderTableRow, "scheduledOn" | "status" | "carriedOver">,
|
||||||
selectedDayIso: string,
|
selectedDayIso: string,
|
||||||
|
weekMonday?: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
return Boolean(row.scheduledOn) && row.scheduledOn.slice(0, 10) === selectedDayIso;
|
if (Boolean(row.scheduledOn) && row.scheduledOn.slice(0, 10) === selectedDayIso) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!weekMonday) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return rowMatchesCarryOverDay(row, selectedDayIso, weekMonday);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildSelectedDayGroup(
|
export function buildSelectedDayGroup(
|
||||||
|
|
@ -83,6 +91,6 @@ export function buildSelectedDayGroup(
|
||||||
key: `${month}/${day}`,
|
key: `${month}/${day}`,
|
||||||
label: formatDayTabLabel(date),
|
label: formatDayTabLabel(date),
|
||||||
date,
|
date,
|
||||||
items: scheduled.filter((row) => rowMatchesSelectedDay(row, date)),
|
items: scheduled.filter((row) => rowMatchesSelectedDay(row, date, weekMonday)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,20 @@ describe("CarriedOverCounterPill", () => {
|
||||||
expect(pill).toHaveTextContent("3");
|
expect(pill).toHaveTextContent("3");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("includes the source week in the accessible name", () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<CarriedOverCounterPill count={3} originalWeek="2026-05-04" status="Scheduled" />,
|
||||||
|
);
|
||||||
|
expect(screen.getByLabelText(/Source week: 5\/4–5\/8/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides after resolution even when the counter remains", () => {
|
||||||
|
const { container } = renderWithProviders(
|
||||||
|
<CarriedOverCounterPill count={3} originalWeek="2026-05-04" status="Completed" />,
|
||||||
|
);
|
||||||
|
expect(container).toBeEmptyDOMElement();
|
||||||
|
});
|
||||||
|
|
||||||
it("is distinguishable from the reschedule pill beside it", () => {
|
it("is distinguishable from the reschedule pill beside it", () => {
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { act, renderHook } from "@testing-library/react";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { useWorkOrdersListFilters } from "@/app/(protected)/workorders/_hooks/use-work-orders-list-filters";
|
||||||
|
import { workOrdersApi } from "@/domain/work-orders/api/work-orders-api";
|
||||||
|
|
||||||
|
vi.mock("@/hooks/use-debounce", () => ({
|
||||||
|
useDebounce: <T,>(value: T) => value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/domain/work-orders/api/work-orders-api", () => ({
|
||||||
|
workOrdersApi: {
|
||||||
|
patchBoardField: vi.fn(),
|
||||||
|
getBoard: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("useWorkOrdersListFilters week navigation", () => {
|
||||||
|
it("changes the viewed week without patching schedule fields", () => {
|
||||||
|
const { result } = renderHook(() => useWorkOrdersListFilters());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.setWeekMonday("2026-05-18");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.weekMonday).toBe("2026-05-18");
|
||||||
|
expect(result.current.selectedDayIso).toBe("2026-05-18");
|
||||||
|
expect(result.current.boardParams.weekStart).toBe("2026-05-18");
|
||||||
|
expect(workOrdersApi.patchBoardField).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { mapWorkOrderTableRow } from "@/domain/work-orders/mappers/work-order-table-row-mapper";
|
import { mapWorkOrderTableRow } from "@/domain/work-orders/mappers/work-order-table-row-mapper";
|
||||||
import { WORK_ORDER_TYPE_API } from "@/domain/work-orders/types/work-order-board";
|
import { WORK_ORDER_TYPE_API } from "@/domain/work-orders/types/work-order-board";
|
||||||
|
import { buildSelectedDayGroup } from "@/domain/work-orders/utils/week-day-nav-utils";
|
||||||
|
|
||||||
describe("mapWorkOrderTableRow isAddOn", () => {
|
describe("mapWorkOrderTableRow isAddOn", () => {
|
||||||
it("reads isAddOn from board DTO when present", () => {
|
it("reads isAddOn from board DTO when present", () => {
|
||||||
|
|
@ -137,3 +138,32 @@ describe("mapWorkOrderTableRow primaryDispatchStatus", () => {
|
||||||
expect(row.primaryDispatchId).toBe(11);
|
expect(row.primaryDispatchId).toBe(11);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("mapWorkOrderTableRow carry-over fields", () => {
|
||||||
|
it("maps carriedOver and originalWeek without rewriting scheduledOn", () => {
|
||||||
|
const row = mapWorkOrderTableRow({
|
||||||
|
id: 41,
|
||||||
|
workOrderType: WORK_ORDER_TYPE_API.PM,
|
||||||
|
scheduledDate: "2026-05-13",
|
||||||
|
carriedOver: 2,
|
||||||
|
originalWeek: "2026-05-11",
|
||||||
|
});
|
||||||
|
expect(row.carriedOver).toBe(2);
|
||||||
|
expect(row.originalWeek).toBe("2026-05-11");
|
||||||
|
expect(row.scheduledOn).toBe("2026-05-13");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows an unresolved prior-week row on the next week's matching day tab", () => {
|
||||||
|
const row = mapWorkOrderTableRow({
|
||||||
|
id: 42,
|
||||||
|
workOrderType: WORK_ORDER_TYPE_API.PM,
|
||||||
|
scheduledDate: "2026-05-13",
|
||||||
|
lifecycleStatus: 3,
|
||||||
|
carriedOver: 2,
|
||||||
|
originalWeek: "2026-05-11",
|
||||||
|
});
|
||||||
|
const group = buildSelectedDayGroup("2026-05-18", "2026-05-20", [row]);
|
||||||
|
expect(row.status).toBe("Scheduled");
|
||||||
|
expect(group.items.map((item) => item.id)).toEqual([42]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
94
src/test/domain/work-orders/utils/carry-over-display.test.ts
Normal file
94
src/test/domain/work-orders/utils/carry-over-display.test.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
applyCarryOverDisplayDayFields,
|
||||||
|
formatCarriedOverHover,
|
||||||
|
isCarryOverDisplayRow,
|
||||||
|
mapCarryOverDisplayDay,
|
||||||
|
resolveCarryOverSourceWeek,
|
||||||
|
shouldShowCarriedOverPill,
|
||||||
|
} from "@/domain/work-orders/utils/carry-over-display";
|
||||||
|
|
||||||
|
describe("carry-over-display", () => {
|
||||||
|
it("hides the pill below 2 and on terminal statuses", () => {
|
||||||
|
expect(shouldShowCarriedOverPill(1, "Scheduled")).toBe(false);
|
||||||
|
expect(shouldShowCarriedOverPill(2, "Scheduled")).toBe(true);
|
||||||
|
expect(shouldShowCarriedOverPill(3, "Completed")).toBe(false);
|
||||||
|
expect(shouldShowCarriedOverPill(3, "Canceled")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("formats hover without a source week", () => {
|
||||||
|
expect(formatCarriedOverHover(2)).toBe(
|
||||||
|
"Carried over 2 times — this WO was not completed and moved to the next period",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("appends the source week label from originalWeek", () => {
|
||||||
|
expect(formatCarriedOverHover(3, "2026-05-04")).toBe(
|
||||||
|
"Carried over 3 times — this WO was not completed and moved to the next period. Source week: 5/4–5/8.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves source week from originalWeek, then originalDate, then scheduledOn", () => {
|
||||||
|
expect(resolveCarryOverSourceWeek({ originalWeek: "2026-05-04" })).toBe("2026-05-04");
|
||||||
|
expect(resolveCarryOverSourceWeek({ originalDate: "2026-05-06" })).toBe("2026-05-04");
|
||||||
|
expect(resolveCarryOverSourceWeek({ scheduledOn: "2026-05-13" })).toBe("2026-05-11");
|
||||||
|
expect(resolveCarryOverSourceWeek({})).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats non-terminal rows scheduled before the viewed week as carry-over", () => {
|
||||||
|
expect(
|
||||||
|
isCarryOverDisplayRow(
|
||||||
|
{ scheduledOn: "2026-05-13", status: "Scheduled", carriedOver: 1 },
|
||||||
|
"2026-05-18",
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
isCarryOverDisplayRow(
|
||||||
|
{ scheduledOn: "2026-05-20", status: "Scheduled", carriedOver: 2 },
|
||||||
|
"2026-05-18",
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
isCarryOverDisplayRow(
|
||||||
|
{ scheduledOn: "2026-05-13", status: "Completed", carriedOver: 2 },
|
||||||
|
"2026-05-18",
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps the original weekday onto the viewed week and weekends onto Monday", () => {
|
||||||
|
expect(mapCarryOverDisplayDay("2026-05-13", "2026-05-18")).toBe("2026-05-20");
|
||||||
|
expect(mapCarryOverDisplayDay("2026-05-16", "2026-05-18")).toBe("2026-05-18");
|
||||||
|
expect(mapCarryOverDisplayDay("2026-05-17", "2026-05-18")).toBe("2026-05-18");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("remaps dayGroup to the viewed weekday without changing scheduledOn", () => {
|
||||||
|
const row = {
|
||||||
|
scheduledOn: "2026-05-13",
|
||||||
|
status: "Scheduled",
|
||||||
|
dayGroup: "05/13",
|
||||||
|
dayLabel: "WED · 05/13",
|
||||||
|
};
|
||||||
|
const next = applyCarryOverDisplayDayFields(row, "2026-05-18");
|
||||||
|
expect(next.scheduledOn).toBe("2026-05-13");
|
||||||
|
expect(next.dayGroup).toBe("05/20");
|
||||||
|
expect(next.dayLabel).toBe("WED · 05/20");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves in-week and terminal rows' dayGroup unchanged", () => {
|
||||||
|
const inWeek = {
|
||||||
|
scheduledOn: "2026-05-20",
|
||||||
|
status: "Scheduled",
|
||||||
|
dayGroup: "05/20",
|
||||||
|
dayLabel: "WED · 05/20",
|
||||||
|
};
|
||||||
|
const completed = {
|
||||||
|
scheduledOn: "2026-05-13",
|
||||||
|
status: "Completed",
|
||||||
|
dayGroup: "05/13",
|
||||||
|
dayLabel: "WED · 05/13",
|
||||||
|
};
|
||||||
|
expect(applyCarryOverDisplayDayFields(inWeek, "2026-05-18")).toEqual(inWeek);
|
||||||
|
expect(applyCarryOverDisplayDayFields(completed, "2026-05-18")).toEqual(completed);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -137,6 +137,28 @@ describe("filterByBarAndSearch", () => {
|
||||||
expect(ids).toEqual(["in"]);
|
expect(ids).toEqual(["in"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps prior-week unresolved carry-over rows that match a one-character search", () => {
|
||||||
|
const carried = makeRow({
|
||||||
|
id: "co",
|
||||||
|
woNumber: "WO-CARRY",
|
||||||
|
scheduledOn: "2026-05-13",
|
||||||
|
status: "Scheduled",
|
||||||
|
...deriveDayFields("2026-05-13"),
|
||||||
|
});
|
||||||
|
const completed = makeRow({
|
||||||
|
id: "done",
|
||||||
|
woNumber: "WO-DONE",
|
||||||
|
scheduledOn: "2026-05-13",
|
||||||
|
status: "Completed",
|
||||||
|
...deriveDayFields("2026-05-13"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ids = filterByBarAndSearch([carried, completed], bar("2026-05-18"), "W").map(
|
||||||
|
(row) => row.id,
|
||||||
|
);
|
||||||
|
expect(ids).toEqual(["co"]);
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps the union of selected types (OR)", () => {
|
it("keeps the union of selected types (OR)", () => {
|
||||||
const pm = makeRow({ id: "pm", type: "PM", scheduledOn: "2026-06-03" });
|
const pm = makeRow({ id: "pm", type: "PM", scheduledOn: "2026-06-03" });
|
||||||
const emergency = makeRow({ id: "em", type: "Emergency", scheduledOn: "2026-06-03" });
|
const emergency = makeRow({ id: "em", type: "Emergency", scheduledOn: "2026-06-03" });
|
||||||
|
|
|
||||||
|
|
@ -179,3 +179,44 @@ describe("week-day-nav-utils (SH-240)", () => {
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("week-day-nav-utils carry-over display", () => {
|
||||||
|
it("places a prior-week unresolved WO on the same weekday of the viewed week", () => {
|
||||||
|
const carried = row({
|
||||||
|
id: "co",
|
||||||
|
scheduledOn: "2026-05-13",
|
||||||
|
dayGroup: "05/13",
|
||||||
|
status: "Scheduled",
|
||||||
|
carriedOver: 2,
|
||||||
|
originalWeek: "2026-05-11",
|
||||||
|
});
|
||||||
|
const nativeWed = row({
|
||||||
|
id: "native",
|
||||||
|
scheduledOn: "2026-05-20",
|
||||||
|
dayGroup: "05/20",
|
||||||
|
status: "Scheduled",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(rowMatchesSelectedDay(carried, "2026-05-20", "2026-05-18")).toBe(true);
|
||||||
|
expect(rowMatchesSelectedDay(carried, "2026-05-18", "2026-05-18")).toBe(false);
|
||||||
|
|
||||||
|
const group = buildSelectedDayGroup("2026-05-18", "2026-05-20", [carried, nativeWed]);
|
||||||
|
expect(group.items.map((item) => item.id)).toEqual(["co", "native"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not map completed carry-overs onto the viewed week", () => {
|
||||||
|
const completed = row({
|
||||||
|
id: "done",
|
||||||
|
scheduledOn: "2026-05-13",
|
||||||
|
status: "Completed",
|
||||||
|
carriedOver: 2,
|
||||||
|
});
|
||||||
|
expect(rowMatchesSelectedDay(completed, "2026-05-20", "2026-05-18")).toBe(false);
|
||||||
|
expect(buildSelectedDayGroup("2026-05-18", "2026-05-20", [completed]).items).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps unscheduled rows out of day groups", () => {
|
||||||
|
const unscheduled = row({ id: "us", scheduledOn: "", scheduleWeekOnly: true, carriedOver: 2 });
|
||||||
|
expect(rowMatchesSelectedDay(unscheduled, "2026-05-18", "2026-05-18")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue