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
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die README; an keiner Stelle Bedeutung, nur Zeichensetzung.
129 lines
4.0 KiB
TypeScript
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 },
|
|
})
|
|
})
|
|
}
|