diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-row-reorder-handle.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-row-reorder-handle.tsx index 1a934b33..f9d67c27 100644 --- a/src/app/(protected)/workorders/_components/list/table/wo-table-row-reorder-handle.tsx +++ b/src/app/(protected)/workorders/_components/list/table/wo-table-row-reorder-handle.tsx @@ -28,9 +28,17 @@ export function WoTableRowReorderHandle({ aria-label={woNumber ? `Reorder work order ${woNumber}` : "Reorder work order"} className="flex items-center justify-center bg-transparent p-0" style={{ + appearance: "none", cursor: gripCursor(disabled, isDragging), color: "var(--color-text-muted)", border: "none", + background: "transparent", + padding: 0, + margin: 0, + lineHeight: 0, + font: "inherit", + minWidth: 0, + minHeight: 0, }} onClick={(event) => event.stopPropagation()} onKeyDown={(event) => { diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-row.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-row.tsx index 6e6dcd79..459d775a 100644 --- a/src/app/(protected)/workorders/_components/list/table/wo-table-row.tsx +++ b/src/app/(protected)/workorders/_components/list/table/wo-table-row.tsx @@ -161,7 +161,12 @@ export function WoTableRow({ isDragging={isDragging} woNumber={row.woNumber} onArrowReorder={(delta) => { - const overId = neighborIdForReorder(allRows, row, delta); + const overId = neighborIdForReorder( + allRows, + row, + delta, + (item) => !isWorkOrderFullyLocked(item.status), + ); if (overId == null) return; handlers.onReorder(row.id, overId); }} diff --git a/src/app/(protected)/workorders/_hooks/resolve-board-row-order-owner-id.ts b/src/app/(protected)/workorders/_hooks/resolve-board-row-order-owner-id.ts new file mode 100644 index 00000000..b6b2032f --- /dev/null +++ b/src/app/(protected)/workorders/_hooks/resolve-board-row-order-owner-id.ts @@ -0,0 +1,6 @@ +import { getAuthUser } from "@/lib/auth/auth-storage"; + +export function resolveBoardRowOrderOwnerId(): string { + const user = getAuthUser(); + return user?.id == null ? "" : String(user.id); +} diff --git a/src/app/(protected)/workorders/_hooks/use-work-orders-table-data.ts b/src/app/(protected)/workorders/_hooks/use-work-orders-table-data.ts index 260794cc..2163bcfb 100644 --- a/src/app/(protected)/workorders/_hooks/use-work-orders-table-data.ts +++ b/src/app/(protected)/workorders/_hooks/use-work-orders-table-data.ts @@ -1,26 +1,13 @@ import { useMemo } from "react"; import type { useWorkOrdersListFilters } from "@/app/(protected)/workorders/_hooks/use-work-orders-list-filters"; +import { resolveBoardRowOrderOwnerId } from "@/app/(protected)/workorders/_hooks/resolve-board-row-order-owner-id"; import { useSyncOutOfRangeSearchPage } from "@/app/(protected)/workorders/_hooks/use-sync-out-of-range-search-page"; import { useWorkOrdersBoardSource } from "@/app/(protected)/workorders/_hooks/use-work-orders-board-source"; -import { useWorkOrdersDayPagination } from "@/app/(protected)/workorders/_hooks/use-work-orders-day-pagination"; +import { useWorkOrdersTableDayView } from "@/app/(protected)/workorders/_hooks/use-work-orders-table-day-view"; import { useWorkOrdersTableLocalState } from "@/app/(protected)/workorders/_hooks/use-work-orders-table-local-state"; -import { getAuthUser } from "@/lib/auth/auth-storage"; -import { currentWeekMonday } from "@/domain/work-orders/utils/list-week-utils"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; -import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils"; -import { - pickBoardPager, - pageSelectedDayGroups, - type DayPageSize, -} from "@/domain/work-orders/utils/day-pagination"; -import { - buildSelectedDayGroup, - isDayScopedSearchEmpty, -} from "@/domain/work-orders/utils/week-day-nav-utils"; -import { - filterByBarAndSearch, - splitScheduledUnscheduled, -} from "@/domain/work-orders/utils/table-filter-utils"; +import { pickBoardPager, type DayPageSize } from "@/domain/work-orders/utils/day-pagination"; +import { filterByBarAndSearch } from "@/domain/work-orders/utils/table-filter-utils"; export type { DragVizState } from "@/app/(protected)/workorders/_hooks/use-work-orders-table-local-state"; @@ -34,8 +21,7 @@ export function useWorkOrdersTableData(filters: FiltersReturn) { useWorkOrdersBoardSource(filters); const weeklyItems = data?.items ?? EMPTY_WEEKLY_ITEMS; - const user = getAuthUser(); - const ownerId = user?.id == null ? "" : String(user.id); + const ownerId = resolveBoardRowOrderOwnerId(); const local = useWorkOrdersTableLocalState({ items: weeklyItems, @@ -58,57 +44,22 @@ export function useWorkOrdersTableData(filters: FiltersReturn) { return displayRows; }, [displayRows, filters.barFilters, filters.search, advActive]); - const { scheduled, unscheduled } = useMemo(() => { - if (advActive) { - return splitScheduledUnscheduled(advResults); - } - return splitScheduledUnscheduled(filtered); - }, [advActive, advResults, filtered]); - - const dayGroups = useMemo( - () => - advActive - ? [] - : [buildSelectedDayGroup(filters.weekMonday, filters.selectedDayIso, scheduled)], - [advActive, filters.selectedDayIso, filters.weekMonday, scheduled], - ); - const pagination = useWorkOrdersDayPagination({ - weekMonday: filters.weekMonday, - selectedDayIso: filters.selectedDayIso, + const dayView = useWorkOrdersTableDayView({ + filters, advActive, - dayItems: dayGroups[0]?.items ?? EMPTY_WEEKLY_ITEMS, + filtered, + advResults, onRevealed: local.triggerFlash, }); - const pagedDay = useMemo( - () => pageSelectedDayGroups(dayGroups, pagination.page, pagination.pageSize), - [dayGroups, pagination.page, pagination.pageSize], - ); - - const searchActive = filters.search.trim() !== ""; - const displayGroups = pagedDay.groups; - const dayTotalCount = pagedDay.totalCount; - const dayVisibleCount = pagedDay.groups[0]?.items.length ?? 0; - const noSearchResults = - !advActive && - isDayScopedSearchEmpty({ - searchActive, - unassignedCount: 0, - dayItemCount: dayTotalCount, - unassignedQueueReady: true, - }); - const isEmptyWeek = !advActive && scheduled.length === 0 && !searchActive; - - const todayKey = - filters.weekMonday === currentWeekMonday() - ? deriveDayFields(new Date().toISOString().slice(0, 10)).dayGroup - : null; const selectedRow = useMemo( () => displayRows.find((row) => String(row.id) === String(local.selectedId)) ?? null, [displayRows, local.selectedId], ); - const scopeTotalCount = advActive ? (data?.totalCount ?? advResults.length) : dayTotalCount; + const scopeTotalCount = advActive + ? (data?.totalCount ?? advResults.length) + : dayView.dayTotalCount; useSyncOutOfRangeSearchPage({ enabled: advActive, ready: !isLoading, @@ -117,11 +68,11 @@ export function useWorkOrdersTableData(filters: FiltersReturn) { totalCount: scopeTotalCount, setPage: filters.setPage, }); - const listTotalCount = advActive ? advResults.length : dayVisibleCount; + const listTotalCount = advActive ? advResults.length : dayView.dayVisibleCount; const pager = pickBoardPager({ advActive, isLoading, - noSearchResults, + noSearchResults: dayView.noSearchResults, advResultCount: advResults.length, search: { page: filters.page, @@ -129,11 +80,11 @@ export function useWorkOrdersTableData(filters: FiltersReturn) { totalCount: scopeTotalCount, }, day: { - pageSize: pagination.pageSize, - pageIndex: pagedDay.pageIndex, - pageCount: pagedDay.pageCount, - itemCount: dayTotalCount, - footerLabel: displayGroups[0]?.label ?? "", + pageSize: dayView.pagination.pageSize, + pageIndex: dayView.pagedDay.pageIndex, + pageCount: dayView.pagedDay.pageCount, + itemCount: dayView.dayTotalCount, + footerLabel: dayView.displayGroups[0]?.label ?? "", }, }); @@ -149,12 +100,12 @@ export function useWorkOrdersTableData(filters: FiltersReturn) { unassignedTotalCount: 0, unassignedError: null, unassignedLoading: false, - unscheduled, - displayGroups, - searchActive, - noSearchResults, - isEmptyWeek, - todayKey, + unscheduled: dayView.unscheduled, + displayGroups: dayView.displayGroups, + searchActive: dayView.searchActive, + noSearchResults: dayView.noSearchResults, + isEmptyWeek: dayView.isEmptyWeek, + todayKey: dayView.todayKey, ...localUi, selectedRow, scopeTotalCount, @@ -171,14 +122,14 @@ export function useWorkOrdersTableData(filters: FiltersReturn) { filters.setPageSize(next); return; } - pagination.setPageSize(next); + dayView.pagination.setPageSize(next); }, goToPrevDayPage: advActive ? () => filters.setPage(Math.max(0, pager.pageIndex - 1)) - : pagination.goToPrevPage, + : dayView.pagination.goToPrevPage, goToNextDayPage: advActive ? () => filters.setPage(Math.min(pager.pageIndex + 1, Math.max(0, pager.pageCount - 1))) - : pagination.goToNextPage, - revealScheduledRow: pagination.revealRow, + : dayView.pagination.goToNextPage, + revealScheduledRow: dayView.pagination.revealRow, }; } diff --git a/src/app/(protected)/workorders/_hooks/use-work-orders-table-day-view.ts b/src/app/(protected)/workorders/_hooks/use-work-orders-table-day-view.ts new file mode 100644 index 00000000..3f15b3cb --- /dev/null +++ b/src/app/(protected)/workorders/_hooks/use-work-orders-table-day-view.ts @@ -0,0 +1,90 @@ +import { useMemo } from "react"; +import type { useWorkOrdersListFilters } from "@/app/(protected)/workorders/_hooks/use-work-orders-list-filters"; +import { useWorkOrdersDayPagination } from "@/app/(protected)/workorders/_hooks/use-work-orders-day-pagination"; +import { currentWeekMonday } from "@/domain/work-orders/utils/list-week-utils"; +import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; +import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils"; +import { pageSelectedDayGroups } from "@/domain/work-orders/utils/day-pagination"; +import { + buildSelectedDayGroup, + isDayScopedSearchEmpty, +} from "@/domain/work-orders/utils/week-day-nav-utils"; +import { splitScheduledUnscheduled } from "@/domain/work-orders/utils/table-filter-utils"; + +type FiltersReturn = ReturnType; + +export type UseWorkOrdersTableDayViewParams = { + filters: FiltersReturn; + advActive: boolean; + filtered: WorkOrderTableRow[]; + advResults: WorkOrderTableRow[]; + onRevealed: (id: string | number) => void; +}; + +const EMPTY_WEEKLY_ITEMS: WorkOrderTableRow[] = []; + +export function useWorkOrdersTableDayView({ + filters, + advActive, + filtered, + advResults, + onRevealed, +}: UseWorkOrdersTableDayViewParams) { + const { scheduled, unscheduled } = useMemo(() => { + if (advActive) { + return splitScheduledUnscheduled(advResults); + } + return splitScheduledUnscheduled(filtered); + }, [advActive, advResults, filtered]); + + const dayGroups = useMemo( + () => + advActive + ? [] + : [buildSelectedDayGroup(filters.weekMonday, filters.selectedDayIso, scheduled)], + [advActive, filters.selectedDayIso, filters.weekMonday, scheduled], + ); + const pagination = useWorkOrdersDayPagination({ + weekMonday: filters.weekMonday, + selectedDayIso: filters.selectedDayIso, + advActive, + dayItems: dayGroups[0]?.items ?? EMPTY_WEEKLY_ITEMS, + onRevealed, + }); + const pagedDay = useMemo( + () => pageSelectedDayGroups(dayGroups, pagination.page, pagination.pageSize), + [dayGroups, pagination.page, pagination.pageSize], + ); + + const searchActive = filters.search.trim() !== ""; + const displayGroups = pagedDay.groups; + const dayTotalCount = pagedDay.totalCount; + const dayVisibleCount = pagedDay.groups[0]?.items.length ?? 0; + const noSearchResults = + !advActive && + isDayScopedSearchEmpty({ + searchActive, + unassignedCount: 0, + dayItemCount: dayTotalCount, + unassignedQueueReady: true, + }); + const isEmptyWeek = !advActive && scheduled.length === 0 && !searchActive; + const todayKey = + filters.weekMonday === currentWeekMonday() + ? deriveDayFields(new Date().toISOString().slice(0, 10)).dayGroup + : null; + + return { + scheduled, + unscheduled, + displayGroups, + searchActive, + noSearchResults, + isEmptyWeek, + todayKey, + dayTotalCount, + dayVisibleCount, + pagination, + pagedDay, + }; +} diff --git a/src/app/(protected)/workorders/_hooks/use-work-orders-table-local-state.ts b/src/app/(protected)/workorders/_hooks/use-work-orders-table-local-state.ts index 92ce2f98..86110721 100644 --- a/src/app/(protected)/workorders/_hooks/use-work-orders-table-local-state.ts +++ b/src/app/(protected)/workorders/_hooks/use-work-orders-table-local-state.ts @@ -1,14 +1,15 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "react-toastify"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; +import { applyBoardRowOrderOnScheduleChange } from "@/domain/work-orders/utils/apply-board-row-order-on-schedule-change"; import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils"; import { enrichTableRow } from "@/domain/work-orders/utils/table-filter-utils"; +import { isWorkOrderFullyLocked } from "@/domain/work-orders/utils/work-order-lock"; import { applyRowOrderByDay, boardRowOrderStorageKey, loadBoardRowOrder, mergeDayOrder, - moveIdToDayOrder, reorderIdsInList, saveBoardRowOrder, type BoardRowOrderByDay, @@ -115,15 +116,15 @@ export function useWorkOrdersTableLocalState({ if (patch.scheduledOn === undefined || advActive) return; - const nextDayGroup = deriveDayFields(patch.scheduledOn).dayGroup; - if (!previousDayGroup || previousDayGroup === nextDayGroup) return; - - setRowOrderByDay((prev) => { - const next = moveIdToDayOrder(prev, key, nextDayGroup); - if (next === prev) return prev; - saveBoardRowOrder(weekMonday, next, ownerId); - return next; - }); + setRowOrderByDay((prev) => + applyBoardRowOrderOnScheduleChange(prev, { + weekMonday, + ownerId, + id: key, + scheduledOn: patch.scheduledOn!, + previousDayGroup, + }), + ); }, [advActive, displayRows, localRows, ownerId, weekMonday], ); @@ -135,6 +136,7 @@ export function useWorkOrdersTableLocalState({ const dragRow = displayRows.find((r) => String(r.id) === dragKey); const overRow = displayRows.find((r) => String(r.id) === overKey); if (!dragRow || !overRow || dragRow.dayGroup !== overRow.dayGroup) return; + if (isWorkOrderFullyLocked(dragRow.status) || isWorkOrderFullyLocked(overRow.status)) return; const dayGroup = dragRow.dayGroup; const dayIds = displayRows diff --git a/src/domain/work-orders/utils/apply-board-row-order-on-schedule-change.ts b/src/domain/work-orders/utils/apply-board-row-order-on-schedule-change.ts new file mode 100644 index 00000000..7d467bb8 --- /dev/null +++ b/src/domain/work-orders/utils/apply-board-row-order-on-schedule-change.ts @@ -0,0 +1,46 @@ +import type { BoardRowOrderByDay } from "@/domain/work-orders/utils/board-row-order-storage"; +import { + moveIdToDayOrder, + removeIdFromRowOrder, + saveBoardRowOrder, + transferIdToDestinationWeekOrder, +} from "@/domain/work-orders/utils/board-row-order-storage"; +import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils"; +import { mondayOf } from "@/domain/work-orders/utils/wizard-date-utils"; + +export type ApplyBoardRowOrderOnScheduleChangeInput = { + weekMonday: string; + ownerId: string; + id: string; + scheduledOn: string; + previousDayGroup?: string; +}; + +export function applyBoardRowOrderOnScheduleChange( + orderByDay: BoardRowOrderByDay, + input: ApplyBoardRowOrderOnScheduleChangeInput, +): BoardRowOrderByDay { + const { weekMonday, ownerId, id, scheduledOn, previousDayGroup } = input; + const nextDayGroup = deriveDayFields(scheduledOn).dayGroup; + if (!previousDayGroup || previousDayGroup === nextDayGroup) { + return orderByDay; + } + + const destinationWeekMonday = mondayOf(scheduledOn.slice(0, 10)); + if (destinationWeekMonday !== weekMonday) { + transferIdToDestinationWeekOrder({ + sourceWeekMonday: weekMonday, + destinationWeekMonday, + ownerId, + id, + nextDayGroup, + }); + return removeIdFromRowOrder(orderByDay, id); + } + + const next = moveIdToDayOrder(orderByDay, id, nextDayGroup); + if (next !== orderByDay) { + saveBoardRowOrder(weekMonday, next, ownerId); + } + return next; +} diff --git a/src/domain/work-orders/utils/board-row-order-storage.ts b/src/domain/work-orders/utils/board-row-order-storage.ts index cac42107..18b7230c 100644 --- a/src/domain/work-orders/utils/board-row-order-storage.ts +++ b/src/domain/work-orders/utils/board-row-order-storage.ts @@ -127,13 +127,20 @@ export function neighborIdForReorder boolean = () => true, ): string | number | null { const dayRows = rows.filter((item) => item.dayGroup === row.dayGroup); const index = dayRows.findIndex((item) => String(item.id) === String(row.id)); if (index < 0) { return null; } - return dayRows[index + delta]?.id ?? null; + for (let cursor = index + delta; cursor >= 0 && cursor < dayRows.length; cursor += delta) { + const candidate = dayRows[cursor]; + if (candidate && isReorderTarget(candidate)) { + return candidate.id; + } + } + return null; } export function reorderIdsInList(ids: string[], dragId: string, overId: string): string[] | null { @@ -170,6 +177,43 @@ export function removeIdFromRowOrder( } /** Drop the id from every day, then append it to the destination day. */ +export type TransferIdToDestinationWeekOrderInput = { + sourceWeekMonday: string; + destinationWeekMonday: string; + ownerId: string; + id: string; + nextDayGroup: string; +}; + +/** Persist a moved row under the destination week when schedule crosses week boundaries. */ +export function transferIdToDestinationWeekOrder({ + sourceWeekMonday, + destinationWeekMonday, + ownerId, + id, + nextDayGroup, +}: TransferIdToDestinationWeekOrderInput): void { + if (!sourceWeekMonday || !destinationWeekMonday || !nextDayGroup) { + return; + } + if (sourceWeekMonday === destinationWeekMonday) { + return; + } + + const sourceOrder = loadBoardRowOrder(sourceWeekMonday, ownerId); + const sourceWithoutId = removeIdFromRowOrder(sourceOrder, id); + if (sourceWithoutId !== sourceOrder) { + saveBoardRowOrder(sourceWeekMonday, sourceWithoutId, ownerId); + } + + const destinationOrder = loadBoardRowOrder(destinationWeekMonday, ownerId); + saveBoardRowOrder( + destinationWeekMonday, + moveIdToDayOrder(destinationOrder, id, nextDayGroup), + ownerId, + ); +} + export function moveIdToDayOrder( orderByDay: BoardRowOrderByDay, id: string, diff --git a/src/test/app/(protected)/workorders/use-work-orders-table-local-state-order.test.tsx b/src/test/app/(protected)/workorders/use-work-orders-table-local-state-order.test.tsx index 5a1bc5e4..f99a0ea8 100644 --- a/src/test/app/(protected)/workorders/use-work-orders-table-local-state-order.test.tsx +++ b/src/test/app/(protected)/workorders/use-work-orders-table-local-state-order.test.tsx @@ -79,6 +79,7 @@ describe("useWorkOrdersTableLocalState row order", () => { afterEach(() => { localStorage.removeItem(boardRowOrderStorageKey(WEEK, "u1")); localStorage.removeItem(boardRowOrderStorageKey(WEEK)); + localStorage.removeItem(boardRowOrderStorageKey("2026-07-20", "u1")); }); it("reorders within a day and persists when advanced search is off", async () => { @@ -97,6 +98,25 @@ describe("useWorkOrdersTableLocalState row order", () => { expect(loadBoardRowOrder(WEEK, "u1")).toEqual({ "07/15": ["2", "1"] }); }); + it("does not persist a reorder onto a canceled row", async () => { + const items = [ + makeRow({ id: "1" }), + makeRow({ id: "2", woNumber: "WO-2", status: "Canceled" }), + ]; + const { result } = renderHook(() => useWorkOrdersTableLocalState(hookParams(items))); + + await waitFor(() => { + expect(result.current.displayRows.map((row) => String(row.id))).toEqual(["1", "2"]); + }); + + act(() => { + result.current.reorderRows("1", "2"); + }); + + expect(result.current.displayRows.map((row) => String(row.id))).toEqual(["1", "2"]); + expect(loadBoardRowOrder(WEEK, "u1")).toEqual({}); + }); + it("does not write storage while advanced search is active", async () => { const items = [makeRow({ id: "1" }), makeRow({ id: "2", woNumber: "WO-2" })]; const { result } = renderHook(() => @@ -141,6 +161,23 @@ describe("useWorkOrdersTableLocalState row order", () => { expect(loadBoardRowOrder(WEEK, "u1")).toEqual({ "07/15": ["2"], "07/16": ["1"] }); }); + it("writes cross-week schedule moves under the destination week key", async () => { + saveBoardRowOrder(WEEK, { "07/15": ["1", "2"] }, "u1"); + const items = [makeRow({ id: "1" }), makeRow({ id: "2", woNumber: "WO-2" })]; + const { result } = renderHook(() => useWorkOrdersTableLocalState(hookParams(items))); + + await waitFor(() => { + expect(result.current.displayRows).toHaveLength(2); + }); + + act(() => { + result.current.patchRow("1", { scheduledOn: "2026-07-20" }); + }); + + expect(loadBoardRowOrder(WEEK, "u1")).toEqual({ "07/15": ["2"] }); + expect(loadBoardRowOrder("2026-07-20", "u1")).toEqual({ "07/20": ["1"] }); + }); + it("reloads stored order when another session writes the same key", async () => { const items = [makeRow({ id: "1" }), makeRow({ id: "2", woNumber: "WO-2" })]; const { result } = renderHook(() => useWorkOrdersTableLocalState(hookParams(items))); diff --git a/src/test/app/(protected)/workorders/wo-table-row-reorder-keyboard.test.tsx b/src/test/app/(protected)/workorders/wo-table-row-reorder-keyboard.test.tsx index 3b59148c..13cfd51e 100644 --- a/src/test/app/(protected)/workorders/wo-table-row-reorder-keyboard.test.tsx +++ b/src/test/app/(protected)/workorders/wo-table-row-reorder-keyboard.test.tsx @@ -130,4 +130,17 @@ describe("WoTableRow keyboard reorder", () => { fireEvent.keyDown(grip, { key: "ArrowDown" }); expect(onReorder).not.toHaveBeenCalled(); }); + + it("skips a canceled neighbor when reordering an active row", () => { + const onReorder = vi.fn(); + const first = makeRow({ id: "1", woNumber: "WO-1" }); + const canceled = makeRow({ id: "2", woNumber: "WO-2", status: "Canceled" }); + const third = makeRow({ id: "3", woNumber: "WO-3" }); + renderRow(first, [first, canceled, third], handlers({ onReorder })); + + fireEvent.keyDown(screen.getByRole("button", { name: "Reorder work order WO-1" }), { + key: "ArrowDown", + }); + expect(onReorder).toHaveBeenCalledWith("1", "3"); + }); }); diff --git a/src/test/domain/work-orders/utils/board-row-order-storage.test.ts b/src/test/domain/work-orders/utils/board-row-order-storage.test.ts index 20bab014..63d9f984 100644 --- a/src/test/domain/work-orders/utils/board-row-order-storage.test.ts +++ b/src/test/domain/work-orders/utils/board-row-order-storage.test.ts @@ -11,6 +11,7 @@ import { legacyBoardRowOrderStorageKey, reorderIdsInList, saveBoardRowOrder, + transferIdToDestinationWeekOrder, } from "@/domain/work-orders/utils/board-row-order-storage"; describe("applyDayRowOrder", () => { @@ -67,6 +68,17 @@ describe("neighborIdForReorder", () => { expect(neighborIdForReorder(rows, rows[2]!, 1)).toBeNull(); expect(neighborIdForReorder(rows, { id: "x", dayGroup: "07/14" }, 1)).toBeNull(); }); + + it("skips neighbors that fail isReorderTarget", () => { + const dayRows = [ + { id: "a", dayGroup: "07/14", locked: false }, + { id: "b", dayGroup: "07/14", locked: true }, + { id: "c", dayGroup: "07/14", locked: false }, + ]; + expect(neighborIdForReorder(dayRows, dayRows[0]!, 1, (item) => !item.locked)).toBe("c"); + expect(neighborIdForReorder(dayRows, dayRows[2]!, -1, (item) => !item.locked)).toBe("a"); + expect(neighborIdForReorder(dayRows, dayRows[0]!, 1, () => false)).toBeNull(); + }); }); describe("reorderIdsInList", () => { @@ -115,6 +127,31 @@ describe("moveIdToDayOrder", () => { }); }); +describe("transferIdToDestinationWeekOrder", () => { + const sourceWeek = "2026-07-13"; + const destinationWeek = "2026-07-20"; + const ownerKeySource = boardRowOrderStorageKey(sourceWeek, "u1"); + const ownerKeyDestination = boardRowOrderStorageKey(destinationWeek, "u1"); + + afterEach(() => { + localStorage.removeItem(ownerKeySource); + localStorage.removeItem(ownerKeyDestination); + }); + + it("removes the id from the source week and appends it to the destination week day bucket", () => { + saveBoardRowOrder(sourceWeek, { "07/15": ["1", "2"] }, "u1"); + transferIdToDestinationWeekOrder({ + sourceWeekMonday: sourceWeek, + destinationWeekMonday: destinationWeek, + ownerId: "u1", + id: "1", + nextDayGroup: "07/20", + }); + expect(loadBoardRowOrder(sourceWeek, "u1")).toEqual({ "07/15": ["2"] }); + expect(loadBoardRowOrder(destinationWeek, "u1")).toEqual({ "07/20": ["1"] }); + }); +}); + describe("loadBoardRowOrder / saveBoardRowOrder", () => { const week = "2026-07-13"; const key = boardRowOrderStorageKey(week);