Files
ArchiTools/src/modules/registratura/hooks/use-registry.ts
T
AI Assistant c22848b471 perf(registratura): lightweight API mode strips base64 attachments from list
ROOT CAUSE: RegistryEntry stores file attachments as base64 strings in JSON.
A single 5MB PDF becomes ~6.7MB of base64. With 6 entries, the exportAll()
endpoint was sending 30-60MB of JSON on every page load  taking 2+ minutes.

Fix: Added ?lightweight=true parameter to /api/storage GET endpoint.
When enabled, stripHeavyFields() recursively removes large 'data' and
'fileData' string fields (>1KB) from JSON values, replacing with '__stripped__'.

Changes:
- /api/storage route.ts: stripHeavyFields() + lightweight query param
- StorageService.export(): accepts { lightweight?: boolean } option
- DatabaseStorageAdapter.export(): passes lightweight flag to API
- LocalStorageAdapter.export(): accepts option (no-op, localStorage is fast)
- useStorage.exportAll(): passes options through
- registry-service.ts: getAllEntries() uses lightweight=true by default
- registry-service.ts: new getFullEntry() loads single entry with full data
- use-registry.ts: exports loadFullEntry() for on-demand full loading
- registratura-module.tsx: handleEdit/handleNavigateEntry load full entry

Result: List loading transfers ~100KB instead of 30-60MB. Editing loads
full data for a single entry on demand (~5-10MB for one entry vs all).
2026-02-27 22:37:39 +02:00

287 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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<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">,
) => {
// 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<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.
* 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(
<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;
});
/**
* 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<RegistryEntry | null> => {
return getFullEntry(storage, id);
},
[storage],
);
return {
entries: filteredEntries,
allEntries: entries,
loading,
filters,
updateFilter,
addEntry,
updateEntry,
removeEntry,
closeEntry,
loadFullEntry,
addDeadline,
resolveDeadline,
removeDeadline,
refresh,
};
}