"use client"; import { useState, useEffect, useCallback, useRef } 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, } from "../services/registry-service"; import type { RegistryAuditEvent } from "../types"; 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 blobStorage = useStorage("registratura-blobs"); const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(true); const [filters, setFilters] = useState({ search: "", direction: "all", status: "all", documentType: "all", company: "all", }); const migrationRan = useRef(false); const refresh = useCallback(async () => { setLoading(true); const items = await getAllEntries(storage); setEntries(items); setLoading(false); }, [storage]); // On mount: trigger server-side blob migration (fire-and-forget), then load list // eslint-disable-next-line react-hooks/set-state-in-effect useEffect(() => { const init = async () => { // Trigger server-side migration (runs inside Node.js, not browser) if (!migrationRan.current) { migrationRan.current = true; fetch("/api/storage/migrate-blobs", { method: "POST" }).catch(() => {}); } await refresh(); }; init(); }, [refresh]); const addEntry = useCallback( async ( data: Omit, ) => { // Use the API for atomic server-side numbering + audit const res = await fetch("/api/registratura", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ entry: data }), }); const result = await res.json(); if (!result.success) { throw new Error(result.error ?? "Failed to create entry"); } const entry = result.entry as RegistryEntry; setEntries((prev) => [entry, ...prev]); return entry; }, [], ); const updateEntry = useCallback( async (id: string, updates: Partial) => { // Use the API for server-side diff audit const res = await fetch("/api/registratura", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id, updates }), }); const result = await res.json(); if (!result.success) { throw new Error(result.error ?? "Failed to update entry"); } await refresh(); }, [refresh], ); const removeEntry = useCallback( async (id: string) => { // Use the API for sequence validation + audit logging const res = await fetch(`/api/registratura?id=${encodeURIComponent(id)}`, { method: "DELETE", }); const result = await res.json(); if (!result.success) { throw new Error(result.error ?? "Failed to delete entry"); } await refresh(); }, [refresh], ); const closeEntry = useCallback( async (id: string, closeLinked: boolean) => { const entry = entries.find((e) => e.id === id); if (!entry) return; const now = new Date().toISOString(); // Auto-resolve all pending deadlines when closing an entry const resolvedDeadlines = (entry.trackedDeadlines ?? []).map((dl) => { if (dl.resolution === "pending") { return resolveDeadlineFn(dl, "completed", "Rezolvat automat la închiderea înregistrării"); } return dl; }); const closedMain: RegistryEntry = { ...entry, status: "inchis", trackedDeadlines: resolvedDeadlines, updatedAt: now, }; await saveEntry(storage, blobStorage, closedMain); 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) => { const resolvedDls = (e.trackedDeadlines ?? []).map((dl) => dl.resolution === "pending" ? resolveDeadlineFn(dl, "completed", "Rezolvat automat la închiderea înregistrării") : dl, ); return saveEntry(storage, blobStorage, { ...e, status: "inchis", trackedDeadlines: resolvedDls, updatedAt: now, }); }); await Promise.all(saves); } await refresh(); }, [entries, storage, blobStorage, 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, blobStorage, updated); await refresh(); return tracked; }, [entries, storage, blobStorage, 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, blobStorage, updated); const def = getDeadlineType(dl.typeId); await refresh(); if ( def?.chainNextTypeId && (resolution === "completed" || resolution === "aprobat-tacit") ) { return resolved; } return resolved; }, [entries, storage, blobStorage, 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, blobStorage, updated); await refresh(); }, [entries, storage, blobStorage, 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; }); const loadFullEntry = useCallback( async (id: string): Promise => { return getFullEntry(storage, blobStorage, id); }, [storage, blobStorage], ); // ── Reserved slots ── const createReservedSlots = useCallback( async (company: string, year: number, month: number) => { const res = await fetch("/api/registratura/reserved", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ company, year, month }), }); const result = await res.json(); if (result.success) await refresh(); return result; }, [refresh], ); // ── Audit trail ── const loadAuditHistory = useCallback( async (entryId: string): Promise => { const res = await fetch( `/api/registratura/audit?entryId=${encodeURIComponent(entryId)}`, ); const result = await res.json(); return result.events ?? []; }, [], ); return { entries: filteredEntries, allEntries: entries, loading, filters, updateFilter, addEntry, updateEntry, removeEntry, closeEntry, loadFullEntry, addDeadline, resolveDeadline, removeDeadline, createReservedSlots, loadAuditHistory, refresh, }; }