Files
ArchiTools/src/modules/registratura/hooks/use-registry.ts
T
AI Assistant 1361534c98 fix: closed entries no longer show in deadline dashboard
- groupDeadlinesByEntry skips entries with status "inchis"
- closeEntry auto-resolves all pending deadlines on close (main + linked)
- Fixes S-2026-00001 showing as overdue despite being closed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 17:26:20 +02:00

341 lines
10 KiB
TypeScript

"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<RegistryEntry[]>([]);
const [loading, setLoading] = useState(true);
const [filters, setFilters] = useState<RegistryFilters>({
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<RegistryEntry, "id" | "number" | "createdAt" | "updatedAt">,
) => {
// 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<RegistryEntry>) => {
// 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(
<K extends keyof RegistryFilters>(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<TrackedDeadline | null> => {
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<RegistryEntry | null> => {
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<RegistryAuditEvent[]> => {
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,
};
}