import { type Transaction, type TransactionStatusCode, type TransactionsRecord, transactionStatuses, } from "@olenbetong/appframe-updater"; import { useCallback, useEffect, useMemo, useState } from "react"; import type { Server } from "../lib/Server.js"; import type { TransactionsEditorRefreshOptions } from "./useTransactions.js"; type UseTransactionEditsOptions = { normalizedTransactions: Transaction[]; transactionRecordMap: Map; selectedPrimKeys: Set; server: Server; onActionMessage?: (message: string | null, color: "gray" | "red" | "yellow" | "green") => void; refresh: (options?: TransactionsEditorRefreshOptions) => Promise | void; }; type UseTransactionEditsResult = { pendingStatuses: Record; modifiedPrimKeys: Set; cycleStatus: (focusedPrimKey: string | null, options?: { exclusive?: boolean }) => void; save: (options?: { primKey?: string | null }) => Promise; saving: boolean; }; const STATUS_CYCLE = Object.freeze( (Object.values(transactionStatuses) as TransactionStatusCode[]).sort((a, b) => a - b), ); export function useTransactionEdits({ normalizedTransactions, transactionRecordMap, selectedPrimKeys, server, onActionMessage, refresh, }: UseTransactionEditsOptions): UseTransactionEditsResult { let [pendingStatuses, setPendingStatuses] = useState>({}); let [saving, setSaving] = useState(false); let normalizedTransactionMap = useMemo( () => new Map(normalizedTransactions.map((transaction) => [transaction.PrimKey, transaction])), [normalizedTransactions], ); let availablePrimKeys = useMemo( () => new Set(normalizedTransactions.map((transaction) => transaction.PrimKey)), [normalizedTransactions], ); useEffect(() => { setPendingStatuses((current) => { let changed = false; let next: Record = {}; for (let [key, value] of Object.entries(current)) { if (availablePrimKeys.has(key)) { next[key] = value; } else { changed = true; } } return changed ? next : current; }); }, [availablePrimKeys]); let modifiedPrimKeys = useMemo(() => { let modified = new Set(); for (let transaction of normalizedTransactions) { let override = pendingStatuses[transaction.PrimKey]; if (override !== undefined && override !== transaction.Status) { modified.add(transaction.PrimKey); } } return modified; }, [normalizedTransactions, pendingStatuses]); let cycleStatus = useCallback( (focusedPrimKey: string | null, options?: { exclusive?: boolean }) => { let targets: string[] = []; if (options?.exclusive) { if (focusedPrimKey) { targets = [focusedPrimKey]; } } else if (selectedPrimKeys.size > 0) { targets = Array.from(selectedPrimKeys); } else if (focusedPrimKey) { targets = [focusedPrimKey]; } if (targets.length === 0) { return; } setPendingStatuses((current) => { let next = { ...current }; let changed = false; for (let primKey of targets) { let transaction = normalizedTransactionMap.get(primKey); if (!transaction) { continue; } let originalStatus = transaction.Status; let currentStatus = current[primKey] ?? originalStatus; let cycleIndex = STATUS_CYCLE.indexOf(currentStatus); let nextStatus = STATUS_CYCLE[(cycleIndex + 1) % STATUS_CYCLE.length]; if (nextStatus === originalStatus) { if (primKey in next) { delete next[primKey]; changed = true; } } else if (next[primKey] !== nextStatus) { next[primKey] = nextStatus; changed = true; } } return changed ? next : current; }); }, [normalizedTransactionMap, selectedPrimKeys], ); let save = useCallback( async (options?: { primKey?: string | null }) => { if (saving) { return; } let restrictedPrimKeys = options?.primKey ? new Set([options.primKey]) : null; let updates = Object.entries(pendingStatuses) .filter(([primKey]) => !restrictedPrimKeys || restrictedPrimKeys.has(primKey)) .map(([primKey, status]) => { let record = transactionRecordMap.get(primKey); if (!record) { return null; } if (status === record.Status) { return null; } return { record, status }; }) .filter(Boolean) as Array<{ record: TransactionsRecord; status: number }>; if (updates.length === 0) { onActionMessage?.("No changes to save.", "gray"); return; } setSaving(true); onActionMessage?.(`Saving ${updates.length} update${updates.length === 1 ? "" : "s"}…`, "yellow"); try { await Promise.all( updates.map(({ record, status }) => { let payload = record.PrimKey ? { PrimKey: record.PrimKey, Status: status } : { ID: record.ID, Status: status }; return server.dsTransactions.update(payload); }), ); onActionMessage?.(`Saved ${updates.length} update${updates.length === 1 ? "" : "s"}.`, "green"); setPendingStatuses((current) => { let next = { ...current }; for (let { record } of updates) { delete next[record.PrimKey]; } return next; }); let anchor = updates[0]?.record.PrimKey ?? null; await refresh({ preserveViewport: true, anchorPrimKey: anchor }); } catch (caught) { let message = caught instanceof Error ? caught.message : String(caught); onActionMessage?.(`Failed to save changes: ${message}`, "red"); } finally { setSaving(false); } }, [pendingStatuses, refresh, saving, server, transactionRecordMap, onActionMessage], ); return { pendingStatuses, modifiedPrimKeys, cycleStatus, save, saving }; }