a0dd35a066
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>
127 lines
3.3 KiB
TypeScript
127 lines
3.3 KiB
TypeScript
/**
|
|
* Append-only audit trail for Registratura entries.
|
|
* Server-side only — uses Prisma directly.
|
|
*/
|
|
|
|
import { prisma } from "@/core/storage/prisma";
|
|
import { Prisma } from "@prisma/client";
|
|
import type { RegistryAuditAction, RegistryAuditEvent } from "../types";
|
|
|
|
/** Log a single audit event (append-only, never updated or deleted) */
|
|
export async function logAuditEvent(params: {
|
|
entryId: string;
|
|
entryNumber: string;
|
|
action: RegistryAuditAction;
|
|
actor: string;
|
|
actorName?: string;
|
|
company: string;
|
|
detail?: Record<string, unknown>;
|
|
}): Promise<void> {
|
|
await prisma.registryAudit.create({
|
|
data: {
|
|
entryId: params.entryId,
|
|
entryNumber: params.entryNumber,
|
|
action: params.action,
|
|
actor: params.actor,
|
|
actorName: params.actorName ?? null,
|
|
company: params.company,
|
|
detail: (params.detail as Prisma.InputJsonValue) ?? Prisma.JsonNull,
|
|
},
|
|
});
|
|
}
|
|
|
|
/** Get chronological audit history for a single entry */
|
|
export async function getAuditHistory(
|
|
entryId: string,
|
|
): Promise<RegistryAuditEvent[]> {
|
|
const rows = await prisma.registryAudit.findMany({
|
|
where: { entryId },
|
|
orderBy: { createdAt: "asc" },
|
|
});
|
|
return rows.map(mapRow);
|
|
}
|
|
|
|
/** Get audit events for a company, optionally filtered by date range */
|
|
export async function getAuditByCompany(
|
|
company: string,
|
|
options?: { from?: string; to?: string; limit?: number },
|
|
): Promise<RegistryAuditEvent[]> {
|
|
const where: Prisma.RegistryAuditWhereInput = { company };
|
|
|
|
if (options?.from || options?.to) {
|
|
const createdAt: Prisma.DateTimeFilter = {};
|
|
if (options.from) createdAt.gte = new Date(options.from);
|
|
if (options.to) createdAt.lte = new Date(options.to);
|
|
where.createdAt = createdAt;
|
|
}
|
|
|
|
const rows = await prisma.registryAudit.findMany({
|
|
where,
|
|
orderBy: { createdAt: "desc" },
|
|
take: options?.limit ?? 100,
|
|
});
|
|
return rows.map(mapRow);
|
|
}
|
|
|
|
/** Compute diff between old and new entry for "updated" audit events */
|
|
export function computeEntryDiff(
|
|
oldEntry: Record<string, unknown>,
|
|
newEntry: Record<string, unknown>,
|
|
fieldsToTrack: string[],
|
|
): Record<string, { old: unknown; new: unknown }> | null {
|
|
const changes: Record<string, { old: unknown; new: unknown }> = {};
|
|
|
|
for (const field of fieldsToTrack) {
|
|
const oldVal = oldEntry[field];
|
|
const newVal = newEntry[field];
|
|
if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
|
|
changes[field] = { old: oldVal, new: newVal };
|
|
}
|
|
}
|
|
|
|
return Object.keys(changes).length > 0 ? changes : null;
|
|
}
|
|
|
|
// ── Internal helpers ──
|
|
|
|
function mapRow(r: {
|
|
id: string;
|
|
entryId: string;
|
|
entryNumber: string;
|
|
action: string;
|
|
actor: string;
|
|
actorName: string | null;
|
|
company: string;
|
|
detail: Prisma.JsonValue;
|
|
createdAt: Date;
|
|
}): RegistryAuditEvent {
|
|
return {
|
|
id: r.id,
|
|
entryId: r.entryId,
|
|
entryNumber: r.entryNumber,
|
|
action: r.action as RegistryAuditAction,
|
|
actor: r.actor,
|
|
actorName: r.actorName ?? undefined,
|
|
company: r.company,
|
|
detail: (r.detail as Record<string, unknown>) ?? undefined,
|
|
createdAt: r.createdAt.toISOString(),
|
|
};
|
|
}
|
|
|
|
/** Fields tracked for update diffs */
|
|
export const TRACKED_FIELDS = [
|
|
"subject",
|
|
"sender",
|
|
"recipient",
|
|
"direction",
|
|
"documentType",
|
|
"status",
|
|
"company",
|
|
"deadline",
|
|
"assignee",
|
|
"date",
|
|
"notes",
|
|
"tags",
|
|
"expiryDate",
|
|
];
|