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:
AI Assistant
2026-03-10 07:54:32 +02:00
parent f94529c380
commit a0dd35a066
15 changed files with 1354 additions and 124 deletions
+59 -30
View File
@@ -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,
};
}