Files
AfiS-Aktensystem/web/lib/departments/entries.ts
T
Harlekin e0b03ea336
Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
Gedankenstriche durch Bindestriche ersetzt
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die
README; an keiner Stelle Bedeutung, nur Zeichensetzung.
2026-09-04 08:54:52 +02:00

187 lines
6.0 KiB
TypeScript

import { db } from '@/lib/db'
import { writeAudit } from '@/lib/audit/write'
import { AccessError } from '@/lib/errors'
import {
visibleDepartmentEntryWhere,
visibleDepartmentWhere,
} from '@/lib/access/departmentFilter'
import type { EffectivePermissions } from '@/lib/auth/permissions'
/**
* Vermerke an einer Abteilung.
*
* Zusicherung 2 der README gilt hier genauso wie im Verlauf einer Akte:
* **es gibt kein Löschen.** Jede Änderung legt den vorherigen Stand als
* Version ab, jede Zurücknahme lässt einen Grabstein stehen, und beides sieht
* jeder, der den Vermerk sehen darf. Die Regeln stehen in der Datenbank
* (`dept_entry_no_delete`, `dept_entry_immutable_guard`) und nicht nur hier.
*
* Schmaler als der Aktenverlauf ist er trotzdem: keine Korrekturkette, keine
* Anhänge, keine Karenzzeit. Ein Abteilungsvermerk ist ein laufendes
* Protokoll, kein Beweismittel - die Ermittlung gehört in eine Fallakte.
*/
const AUSWAHL = {
id: true,
body: true,
minClearance: true,
authorId: true,
createdAt: true,
version: true,
editedAt: true,
editedById: true,
retractedAt: true,
retractedById: true,
retractionReason: true,
} as const
export async function listEntries(p: EffectivePermissions, departmentId: string, take = 50) {
return db.departmentEntry.findMany({
where: { AND: [visibleDepartmentEntryWhere(p), { departmentId }] },
orderBy: { createdAt: 'desc' },
take,
select: AUSWAHL,
})
}
/** Wer Vermerke schreiben darf. Dasselbe Recht wie für den Aktenverlauf. */
function darfSchreiben(p: EffectivePermissions): boolean {
return p.isAdmin || p.permissions.has('entry.create')
}
export async function createEntry(
p: EffectivePermissions,
input: { id: string; departmentKey: string; body: string; minClearance: number },
): Promise<{ id: string }> {
if (!darfSchreiben(p)) throw new AccessError('no-permission')
const abteilung = await db.department.findFirst({
where: { AND: [visibleDepartmentWhere(p), { key: input.departmentKey }] },
select: { id: true, key: true },
})
if (!abteilung) throw new AccessError('not-found')
const text = input.body.trim()
if (text.length === 0) throw new AccessError('entry-empty')
// Niemand schreibt über seiner eigenen Stufe: sonst legte man einen Vermerk
// an, den man selbst nicht mehr lesen kann. Dieselbe Regel wie bei Akten.
if (!p.isAdmin && input.minClearance > p.clearanceLevel) {
throw new AccessError('clearance-too-low')
}
// Dieselbe Kennung zweimal erzeugt keinen zweiten Vermerk.
const vorhanden = await db.departmentEntry.findUnique({
where: { id: input.id },
select: { id: true },
})
if (vorhanden) return vorhanden
await db.$transaction(async (tx) => {
await tx.departmentEntry.create({
data: {
id: input.id,
departmentId: abteilung.id,
body: text,
minClearance: input.minClearance,
authorId: p.userId,
},
})
await writeAudit(tx, {
actorId: p.userId,
action: 'department.entry.create',
targetType: 'DepartmentEntry',
targetId: input.id,
clearanceAtTime: p.clearanceLevel,
meta: { department: abteilung.key, minClearance: input.minClearance },
})
})
return { id: input.id }
}
/**
* Ändern. Der vorherige Stand wird zur Version - in derselben Transaktion,
* sonst gäbe es einen Moment, in dem der alte Text nirgends mehr steht.
*/
export async function editEntry(
p: EffectivePermissions,
input: { id: string; body: string },
): Promise<void> {
const vermerk = await db.departmentEntry.findFirst({
where: { AND: [visibleDepartmentEntryWhere(p), { id: input.id }] },
select: { id: true, body: true, minClearance: true, authorId: true, version: true },
})
if (!vermerk) throw new AccessError('not-found')
const eigener = vermerk.authorId === p.userId
const darf =
p.isAdmin ||
(eigener && p.permissions.has('entry.edit_own')) ||
p.permissions.has('entry.edit_any')
if (!darf) throw new AccessError('no-permission')
const text = input.body.trim()
if (text.length === 0) throw new AccessError('entry-empty')
if (text === vermerk.body) return
await db.$transaction(async (tx) => {
await tx.departmentEntryVersion.create({
data: {
entryId: vermerk.id,
version: vermerk.version,
body: vermerk.body,
minClearance: vermerk.minClearance,
editedById: p.userId,
},
})
await tx.departmentEntry.update({
where: { id: vermerk.id },
data: { body: text, version: vermerk.version + 1, editedAt: new Date(), editedById: p.userId },
})
await writeAudit(tx, {
actorId: p.userId,
action: 'department.entry.edit',
targetType: 'DepartmentEntry',
targetId: vermerk.id,
clearanceAtTime: p.clearanceLevel,
meta: { version: vermerk.version + 1 },
})
})
}
/**
* Zurückziehen. Kein Löschen: der Vermerk bleibt als Grabstein stehen, mit
* Grund und Namen. Wer etwas anderes sagen will, schreibt einen neuen.
*/
export async function retractEntry(
p: EffectivePermissions,
input: { id: string; reason: string },
): Promise<void> {
const vermerk = await db.departmentEntry.findFirst({
where: { AND: [visibleDepartmentEntryWhere(p), { id: input.id }] },
select: { id: true, retractedAt: true },
})
if (!vermerk) throw new AccessError('not-found')
if (!p.isAdmin && !p.permissions.has('entry.retract')) throw new AccessError('no-permission')
if (vermerk.retractedAt) return
const grund = input.reason.trim()
if (grund.length === 0) throw new AccessError('reason-required')
await db.$transaction(async (tx) => {
await tx.departmentEntry.update({
where: { id: vermerk.id },
data: { retractedAt: new Date(), retractedById: p.userId, retractionReason: grund },
})
await writeAudit(tx, {
actorId: p.userId,
action: 'department.entry.retract',
targetType: 'DepartmentEntry',
targetId: vermerk.id,
clearanceAtTime: p.clearanceLevel,
meta: { reason: grund },
})
})
}