f8c19bb5b4
Root cause: even with SQL-level stripping, PostgreSQL must TOAST-decompress entire multi-MB JSONB values from disk before any processing. For 5 entries with PDF attachments (25-50MB total), this takes several seconds. Fix: store base64 attachment data in separate namespace 'registratura-blobs'. Main entries are inherently small (~1-2KB). List queries never touch heavy data. Changes: - registry-service.ts: extractBlobs/mergeBlobs split base64 on save/load, migrateEntryBlobs() one-time migration for existing entries - use-registry.ts: dual namespace (registratura + registratura-blobs), migration runs on first mount - registratura-module.tsx: removed useContacts/useTags hooks that triggered 2 unnecessary API fetches on page load (write-only ops use direct storage) Before: 3 API calls on mount, one reading 25-50MB from PostgreSQL After: 1 API call on mount, reading ~5-10KB total
288 lines
8.3 KiB
TypeScript
288 lines
8.3 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,
|
|
deleteEntry,
|
|
generateRegistryNumber,
|
|
migrateEntryBlobs,
|
|
} 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 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: run migration (once), then load entries
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
useEffect(() => {
|
|
const init = async () => {
|
|
if (!migrationRan.current) {
|
|
migrationRan.current = true;
|
|
await migrateEntryBlobs(storage, blobStorage);
|
|
}
|
|
await refresh();
|
|
};
|
|
init();
|
|
}, [refresh, storage, blobStorage]);
|
|
|
|
const addEntry = useCallback(
|
|
async (
|
|
data: Omit<RegistryEntry, "id" | "number" | "createdAt" | "updatedAt">,
|
|
) => {
|
|
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, blobStorage, entry);
|
|
setEntries((prev) => [entry, ...prev]);
|
|
return entry;
|
|
},
|
|
[storage, blobStorage],
|
|
);
|
|
|
|
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, blobStorage, updated);
|
|
await refresh();
|
|
},
|
|
[storage, blobStorage, refresh, entries],
|
|
);
|
|
|
|
const removeEntry = useCallback(
|
|
async (id: string) => {
|
|
await deleteEntry(storage, blobStorage, id);
|
|
await refresh();
|
|
},
|
|
[storage, blobStorage, 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();
|
|
const closedMain: RegistryEntry = {
|
|
...entry,
|
|
status: "inchis",
|
|
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) =>
|
|
saveEntry(storage, blobStorage, {
|
|
...e,
|
|
status: "inchis",
|
|
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],
|
|
);
|
|
|
|
return {
|
|
entries: filteredEntries,
|
|
allEntries: entries,
|
|
loading,
|
|
filters,
|
|
updateFilter,
|
|
addEntry,
|
|
updateEntry,
|
|
removeEntry,
|
|
closeEntry,
|
|
loadFullEntry,
|
|
addDeadline,
|
|
resolveDeadline,
|
|
removeDeadline,
|
|
refresh,
|
|
};
|
|
}
|