/
mirsud
/
casectrl
Обзор
Документация
Войти
/
mirsud
/
casectrl
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/lib/data-source/cases.ts
292 строки
10 KB
94StarKing94
ав
28 июл 2026, 13:44
28 июл 2026, 13:44
fd21e96
Код
Авторство
О чём код?
"use client"; /** * Слой данных о делах. * * Объединяет: * - карточки, импортированные из АМИРС (`source: "amirs"`) — read-only; * - локальные правки секретаря (заметки, статус «моё дело», * дополнительные поля). * * Логика: * - локальные дела помечаются `source: "local"` и редактируемы; * - АМИРС-дела помечаются `source: "amirs"` и защищены read-only-баннером; * - секретарь может «закрепить» любое дело (АМИРС или локальное), * закреплениe хранится в локальном оверлее `local-overlay`; * - заметки хранятся в локальном оверлее, чтобы не портить карточку АМИРС. * * Всё чтение идёт из {@link StorageRegistry}; ни UI, ни сторонние * модули не должны обращаться к `localStorage` / `IndexedDB` * напрямую — это контракт проекта. */ import { useCallback, useEffect, useState } from "react"; import type { AmirsCase, CaseCategory } from "../dashboard-data"; import { getStorageRegistry } from "../storage"; import { AMIRS_KEYS, readLocalAmirsSnapshot } from "../amirs"; /** Локальное «продолжающееся» дело, заведённое самим секретарём. */ export interface LocalCase { /** Локальный id (uuid-подобный). */ id: string; /** Номер дела (как ввёл секретарь). */ caseNumber: string; category: CaseCategory; stage: string; judge: string; plaintiff: string; defendant: string; /** Дата поступления (YYYY-MM-DD). */ filedAt: string; /** Дата и время создания локальной записи (ISO). */ createdAt: string; source: "local"; } /** Объединённое «дело» в UI: АМИРС или локальное. */ export type AnyCase = AmirsCase | LocalCase; /** Локальный оверлей — секретарские правки поверх АМИРС-карточки. */ export interface CaseOverlay { /** Закреплено ли дело в «Моих делах». */ pinned: boolean; /** Заметка секретаря по делу. */ note: string; /** Дата добавления записи (ISO). */ addedAt: string; } const LOCAL_CASES_KEY = "local:cases"; const OVERLAYS_KEY = "amirs:overlays"; // ключ для случая, когда меняем АМИРС-карточку const OVERLAY_PREFIX = "amirs:overlay:"; // точечные оверлеи по amirsId /* ------------------------------------------------------------------ */ /* Чтение / запись в реестр */ /* ------------------------------------------------------------------ */ async function readLocalCases(): Promise<LocalCase[]> { const reg = await getStorageRegistry(); return (await reg.kv.get<LocalCase[]>(LOCAL_CASES_KEY)) ?? []; } async function writeLocalCases(list: LocalCase[]): Promise<void> { const reg = await getStorageRegistry(); await reg.kv.set<LocalCase[]>(LOCAL_CASES_KEY, list); } async function readOverlay(amirsId: string): Promise<CaseOverlay | null> { const reg = await getStorageRegistry(); return (await reg.kv.get<CaseOverlay>(OVERLAY_PREFIX + amirsId)) ?? null; } async function writeOverlay(amirsId: string, overlay: CaseOverlay): Promise<void> { const reg = await getStorageRegistry(); await reg.kv.set<CaseOverlay>(OVERLAY_PREFIX + amirsId, overlay); } async function removeOverlay(amirsId: string): Promise<void> { const reg = await getStorageRegistry(); await reg.kv.remove(OVERLAY_PREFIX + amirsId); } /* ------------------------------------------------------------------ */ /* Публичный API */ /* ------------------------------------------------------------------ */ export interface CaseSource { amirsCases: AmirsCase[]; localCases: LocalCase[]; /** Снимок, готовый к UI — с уже применёнными оверлеями. */ unified: AnyCase[]; /** Оверлеи по amirsId. */ overlays: Record<string, CaseOverlay>; /** Загружено ли (первый hydrate после монтирования). */ hydrated: boolean; } export function useCaseSource(): CaseSource & { refresh: () => Promise<void>; addLocalCase: (data: Omit<LocalCase, "id" | "createdAt" | "source">) => Promise<string>; updateLocalCase: (id: string, patch: Partial<LocalCase>) => Promise<void>; removeLocalCase: (id: string) => Promise<void>; togglePin: (caseKey: string, source: "amirs" | "local", id: string) => Promise<void>; setNote: (caseKey: string, source: "amirs" | "local", id: string, note: string) => Promise<void>; isPinned: (caseKey: string) => boolean; getNote: (caseKey: string) => string; } { const [amirsCases, setAmirsCases] = useState<AmirsCase[]>([]); const [localCases, setLocalCases] = useState<LocalCase[]>([]); const [overlays, setOverlays] = useState<Record<string, CaseOverlay>>({}); const [hydrated, setHydrated] = useState(false); const refresh = useCallback(async () => { const reg = await getStorageRegistry(); const snap = await readLocalAmirsSnapshot(); const locals = await readLocalCases(); // Соберём оверлеи: сначала пробуем bulk-ключ, иначе точечные. const bulk = (await reg.kv.get<Record<string, CaseOverlay>>(OVERLAYS_KEY)) ?? {}; const pointIds = snap.cases.map((c) => c.amirsId); const pointEntries = await Promise.all( pointIds.map(async (id) => [id, await readOverlay(id)] as const) ); const pointMap: Record<string, CaseOverlay> = {}; for (const [id, ov] of pointEntries) { if (ov) pointMap[id] = ov; } setAmirsCases(snap.cases); setLocalCases(locals); setOverlays({ ...bulk, ...pointMap }); setHydrated(true); }, []); useEffect(() => { refresh(); }, [refresh]); // Подписка на изменения реестра — чтобы UI автоматически подхватывал // новые синхронизации с АМИРС. useEffect(() => { let unsub: (() => void) | null = null; let cancelled = false; (async () => { const reg = await getStorageRegistry(); if (cancelled) return; unsub = reg.subscribe(() => { refresh(); }); })(); return () => { cancelled = true; unsub?.(); }; }, [refresh]); const unified: AnyCase[] = [ ...amirsCases.map((c): AmirsCase => ({ ...c })), ...localCases.map((c): LocalCase => ({ ...c })), ]; const addLocalCase = useCallback( async (data: Omit<LocalCase, "id" | "createdAt" | "source">) => { const id = `lc-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; const next: LocalCase = { ...data, id, createdAt: new Date().toISOString(), source: "local", }; const current = await readLocalCases(); await writeLocalCases([next, ...current]); await refresh(); return id; }, [refresh] ); const updateLocalCase = useCallback( async (id: string, patch: Partial<LocalCase>) => { const current = await readLocalCases(); const next = current.map((c) => (c.id === id ? { ...c, ...patch } : c)); await writeLocalCases(next); await refresh(); }, [refresh] ); const removeLocalCase = useCallback( async (id: string) => { const current = await readLocalCases(); await writeLocalCases(current.filter((c) => c.id !== id)); await refresh(); }, [refresh] ); const caseKey = (source: "amirs" | "local", id: string) => `${source}:${id}`; const togglePin = useCallback( async (key: string, source: "amirs" | "local", id: string) => { if (source === "local") { const current = await readLocalCases(); const next = current.map((c) => c.id === id ? { ...c, // Локальные дела по умолчанию закреплены; тоггл // снимает/восстанавливает закрепление через оверлей. } : c ); await writeLocalCases(next); const reg = await getStorageRegistry(); const ov = (await reg.kv.get<CaseOverlay>(OVERLAY_PREFIX + key)) ?? { pinned: false, note: "", addedAt: new Date().toISOString(), }; await writeOverlay(key, { ...ov, pinned: !ov.pinned }); } else { const reg = await getStorageRegistry(); const ov = (await reg.kv.get<CaseOverlay>(OVERLAY_PREFIX + key)) ?? { pinned: false, note: "", addedAt: new Date().toISOString(), }; await writeOverlay(key, { ...ov, pinned: !ov.pinned }); } await refresh(); }, [refresh] ); const setNote = useCallback( async (key: string, _source: "amirs" | "local", _id: string, note: string) => { const reg = await getStorageRegistry(); const ov = (await reg.kv.get<CaseOverlay>(OVERLAY_PREFIX + key)) ?? { pinned: false, note: "", addedAt: new Date().toISOString(), }; await writeOverlay(key, { ...ov, note }); await refresh(); }, [refresh] ); const isPinned = useCallback( (key: string) => Boolean(overlays[key]?.pinned), [overlays] ); const getNote = useCallback( (key: string) => overlays[key]?.note ?? "", [overlays] ); return { amirsCases, localCases, unified, overlays, hydrated, refresh, addLocalCase, updateLocalCase, removeLocalCase, togglePin: (key, source, id) => togglePin(key, source, id), setNote: (key, source, id, note) => setNote(key, source, id, note), isPinned: (key) => isPinned(key), getNote: (key) => getNote(key), }; } export { caseKey as makeCaseKey }; /* ------------------------------------------------------------------ */ /* Реэкспорт для удобства */ /* ------------------------------------------------------------------ */ export { AMIRS_KEYS };