feat(registratura): atomic numbering, reserved slots, audit trail, API endpoints + theme toggle animation
Registratura module: - Atomic sequence numbering (BTG-2026-IN-00125 format) via PostgreSQL upsert - Reserved monthly slots (2/company/month) for late registrations - Append-only audit trail with diff tracking - REST API: /api/registratura (CRUD), /api/registratura/reserved, /api/registratura/audit - Auth: NextAuth session + Bearer API key support - New "intern" direction type with UI support (form, filters, table, detail panel) - Prisma models: RegistrySequence, RegistryAudit Theme toggle: - SVG mask-based sun/moon morph with 360° spin animation - Inverted logic (sun in dark mode, moon in light mode) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,8 +16,8 @@ import {
|
||||
getFullEntry,
|
||||
saveEntry,
|
||||
deleteEntry,
|
||||
generateRegistryNumber,
|
||||
} from "../services/registry-service";
|
||||
import type { RegistryAuditEvent } from "../types";
|
||||
import {
|
||||
createTrackedDeadline,
|
||||
resolveDeadline as resolveDeadlineFn,
|
||||
@@ -71,44 +71,42 @@ export function useRegistry() {
|
||||
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);
|
||||
// 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;
|
||||
},
|
||||
[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);
|
||||
// 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();
|
||||
},
|
||||
[storage, blobStorage, refresh, entries],
|
||||
[refresh],
|
||||
);
|
||||
|
||||
const removeEntry = useCallback(
|
||||
@@ -268,6 +266,35 @@ export function useRegistry() {
|
||||
[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,
|
||||
@@ -282,6 +309,8 @@ export function useRegistry() {
|
||||
addDeadline,
|
||||
resolveDeadline,
|
||||
removeDeadline,
|
||||
createReservedSlots,
|
||||
loadAuditHistory,
|
||||
refresh,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user