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.
412 lines
16 KiB
TypeScript
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)
|
|
})
|