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 { 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) })