Files
ArchiTools/src/modules/registratura/hooks/use-registry.ts
Marius Tarau bb01268bcb feat(registratura): add legal deadline tracking system (Termene Legale)
Full deadline tracking engine for Romanian construction permitting:
- 16 deadline types across 5 categories (Avize, Completări, Analiză, Autorizare, Publicitate)
- Working days vs calendar days with Romanian public holidays (Orthodox Easter via Meeus)
- Backward deadlines (AC extension: 45 working days BEFORE expiry)
- Chain deadlines (resolving one prompts adding the next)
- Tacit approval auto-detection (overdue + applicable type)
- Tabbed UI: Registru + Termene legale dashboard with stats/filters/table
- Inline deadline cards in entry form with add/resolve/remove
- Clock icon + count badge on registry table for entries with deadlines

Also adds CLAUDE.md with full project context for AI assistant handoff.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 11:27:34 +02:00

200 lines
6.8 KiB
TypeScript

'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, 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<RegistryEntry[]>([]);
const [loading, setLoading] = useState(true);
const [filters, setFilters] = useState<RegistryFilters>({
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<RegistryEntry, 'id' | 'number' | 'createdAt' | 'updatedAt'>) => {
const now = new Date().toISOString();
const number = generateRegistryNumber(data.company, data.date, entries);
const entry: RegistryEntry = {
...data,
id: uuid(),
number,
createdAt: now,
updatedAt: now,
};
await saveEntry(storage, entry);
await refresh();
return entry;
}, [storage, refresh, entries]);
const updateEntry = useCallback(async (id: string, updates: Partial<RegistryEntry>) => {
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 */
const closeEntry = useCallback(async (id: string, closeLinked: boolean) => {
const entry = entries.find((e) => e.id === id);
if (!entry) return;
await updateEntry(id, { status: 'inchis' });
const linked = entry.linkedEntryIds ?? [];
if (closeLinked && linked.length > 0) {
for (const linkedId of linked) {
const linked = entries.find((e) => e.id === linkedId);
if (linked && linked.status !== 'inchis') {
const updatedLinked: RegistryEntry = {
...linked,
status: 'inchis',
updatedAt: new Date().toISOString(),
};
await saveEntry(storage, updatedLinked);
}
}
await refresh();
}
}, [entries, updateEntry, storage, 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, updated);
await refresh();
return tracked;
}, [entries, storage, 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, 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;
});
return {
entries: filteredEntries,
allEntries: entries,
loading,
filters,
updateFilter,
addEntry,
updateEntry,
removeEntry,
closeEntry,
addDeadline,
resolveDeadline,
removeDeadline,
refresh,
};
}