mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-02 01:33:12 +00:00
248 lines
7.1 KiB
TypeScript
248 lines
7.1 KiB
TypeScript
export type BoardRowOrderByDay = Record<string, string[]>;
|
|
|
|
const STORAGE_PREFIX = "shoc:wo-board-row-order:v1:";
|
|
|
|
export function boardRowOrderStorageKey(weekMonday: string, ownerId = ""): string {
|
|
const owner = ownerId.trim() || "local";
|
|
return `${STORAGE_PREFIX}${owner}:${weekMonday}`;
|
|
}
|
|
|
|
export function legacyBoardRowOrderStorageKey(weekMonday: string): string {
|
|
return `${STORAGE_PREFIX}${weekMonday}`;
|
|
}
|
|
|
|
function isStringArray(value: unknown): value is string[] {
|
|
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
}
|
|
|
|
function isBoardRowOrderByDay(value: unknown): value is BoardRowOrderByDay {
|
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
return false;
|
|
}
|
|
return Object.values(value).every(isStringArray);
|
|
}
|
|
|
|
function readStoredOrder(key: string): BoardRowOrderByDay {
|
|
try {
|
|
const raw = localStorage.getItem(key);
|
|
if (!raw) {
|
|
return {};
|
|
}
|
|
const parsed: unknown = JSON.parse(raw);
|
|
return isBoardRowOrderByDay(parsed) ? parsed : {};
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
export function loadBoardRowOrder(weekMonday: string, ownerId = ""): BoardRowOrderByDay {
|
|
if (typeof window === "undefined" || !weekMonday) {
|
|
return {};
|
|
}
|
|
|
|
const scoped = readStoredOrder(boardRowOrderStorageKey(weekMonday, ownerId));
|
|
if (Object.keys(scoped).length) {
|
|
return scoped;
|
|
}
|
|
return readStoredOrder(legacyBoardRowOrderStorageKey(weekMonday));
|
|
}
|
|
|
|
export function saveBoardRowOrder(
|
|
weekMonday: string,
|
|
orderByDay: BoardRowOrderByDay,
|
|
ownerId = "",
|
|
): void {
|
|
if (typeof window === "undefined" || !weekMonday) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
localStorage.setItem(boardRowOrderStorageKey(weekMonday, ownerId), JSON.stringify(orderByDay));
|
|
} catch {
|
|
// Quota exceeded or storage blocked — keep in-memory order only.
|
|
}
|
|
}
|
|
|
|
/** Reorder `items` so known ids follow `orderedIds`; unknown ids stay at the end (stable). */
|
|
export function applyDayRowOrder<T extends { id: string | number }>(
|
|
items: T[],
|
|
orderedIds: string[] | undefined,
|
|
): T[] {
|
|
if (!orderedIds?.length || items.length <= 1) {
|
|
return items;
|
|
}
|
|
|
|
const orderMap = new Map(orderedIds.map((id, idx) => [id, idx]));
|
|
return [...items].sort((a, b) => {
|
|
const ai = orderMap.get(String(a.id));
|
|
const bi = orderMap.get(String(b.id));
|
|
if (ai === undefined && bi === undefined) {
|
|
return 0;
|
|
}
|
|
if (ai === undefined) {
|
|
return 1;
|
|
}
|
|
if (bi === undefined) {
|
|
return -1;
|
|
}
|
|
return ai - bi;
|
|
});
|
|
}
|
|
|
|
/** Apply saved per-day orders across a flat row list (preserves relative order across days). */
|
|
export function applyRowOrderByDay<T extends { id: string | number; dayGroup: string }>(
|
|
items: T[],
|
|
orderByDay: BoardRowOrderByDay,
|
|
): T[] {
|
|
if (!items.length || !Object.keys(orderByDay).length) {
|
|
return items;
|
|
}
|
|
|
|
const byDay = new Map<string, T[]>();
|
|
for (const item of items) {
|
|
const group = byDay.get(item.dayGroup);
|
|
if (group) {
|
|
group.push(item);
|
|
} else {
|
|
byDay.set(item.dayGroup, [item]);
|
|
}
|
|
}
|
|
|
|
const orderedByDay = new Map<string, T[]>();
|
|
for (const [dayGroup, dayItems] of byDay) {
|
|
orderedByDay.set(dayGroup, applyDayRowOrder(dayItems, orderByDay[dayGroup]));
|
|
}
|
|
|
|
const nextIndex = new Map<string, number>();
|
|
return items.map((item) => {
|
|
const dayItems = orderedByDay.get(item.dayGroup)!;
|
|
const idx = nextIndex.get(item.dayGroup) ?? 0;
|
|
nextIndex.set(item.dayGroup, idx + 1);
|
|
return dayItems[idx]!;
|
|
});
|
|
}
|
|
|
|
/** Same-day neighbor for keyboard reorder; null at list ends or if the row is missing. */
|
|
export function neighborIdForReorder<T extends { id: string | number; dayGroup: string }>(
|
|
rows: T[],
|
|
row: T,
|
|
delta: -1 | 1,
|
|
isReorderTarget: (item: T) => 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;
|
|
}
|
|
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 {
|
|
const dragIdx = ids.indexOf(dragId);
|
|
const overIdx = ids.indexOf(overId);
|
|
if (dragIdx < 0 || overIdx < 0 || dragIdx === overIdx) {
|
|
return null;
|
|
}
|
|
const next = [...ids];
|
|
next.splice(dragIdx, 1);
|
|
next.splice(overIdx, 0, dragId);
|
|
return next;
|
|
}
|
|
|
|
/** Remove an id from every day bucket (used when a row changes dayGroup). */
|
|
export function removeIdFromRowOrder(
|
|
orderByDay: BoardRowOrderByDay,
|
|
id: string,
|
|
): BoardRowOrderByDay {
|
|
let changed = false;
|
|
const next: BoardRowOrderByDay = {};
|
|
for (const [dayGroup, ids] of Object.entries(orderByDay)) {
|
|
if (!ids.includes(id)) {
|
|
next[dayGroup] = ids;
|
|
continue;
|
|
}
|
|
changed = true;
|
|
const filtered = ids.filter((item) => item !== id);
|
|
if (filtered.length) {
|
|
next[dayGroup] = filtered;
|
|
}
|
|
}
|
|
return changed ? next : orderByDay;
|
|
}
|
|
|
|
/** 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,
|
|
nextDayGroup: string,
|
|
): BoardRowOrderByDay {
|
|
if (!nextDayGroup) {
|
|
return removeIdFromRowOrder(orderByDay, id);
|
|
}
|
|
const without = removeIdFromRowOrder(orderByDay, id);
|
|
const dest = without[nextDayGroup] ?? [];
|
|
if (dest.includes(id)) {
|
|
return without;
|
|
}
|
|
return { ...without, [nextDayGroup]: [...dest, id] };
|
|
}
|
|
|
|
/**
|
|
* Merge a visible day's new order into stored order: visible ids take the new sequence,
|
|
* ids that were stored but not currently visible stay after the visible block (preserving
|
|
* their prior relative order).
|
|
*/
|
|
export function mergeDayOrder(
|
|
previousDayIds: string[] | undefined,
|
|
visibleOrderedIds: string[],
|
|
): string[] {
|
|
if (!previousDayIds?.length) {
|
|
return visibleOrderedIds;
|
|
}
|
|
const visibleSet = new Set(visibleOrderedIds);
|
|
const hidden = previousDayIds.filter((id) => !visibleSet.has(id));
|
|
return [...visibleOrderedIds, ...hidden];
|
|
}
|