mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 17:02:05 +00:00
fix(work-orders): skip canceled keyboard reorder and persist destination-week order
This commit is contained in:
parent
dc178340de
commit
ddd83ffec1
11 changed files with 329 additions and 90 deletions
|
|
@ -28,9 +28,17 @@ export function WoTableRowReorderHandle({
|
||||||
aria-label={woNumber ? `Reorder work order ${woNumber}` : "Reorder work order"}
|
aria-label={woNumber ? `Reorder work order ${woNumber}` : "Reorder work order"}
|
||||||
className="flex items-center justify-center bg-transparent p-0"
|
className="flex items-center justify-center bg-transparent p-0"
|
||||||
style={{
|
style={{
|
||||||
|
appearance: "none",
|
||||||
cursor: gripCursor(disabled, isDragging),
|
cursor: gripCursor(disabled, isDragging),
|
||||||
color: "var(--color-text-muted)",
|
color: "var(--color-text-muted)",
|
||||||
border: "none",
|
border: "none",
|
||||||
|
background: "transparent",
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
lineHeight: 0,
|
||||||
|
font: "inherit",
|
||||||
|
minWidth: 0,
|
||||||
|
minHeight: 0,
|
||||||
}}
|
}}
|
||||||
onClick={(event) => event.stopPropagation()}
|
onClick={(event) => event.stopPropagation()}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,12 @@ export function WoTableRow({
|
||||||
isDragging={isDragging}
|
isDragging={isDragging}
|
||||||
woNumber={row.woNumber}
|
woNumber={row.woNumber}
|
||||||
onArrowReorder={(delta) => {
|
onArrowReorder={(delta) => {
|
||||||
const overId = neighborIdForReorder(allRows, row, delta);
|
const overId = neighborIdForReorder(
|
||||||
|
allRows,
|
||||||
|
row,
|
||||||
|
delta,
|
||||||
|
(item) => !isWorkOrderFullyLocked(item.status),
|
||||||
|
);
|
||||||
if (overId == null) return;
|
if (overId == null) return;
|
||||||
handlers.onReorder(row.id, overId);
|
handlers.onReorder(row.id, overId);
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -1,26 +1,13 @@
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import type { useWorkOrdersListFilters } from "@/app/(protected)/workorders/_hooks/use-work-orders-list-filters";
|
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 { 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 { 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 { 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 type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils";
|
import { pickBoardPager, type DayPageSize } from "@/domain/work-orders/utils/day-pagination";
|
||||||
import {
|
import { filterByBarAndSearch } from "@/domain/work-orders/utils/table-filter-utils";
|
||||||
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";
|
|
||||||
|
|
||||||
export type { DragVizState } from "@/app/(protected)/workorders/_hooks/use-work-orders-table-local-state";
|
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);
|
useWorkOrdersBoardSource(filters);
|
||||||
|
|
||||||
const weeklyItems = data?.items ?? EMPTY_WEEKLY_ITEMS;
|
const weeklyItems = data?.items ?? EMPTY_WEEKLY_ITEMS;
|
||||||
const user = getAuthUser();
|
const ownerId = resolveBoardRowOrderOwnerId();
|
||||||
const ownerId = user?.id == null ? "" : String(user.id);
|
|
||||||
|
|
||||||
const local = useWorkOrdersTableLocalState({
|
const local = useWorkOrdersTableLocalState({
|
||||||
items: weeklyItems,
|
items: weeklyItems,
|
||||||
|
|
@ -58,57 +44,22 @@ export function useWorkOrdersTableData(filters: FiltersReturn) {
|
||||||
return displayRows;
|
return displayRows;
|
||||||
}, [displayRows, filters.barFilters, filters.search, advActive]);
|
}, [displayRows, filters.barFilters, filters.search, advActive]);
|
||||||
|
|
||||||
const { scheduled, unscheduled } = useMemo(() => {
|
const dayView = useWorkOrdersTableDayView({
|
||||||
if (advActive) {
|
filters,
|
||||||
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,
|
advActive,
|
||||||
dayItems: dayGroups[0]?.items ?? EMPTY_WEEKLY_ITEMS,
|
filtered,
|
||||||
|
advResults,
|
||||||
onRevealed: local.triggerFlash,
|
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(
|
const selectedRow = useMemo(
|
||||||
() => displayRows.find((row) => String(row.id) === String(local.selectedId)) ?? null,
|
() => displayRows.find((row) => String(row.id) === String(local.selectedId)) ?? null,
|
||||||
[displayRows, local.selectedId],
|
[displayRows, local.selectedId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const scopeTotalCount = advActive ? (data?.totalCount ?? advResults.length) : dayTotalCount;
|
const scopeTotalCount = advActive
|
||||||
|
? (data?.totalCount ?? advResults.length)
|
||||||
|
: dayView.dayTotalCount;
|
||||||
useSyncOutOfRangeSearchPage({
|
useSyncOutOfRangeSearchPage({
|
||||||
enabled: advActive,
|
enabled: advActive,
|
||||||
ready: !isLoading,
|
ready: !isLoading,
|
||||||
|
|
@ -117,11 +68,11 @@ export function useWorkOrdersTableData(filters: FiltersReturn) {
|
||||||
totalCount: scopeTotalCount,
|
totalCount: scopeTotalCount,
|
||||||
setPage: filters.setPage,
|
setPage: filters.setPage,
|
||||||
});
|
});
|
||||||
const listTotalCount = advActive ? advResults.length : dayVisibleCount;
|
const listTotalCount = advActive ? advResults.length : dayView.dayVisibleCount;
|
||||||
const pager = pickBoardPager({
|
const pager = pickBoardPager({
|
||||||
advActive,
|
advActive,
|
||||||
isLoading,
|
isLoading,
|
||||||
noSearchResults,
|
noSearchResults: dayView.noSearchResults,
|
||||||
advResultCount: advResults.length,
|
advResultCount: advResults.length,
|
||||||
search: {
|
search: {
|
||||||
page: filters.page,
|
page: filters.page,
|
||||||
|
|
@ -129,11 +80,11 @@ export function useWorkOrdersTableData(filters: FiltersReturn) {
|
||||||
totalCount: scopeTotalCount,
|
totalCount: scopeTotalCount,
|
||||||
},
|
},
|
||||||
day: {
|
day: {
|
||||||
pageSize: pagination.pageSize,
|
pageSize: dayView.pagination.pageSize,
|
||||||
pageIndex: pagedDay.pageIndex,
|
pageIndex: dayView.pagedDay.pageIndex,
|
||||||
pageCount: pagedDay.pageCount,
|
pageCount: dayView.pagedDay.pageCount,
|
||||||
itemCount: dayTotalCount,
|
itemCount: dayView.dayTotalCount,
|
||||||
footerLabel: displayGroups[0]?.label ?? "",
|
footerLabel: dayView.displayGroups[0]?.label ?? "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -149,12 +100,12 @@ export function useWorkOrdersTableData(filters: FiltersReturn) {
|
||||||
unassignedTotalCount: 0,
|
unassignedTotalCount: 0,
|
||||||
unassignedError: null,
|
unassignedError: null,
|
||||||
unassignedLoading: false,
|
unassignedLoading: false,
|
||||||
unscheduled,
|
unscheduled: dayView.unscheduled,
|
||||||
displayGroups,
|
displayGroups: dayView.displayGroups,
|
||||||
searchActive,
|
searchActive: dayView.searchActive,
|
||||||
noSearchResults,
|
noSearchResults: dayView.noSearchResults,
|
||||||
isEmptyWeek,
|
isEmptyWeek: dayView.isEmptyWeek,
|
||||||
todayKey,
|
todayKey: dayView.todayKey,
|
||||||
...localUi,
|
...localUi,
|
||||||
selectedRow,
|
selectedRow,
|
||||||
scopeTotalCount,
|
scopeTotalCount,
|
||||||
|
|
@ -171,14 +122,14 @@ export function useWorkOrdersTableData(filters: FiltersReturn) {
|
||||||
filters.setPageSize(next);
|
filters.setPageSize(next);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pagination.setPageSize(next);
|
dayView.pagination.setPageSize(next);
|
||||||
},
|
},
|
||||||
goToPrevDayPage: advActive
|
goToPrevDayPage: advActive
|
||||||
? () => filters.setPage(Math.max(0, pager.pageIndex - 1))
|
? () => filters.setPage(Math.max(0, pager.pageIndex - 1))
|
||||||
: pagination.goToPrevPage,
|
: dayView.pagination.goToPrevPage,
|
||||||
goToNextDayPage: advActive
|
goToNextDayPage: advActive
|
||||||
? () => filters.setPage(Math.min(pager.pageIndex + 1, Math.max(0, pager.pageCount - 1)))
|
? () => filters.setPage(Math.min(pager.pageIndex + 1, Math.max(0, pager.pageCount - 1)))
|
||||||
: pagination.goToNextPage,
|
: dayView.pagination.goToNextPage,
|
||||||
revealScheduledRow: pagination.revealRow,
|
revealScheduledRow: dayView.pagination.revealRow,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<typeof useWorkOrdersListFilters>;
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
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 { applyBoardRowOrderOnScheduleChange } from "@/domain/work-orders/utils/apply-board-row-order-on-schedule-change";
|
||||||
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 { isWorkOrderFullyLocked } from "@/domain/work-orders/utils/work-order-lock";
|
||||||
import {
|
import {
|
||||||
applyRowOrderByDay,
|
applyRowOrderByDay,
|
||||||
boardRowOrderStorageKey,
|
boardRowOrderStorageKey,
|
||||||
loadBoardRowOrder,
|
loadBoardRowOrder,
|
||||||
mergeDayOrder,
|
mergeDayOrder,
|
||||||
moveIdToDayOrder,
|
|
||||||
reorderIdsInList,
|
reorderIdsInList,
|
||||||
saveBoardRowOrder,
|
saveBoardRowOrder,
|
||||||
type BoardRowOrderByDay,
|
type BoardRowOrderByDay,
|
||||||
|
|
@ -115,15 +116,15 @@ export function useWorkOrdersTableLocalState({
|
||||||
|
|
||||||
if (patch.scheduledOn === undefined || advActive) return;
|
if (patch.scheduledOn === undefined || advActive) return;
|
||||||
|
|
||||||
const nextDayGroup = deriveDayFields(patch.scheduledOn).dayGroup;
|
setRowOrderByDay((prev) =>
|
||||||
if (!previousDayGroup || previousDayGroup === nextDayGroup) return;
|
applyBoardRowOrderOnScheduleChange(prev, {
|
||||||
|
weekMonday,
|
||||||
setRowOrderByDay((prev) => {
|
ownerId,
|
||||||
const next = moveIdToDayOrder(prev, key, nextDayGroup);
|
id: key,
|
||||||
if (next === prev) return prev;
|
scheduledOn: patch.scheduledOn!,
|
||||||
saveBoardRowOrder(weekMonday, next, ownerId);
|
previousDayGroup,
|
||||||
return next;
|
}),
|
||||||
});
|
);
|
||||||
},
|
},
|
||||||
[advActive, displayRows, localRows, ownerId, weekMonday],
|
[advActive, displayRows, localRows, ownerId, weekMonday],
|
||||||
);
|
);
|
||||||
|
|
@ -135,6 +136,7 @@ export function useWorkOrdersTableLocalState({
|
||||||
const dragRow = displayRows.find((r) => String(r.id) === dragKey);
|
const dragRow = displayRows.find((r) => String(r.id) === dragKey);
|
||||||
const overRow = displayRows.find((r) => String(r.id) === overKey);
|
const overRow = displayRows.find((r) => String(r.id) === overKey);
|
||||||
if (!dragRow || !overRow || dragRow.dayGroup !== overRow.dayGroup) return;
|
if (!dragRow || !overRow || dragRow.dayGroup !== overRow.dayGroup) return;
|
||||||
|
if (isWorkOrderFullyLocked(dragRow.status) || isWorkOrderFullyLocked(overRow.status)) return;
|
||||||
|
|
||||||
const dayGroup = dragRow.dayGroup;
|
const dayGroup = dragRow.dayGroup;
|
||||||
const dayIds = displayRows
|
const dayIds = displayRows
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -127,13 +127,20 @@ export function neighborIdForReorder<T extends { id: string | number; dayGroup:
|
||||||
rows: T[],
|
rows: T[],
|
||||||
row: T,
|
row: T,
|
||||||
delta: -1 | 1,
|
delta: -1 | 1,
|
||||||
|
isReorderTarget: (item: T) => boolean = () => true,
|
||||||
): string | number | null {
|
): string | number | null {
|
||||||
const dayRows = rows.filter((item) => item.dayGroup === row.dayGroup);
|
const dayRows = rows.filter((item) => item.dayGroup === row.dayGroup);
|
||||||
const index = dayRows.findIndex((item) => String(item.id) === String(row.id));
|
const index = dayRows.findIndex((item) => String(item.id) === String(row.id));
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
return null;
|
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 {
|
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. */
|
/** 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(
|
export function moveIdToDayOrder(
|
||||||
orderByDay: BoardRowOrderByDay,
|
orderByDay: BoardRowOrderByDay,
|
||||||
id: string,
|
id: string,
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ describe("useWorkOrdersTableLocalState row order", () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
localStorage.removeItem(boardRowOrderStorageKey(WEEK, "u1"));
|
localStorage.removeItem(boardRowOrderStorageKey(WEEK, "u1"));
|
||||||
localStorage.removeItem(boardRowOrderStorageKey(WEEK));
|
localStorage.removeItem(boardRowOrderStorageKey(WEEK));
|
||||||
|
localStorage.removeItem(boardRowOrderStorageKey("2026-07-20", "u1"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reorders within a day and persists when advanced search is off", async () => {
|
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"] });
|
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 () => {
|
it("does not write storage while advanced search is active", async () => {
|
||||||
const items = [makeRow({ id: "1" }), makeRow({ id: "2", woNumber: "WO-2" })];
|
const items = [makeRow({ id: "1" }), makeRow({ id: "2", woNumber: "WO-2" })];
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
|
|
@ -141,6 +161,23 @@ describe("useWorkOrdersTableLocalState row order", () => {
|
||||||
expect(loadBoardRowOrder(WEEK, "u1")).toEqual({ "07/15": ["2"], "07/16": ["1"] });
|
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 () => {
|
it("reloads stored order when another session writes the same key", async () => {
|
||||||
const items = [makeRow({ id: "1" }), makeRow({ id: "2", woNumber: "WO-2" })];
|
const items = [makeRow({ id: "1" }), makeRow({ id: "2", woNumber: "WO-2" })];
|
||||||
const { result } = renderHook(() => useWorkOrdersTableLocalState(hookParams(items)));
|
const { result } = renderHook(() => useWorkOrdersTableLocalState(hookParams(items)));
|
||||||
|
|
|
||||||
|
|
@ -130,4 +130,17 @@ describe("WoTableRow keyboard reorder", () => {
|
||||||
fireEvent.keyDown(grip, { key: "ArrowDown" });
|
fireEvent.keyDown(grip, { key: "ArrowDown" });
|
||||||
expect(onReorder).not.toHaveBeenCalled();
|
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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
legacyBoardRowOrderStorageKey,
|
legacyBoardRowOrderStorageKey,
|
||||||
reorderIdsInList,
|
reorderIdsInList,
|
||||||
saveBoardRowOrder,
|
saveBoardRowOrder,
|
||||||
|
transferIdToDestinationWeekOrder,
|
||||||
} from "@/domain/work-orders/utils/board-row-order-storage";
|
} from "@/domain/work-orders/utils/board-row-order-storage";
|
||||||
|
|
||||||
describe("applyDayRowOrder", () => {
|
describe("applyDayRowOrder", () => {
|
||||||
|
|
@ -67,6 +68,17 @@ describe("neighborIdForReorder", () => {
|
||||||
expect(neighborIdForReorder(rows, rows[2]!, 1)).toBeNull();
|
expect(neighborIdForReorder(rows, rows[2]!, 1)).toBeNull();
|
||||||
expect(neighborIdForReorder(rows, { id: "x", dayGroup: "07/14" }, 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", () => {
|
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", () => {
|
describe("loadBoardRowOrder / saveBoardRowOrder", () => {
|
||||||
const week = "2026-07-13";
|
const week = "2026-07-13";
|
||||||
const key = boardRowOrderStorageKey(week);
|
const key = boardRowOrderStorageKey(week);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue