Files
AfiS-Aktensystem/web/lib/cases/links.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

129 lines
4.0 KiB
TypeScript

import { db } from '@/lib/db'
import { writeAudit } from '@/lib/audit/write'
import { visibleCaseWhere } from '@/lib/access/caseFilter'
import { canEditCase } from '@/lib/access/caseWrite'
import { AccessError, must } from '@/lib/errors'
import { loadVisibleCase } from '@/lib/cases/service'
import type { LinkKindName } from '@/lib/cases/terms'
import type { EffectivePermissions } from '@/lib/auth/permissions'
/**
* Verknüpfungen zwischen Akten.
*
* Die Richtung trägt Bedeutung: `SUCCESSOR` und `MERGED_INTO` sagen etwas
* anderes, je nachdem, von welcher Seite man liest. `RELATED` ist symmetrisch.
*
* Beide Seiten laufen durch die Sichtbarkeitsprüfung. Eine Akte, die der Leser
* nicht sehen darf, taucht auch als Verknüpfung nicht auf - sonst verriete die
* Gegenseite ihre Existenz. Das gilt in **beide** Richtungen, deshalb wird der
* Filter zweimal angewandt.
*/
const AKTE = {
id: true,
caseNumber: true,
title: true,
status: true,
classification: true,
updatedAt: true,
} as const
/** Alle Verknüpfungen einer Akte, aus beiden Richtungen, gefiltert. */
export async function listLinks(p: EffectivePermissions, caseId: string) {
const sichtbar = visibleCaseWhere(p)
const [hinaus, herein] = await Promise.all([
db.caseLink.findMany({
where: { AND: [{ fromId: caseId }, { to: { is: sichtbar } }] },
select: { kind: true, createdAt: true, to: { select: AKTE } },
orderBy: { createdAt: 'desc' },
}),
db.caseLink.findMany({
where: { AND: [{ toId: caseId }, { from: { is: sichtbar } }] },
select: { kind: true, createdAt: true, from: { select: AKTE } },
orderBy: { createdAt: 'desc' },
}),
])
return [
...hinaus.map((link) => ({
kind: link.kind,
direction: 'out' as const,
other: link.to,
createdAt: link.createdAt,
})),
...herein.map((link) => ({
kind: link.kind,
direction: 'in' as const,
other: link.from,
createdAt: link.createdAt,
})),
]
}
export async function linkCases(
p: EffectivePermissions,
input: { fromCaseNumber: string; toCaseNumber: string; kind: LinkKindName },
): Promise<void> {
const from = await loadVisibleCase(p, input.fromCaseNumber)
if (!from) throw new AccessError('not-found')
must(canEditCase(p, from))
const to = await loadVisibleCase(p, input.toCaseNumber.trim())
if (!to) throw new AccessError('not-found')
if (to.id === from.id) throw new AccessError('link-self')
await db.$transaction(async (tx) => {
await tx.caseLink.upsert({
where: { fromId_toId_kind: { fromId: from.id, toId: to.id, kind: input.kind } },
create: { fromId: from.id, toId: to.id, kind: input.kind, createdById: p.userId },
update: {},
})
await writeAudit(tx, {
actorId: p.userId,
action: 'case.link',
targetType: 'Case',
targetId: from.id,
clearanceAtTime: p.clearanceLevel,
meta: { from: from.caseNumber, to: to.caseNumber, kind: input.kind },
})
})
}
/**
* Verknüpfung lösen. Umkehrbar und deshalb ohne Bestätigungsdialog, siehe
* docs/09-performance-und-ux.md. Das Protokoll behält sie.
*/
export async function unlinkCases(
p: EffectivePermissions,
input: { fromCaseNumber: string; toCaseNumber: string; kind: LinkKindName },
): Promise<void> {
const from = await loadVisibleCase(p, input.fromCaseNumber)
if (!from) throw new AccessError('not-found')
must(canEditCase(p, from))
const to = await loadVisibleCase(p, input.toCaseNumber)
if (!to) throw new AccessError('not-found')
await db.$transaction(async (tx) => {
await tx.caseLink.deleteMany({
where: {
OR: [
{ fromId: from.id, toId: to.id, kind: input.kind },
{ fromId: to.id, toId: from.id, kind: input.kind },
],
},
})
await writeAudit(tx, {
actorId: p.userId,
action: 'case.unlink',
targetType: 'Case',
targetId: from.id,
clearanceAtTime: p.clearanceLevel,
meta: { from: from.caseNumber, to: to.caseNumber, kind: input.kind },
})
})
}