Files
AfiS-Aktensystem/packages/db/prisma/seed.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

412 lines
16 KiB
TypeScript

import { createPrismaClient } from '../src/index.js'
/**
* Grundzustand der Anwendung. Alles hier ist Konfiguration, ohne die die App
* nicht sinnvoll läuft, kein Testinhalt.
*
* Die Startvorlagen stehen hier, weil ohne Vorlage weder eine Akte noch ein
* Dokument angelegt werden kann. Geändert werden sie im Vorlagen-Editor.
*/
const db = createPrismaClient()
/**
* Auslieferungszustand. Ab Stufe 3 ist die Existenz einer Akte für niedrigere
* Stufen verborgen, darunter ist nur der Inhalt gesperrt. Der Leiter kann das
* im Adminbereich ändern.
*
* Die Namen entsprechen den Zugangsrollen auf dem Discord und bleiben deshalb
* englisch, siehe docs/13-entscheidungen.md. Stufe 0 hat keine eigene
* Zugangsrolle: sie gilt für jedes AfiS-Mitglied ohne eine solche Rolle.
*/
const CLEARANCE_LEVELS = [
{ level: 0, name: 'Unclassified', hiddenBelow: false, colorToken: 'stamp-0' },
{ level: 1, name: 'Restricted Access', hiddenBelow: false, colorToken: 'stamp-1' },
{ level: 2, name: 'Confidential Access', hiddenBelow: false, colorToken: 'stamp-2' },
{ level: 3, name: 'Sensitive Access', hiddenBelow: true, colorToken: 'stamp-3' },
{ level: 4, name: 'Critical Operations', hiddenBelow: true, colorToken: 'stamp-4' },
{ level: 5, name: 'Executive Oversight', hiddenBelow: true, colorToken: 'stamp-5' },
]
/**
* Startvorlagen nach docs/05-akten-und-vorlagen.md. Die Feldschlüssel sind
* stabil und werden nie geändert - eine Akte merkt sich ihre Vorlage über
* `templateId`, und eine neue Vorlagenversion lässt bestehende Akten unberührt.
*
* Der Zuschnitt der Felder ist bewusst schmal gehalten: den Vorlagen-Editor
* mit allen Feldtypen baut Phase 3, hier steht nur, was eine Akte zum Laufen
* braucht.
*/
type Vorlage = {
key: string
name: string
description: string
uses?: ('CASE' | 'CASE_DOCUMENT' | 'PERSON_DOCUMENT' | 'STANDALONE')[]
layout?: 'FORM' | 'PROSE' | 'NOTE' | 'DOSSIER'
sheetTitle?: string
sheetSubtitle?: string
numberPrefix?: string
numberPadding?: number
fields?: unknown[]
}
/** Fallarten: sie bestimmen die Abschnitte einer Fallakte. */
const TEMPLATES: Vorlage[] = [
{
key: 'korruptionsverdacht',
name: 'Korruptionsverdacht',
description: 'Verdacht auf Vorteilsnahme, Bestechlichkeit oder Interessenkonflikt',
},
{
key: 'dienstvergehen',
name: 'Dienstvergehen',
description: 'Verstoß gegen Dienstpflichten oder interne Vorschriften',
},
{
key: 'sicherheitsvorfall',
name: 'Sicherheitsvorfall',
description: 'Containment-Bruch, Datenabfluss oder unbefugter Zugriff',
},
{
key: 'routinepruefung',
name: 'Routineprüfung',
description: 'Turnusmäßige Kontrolle ohne konkreten Anfangsverdacht',
},
]
const TEMPLATE_FIELDS = [
// Nachgebaut nach dem Vorfallsbericht der Abteilung. Die Foundation-Freigabe
// steht im Kopf des Dokuments, steuert aber keinen Zugriff - das tut allein
// die interne Freigabe. Siehe docs/13-entscheidungen.md.
{
key: 'foundation_freigabe',
label: 'Foundation-Freigabe',
type: 'select',
options: ['0', '1', '2', '3', '4', '5'],
},
{
key: 'berichtsart',
label: 'Berichtsart',
type: 'multiselect',
options: [
'Vorfallsbericht',
'Sicherheitsanalyse',
'Interne Untersuchung',
'Allg. Bericht',
'Sonstiges',
],
},
{
key: 'gefahreneinschaetzung',
label: 'Gefahreneinschätzung',
type: 'select',
options: ['Keine', 'Leichte', 'Mittlere', 'Erhöhte', 'Kritische'],
},
{ key: 'vorfallsdatum', label: 'Datum des Vorfalls', type: 'date' },
{ key: 'sachverhalt', label: 'Sachverhalt', type: 'longtext', required: true },
{ key: 'beteiligte', label: 'Beteiligte Personen', type: 'longtext' },
{ key: 'feststellungen', label: 'Feststellungen', type: 'longtext' },
{ key: 'anmerkungen', label: 'Anmerkungen', type: 'longtext' },
{ key: 'massnahmen', label: 'Maßnahmen', type: 'longtext' },
{
key: 'bewertung',
label: 'Abschließende Bewertung',
type: 'longtext',
requiredToClose: true,
help: 'Muss gefüllt sein, bevor der Fall abgeschlossen werden kann.',
},
]
/**
* Status einer Person. Mehrere zugleich möglich: "Gekündigt" und "Verhaftet"
* schließen einander nicht aus. Im Adminbereich erweiterbar.
*/
const STATUS_TAGS = [
{ key: 'lebt', name: 'Lebt', colorToken: 'stamp-2', sortOrder: 10 },
{ key: 'im_dienst', name: 'Im Dienst', colorToken: 'stamp-2', sortOrder: 20 },
{ key: 'beurlaubt', name: 'Beurlaubt', colorToken: 'stamp-1', sortOrder: 30 },
{ key: 'suspendiert', name: 'Suspendiert', colorToken: 'stamp-3', sortOrder: 40 },
{ key: 'gekuendigt', name: 'Gekündigt', colorToken: 'stamp-3', sortOrder: 50 },
{ key: 'verhaftet', name: 'Verhaftet', colorToken: 'stamp-4', sortOrder: 60 },
{ key: 'vermisst', name: 'Vermisst', colorToken: 'stamp-4', sortOrder: 70 },
{ key: 'amnestiert', name: 'Amnestiert', colorToken: 'stamp-5', sortOrder: 80 },
{ key: 'tot', name: 'Tot', colorToken: 'stamp-4', sortOrder: 90 },
]
/**
* Die Freigabestufen der Foundation, wie sie im Kopf eines Aktenblatts stehen.
* **Sie steuern keinen Zugriff.** Das tut allein die interne Stufe.
* Siehe docs/13-entscheidungen.md, "Zwei Freigaben im Kopf".
*/
const FOUNDATION_CLEARANCES = [
{ level: 1, name: 'Vertraulich', colorHex: '#1e3a5f', sortOrder: 10 },
{ level: 2, name: 'Eingeschränkt', colorHex: '#1a4d2e', sortOrder: 20 },
{ level: 3, name: 'Geheim', colorHex: '#7c2d12', sortOrder: 30 },
{ level: 4, name: 'Streng Geheim', colorHex: '#450a0a', sortOrder: 40 },
{ level: 5, name: 'Thaumiel', colorHex: '#2d0060', sortOrder: 50 },
]
/**
* Das Aktenblatt der Personalabteilung, nachgebaut nach dem Formular, das die
* Abteilung bislang von Hand ausgefüllt hat. Reihenfolge und Breiten (`span`,
* vier Spalten) ergeben genau dessen Raster.
*/
const PERSONALAKTE_FIELDS = [
{
key: 'foundation_klassifikation',
label: 'Klassifikation',
type: 'select',
options: ['1', '2', '3', '4', '5'],
slot: 'classbar',
help: 'Steht im Balken des Blattes. Steuert keinen Zugriff im Aktensystem.',
},
{ key: 'lichtbild', label: 'Lichtbild', type: 'photo', rows: 4 },
{ key: 'name', label: 'Name', type: 'shorttext', span: 2, required: true },
{ key: 'vorname', label: 'Vorname', type: 'shorttext', span: 2, required: true },
{ key: 'alter', label: 'Alter', type: 'shorttext', span: 2 },
{ key: 'geschlecht', label: 'Geschlecht', type: 'shorttext', span: 2 },
{ key: 'groesse', label: 'Größe', type: 'shorttext', span: 4 },
{ key: 'abteilung', label: 'Abteilung', type: 'shorttext', span: 2 },
{ key: 'dienstgrad', label: 'Dienstgrad', type: 'shorttext', span: 2 },
{
key: 'sicherheitsfreigabe',
label: 'Sicherheitsfreigabe',
type: 'select',
options: ['0', '1', '2', '3', '4', '5'],
span: 3,
emphasis: true,
help: 'Steht im Kopf des Blattes und steuert keinen Zugriff im Aktensystem.',
},
{ key: 'wissensfreigabe', label: 'Wissensfreigabe', type: 'shorttext', span: 2, emphasis: true },
{ key: 'konfession', label: 'Konfession', type: 'shorttext', span: 2 },
{ key: 'adresse', label: 'Adresse', type: 'shorttext', span: 3 },
{ key: 'anmerkungen', label: 'Anmerkungen / Bemerkungen', type: 'longtext', span: 5 },
{
key: 'personalarbeiter',
label: 'Personalarbeiter',
type: 'signature',
span: 3,
dateKey: 'personalarbeiter_datum',
},
{ key: 'personalarbeiter_datum', label: 'Datum Personalarbeiter', type: 'date' },
{
key: 'standortleiter',
label: 'Standortleiter',
type: 'signature',
span: 2,
dateKey: 'standortleiter_datum',
},
{ key: 'standortleiter_datum', label: 'Datum Standortleiter', type: 'date' },
]
/**
* Dokumentarten. Sie hängen an einer Fallakte, an einer Personalakte oder an
* nichts - was davon, steht in `uses`. Eine weitere Art anzulegen ist ein
* Datensatz, kein Deployment: der Vorlagen-Editor reicht.
* Siehe docs/13-entscheidungen.md.
*/
const DOCUMENT_TEMPLATES: Vorlage[] = [
{
key: 'personalakte',
name: 'Personalakte',
description: 'Das Aktenblatt der Personalabteilung, meist der erste Eintrag zu einer Person',
uses: ['PERSON_DOCUMENT'],
layout: 'DOSSIER',
sheetTitle: 'SCP Foundation',
sheetSubtitle: 'Personalakte // Personalabteilung',
numberPrefix: 'PA',
fields: PERSONALAKTE_FIELDS,
},
{
key: 'vermerk',
name: 'Vermerk',
description: 'Kurze Notiz mit Datum und Verfasser, an einer Akte oder an einer Person',
uses: ['CASE_DOCUMENT', 'PERSON_DOCUMENT'],
layout: 'NOTE',
numberPrefix: 'VM',
fields: [{ key: 'text', label: 'Vermerk', type: 'longtext', required: true }],
},
{
key: 'vorfallsbericht',
name: 'Vorfallsbericht',
description: 'Bericht über einen Vorfall, mit Gefahreneinschätzung und Maßnahmen',
uses: ['CASE_DOCUMENT', 'PERSON_DOCUMENT', 'STANDALONE'],
layout: 'FORM',
numberPrefix: 'VB',
fields: TEMPLATE_FIELDS.filter((field) => field.key !== 'bewertung'),
},
{
key: 'sonderarbeitsvertrag',
name: 'Sonderarbeitsvertrag',
description: 'Vertrag über eine befristete Sondertätigkeit, mehrseitig mit Unterschriften',
uses: ['PERSON_DOCUMENT'],
layout: 'PROSE',
numberPrefix: 'SAV',
// Aufbau eines Vertragstexts: feste Bausteine, die Platzhalter ziehen die
// Werte der Eingabefelder darüber. Der Wortlaut gehört der Abteilung und
// wird im Vorlagen-Editor gepflegt, nicht hier festgeschrieben.
fields: [
{ key: 'vertragspartner', label: 'Vertragspartner', type: 'shorttext', required: true },
{ key: 'dienstgrad', label: 'Dienstgrad', type: 'shorttext' },
{ key: 'taetigkeit', label: 'Tätigkeit', type: 'shorttext', required: true },
{ key: 'beginn', label: 'Beginn', type: 'date', required: true },
{ key: 'ende', label: 'Ende', type: 'date' },
{ key: 'verguetung', label: 'Vergütung', type: 'shorttext' },
{ key: 'ueberschrift', label: 'Überschrift', type: 'heading', text: 'Sonderarbeitsvertrag' },
{
key: 'praeambel',
label: 'Präambel',
type: 'paragraph',
text:
'Zwischen der Abteilung für innere Sicherheit, vertreten durch die Abteilungsleitung, ' +
'und {{vertragspartner}} ({{dienstgrad}}) wird der folgende Sonderarbeitsvertrag geschlossen.',
},
{
key: 'paragraph_1',
label: '§ 1 Gegenstand',
type: 'paragraph',
text:
'§ 1 Gegenstand\n\nGegenstand dieses Vertrages ist die Tätigkeit als {{taetigkeit}}. ' +
'Die Tätigkeit wird ab dem {{beginn}} ausgeübt und endet am {{ende}}.',
},
{
key: 'paragraph_2',
label: '§ 2 Vergütung',
type: 'paragraph',
text: '§ 2 Vergütung\n\nDie Vergütung beträgt {{verguetung}} und wird monatlich abgerechnet.',
},
{
key: 'paragraph_3',
label: '§ 3 Verschwiegenheit',
type: 'paragraph',
text:
'§ 3 Verschwiegenheit\n\nDer Vertragspartner verpflichtet sich, über alle im Rahmen ' +
'der Tätigkeit bekannt gewordenen Vorgänge Stillschweigen zu bewahren. Die Pflicht ' +
'besteht über das Ende des Vertrages hinaus fort.',
},
{ key: 'unterschrift_partner', label: '{{vertragspartner}}', type: 'signature' },
{ key: 'unterschrift_leitung', label: 'Abteilungsleitung', type: 'signature' },
],
},
{
key: 'bussgeld',
name: 'Bußgeldbescheid',
description: 'Geldbuße wegen eines Dienstvergehens, an einer Person oder für sich stehend',
uses: ['PERSON_DOCUMENT', 'STANDALONE'],
layout: 'FORM',
numberPrefix: 'BG',
fields: [
{ key: 'betroffener', label: 'Betroffene Person', type: 'shorttext', required: true },
{ key: 'tatzeitpunkt', label: 'Tatzeitpunkt', type: 'date', required: true },
{ key: 'tatvorwurf', label: 'Tatvorwurf', type: 'longtext', required: true },
{ key: 'rechtsgrundlage', label: 'Rechtsgrundlage', type: 'shorttext' },
{ key: 'betrag', label: 'Betrag', type: 'number', required: true },
{ key: 'zahlungsfrist', label: 'Zahlungsfrist', type: 'date' },
{ key: 'rechtsmittel', label: 'Rechtsmittelbelehrung', type: 'longtext' },
{ key: 'unterschrift', label: 'Abteilungsleitung', type: 'signature' },
],
},
]
async function main(): Promise<void> {
for (const config of CLEARANCE_LEVELS) {
await db.clearanceLevelConfig.upsert({
where: { level: config.level },
create: config,
update: { name: config.name, colorToken: config.colorToken },
})
}
console.log(`[seed] ${CLEARANCE_LEVELS.length} Freigabestufen`)
// Änderungsrichtlinie im Auslieferungszustand: eigene Einträge sind 15 Minuten
// frei änderbar, danach nur mit Recht. Eine Änderung in dieser Karenzzeit wird
// versioniert und protokolliert, aber nicht sichtbar markiert.
// Siehe docs/05-akten-und-vorlagen.md.
await db.appSetting.upsert({
where: { id: 1 },
create: { id: 1 },
update: {},
})
console.log('[seed] Änderungsrichtlinie GRACE_WINDOW, 15 Minuten')
for (const tag of STATUS_TAGS) {
await db.personnelStatusTag.upsert({
where: { key: tag.key },
create: tag,
update: { name: tag.name, colorToken: tag.colorToken, sortOrder: tag.sortOrder },
})
}
console.log(`[seed] ${STATUS_TAGS.length} Status-Etiketten`)
for (const stufe of FOUNDATION_CLEARANCES) {
await db.foundationClearance.upsert({
where: { level: stufe.level },
create: stufe,
update: { name: stufe.name, colorHex: stufe.colorHex, sortOrder: stufe.sortOrder },
})
}
console.log(`[seed] ${FOUNDATION_CLEARANCES.length} Foundation-Freigabestufen`)
const alle = [...TEMPLATES, ...DOCUMENT_TEMPLATES]
for (const template of alle) {
const fields = (template.fields ?? TEMPLATE_FIELDS) as object[]
const uses = template.uses ?? ['CASE']
await db.documentTemplate.upsert({
where: { key_version: { key: template.key, version: 1 } },
create: {
key: template.key,
name: template.name,
description: template.description,
uses,
layout: template.layout ?? 'FORM',
sheetTitle: template.sheetTitle ?? null,
sheetSubtitle: template.sheetSubtitle ?? null,
numberPrefix: template.numberPrefix ?? 'AfiS',
numberPadding: template.numberPadding ?? 4,
version: 1,
fields,
isActive: true,
},
// Version 1 gehört dem Seed und wird von ihm aktuell gehalten. Ändert
// der Leiter eine Vorlage im Editor, entsteht Version 2 - die fasst der
// Seed nie an, und bestehende Akten behalten ohnehin ihre Version.
update: {
name: template.name,
description: template.description,
uses,
layout: template.layout ?? 'FORM',
sheetTitle: template.sheetTitle ?? null,
sheetSubtitle: template.sheetSubtitle ?? null,
fields,
},
})
}
console.log(`[seed] ${TEMPLATES.length} Fallarten, ${DOCUMENT_TEMPLATES.length} Dokumentarten`)
// Ein Nummernkreis je Präfix und Jahr. Angelegt wird er beim ersten Fall
// von selbst; hier steht er nur, damit ein frischer Bestand nachvollziehbar
// bei null beginnt.
const year = new Date().getFullYear()
for (const prefix of [...new Set(alle.map((t) => t.numberPrefix ?? 'AfiS'))]) {
await db.numberSeries.upsert({
where: { prefix_year: { prefix, year } },
create: { prefix, year, lastNumber: 0 },
update: {},
})
}
console.log(`[seed] Nummernkreise für ${year}`)
// entry.edit_own, entry.edit_any und entry.retract sind bewusst keiner Rolle
// zugeordnet. Der Leiter schaltet sie frei, wenn er sie will.
// Siehe docs/03-auth-und-rechte.md.
const mappings = await db.roleMapping.count()
console.log(`[seed] ${mappings} Rollen-Zuordnungen vorhanden, keine angelegt`)
}
main()
.then(() => db.$disconnect())
.catch(async (error: unknown) => {
console.error('[seed] fehlgeschlagen', error)
await db.$disconnect()
process.exit(1)
})