"use client"; import { useState, useEffect, useCallback } from "react"; import { useStorage } from "@/core/storage"; import { v4 as uuid } from "uuid"; import type { RegistryEntry, RegistryDirection, RegistryStatus, DocumentType, TrackedDeadline, DeadlineResolution, } from "../types"; import { getAllEntries, getFullEntry, saveEntry, deleteEntry, generateRegistryNumber, } from "../services/registry-service"; import { createTrackedDeadline, resolveDeadline as resolveDeadlineFn, } from "../services/deadline-service"; import { getDeadlineType } from "../services/deadline-catalog"; export interface RegistryFilters { search: string; direction: RegistryDirection | "all"; status: RegistryStatus | "all"; documentType: DocumentType | "all"; company: string; } export function useRegistry() { const storage = useStorage("registratura"); const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(true); const [filters, setFilters] = useState({ search: "", direction: "all", status: "all", documentType: "all", company: "all", }); const refresh = useCallback(async () => { setLoading(true); const items = await getAllEntries(storage); setEntries(items); setLoading(false); }, [storage]); // eslint-disable-next-line react-hooks/set-state-in-effect useEffect(() => { refresh(); }, [refresh]); const addEntry = useCallback( async ( data: Omit, ) => { // Fetch fresh entries to prevent duplicate numbers from stale state. // This single call replaces what was previously list() + N×get(). const freshEntries = await getAllEntries(storage); const now = new Date().toISOString(); const number = generateRegistryNumber( data.company, data.date, freshEntries, ); const entry: RegistryEntry = { ...data, id: uuid(), number, registrationDate: new Date().toISOString().slice(0, 10), createdAt: now, updatedAt: now, }; await saveEntry(storage, entry); // Update local state directly to avoid a second full fetch setEntries((prev) => [entry, ...prev]); return entry; }, [storage], ); const updateEntry = useCallback( async (id: string, updates: Partial) => { const existing = entries.find((e) => e.id === id); if (!existing) return; const updated: RegistryEntry = { ...existing, ...updates, id: existing.id, number: existing.number, createdAt: existing.createdAt, updatedAt: new Date().toISOString(), }; await saveEntry(storage, updated); await refresh(); }, [storage, refresh, entries], ); const removeEntry = useCallback( async (id: string) => { await deleteEntry(storage, id); await refresh(); }, [storage, refresh], ); /** Close an entry and optionally its linked entries. * Batches all saves, then does a single refresh at the end. */ const closeEntry = useCallback( async (id: string, closeLinked: boolean) => { const entry = entries.find((e) => e.id === id); if (!entry) return; // Save main entry as closed const now = new Date().toISOString(); const closedMain: RegistryEntry = { ...entry, status: "inchis", updatedAt: now, }; await saveEntry(storage, closedMain); // Close linked entries in parallel const linked = entry.linkedEntryIds ?? []; if (closeLinked && linked.length > 0) { const saves = linked .map((linkedId) => entries.find((e) => e.id === linkedId)) .filter((e): e is RegistryEntry => !!e && e.status !== "inchis") .map((e) => saveEntry(storage, { ...e, status: "inchis", updatedAt: now }), ); await Promise.all(saves); } // Single refresh at the end await refresh(); }, [entries, storage, refresh], ); const updateFilter = useCallback( (key: K, value: RegistryFilters[K]) => { setFilters((prev) => ({ ...prev, [key]: value })); }, [], ); // ── Deadline operations ── const addDeadline = useCallback( async ( entryId: string, typeId: string, startDate: string, chainParentId?: string, ) => { const entry = entries.find((e) => e.id === entryId); if (!entry) return null; const tracked = createTrackedDeadline(typeId, startDate, chainParentId); if (!tracked) return null; const existing = entry.trackedDeadlines ?? []; const updated: RegistryEntry = { ...entry, trackedDeadlines: [...existing, tracked], updatedAt: new Date().toISOString(), }; await saveEntry(storage, updated); await refresh(); return tracked; }, [entries, storage, refresh], ); const resolveDeadline = useCallback( async ( entryId: string, deadlineId: string, resolution: DeadlineResolution, note?: string, ): Promise => { const entry = entries.find((e) => e.id === entryId); if (!entry) return null; const deadlines = entry.trackedDeadlines ?? []; const idx = deadlines.findIndex((d) => d.id === deadlineId); if (idx === -1) return null; const dl = deadlines[idx]; if (!dl) return null; const resolved = resolveDeadlineFn(dl, resolution, note); const updatedDeadlines = [...deadlines]; updatedDeadlines[idx] = resolved; const updated: RegistryEntry = { ...entry, trackedDeadlines: updatedDeadlines, updatedAt: new Date().toISOString(), }; await saveEntry(storage, updated); // If the resolved deadline has a chain, automatically check for the next type const def = getDeadlineType(dl.typeId); await refresh(); if ( def?.chainNextTypeId && (resolution === "completed" || resolution === "aprobat-tacit") ) { return resolved; } return resolved; }, [entries, storage, refresh], ); const removeDeadline = useCallback( async (entryId: string, deadlineId: string) => { const entry = entries.find((e) => e.id === entryId); if (!entry) return; const deadlines = entry.trackedDeadlines ?? []; const updated: RegistryEntry = { ...entry, trackedDeadlines: deadlines.filter((d) => d.id !== deadlineId), updatedAt: new Date().toISOString(), }; await saveEntry(storage, updated); await refresh(); }, [entries, storage, refresh], ); const filteredEntries = entries.filter((entry) => { if (filters.direction !== "all" && entry.direction !== filters.direction) return false; if (filters.status !== "all" && entry.status !== filters.status) return false; if ( filters.documentType !== "all" && entry.documentType !== filters.documentType ) return false; if (filters.company !== "all" && entry.company !== filters.company) return false; if (filters.search) { const q = filters.search.toLowerCase(); return ( entry.subject.toLowerCase().includes(q) || entry.sender.toLowerCase().includes(q) || entry.recipient.toLowerCase().includes(q) || entry.number.toLowerCase().includes(q) ); } return true; }); /** * Load a single entry WITH full attachment data (for editing). * The list uses lightweight mode that strips base64 data. */ const loadFullEntry = useCallback( async (id: string): Promise => { return getFullEntry(storage, id); }, [storage], ); return { entries: filteredEntries, allEntries: entries, loading, filters, updateFilter, addEntry, updateEntry, removeEntry, closeEntry, loadFullEntry, addDeadline, resolveDeadline, removeDeadline, refresh, }; }