Rollback - PDF Import funzt so semi
Dieser Commit ist enthalten in:
141
backend/mock-server.js
Normale Datei
141
backend/mock-server.js
Normale Datei
@ -0,0 +1,141 @@
|
||||
const express = require('express');
|
||||
const multer = require('multer');
|
||||
const cors = require('cors');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const pdfParse = require('pdf-parse');
|
||||
|
||||
const app = express();
|
||||
const upload = multer({ dest: 'uploads/temp/' });
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Mock auth endpoint
|
||||
app.post('/api/auth/login', (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
if (username === 'admin' && password === 'admin') {
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
token: 'mock-token',
|
||||
user: {
|
||||
id: '1',
|
||||
username: 'admin',
|
||||
role: 'admin'
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
res.status(401).json({ success: false, error: { message: 'Invalid credentials' } });
|
||||
}
|
||||
});
|
||||
|
||||
// Mock organization hierarchy
|
||||
app.get('/api/organization/hierarchy', (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
data: [
|
||||
{
|
||||
id: '1',
|
||||
code: 'DIR',
|
||||
name: 'Direktor LKA NRW',
|
||||
type: 'direktion',
|
||||
level: 0,
|
||||
parentId: null,
|
||||
children: []
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
// PDF Import endpoint
|
||||
app.post('/api/organization/import-pdf', upload.single('pdf'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: { message: 'No PDF file uploaded' }
|
||||
});
|
||||
}
|
||||
|
||||
const pdfBuffer = fs.readFileSync(req.file.path);
|
||||
|
||||
// Parse PDF
|
||||
try {
|
||||
const pdfData = await pdfParse(pdfBuffer);
|
||||
console.log('PDF parsed:', pdfData.numpages, 'pages');
|
||||
console.log('Text preview:', pdfData.text.substring(0, 500));
|
||||
|
||||
// Simplified parsing
|
||||
const lines = pdfData.text.split('\n').filter(line => line.trim());
|
||||
const units = [];
|
||||
|
||||
lines.forEach(line => {
|
||||
if (line.includes('Direktor')) {
|
||||
units.push({ code: 'DIR', name: 'Direktor LKA NRW', type: 'direktion' });
|
||||
} else if (line.match(/Abteilung\s+\d+/)) {
|
||||
const match = line.match(/Abteilung\s+(\d+)/);
|
||||
units.push({
|
||||
code: `Abt ${match[1]}`,
|
||||
name: line.trim(),
|
||||
type: 'abteilung'
|
||||
});
|
||||
} else if (line.match(/Dezernat\s+\d+/)) {
|
||||
const match = line.match(/Dezernat\s+(\d+)/);
|
||||
units.push({
|
||||
code: `Dez ${match[1]}`,
|
||||
name: line.trim(),
|
||||
type: 'dezernat'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up temp file
|
||||
fs.unlinkSync(req.file.path);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully imported ${units.length} organizational units from PDF`,
|
||||
unitsImported: units.length,
|
||||
units: units
|
||||
}
|
||||
});
|
||||
|
||||
} catch (parseError) {
|
||||
console.error('PDF parse error:', parseError);
|
||||
fs.unlinkSync(req.file.path);
|
||||
|
||||
// Fallback response
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
message: 'Imported sample organizational units (PDF parsing failed)',
|
||||
unitsImported: 5,
|
||||
units: [
|
||||
{ code: 'DIR', name: 'Direktor LKA NRW', type: 'direktion' },
|
||||
{ code: 'Abt 1', name: 'Abteilung 1 - OK', type: 'abteilung' },
|
||||
{ code: 'Abt 2', name: 'Abteilung 2 - Staatsschutz', type: 'abteilung' },
|
||||
{ code: 'Dez 11', name: 'Dezernat 11', type: 'dezernat' },
|
||||
{ code: 'SG 11.1', name: 'Sachgebiet 11.1', type: 'sachgebiet' }
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Import error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: { message: 'Failed to import PDF: ' + error.message }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const PORT = 3004;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Mock backend server running on http://localhost:${PORT}`);
|
||||
console.log('Login with admin/admin');
|
||||
console.log('PDF import endpoint ready at POST /api/organization/import-pdf');
|
||||
});
|
||||
@ -31,6 +31,7 @@
|
||||
"multer": "^2.0.1",
|
||||
"node-cron": "^4.2.1",
|
||||
"nodemailer": "^7.0.6",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"sqlite3": "^5.1.6",
|
||||
"uuid": "^9.0.1",
|
||||
"winston": "^3.11.0"
|
||||
|
||||
467
backend/scripts/seed-lka-structure.js
Normale Datei
467
backend/scripts/seed-lka-structure.js
Normale Datei
@ -0,0 +1,467 @@
|
||||
const Database = require('better-sqlite3')
|
||||
const path = require('path')
|
||||
const { v4: uuidv4 } = require('uuid')
|
||||
|
||||
// Open database
|
||||
const dbPath = path.join(__dirname, '..', 'skillmate.dev.encrypted.db')
|
||||
const db = new Database(dbPath)
|
||||
|
||||
// Enable foreign keys
|
||||
db.pragma('foreign_keys = ON')
|
||||
|
||||
console.log('🏢 Seeding Complete LKA NRW Organizational Structure...')
|
||||
|
||||
const now = new Date().toISOString()
|
||||
|
||||
// Store IDs for reference
|
||||
const unitIds = {}
|
||||
|
||||
// Helper function to insert organizational unit
|
||||
function insertUnit(code, name, type, level, parentId = null, options = {}) {
|
||||
const id = uuidv4()
|
||||
|
||||
db.prepare(`
|
||||
INSERT OR REPLACE INTO organizational_units (
|
||||
id, code, name, type, level, parent_id,
|
||||
color, order_index, has_fuehrungsstelle,
|
||||
fuehrungsstelle_name, is_active, created_at, updated_at,
|
||||
position_x, position_y
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id, code, name, type, level, parentId,
|
||||
options.color || null,
|
||||
options.orderIndex || 0,
|
||||
options.hasFuehrungsstelle || 0,
|
||||
options.fuehrungsstelleName || null,
|
||||
1, now, now,
|
||||
options.positionX || null,
|
||||
options.positionY || null
|
||||
)
|
||||
|
||||
unitIds[code] = id
|
||||
return id
|
||||
}
|
||||
|
||||
// Clear existing data
|
||||
console.log('Clearing existing organizational data...')
|
||||
db.prepare('DELETE FROM deputy_delegations').run()
|
||||
db.prepare('DELETE FROM deputy_assignments').run()
|
||||
db.prepare('DELETE FROM special_positions').run()
|
||||
db.prepare('DELETE FROM employee_unit_assignments').run()
|
||||
db.prepare('DELETE FROM organizational_units').run()
|
||||
|
||||
// ========================================
|
||||
// Level 0: Direktor
|
||||
// ========================================
|
||||
const direktorId = insertUnit('DIR', 'Direktor LKA NRW', 'direktion', 0, null, {
|
||||
color: '#1e3a8a',
|
||||
orderIndex: 0,
|
||||
positionX: 400,
|
||||
positionY: 50
|
||||
})
|
||||
|
||||
// ========================================
|
||||
// Level 1: Direct Reports to Direktor
|
||||
// ========================================
|
||||
|
||||
// Leitungsstab - directly under Direktor
|
||||
const leitungsstabId = insertUnit('LStab', 'Leitungsstab', 'stabsstelle', 1, direktorId, {
|
||||
color: '#6b7280',
|
||||
orderIndex: 1,
|
||||
positionX: 200,
|
||||
positionY: 150
|
||||
})
|
||||
|
||||
// Leitungsstab Sub-units (Level 2)
|
||||
insertUnit('LStab 1', 'Grundsatzangelegenheiten, Gremien, internationale polizeiliche Zusammenarbeit, Informations- und Vorgangssteuerung, Einsatz/BAO', 'sachgebiet', 2, leitungsstabId)
|
||||
insertUnit('LStab 2', 'Strategische Steuerung, Qualitätsmanagement, Controlling, Wissenmanagement', 'sachgebiet', 2, leitungsstabId)
|
||||
insertUnit('LStab 3', 'Presse- und Öffentlichkeitsarbeit', 'sachgebiet', 2, leitungsstabId)
|
||||
|
||||
// ========================================
|
||||
// Sondereinheiten (Special Units) - Parallel to hierarchy
|
||||
// ========================================
|
||||
|
||||
// These are NOT hierarchically under Direktor but parallel/advisory roles
|
||||
const personalratId = insertUnit('PR', 'Personalrat', 'sondereinheit', 1, null, {
|
||||
color: '#059669',
|
||||
orderIndex: 100,
|
||||
positionX: 50,
|
||||
positionY: 250
|
||||
})
|
||||
|
||||
const schwerbehindertenId = insertUnit('SBV', 'Schwerbehindertenvertretung', 'sondereinheit', 1, null, {
|
||||
color: '#059669',
|
||||
orderIndex: 101,
|
||||
positionX: 50,
|
||||
positionY: 300
|
||||
})
|
||||
|
||||
// Individual Beauftragte (as separate units without hierarchy)
|
||||
insertUnit('DSB', 'Datenschutzbeauftragter', 'sondereinheit', 1, null, {
|
||||
color: '#0891b2',
|
||||
orderIndex: 102,
|
||||
positionX: 50,
|
||||
positionY: 350
|
||||
})
|
||||
|
||||
insertUnit('GSB', 'Gleichstellungsbeauftragte', 'sondereinheit', 1, null, {
|
||||
color: '#0891b2',
|
||||
orderIndex: 103,
|
||||
positionX: 50,
|
||||
positionY: 400
|
||||
})
|
||||
|
||||
insertUnit('IKB', 'Inklusionsbeauftragter', 'sondereinheit', 1, null, {
|
||||
color: '#0891b2',
|
||||
orderIndex: 104,
|
||||
positionX: 50,
|
||||
positionY: 450
|
||||
})
|
||||
|
||||
insertUnit('ISB', 'Informationssicherheitsbeauftragter', 'sondereinheit', 1, null, {
|
||||
color: '#0891b2',
|
||||
orderIndex: 105,
|
||||
positionX: 50,
|
||||
positionY: 500
|
||||
})
|
||||
|
||||
insertUnit('GHSB', 'Geheimschutzbeauftragte', 'sondereinheit', 1, null, {
|
||||
color: '#0891b2',
|
||||
orderIndex: 106,
|
||||
positionX: 50,
|
||||
positionY: 550
|
||||
})
|
||||
|
||||
insertUnit('EXB', 'Extremismusbeauftragter', 'sondereinheit', 1, null, {
|
||||
color: '#dc2626',
|
||||
orderIndex: 107,
|
||||
positionX: 50,
|
||||
positionY: 600
|
||||
})
|
||||
|
||||
insertUnit('EXBV', 'Extremismusbeauftragter Vertreter', 'sondereinheit', 1, null, {
|
||||
color: '#dc2626',
|
||||
orderIndex: 108,
|
||||
positionX: 50,
|
||||
positionY: 650
|
||||
})
|
||||
|
||||
// Innenrevision
|
||||
insertUnit('IR', 'Innenrevision', 'sondereinheit', 1, null, {
|
||||
color: '#7c3aed',
|
||||
orderIndex: 109,
|
||||
positionX: 50,
|
||||
positionY: 700
|
||||
})
|
||||
|
||||
// Fachkräfte für Arbeitssicherheit
|
||||
insertUnit('FAS', 'Fachkräfte für Arbeitssicherheit', 'sondereinheit', 1, null, {
|
||||
color: '#059669',
|
||||
orderIndex: 110,
|
||||
positionX: 50,
|
||||
positionY: 750
|
||||
})
|
||||
|
||||
// ========================================
|
||||
// Abteilungen (Level 1 - Under Direktor)
|
||||
// ========================================
|
||||
|
||||
const abteilungen = [
|
||||
{
|
||||
code: 'Abt 1',
|
||||
name: 'Organisierte Kriminalität',
|
||||
color: '#dc2626',
|
||||
hasFuehrungsstelle: true,
|
||||
positionX: 100,
|
||||
positionY: 200
|
||||
},
|
||||
{
|
||||
code: 'Abt 2',
|
||||
name: 'Terrorismusbekämpfung und Staatsschutz',
|
||||
color: '#ea580c',
|
||||
hasFuehrungsstelle: true,
|
||||
positionX: 250,
|
||||
positionY: 200
|
||||
},
|
||||
{
|
||||
code: 'Abt 3',
|
||||
name: 'Strategische Kriminalitätsbekämpfung',
|
||||
color: '#0891b2',
|
||||
hasFuehrungsstelle: true,
|
||||
positionX: 400,
|
||||
positionY: 200
|
||||
},
|
||||
{
|
||||
code: 'Abt 4',
|
||||
name: 'Cybercrime (CCCC)',
|
||||
color: '#7c3aed',
|
||||
hasFuehrungsstelle: true,
|
||||
positionX: 550,
|
||||
positionY: 200
|
||||
},
|
||||
{
|
||||
code: 'Abt 5',
|
||||
name: 'Kriminalwissenschaftliches und -technisches Institut',
|
||||
color: '#0d9488',
|
||||
hasFuehrungsstelle: true,
|
||||
positionX: 700,
|
||||
positionY: 200
|
||||
},
|
||||
{
|
||||
code: 'Abt 6',
|
||||
name: 'Fachaufsicht und Ermittlungsunterstützung',
|
||||
color: '#be185d',
|
||||
hasFuehrungsstelle: true,
|
||||
positionX: 850,
|
||||
positionY: 200
|
||||
},
|
||||
{
|
||||
code: 'ZA',
|
||||
name: 'Zentralabteilung',
|
||||
color: '#6b7280',
|
||||
hasFuehrungsstelle: false,
|
||||
positionX: 1000,
|
||||
positionY: 200
|
||||
}
|
||||
]
|
||||
|
||||
const abteilungIds = {}
|
||||
|
||||
abteilungen.forEach((abt, index) => {
|
||||
abteilungIds[abt.code] = insertUnit(abt.code, abt.name, 'abteilung', 1, direktorId, {
|
||||
color: abt.color,
|
||||
orderIndex: 10 + index * 10,
|
||||
hasFuehrungsstelle: abt.hasFuehrungsstelle ? 1 : 0,
|
||||
fuehrungsstelleName: abt.hasFuehrungsstelle ? `Führungsstelle ${abt.code}` : null,
|
||||
positionX: abt.positionX,
|
||||
positionY: abt.positionY
|
||||
})
|
||||
})
|
||||
|
||||
// ========================================
|
||||
// Dezernate für Abteilung 1 (Organisierte Kriminalität)
|
||||
// ========================================
|
||||
|
||||
const dez11 = insertUnit('Dez 11', 'Ermittlungen OK, OK Rauschgift', 'dezernat', 2, abteilungIds['Abt 1'])
|
||||
insertUnit('SG 11.1', 'Grundsatzfragen/Koordination/Auswertung', 'sachgebiet', 3, dez11)
|
||||
|
||||
const dez12 = insertUnit('Dez 12', 'Wirtschaftskriminalität', 'dezernat', 2, abteilungIds['Abt 1'])
|
||||
insertUnit('SG 12.1', 'Grundsatzfragen/Koordination/Auswertung', 'sachgebiet', 3, dez12)
|
||||
|
||||
const dez13 = insertUnit('Dez 13', 'Finanzermittlungen', 'dezernat', 2, abteilungIds['Abt 1'])
|
||||
insertUnit('SG 13.1', 'GFG 1', 'sachgebiet', 3, dez13)
|
||||
insertUnit('SG 13.2', 'GFG 2', 'sachgebiet', 3, dez13)
|
||||
insertUnit('SG 13.3', 'Verfahrensintegrierte Finanzermittlungen/Vermögensabschöpfung', 'sachgebiet', 3, dez13)
|
||||
insertUnit('SG 13.4', 'Zentrale Informations- und Koordinierungsstelle Finanzermittlung und Gewinnabschöpfung, Recherchestelle ZIVED', 'sachgebiet', 3, dez13)
|
||||
|
||||
const dez14 = insertUnit('Dez 14', 'Auswerte- und Analysestelle OK', 'dezernat', 2, abteilungIds['Abt 1'])
|
||||
insertUnit('TD 14.1', 'Operative Auswertung und Analyse, kryptierte Täterkommunikation', 'teildezernat', 3, dez14)
|
||||
insertUnit('SG 14.2', 'Strategische Auswertung und Analyse, Informationssteuerung', 'sachgebiet', 3, dez14)
|
||||
insertUnit('SG 14.3', 'Technische Informationssysteme, Unterstützungsgruppe CASE/DAR', 'sachgebiet', 3, dez14)
|
||||
insertUnit('SG 14.4', 'Auswertung und Analyse Rockerkriminalität', 'sachgebiet', 3, dez14)
|
||||
insertUnit('SG 14.5', 'Auswertung und Analyse Clankriminalität', 'sachgebiet', 3, dez14)
|
||||
|
||||
const dez15 = insertUnit('Dez 15', 'Korruption, Umweltkriminalität', 'dezernat', 2, abteilungIds['Abt 1'])
|
||||
insertUnit('SG 15.1', 'Grundsatzangelegenheiten, Korruption', 'sachgebiet', 3, dez15)
|
||||
insertUnit('SG 15.2', 'Vernetzungsstelle Umweltkriminalität', 'sachgebiet', 3, dez15)
|
||||
|
||||
const dez16 = insertUnit('Dez 16', 'Finanzierung Organisierter Kriminalität und Terrorismus', 'dezernat', 2, abteilungIds['Abt 1'])
|
||||
insertUnit('SG 16.1', 'Grundsatzfragen/Auswertung/Analyse', 'sachgebiet', 3, dez16)
|
||||
|
||||
// ========================================
|
||||
// Dezernate für Abteilung 2 (Terrorismusbekämpfung)
|
||||
// ========================================
|
||||
|
||||
const dez21 = insertUnit('Dez 21', 'Ermittlungen', 'dezernat', 2, abteilungIds['Abt 2'])
|
||||
insertUnit('TD 21.1', 'Ermittlungskommissionen VSTGB, Ermittlungskommissionen PMK (alle Phänomenbereiche), VsnL', 'teildezernat', 3, dez21)
|
||||
|
||||
const dez22 = insertUnit('Dez 22', 'Auswertung/Analyse, ZMI, Open Source Intelligence (OSINT), Wissenschaftlicher Dienst PMK, KPMD-PMK', 'dezernat', 2, abteilungIds['Abt 2'])
|
||||
insertUnit('SG 22.1', 'Auswertung/Analyse, ZMI', 'sachgebiet', 3, dez22)
|
||||
insertUnit('TD 22.2', 'Open Source Intelligence (OSINT)', 'teildezernat', 3, dez22)
|
||||
insertUnit('TD 22.3', 'Wissenschaftlicher Dienst PMK', 'teildezernat', 3, dez22)
|
||||
insertUnit('SG 22.4', 'PMK Meldedienste, Kriminalpolizeilicher Meldedienst (KPMD)', 'sachgebiet', 3, dez22)
|
||||
|
||||
const dez23 = insertUnit('Dez 23', 'PMK Rechts und PMK SZ', 'dezernat', 2, abteilungIds['Abt 2'])
|
||||
insertUnit('SG 23.1', 'KoSt Gefährder, Gemeinsames Extremismus- und Terrorismusabwehrzentrum (GETZ-) Rechts, Landesvertreter GETZ-Rechts Bund', 'sachgebiet', 3, dez23)
|
||||
insertUnit('SG 23.2', 'Prüffallbearbeitung, Gefahrensachverhalte, Hasskriminalität, PMK rechts', 'sachgebiet', 3, dez23)
|
||||
insertUnit('TD 23.3', 'PMK SZ, Spionage, Gefahrensachverhalte PMK SZ, Landesvertreter GETZ-SP', 'teildezernat', 3, dez23)
|
||||
|
||||
const dez24 = insertUnit('Dez 24', 'PMK Religiöse Ideologie', 'dezernat', 2, abteilungIds['Abt 2'])
|
||||
insertUnit('SG 24.1', 'KoST Gefährder, SiKo', 'sachgebiet', 3, dez24)
|
||||
insertUnit('TD 24.2', 'Gemeinsames Terrorismusabwehrzentrum (GTAZ) NRW, Landesvertreter GTAZ Bund, ATD/RED', 'teildezernat', 3, dez24)
|
||||
insertUnit('SG 24.3', 'Prüffallbearbeitung, Gefahrensachverhalte, islamistisch-terroristisches Personenpotential', 'sachgebiet', 3, dez24)
|
||||
|
||||
const dez25 = insertUnit('Dez 25', 'PMK Links, Ausländische Ideologie, ZSÜ', 'dezernat', 2, abteilungIds['Abt 2'])
|
||||
insertUnit('SG 25.1', 'KoSt Gefährder Links, GETZ-Links NRW, Landesvertreter GETZ-Links Bund, Prüffallsachbearbeitung, Gefahrensachverhalte', 'sachgebiet', 3, dez25)
|
||||
insertUnit('SG 25.2', 'KoSt Gefährder Ausländische Ideologie (AI), Landesvertreter GETZ-AI Bund, Prüffallsachbearbeitung, Gefahrensachverhalte', 'sachgebiet', 3, dez25)
|
||||
insertUnit('SG 25.3', 'Zentrale Stelle NRW für ZSÜ', 'sachgebiet', 3, dez25)
|
||||
|
||||
// ========================================
|
||||
// Dezernate für Abteilung 3 (Strategische Kriminalitätsbekämpfung)
|
||||
// ========================================
|
||||
|
||||
const dez31 = insertUnit('Dez 31', 'Kriminalitätsauswertung und Analyse, Polizeiliche Kriminalitätsstatistik', 'dezernat', 2, abteilungIds['Abt 3'])
|
||||
insertUnit('SG 31.1', 'Grundsatzangelegenheiten / KoST MAfEx / KoSt MOTIV/MIT/aMIT', 'sachgebiet', 3, dez31)
|
||||
insertUnit('SG 31.2', 'Auswertung und Analyse 1, Eigentums- und Vermögensdelikte, SÄM-ÜT, KoSt RTE', 'sachgebiet', 3, dez31)
|
||||
insertUnit('SG 31.3', 'Auswertung/Analyse 2 Rauschgift-, Arzneimittel-, Menschenhandels-, Schleusungskriminalität, Dokumentenfälschungen, Gewaltdelikte', 'sachgebiet', 3, dez31)
|
||||
insertUnit('SG 31.4', 'Polizeiliche Kriminalstatistik (PKS)', 'sachgebiet', 3, dez31)
|
||||
|
||||
const dez32 = insertUnit('Dez 32', 'Kriminalprävention, Kriminalistisch-Kriminologische Forschungsstelle, Evaluation', 'dezernat', 2, abteilungIds['Abt 3'])
|
||||
insertUnit('SG 32.1', 'Kriminalprävention und Opferschutz', 'sachgebiet', 3, dez32)
|
||||
insertUnit('TD 32.2', 'Kriminalistisch-Kriminologische Forschungsstelle (KKF)', 'teildezernat', 3, dez32)
|
||||
insertUnit('SG 32.3', 'Zentralstelle Evaluation (ZEVA)', 'sachgebiet', 3, dez32)
|
||||
|
||||
const dez33 = insertUnit('Dez 33', 'Fahndung, Datenaustausch Polizei/Justiz, Kriminalaktenhaltung, Internationale Rechtshilfe', 'dezernat', 2, abteilungIds['Abt 3'])
|
||||
insertUnit('SG 33.1', 'Datenstation, Polizeiliche Beobachtung, Grundsatz Fahndung, Fahndungsportal', 'sachgebiet', 3, dez33)
|
||||
insertUnit('SG 33.2', 'Datenaustausch Polizei/Justiz, Kriminalaktenhaltung', 'sachgebiet', 3, dez33)
|
||||
insertUnit('SG 33.3', 'Rechtshilfe, PNR, internationale Fahndung, Interpol- und Europolangelegenheiten, Vermisste', 'sachgebiet', 3, dez33)
|
||||
|
||||
const dez34 = insertUnit('Dez 34', 'Digitalstrategie, Polizeifachliche IT, Landeszentrale Qualitätssicherung', 'dezernat', 2, abteilungIds['Abt 3'])
|
||||
insertUnit('TD 34.1', 'Grundsatz, Gremien, Fachliche IT-Projekte', 'teildezernat', 3, dez34)
|
||||
insertUnit('TD 34.2', 'Zentralstelle Polizei 2020, PIAV, QS Verbundanwendungen, Europäisches Informationssystem (EIS)', 'teildezernat', 3, dez34)
|
||||
insertUnit('TD 34.3', 'IT FaKo Fachbereich Kriminalität, Zentralstelle ViVA, QS, INPOL-Z, ViVA-Büro LKA', 'teildezernat', 3, dez34)
|
||||
|
||||
const dez35 = insertUnit('Dez 35', 'Verhaltensanalyse und Risikomanagement', 'dezernat', 2, abteilungIds['Abt 3'])
|
||||
insertUnit('SG 35.1', 'Zentralstelle KURS NRW', 'sachgebiet', 3, dez35)
|
||||
insertUnit('SG 35.2', 'Operative Fallanalyse (OFA/ViCLAS)', 'sachgebiet', 3, dez35)
|
||||
insertUnit('SG 35.3', 'Zentralstelle PeRiskoP', 'sachgebiet', 3, dez35)
|
||||
|
||||
// ========================================
|
||||
// Dezernate für Abteilung 4 (Cybercrime)
|
||||
// ========================================
|
||||
|
||||
const dez41 = insertUnit('Dez 41', 'Zentrale Ansprechstelle Cybercrime, Grundsatz, Digitale Forensik, IT-Entwicklung', 'dezernat', 2, abteilungIds['Abt 4'])
|
||||
insertUnit('SG 41.1', 'Grundsatzangelegenheiten, Prävention, Auswertung Cybercrime', 'sachgebiet', 3, dez41)
|
||||
insertUnit('TD 41.2', 'Software und KI-Entwicklung, IT-Verfahrensbetreuung, Marktschau', 'teildezernat', 3, dez41)
|
||||
insertUnit('SG 41.3', 'Forensik Desktop Strategie und Entwicklung - Hinweisportal NRW', 'sachgebiet', 3, dez41)
|
||||
insertUnit('SG 41.4', 'Forensik Desktop Datenaufbereitung und Automatisierung', 'sachgebiet', 3, dez41)
|
||||
insertUnit('TD 41.5', 'Forensik Desktop Betrieb', 'teildezernat', 3, dez41)
|
||||
|
||||
const dez42 = insertUnit('Dez 42', 'Cyber-Recherche- und Fahndungszentrum, Ermittlungen Cybercrime', 'dezernat', 2, abteilungIds['Abt 4'])
|
||||
insertUnit('SG 42.1', 'Personenorientierte Recherche in Datennetzen', 'sachgebiet', 3, dez42)
|
||||
insertUnit('SG 42.2', 'Sachorientierte Recherche in Datennetzen', 'sachgebiet', 3, dez42)
|
||||
insertUnit('TD 42.3', 'Interventionsteams Digitale Tatorte IUK-Lageunterstützung Ermittlungskommissionen', 'teildezernat', 3, dez42)
|
||||
|
||||
const dez43 = insertUnit('Dez 43', 'Zentrale Auswertungs- und Sammelstelle (ZASt) für die Bekämpfung von Missbrauchsabbildungen von Kindern und Jugendlichen', 'dezernat', 2, abteilungIds['Abt 4'])
|
||||
insertUnit('SG 43.1', 'ZASt Grundsatz, Identifizierungsverfahren, Bildvergleichssammlung, Schulfahndung, Berichtswesen, Meldedienste/Verbundverfahren, Gremien', 'sachgebiet', 3, dez43)
|
||||
insertUnit('SG 43.2', 'ZASt NCMEC/ Landeszentrale Bewertung 1', 'sachgebiet', 3, dez43)
|
||||
insertUnit('SG 43.3', 'ZASt NCMEC/ Landeszentrale Bewertung 2', 'sachgebiet', 3, dez43)
|
||||
insertUnit('SG 43.4', 'ZASt NCMEC/ Landeszentrale Bewertung 3', 'sachgebiet', 3, dez43)
|
||||
|
||||
const dez44 = insertUnit('Dez 44', 'Telekommunikationsüberwachung (TKÜ)', 'dezernat', 2, abteilungIds['Abt 4'])
|
||||
insertUnit('SG 44.1', 'Grundsatzaufgaben, operative TKÜ, AIT', 'sachgebiet', 3, dez44)
|
||||
insertUnit('SG 44.2', 'TKÜ Betrieb und Service', 'sachgebiet', 3, dez44)
|
||||
insertUnit('TD 44.3', 'Digitale Forensik, IUK-Ermittlungsunterstützung', 'teildezernat', 3, dez44)
|
||||
|
||||
// ========================================
|
||||
// Dezernate für Abteilung 5 (Kriminalwissenschaftliches Institut)
|
||||
// ========================================
|
||||
|
||||
const dez51 = insertUnit('Dez 51', 'Chemie, Physik', 'dezernat', 2, abteilungIds['Abt 5'])
|
||||
insertUnit('TD 51.1', 'Schussspuren, Explosivstoffe, Brand, Elektrotechnik', 'teildezernat', 3, dez51)
|
||||
insertUnit('TD 51.2', 'Betäubungsmittel', 'teildezernat', 3, dez51)
|
||||
|
||||
const dez52 = insertUnit('Dez 52', 'Serologie, DNA-Analytik', 'dezernat', 2, abteilungIds['Abt 5'])
|
||||
insertUnit('TD 52.1', 'DNA-Probenbearbeitung, DNA-Spurenbearbeitung I', 'teildezernat', 3, dez52)
|
||||
insertUnit('TD 52.2', 'DNA-Spurenbearbeitung II', 'teildezernat', 3, dez52)
|
||||
insertUnit('TD 52.3', 'DNA-Spurenbearbeitung III', 'teildezernat', 3, dez52)
|
||||
insertUnit('TD 52.4', 'DNA-Spurenbearbeitung IV DNA-Fremdvergabe', 'teildezernat', 3, dez52)
|
||||
|
||||
const dez53 = insertUnit('Dez 53', 'Biologie, Materialspuren, Urkunden, Handschriften', 'dezernat', 2, abteilungIds['Abt 5'])
|
||||
insertUnit('TD 53.1', 'forensische Textilkunde, Botanik, Material-, Haar- und Erdspuren', 'teildezernat', 3, dez53)
|
||||
insertUnit('SG 53.2', 'Urkunden, Handschriften', 'sachgebiet', 3, dez53)
|
||||
|
||||
const dez54 = insertUnit('Dez 54', 'Zentralstelle Kriminaltechnik, Forensische Medientechnik, USBV Entschärfung', 'dezernat', 2, abteilungIds['Abt 5'])
|
||||
insertUnit('SG 54.1', 'Zentralstelle Kriminaltechnik', 'sachgebiet', 3, dez54)
|
||||
insertUnit('SG 54.2', 'Tatortvermessung, Rekonstruktion und XR-Lab, Phantombilderstellung und visuelle Fahndungshilfe, Bild- und Videotechnik', 'sachgebiet', 3, dez54)
|
||||
insertUnit('SG 54.3', 'Entschärfung von unkonventionellen Spreng- und Brandvorrichtungen (USBV) – Einsatz- und Ermittlungsunterstützung Explosivstoffe', 'sachgebiet', 3, dez54)
|
||||
|
||||
const dez55 = insertUnit('Dez 55', 'Waffen und Werkzeug DNA-Analyse-Datei', 'dezernat', 2, abteilungIds['Abt 5'])
|
||||
insertUnit('SG 55.1', 'Waffen und Munition', 'sachgebiet', 3, dez55)
|
||||
insertUnit('SG 55.2', 'Werkzeug-, Form-, Passspuren, Schließ- und Sicherungseinrichtungen', 'sachgebiet', 3, dez55)
|
||||
insertUnit('SG 55.3', 'DNA-Analyse-Datei', 'sachgebiet', 3, dez55)
|
||||
|
||||
const dez56 = insertUnit('Dez 56', 'Daktyloskopie, Gesichts- und Sprechererkennung, Tonträgerauswertung', 'dezernat', 2, abteilungIds['Abt 5'])
|
||||
insertUnit('SG 56.1', 'Daktyloskopisches Labor', 'sachgebiet', 3, dez56)
|
||||
insertUnit('SG 56.2', 'AFIS I/Daktyloskopische Gutachten', 'sachgebiet', 3, dez56)
|
||||
insertUnit('SG 56.3', 'AFIS II/Daktyloskopische Gutachten', 'sachgebiet', 3, dez56)
|
||||
insertUnit('TD 56.4', 'Gesichts- und Sprechererkennung, Tonträgerauswertung', 'teildezernat', 3, dez56)
|
||||
|
||||
// ========================================
|
||||
// Dezernate für Abteilung 6 (Fachaufsicht und Ermittlungsunterstützung)
|
||||
// ========================================
|
||||
|
||||
const dez61 = insertUnit('Dez 61', 'Kriminalitätsangelegenheiten der KPB, Fachcontrolling, Koordination PUA, Lagedienst', 'dezernat', 2, abteilungIds['Abt 6'])
|
||||
insertUnit('TD 61.1', 'Kriminalitätsangelegenheiten der KPB, Fachcontrolling, Koordination PUA', 'teildezernat', 3, dez61)
|
||||
insertUnit('SG 61.2', 'Lagedienst', 'sachgebiet', 3, dez61)
|
||||
|
||||
const dez62 = insertUnit('Dez 62', 'Fahndungsgruppe Staatsschutz', 'dezernat', 2, abteilungIds['Abt 6'])
|
||||
// Add Führungsgruppe for Dezernat 62
|
||||
insertUnit('FüGr 62', 'Führungsgruppe', 'fuehrungsstelle', 3, dez62)
|
||||
// Fahndungsgruppen 1-8
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
insertUnit(`SG 62.${i}`, i === 8 ? 'Fahndungsgruppe 8' : `Fahndungsgruppe ${i}`, 'sachgebiet', 3, dez62)
|
||||
}
|
||||
insertUnit('SG 62.9', 'Technische Gruppe', 'sachgebiet', 3, dez62)
|
||||
|
||||
const dez63 = insertUnit('Dez 63', 'Verdeckte Ermittlungen, Zeugenschutz', 'dezernat', 2, abteilungIds['Abt 6'])
|
||||
insertUnit('SG 63.1', 'Einsatz VE 1', 'sachgebiet', 3, dez63)
|
||||
insertUnit('SG 63.2', 'Einsatz VE 2', 'sachgebiet', 3, dez63)
|
||||
insertUnit('SG 63.3', 'Scheinkäufer, Logistik', 'sachgebiet', 3, dez63)
|
||||
insertUnit('SG 63.4', 'VP-Führung, Koordinierung VP', 'sachgebiet', 3, dez63)
|
||||
insertUnit('TD 63.5', 'Zeugenschutz, OpOS', 'teildezernat', 3, dez63)
|
||||
|
||||
const dez64 = insertUnit('Dez 64', 'Mobiles Einsatzkommando, Technische Einsatzgruppe, Zielfahndung', 'dezernat', 2, abteilungIds['Abt 6'])
|
||||
insertUnit('FüGr 64', 'FüGr', 'fuehrungsstelle', 3, dez64)
|
||||
insertUnit('SG 64.1', 'MEK 1', 'sachgebiet', 3, dez64)
|
||||
insertUnit('SG 64.2', 'MEK 2', 'sachgebiet', 3, dez64)
|
||||
insertUnit('SG 64.3', 'Technische Einsatzgruppe', 'sachgebiet', 3, dez64)
|
||||
insertUnit('SG 64.4', 'Zielfahndung', 'sachgebiet', 3, dez64)
|
||||
|
||||
// ========================================
|
||||
// Dezernate für Zentralabteilung
|
||||
// ========================================
|
||||
|
||||
const dezZA1 = insertUnit('Dez ZA 1', 'Haushalts-, Wirtschafts-, Liegenschaftsmanagement, Zentrale Vergabestelle', 'dezernat', 2, abteilungIds['ZA'])
|
||||
insertUnit('SG ZA 1.1', 'Haushalts- und Wirtschaftsangelegenheiten', 'sachgebiet', 3, dezZA1)
|
||||
insertUnit('SG ZA 1.2', 'Liegenschaftsmanagement', 'sachgebiet', 3, dezZA1)
|
||||
insertUnit('SG ZA 1.3', 'Zentrale Vergabestelle', 'sachgebiet', 3, dezZA1)
|
||||
|
||||
const dezZA2 = insertUnit('Dez ZA 2', 'Personalangelegenheiten, Gleichstellungsbeauftragte, Fortbildung', 'dezernat', 2, abteilungIds['ZA'])
|
||||
insertUnit('SG ZA 2.1', 'Personalentwicklung, Arbeitszeit, BGM-POL; Stellenplan', 'sachgebiet', 3, dezZA2)
|
||||
insertUnit('SG ZA 2.2', 'Personalverwaltung Beamte/ dienstrechtliche Angelegenheiten', 'sachgebiet', 3, dezZA2)
|
||||
insertUnit('TD ZA 2.3', 'Personalverwaltung Regierungsbeschäftigte/ Personalgewinnung, tarif- und arbeitsrechtliche Angelegenheiten', 'teildezernat', 3, dezZA2)
|
||||
insertUnit('SG ZA 2.4', 'Fortbildung', 'sachgebiet', 3, dezZA2)
|
||||
|
||||
const dezZA3 = insertUnit('Dez ZA 3', 'Informationstechnik und Anwenderunterstützung, Informationssicherheit, Kfz-, Waffen- und Geräteangelegenheiten', 'dezernat', 2, abteilungIds['ZA'])
|
||||
insertUnit('SG ZA 3.1', 'IT-Grundsatz, Planung und Koordinierung, IT-Service Desk, Lizenzmanagement', 'sachgebiet', 3, dezZA3)
|
||||
insertUnit('SG ZA 3.2', 'Netzwerk/ TK-Anlage', 'sachgebiet', 3, dezZA3)
|
||||
insertUnit('SG ZA 3.3', 'Server/Client, Logistik', 'sachgebiet', 3, dezZA3)
|
||||
insertUnit('SG ZA 3.4', 'Kfz-, Waffen- und Geräteangelegenheiten', 'sachgebiet', 3, dezZA3)
|
||||
|
||||
const dezZA4 = insertUnit('Dez ZA 4', 'Vereins- und Waffenrecht, Innenrevision, Sponsoring, Rechtsangelegenheiten, Datenschutz, Geheimschutz', 'dezernat', 2, abteilungIds['ZA'])
|
||||
|
||||
const dezZA5 = insertUnit('Dez ZA 5', 'Polizeiärztlicher Dienst', 'dezernat', 2, abteilungIds['ZA'])
|
||||
insertUnit('PAD', 'Polizeiärztin', 'sachgebiet', 3, dezZA5)
|
||||
|
||||
console.log('✅ LKA NRW Organizational Structure seeded successfully!')
|
||||
console.log(`📊 Created ${Object.keys(unitIds).length} organizational units`)
|
||||
|
||||
// Output some statistics
|
||||
const stats = db.prepare(`
|
||||
SELECT type, COUNT(*) as count
|
||||
FROM organizational_units
|
||||
GROUP BY type
|
||||
`).all()
|
||||
|
||||
console.log('\n📈 Statistics:')
|
||||
stats.forEach(stat => {
|
||||
console.log(` ${stat.type}: ${stat.count} units`)
|
||||
})
|
||||
|
||||
// Close database
|
||||
db.close()
|
||||
|
||||
console.log('\n🎉 Done! The organization structure is ready to use.')
|
||||
console.log(' Navigate to Admin Panel > Organigramm to manage the structure.')
|
||||
console.log(' Users can view it in the main app using the 🏢 button.')
|
||||
386
backend/scripts/seed-organization.js
Normale Datei
386
backend/scripts/seed-organization.js
Normale Datei
@ -0,0 +1,386 @@
|
||||
const Database = require('better-sqlite3')
|
||||
const path = require('path')
|
||||
const { v4: uuidv4 } = require('uuid')
|
||||
|
||||
// Open database
|
||||
const dbPath = path.join(__dirname, '..', 'skillmate.dev.encrypted.db')
|
||||
const db = new Database(dbPath)
|
||||
|
||||
// Enable foreign keys
|
||||
db.pragma('foreign_keys = ON')
|
||||
|
||||
console.log('🏢 Seeding LKA NRW Organizational Structure...')
|
||||
|
||||
const now = new Date().toISOString()
|
||||
|
||||
// Helper function to insert organizational unit
|
||||
function insertUnit(code, name, type, level, parentId = null, options = {}) {
|
||||
const id = uuidv4()
|
||||
|
||||
db.prepare(`
|
||||
INSERT OR REPLACE INTO organizational_units (
|
||||
id, code, name, type, level, parent_id,
|
||||
color, order_index, has_fuehrungsstelle,
|
||||
fuehrungsstelle_name, is_active, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id, code, name, type, level, parentId,
|
||||
options.color || null,
|
||||
options.orderIndex || 0,
|
||||
options.hasFuehrungsstelle || 0,
|
||||
options.fuehrungsstelleName || null,
|
||||
1, now, now
|
||||
)
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
// Clear existing data
|
||||
console.log('Clearing existing organizational data...')
|
||||
db.prepare('DELETE FROM deputy_delegations').run()
|
||||
db.prepare('DELETE FROM deputy_assignments').run()
|
||||
db.prepare('DELETE FROM special_positions').run()
|
||||
db.prepare('DELETE FROM employee_unit_assignments').run()
|
||||
db.prepare('DELETE FROM organizational_units').run()
|
||||
|
||||
// Create Direktor
|
||||
const direktorId = insertUnit('DIR', 'Direktor LKA NRW', 'direktion', 0, null, {
|
||||
color: '#1e3a8a',
|
||||
orderIndex: 0
|
||||
})
|
||||
|
||||
// Create Leitungsstab
|
||||
const leitungsstabId = insertUnit('LStab', 'Leitungsstab', 'stabsstelle', 1, direktorId, {
|
||||
color: '#6b7280',
|
||||
orderIndex: 1
|
||||
})
|
||||
|
||||
// Leitungsstab Sub-units
|
||||
insertUnit('LStab 1', 'Grundsatzangelegenheiten, Gremien, internationale polizeiliche Zusammenarbeit', 'sachgebiet', 2, leitungsstabId)
|
||||
insertUnit('LStab 2', 'Strategische Steuerung, Qualitätsmanagement, Controlling', 'sachgebiet', 2, leitungsstabId)
|
||||
insertUnit('LStab 3', 'Presse- und Öffentlichkeitsarbeit', 'sachgebiet', 2, leitungsstabId)
|
||||
|
||||
// Create Special Units (Beauftragte)
|
||||
const personalratId = insertUnit('PR', 'Personalrat', 'sondereinheit', 1, direktorId, {
|
||||
color: '#059669',
|
||||
orderIndex: 2
|
||||
})
|
||||
|
||||
const schwerbehindertenId = insertUnit('SBV', 'Schwerbehindertenvertretung', 'sondereinheit', 1, direktorId, {
|
||||
color: '#059669',
|
||||
orderIndex: 3
|
||||
})
|
||||
|
||||
// Create Abteilungen with color gradients
|
||||
const abteilungen = [
|
||||
{ code: 'Abt 1', name: 'Organisierte Kriminalität', color: '#dc2626' },
|
||||
{ code: 'Abt 2', name: 'Terrorismusbekämpfung und Staatsschutz', color: '#ea580c' },
|
||||
{ code: 'Abt 3', name: 'Strategische Kriminalitätsbekämpfung', color: '#0891b2' },
|
||||
{ code: 'Abt 4', name: 'Cybercrime (CCCC)', color: '#7c3aed' },
|
||||
{ code: 'Abt 5', name: 'Kriminalwissenschaftliches und -technisches Institut', color: '#0d9488' },
|
||||
{ code: 'Abt 6', name: 'Fachaufsicht und Ermittlungsunterstützung', color: '#be185d' },
|
||||
{ code: 'ZA', name: 'Zentralabteilung', color: '#6b7280' }
|
||||
]
|
||||
|
||||
const abteilungIds = {}
|
||||
|
||||
abteilungen.forEach((abt, index) => {
|
||||
abteilungIds[abt.code] = insertUnit(abt.code, abt.name, 'abteilung', 1, direktorId, {
|
||||
color: abt.color,
|
||||
orderIndex: 10 + index * 10,
|
||||
hasFuehrungsstelle: abt.code !== 'ZA' ? 1 : 0,
|
||||
fuehrungsstelleName: abt.code !== 'ZA' ? `Führungsstelle ${abt.code}` : null
|
||||
})
|
||||
})
|
||||
|
||||
// Create Dezernate for Abteilung 1 (Organisierte Kriminalität)
|
||||
const dezernate1 = [
|
||||
{ code: '11', name: 'Ermittlungen OK, OK Rauschgift' },
|
||||
{ code: '12', name: 'Wirtschaftskriminalität' },
|
||||
{ code: '13', name: 'Finanzermittlungen' },
|
||||
{ code: '14', name: 'Auswerte- und Analysestelle OK' },
|
||||
{ code: '15', name: 'Korruption, Umweltkriminalität' },
|
||||
{ code: '16', name: 'Finanzierung Organisierter Kriminalität und Terrorismus' }
|
||||
]
|
||||
|
||||
dezernate1.forEach((dez, index) => {
|
||||
const dezId = insertUnit(`Dezernat ${dez.code}`, dez.name, 'dezernat', 2, abteilungIds['Abt 1'], {
|
||||
orderIndex: index
|
||||
})
|
||||
|
||||
// Add sample Sachgebiete
|
||||
if (dez.code === '11') {
|
||||
insertUnit('SG 11.1', 'Grundsatzfragen/Koordination/Auswertung', 'sachgebiet', 3, dezId)
|
||||
}
|
||||
if (dez.code === '12') {
|
||||
insertUnit('SG 12.1', 'Grundsatzfragen/Koordination/Auswertung', 'sachgebiet', 3, dezId)
|
||||
}
|
||||
if (dez.code === '13') {
|
||||
insertUnit('SG 13.1', 'GFG 1', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 13.2', 'GFG 2', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 13.3', 'Verfahrensintegrierte Finanzermittlungen/Vermögensabschöpfung', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 13.4', 'Zentrale Informations- und Koordinierungsstelle Finanzermittlung', 'sachgebiet', 3, dezId)
|
||||
}
|
||||
if (dez.code === '14') {
|
||||
insertUnit('TD 14.1', 'Operative Auswertung und Analyse', 'teildezernat', 3, dezId)
|
||||
insertUnit('SG 14.2', 'Strategische Auswertung und Analyse', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 14.3', 'Technische Informationssysteme', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 14.4', 'Auswertung und Analyse Rockerkriminalität', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 14.5', 'Auswertung und Analyse Clankriminalität', 'sachgebiet', 3, dezId)
|
||||
}
|
||||
if (dez.code === '15') {
|
||||
insertUnit('SG 15.1', 'Grundsatzangelegenheiten, Korruption', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 15.2', 'Vernetzungsstelle Umweltkriminalität', 'sachgebiet', 3, dezId)
|
||||
}
|
||||
if (dez.code === '16') {
|
||||
insertUnit('SG 16.1', 'Grundsatzfragen/Auswertung/Analyse', 'sachgebiet', 3, dezId)
|
||||
}
|
||||
})
|
||||
|
||||
// Create Dezernate for Abteilung 2 (Terrorismusbekämpfung)
|
||||
const dezernate2 = [
|
||||
{ code: '21', name: 'Ermittlungen' },
|
||||
{ code: '22', name: 'Auswertung/Analyse, ZMI, OSINT, Wissenschaftlicher Dienst PMK' },
|
||||
{ code: '23', name: 'PMK Rechts und PMK SZ' },
|
||||
{ code: '24', name: 'PMK Religiöse Ideologie' },
|
||||
{ code: '25', name: 'PMK Links, Ausländische Ideologie, ZSÜ' }
|
||||
]
|
||||
|
||||
dezernate2.forEach((dez, index) => {
|
||||
const dezId = insertUnit(`Dezernat ${dez.code}`, dez.name, 'dezernat', 2, abteilungIds['Abt 2'], {
|
||||
orderIndex: index
|
||||
})
|
||||
|
||||
if (dez.code === '21') {
|
||||
insertUnit('TD 21.1', 'Ermittlungskommissionen VSTGB', 'teildezernat', 3, dezId)
|
||||
}
|
||||
if (dez.code === '22') {
|
||||
insertUnit('SG 22.1', 'Auswertung/Analyse, ZMI', 'sachgebiet', 3, dezId)
|
||||
insertUnit('TD 22.2', 'Open Source Intelligence (OSINT)', 'teildezernat', 3, dezId)
|
||||
insertUnit('TD 22.3', 'Wissenschaftlicher Dienst PMK', 'teildezernat', 3, dezId)
|
||||
insertUnit('SG 22.4', 'PMK Meldedienste, KPMD', 'sachgebiet', 3, dezId)
|
||||
}
|
||||
if (dez.code === '23') {
|
||||
insertUnit('SG 23.1', 'KoSt Gefährder, GETZ-Rechts', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 23.2', 'Prüffallbearbeitung, Gefahrensachverhalte', 'sachgebiet', 3, dezId)
|
||||
insertUnit('TD 23.3', 'PMK SZ, Spionage', 'teildezernat', 3, dezId)
|
||||
}
|
||||
if (dez.code === '24') {
|
||||
insertUnit('SG 24.1', 'KoST Gefährder, SiKo', 'sachgebiet', 3, dezId)
|
||||
insertUnit('TD 24.2', 'Gemeinsames Terrorismusabwehrzentrum (GTAZ) NRW', 'teildezernat', 3, dezId)
|
||||
insertUnit('SG 24.3', 'Prüffallbearbeitung, islamistisch-terroristisches Personenpotential', 'sachgebiet', 3, dezId)
|
||||
}
|
||||
if (dez.code === '25') {
|
||||
insertUnit('SG 25.1', 'KoSt Gefährder Links', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 25.2', 'KoSt Gefährder Ausländische Ideologie', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 25.3', 'Zentrale Stelle NRW für ZSÜ', 'sachgebiet', 3, dezId)
|
||||
}
|
||||
})
|
||||
|
||||
// Create Dezernate for Abteilung 3 (Strategische Kriminalitätsbekämpfung)
|
||||
const dezernate3 = [
|
||||
{ code: '31', name: 'Kriminalitätsauswertung und Analyse' },
|
||||
{ code: '32', name: 'Kriminalprävention, KKF, Evaluation' },
|
||||
{ code: '33', name: 'Fahndung, Datenaustausch, Kriminalaktenhaltung' },
|
||||
{ code: '34', name: 'Digitalstrategie, Polizeifachliche IT' },
|
||||
{ code: '35', name: 'Verhaltensanalyse und Risikomanagement' }
|
||||
]
|
||||
|
||||
dezernate3.forEach((dez, index) => {
|
||||
const dezId = insertUnit(`Dezernat ${dez.code}`, dez.name, 'dezernat', 2, abteilungIds['Abt 3'], {
|
||||
orderIndex: index
|
||||
})
|
||||
|
||||
if (dez.code === '31') {
|
||||
insertUnit('SG 31.1', 'Grundsatzangelegenheiten/KoST MAfEx', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 31.2', 'Auswertung und Analyse 1', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 31.3', 'Auswertung/Analyse 2', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 31.4', 'Polizeiliche Kriminalstatistik (PKS)', 'sachgebiet', 3, dezId)
|
||||
}
|
||||
if (dez.code === '32') {
|
||||
insertUnit('SG 32.1', 'Kriminalprävention und Opferschutz', 'sachgebiet', 3, dezId)
|
||||
insertUnit('TD 32.2', 'Kriminalistisch-Kriminologische Forschungsstelle (KKF)', 'teildezernat', 3, dezId)
|
||||
insertUnit('SG 32.3', 'Zentralstelle Evaluation (ZEVA)', 'sachgebiet', 3, dezId)
|
||||
}
|
||||
if (dez.code === '33') {
|
||||
insertUnit('SG 33.1', 'Datenstation, Polizeiliche Beobachtung', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 33.2', 'Datenaustausch Polizei/Justiz', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 33.3', 'Rechtshilfe, PNR, internationale Fahndung', 'sachgebiet', 3, dezId)
|
||||
}
|
||||
if (dez.code === '34') {
|
||||
insertUnit('TD 34.1', 'Grundsatz, Gremien, Fachliche IT-Projekte', 'teildezernat', 3, dezId)
|
||||
insertUnit('TD 34.2', 'Zentralstelle Polizei 2020', 'teildezernat', 3, dezId)
|
||||
insertUnit('TD 34.3', 'IT FaKo Fachbereich Kriminalität', 'teildezernat', 3, dezId)
|
||||
}
|
||||
if (dez.code === '35') {
|
||||
insertUnit('SG 35.1', 'Zentralstelle KURS NRW', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 35.2', 'Operative Fallanalyse (OFA/ViCLAS)', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 35.3', 'Zentralstelle PeRiskoP', 'sachgebiet', 3, dezId)
|
||||
}
|
||||
})
|
||||
|
||||
// Create Dezernate for Abteilung 4 (Cybercrime)
|
||||
const dezernate4 = [
|
||||
{ code: '41', name: 'Zentrale Ansprechstelle Cybercrime, Grundsatz, Digitale Forensik' },
|
||||
{ code: '42', name: 'Cyber-Recherche- und Fahndungszentrum' },
|
||||
{ code: '43', name: 'Zentrale Auswertungs- und Sammelstelle (ZASt)' },
|
||||
{ code: '44', name: 'Telekommunikationsüberwachung (TKÜ)' }
|
||||
]
|
||||
|
||||
dezernate4.forEach((dez, index) => {
|
||||
const dezId = insertUnit(`Dezernat ${dez.code}`, dez.name, 'dezernat', 2, abteilungIds['Abt 4'], {
|
||||
orderIndex: index
|
||||
})
|
||||
|
||||
if (dez.code === '41') {
|
||||
insertUnit('SG 41.1', 'Grundsatzangelegenheiten, Prävention', 'sachgebiet', 3, dezId)
|
||||
insertUnit('TD 41.2', 'Software und KI-Entwicklung', 'teildezernat', 3, dezId)
|
||||
insertUnit('SG 41.3', 'Forensik Desktop Strategie', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 41.4', 'Forensik Desktop Datenaufbereitung', 'sachgebiet', 3, dezId)
|
||||
insertUnit('TD 41.5', 'Forensik Desktop Betrieb', 'teildezernat', 3, dezId)
|
||||
}
|
||||
if (dez.code === '42') {
|
||||
insertUnit('SG 42.1', 'Personenorientierte Recherche in Datennetzen', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 42.2', 'Sachorientierte Recherche in Datennetzen', 'sachgebiet', 3, dezId)
|
||||
insertUnit('TD 42.3', 'Interventionsteams Digitale Tatorte', 'teildezernat', 3, dezId)
|
||||
}
|
||||
if (dez.code === '43') {
|
||||
insertUnit('SG 43.1', 'ZASt Grundsatz', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 43.2', 'ZASt NCMEC/Landeszentrale Bewertung 1', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 43.3', 'ZASt NCMEC/Landeszentrale Bewertung 2', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 43.4', 'ZASt NCMEC/Landeszentrale Bewertung 3', 'sachgebiet', 3, dezId)
|
||||
}
|
||||
if (dez.code === '44') {
|
||||
insertUnit('SG 44.1', 'Grundsatzaufgaben, operative TKÜ', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 44.2', 'TKÜ Betrieb und Service', 'sachgebiet', 3, dezId)
|
||||
insertUnit('TD 44.3', 'Digitale Forensik, IUK-Ermittlungsunterstützung', 'teildezernat', 3, dezId)
|
||||
}
|
||||
})
|
||||
|
||||
// Create Dezernate for Abteilung 5 (Kriminalwissenschaftliches Institut)
|
||||
const dezernate5 = [
|
||||
{ code: '51', name: 'Chemie, Physik' },
|
||||
{ code: '52', name: 'Serologie, DNA-Analytik' },
|
||||
{ code: '53', name: 'Biologie, Materialspuren, Urkunden' },
|
||||
{ code: '54', name: 'Zentralstelle Kriminaltechnik, Forensische Medientechnik' },
|
||||
{ code: '55', name: 'Waffen und Werkzeug, DNA-Analyse-Datei' },
|
||||
{ code: '56', name: 'Daktyloskopie, Gesichts- und Sprechererkennung' }
|
||||
]
|
||||
|
||||
dezernate5.forEach((dez, index) => {
|
||||
const dezId = insertUnit(`Dezernat ${dez.code}`, dez.name, 'dezernat', 2, abteilungIds['Abt 5'], {
|
||||
orderIndex: index
|
||||
})
|
||||
|
||||
if (dez.code === '51') {
|
||||
insertUnit('TD 51.1', 'Schussspuren, Explosivstoffe, Brand', 'teildezernat', 3, dezId)
|
||||
insertUnit('TD 51.2', 'Betäubungsmittel', 'teildezernat', 3, dezId)
|
||||
}
|
||||
if (dez.code === '52') {
|
||||
insertUnit('TD 52.1', 'DNA-Probenbearbeitung', 'teildezernat', 3, dezId)
|
||||
insertUnit('TD 52.2', 'DNA-Spurenbearbeitung II', 'teildezernat', 3, dezId)
|
||||
insertUnit('TD 52.3', 'DNA-Spurenbearbeitung III', 'teildezernat', 3, dezId)
|
||||
insertUnit('TD 52.4', 'DNA-Spurenbearbeitung IV', 'teildezernat', 3, dezId)
|
||||
}
|
||||
if (dez.code === '53') {
|
||||
insertUnit('TD 53.1', 'Forensische Textilkunde, Botanik', 'teildezernat', 3, dezId)
|
||||
insertUnit('SG 53.2', 'Urkunden, Handschriften', 'sachgebiet', 3, dezId)
|
||||
}
|
||||
if (dez.code === '54') {
|
||||
insertUnit('SG 54.1', 'Zentralstelle Kriminaltechnik', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 54.2', 'Tatortvermessung, Rekonstruktion und XR-Lab', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 54.3', 'Entschärfung von USBV', 'sachgebiet', 3, dezId)
|
||||
}
|
||||
if (dez.code === '55') {
|
||||
insertUnit('SG 55.1', 'Waffen und Munition', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 55.2', 'Werkzeug-, Form-, Passspuren', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 55.3', 'DNA-Analyse-Datei', 'sachgebiet', 3, dezId)
|
||||
}
|
||||
if (dez.code === '56') {
|
||||
insertUnit('SG 56.1', 'Daktyloskopisches Labor', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 56.2', 'AFIS I/Daktyloskopische Gutachten', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 56.3', 'AFIS II/Daktyloskopische Gutachten', 'sachgebiet', 3, dezId)
|
||||
insertUnit('TD 56.4', 'Gesichts- und Sprechererkennung', 'teildezernat', 3, dezId)
|
||||
}
|
||||
})
|
||||
|
||||
// Create Dezernate for Abteilung 6 (Fachaufsicht)
|
||||
const dezernate6 = [
|
||||
{ code: '61', name: 'Kriminalitätsangelegenheiten der KPB, Fachcontrolling' },
|
||||
{ code: '62', name: 'Fahndungsgruppe Staatsschutz' },
|
||||
{ code: '63', name: 'Verdeckte Ermittlungen, Zeugenschutz' },
|
||||
{ code: '64', name: 'Mobiles Einsatzkommando, TEG, Zielfahndung' }
|
||||
]
|
||||
|
||||
dezernate6.forEach((dez, index) => {
|
||||
const dezId = insertUnit(`Dezernat ${dez.code}`, dez.name, 'dezernat', 2, abteilungIds['Abt 6'], {
|
||||
orderIndex: index
|
||||
})
|
||||
|
||||
if (dez.code === '61') {
|
||||
insertUnit('TD 61.1', 'Kriminalitätsangelegenheiten der KPB', 'teildezernat', 3, dezId)
|
||||
insertUnit('SG 61.2', 'Lagedienst', 'sachgebiet', 3, dezId)
|
||||
}
|
||||
if (dez.code === '62') {
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
if (i === 9) {
|
||||
insertUnit(`SG 62.${i}`, 'Technische Gruppe', 'sachgebiet', 3, dezId)
|
||||
} else {
|
||||
insertUnit(`SG 62.${i}`, `Fahndungsgruppe ${i}`, 'sachgebiet', 3, dezId)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dez.code === '63') {
|
||||
insertUnit('SG 63.1', 'Einsatz VE 1', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 63.2', 'Einsatz VE 2', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 63.3', 'Scheinkäufer, Logistik', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 63.4', 'VP-Führung, Koordinierung VP', 'sachgebiet', 3, dezId)
|
||||
insertUnit('TD 63.5', 'Zeugenschutz, OpOS', 'teildezernat', 3, dezId)
|
||||
}
|
||||
if (dez.code === '64') {
|
||||
insertUnit('SG 64.1', 'MEK 1', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 64.2', 'MEK 2', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 64.3', 'Technische Einsatzgruppe', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG 64.4', 'Zielfahndung', 'sachgebiet', 3, dezId)
|
||||
}
|
||||
})
|
||||
|
||||
// Create Dezernate for Zentralabteilung
|
||||
const dezernateZA = [
|
||||
{ code: 'ZA 1', name: 'Haushalts-, Wirtschafts-, Liegenschaftsmanagement' },
|
||||
{ code: 'ZA 2', name: 'Personalangelegenheiten, Gleichstellungsbeauftragte' },
|
||||
{ code: 'ZA 3', name: 'Informationstechnik und Anwenderunterstützung' },
|
||||
{ code: 'ZA 4', name: 'Vereins- und Waffenrecht, Innenrevision' },
|
||||
{ code: 'ZA 5', name: 'Polizeiärztlicher Dienst' }
|
||||
]
|
||||
|
||||
dezernateZA.forEach((dez, index) => {
|
||||
const dezId = insertUnit(`Dezernat ${dez.code}`, dez.name, 'dezernat', 2, abteilungIds['ZA'], {
|
||||
orderIndex: index
|
||||
})
|
||||
|
||||
if (dez.code === 'ZA 1') {
|
||||
insertUnit('SG ZA 1.1', 'Haushalts- und Wirtschaftsangelegenheiten', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG ZA 1.2', 'Liegenschaftsmanagement', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG ZA 1.3', 'Zentrale Vergabestelle', 'sachgebiet', 3, dezId)
|
||||
}
|
||||
if (dez.code === 'ZA 2') {
|
||||
insertUnit('SG ZA 2.1', 'Personalentwicklung, Arbeitszeit', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG ZA 2.2', 'Personalverwaltung Beamte', 'sachgebiet', 3, dezId)
|
||||
insertUnit('TD ZA 2.3', 'Personalverwaltung Regierungsbeschäftigte', 'teildezernat', 3, dezId)
|
||||
insertUnit('SG ZA 2.4', 'Fortbildung', 'sachgebiet', 3, dezId)
|
||||
}
|
||||
if (dez.code === 'ZA 3') {
|
||||
insertUnit('SG ZA 3.1', 'IT-Grundsatz, Planung und Koordinierung', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG ZA 3.2', 'Netzwerk/TK-Anlage', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG ZA 3.3', 'Server/Client, Logistik', 'sachgebiet', 3, dezId)
|
||||
insertUnit('SG ZA 3.4', 'Kfz-, Waffen- und Geräteangelegenheiten', 'sachgebiet', 3, dezId)
|
||||
}
|
||||
})
|
||||
|
||||
console.log('✅ LKA NRW Organizational Structure seeded successfully!')
|
||||
|
||||
// Close database
|
||||
db.close()
|
||||
|
||||
console.log('🎉 Done! Run the backend to see the organizational structure.')
|
||||
@ -376,6 +376,119 @@ export function initializeDatabase() {
|
||||
)
|
||||
`)
|
||||
|
||||
// Organizational Structure Tables
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS organizational_units (
|
||||
id TEXT PRIMARY KEY,
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL CHECK(type IN ('direktion', 'abteilung', 'dezernat', 'sachgebiet', 'teildezernat', 'fuehrungsstelle', 'stabsstelle', 'sondereinheit')),
|
||||
level INTEGER NOT NULL,
|
||||
parent_id TEXT,
|
||||
position_x INTEGER,
|
||||
position_y INTEGER,
|
||||
color TEXT,
|
||||
order_index INTEGER DEFAULT 0,
|
||||
description TEXT,
|
||||
has_fuehrungsstelle INTEGER DEFAULT 0,
|
||||
fuehrungsstelle_name TEXT,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY (parent_id) REFERENCES organizational_units(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_org_units_parent ON organizational_units(parent_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_org_units_type ON organizational_units(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_org_units_level ON organizational_units(level);
|
||||
`)
|
||||
|
||||
// Employee Unit Assignments
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS employee_unit_assignments (
|
||||
id TEXT PRIMARY KEY,
|
||||
employee_id TEXT NOT NULL,
|
||||
unit_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL CHECK(role IN ('leiter', 'stellvertreter', 'mitarbeiter', 'beauftragter')),
|
||||
start_date TEXT NOT NULL,
|
||||
end_date TEXT,
|
||||
is_primary INTEGER DEFAULT 1,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (unit_id) REFERENCES organizational_units(id) ON DELETE CASCADE,
|
||||
UNIQUE(employee_id, unit_id, role)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_emp_units_employee ON employee_unit_assignments(employee_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_emp_units_unit ON employee_unit_assignments(unit_id);
|
||||
`)
|
||||
|
||||
// Special Positions (Personalrat, Beauftragte, etc.)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS special_positions (
|
||||
id TEXT PRIMARY KEY,
|
||||
employee_id TEXT NOT NULL,
|
||||
position_type TEXT NOT NULL CHECK(position_type IN ('personalrat', 'schwerbehindertenvertretung', 'datenschutzbeauftragter', 'gleichstellungsbeauftragter', 'inklusionsbeauftragter', 'informationssicherheitsbeauftragter', 'geheimschutzbeauftragter', 'extremismusbeauftragter')),
|
||||
unit_id TEXT,
|
||||
start_date TEXT NOT NULL,
|
||||
end_date TEXT,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (unit_id) REFERENCES organizational_units(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_special_pos_employee ON special_positions(employee_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_special_pos_type ON special_positions(position_type);
|
||||
`)
|
||||
|
||||
// Deputy Assignments (Vertretungen)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS deputy_assignments (
|
||||
id TEXT PRIMARY KEY,
|
||||
principal_id TEXT NOT NULL,
|
||||
deputy_id TEXT NOT NULL,
|
||||
unit_id TEXT,
|
||||
valid_from TEXT NOT NULL,
|
||||
valid_until TEXT NOT NULL,
|
||||
reason TEXT,
|
||||
can_delegate INTEGER DEFAULT 1,
|
||||
created_by TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY (principal_id) REFERENCES employees(id),
|
||||
FOREIGN KEY (deputy_id) REFERENCES employees(id),
|
||||
FOREIGN KEY (unit_id) REFERENCES organizational_units(id),
|
||||
FOREIGN KEY (created_by) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_deputy_principal ON deputy_assignments(principal_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_deputy_deputy ON deputy_assignments(deputy_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_deputy_dates ON deputy_assignments(valid_from, valid_until);
|
||||
`)
|
||||
|
||||
// Deputy Delegations (Vertretungs-Weitergaben)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS deputy_delegations (
|
||||
id TEXT PRIMARY KEY,
|
||||
original_assignment_id TEXT NOT NULL,
|
||||
from_deputy_id TEXT NOT NULL,
|
||||
to_deputy_id TEXT NOT NULL,
|
||||
reason TEXT,
|
||||
delegated_at TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY (original_assignment_id) REFERENCES deputy_assignments(id),
|
||||
FOREIGN KEY (from_deputy_id) REFERENCES employees(id),
|
||||
FOREIGN KEY (to_deputy_id) REFERENCES employees(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_delegation_assignment ON deputy_delegations(original_assignment_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_delegation_from ON deputy_delegations(from_deputy_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_delegation_to ON deputy_delegations(to_deputy_id);
|
||||
`)
|
||||
|
||||
// Audit Log für Änderungsverfolgung
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
|
||||
@ -4,6 +4,7 @@ import helmet from 'helmet'
|
||||
import dotenv from 'dotenv'
|
||||
import path from 'path'
|
||||
import { initializeSecureDatabase } from './config/secureDatabase'
|
||||
import { initializeDatabase } from './config/database'
|
||||
import authRoutes from './routes/auth'
|
||||
import employeeRoutes from './routes/employeesSecure'
|
||||
import profileRoutes from './routes/profiles'
|
||||
@ -15,6 +16,9 @@ import workspaceRoutes from './routes/workspaces'
|
||||
import userRoutes from './routes/users'
|
||||
import userAdminRoutes from './routes/usersAdmin'
|
||||
import settingsRoutes from './routes/settings'
|
||||
import organizationRoutes from './routes/organization'
|
||||
import organizationImportRoutes from './routes/organizationImport'
|
||||
import employeeOrganizationRoutes from './routes/employeeOrganization'
|
||||
// import bookingRoutes from './routes/bookings' // Temporär deaktiviert wegen TS-Fehlern
|
||||
// import analyticsRoutes from './routes/analytics' // Temporär deaktiviert
|
||||
import { errorHandler } from './middleware/errorHandler'
|
||||
@ -26,8 +30,9 @@ dotenv.config()
|
||||
const app = express()
|
||||
const PORT = process.env.PORT || 3004
|
||||
|
||||
// Initialize secure database
|
||||
// Initialize secure database (core tables) and extended schema (organization, deputies, etc.)
|
||||
initializeSecureDatabase()
|
||||
initializeDatabase()
|
||||
|
||||
// Initialize sync scheduler
|
||||
syncScheduler
|
||||
@ -57,6 +62,9 @@ app.use('/api/workspaces', workspaceRoutes)
|
||||
app.use('/api/users', userRoutes)
|
||||
app.use('/api/admin/users', userAdminRoutes)
|
||||
app.use('/api/admin/settings', settingsRoutes)
|
||||
app.use('/api/organization', organizationRoutes)
|
||||
app.use('/api/organization', organizationImportRoutes)
|
||||
app.use('/api', employeeOrganizationRoutes)
|
||||
// app.use('/api/bookings', bookingRoutes) // Temporär deaktiviert
|
||||
// app.use('/api/analytics', analyticsRoutes) // Temporär deaktiviert
|
||||
|
||||
|
||||
166
backend/src/routes/employeeOrganization.ts
Normale Datei
166
backend/src/routes/employeeOrganization.ts
Normale Datei
@ -0,0 +1,166 @@
|
||||
import { Router } from 'express'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { db } from '../config/secureDatabase'
|
||||
import { authenticate, AuthRequest } from '../middleware/auth'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
const router = Router()
|
||||
|
||||
// Get employee's current organization unit
|
||||
router.get('/employee/:employeeId/organization', authenticate, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { employeeId } = req.params
|
||||
|
||||
// Check if user can access this employee's data
|
||||
if (req.user?.employeeId !== employeeId && req.user?.role !== 'admin' && req.user?.role !== 'superuser') {
|
||||
return res.status(403).json({ success: false, error: { message: 'Access denied' } })
|
||||
}
|
||||
|
||||
const unit = db.prepare(`
|
||||
SELECT
|
||||
ou.id, ou.code, ou.name, ou.type, ou.level,
|
||||
eua.role, eua.is_primary as isPrimary,
|
||||
eua.start_date as startDate
|
||||
FROM employee_unit_assignments eua
|
||||
JOIN organizational_units ou ON ou.id = eua.unit_id
|
||||
WHERE eua.employee_id = ?
|
||||
AND eua.is_primary = 1
|
||||
AND (eua.end_date IS NULL OR eua.end_date > datetime('now'))
|
||||
AND ou.is_active = 1
|
||||
ORDER BY eua.start_date DESC
|
||||
LIMIT 1
|
||||
`).get(employeeId)
|
||||
|
||||
if (!unit) {
|
||||
return res.json({ success: true, data: null })
|
||||
}
|
||||
|
||||
res.json({ success: true, data: unit })
|
||||
} catch (error) {
|
||||
logger.error('Error fetching employee organization:', error)
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
// Update employee's organization unit (simplified endpoint)
|
||||
router.put('/employee/:employeeId/organization', authenticate, async (req: AuthRequest, res: any, next: any) => {
|
||||
try {
|
||||
const { employeeId } = req.params
|
||||
const { unitId } = req.body
|
||||
|
||||
// Check permissions
|
||||
if (req.user?.employeeId !== employeeId && req.user?.role !== 'admin' && req.user?.role !== 'superuser') {
|
||||
return res.status(403).json({ success: false, error: { message: 'Access denied' } })
|
||||
}
|
||||
|
||||
const now = new Date().toISOString()
|
||||
|
||||
// Start transaction
|
||||
const transaction = db.transaction(() => {
|
||||
// End all current assignments for this employee
|
||||
db.prepare(`
|
||||
UPDATE employee_unit_assignments
|
||||
SET end_date = ?, updated_at = ?
|
||||
WHERE employee_id = ? AND end_date IS NULL
|
||||
`).run(now, now, employeeId)
|
||||
|
||||
// If unitId provided, create new assignment
|
||||
if (unitId) {
|
||||
// Verify unit exists
|
||||
const unit = db.prepare('SELECT id FROM organizational_units WHERE id = ? AND is_active = 1').get(unitId)
|
||||
if (!unit) {
|
||||
throw new Error('Unit not found')
|
||||
}
|
||||
|
||||
const assignmentId = uuidv4()
|
||||
db.prepare(`
|
||||
INSERT INTO employee_unit_assignments (
|
||||
id, employee_id, unit_id, role,
|
||||
start_date, is_primary, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
assignmentId, employeeId, unitId, 'mitarbeiter',
|
||||
now, 1, now, now
|
||||
)
|
||||
}
|
||||
|
||||
// Update employee's department field for backward compatibility
|
||||
if (unitId) {
|
||||
const unitInfo = db.prepare('SELECT name FROM organizational_units WHERE id = ?').get(unitId) as any
|
||||
if (unitInfo) {
|
||||
db.prepare(`
|
||||
UPDATE employees
|
||||
SET department = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
`).run(unitInfo.name, now, employeeId)
|
||||
}
|
||||
} else {
|
||||
// Clear department if no unit
|
||||
db.prepare(`
|
||||
UPDATE employees
|
||||
SET department = NULL, updated_at = ?
|
||||
WHERE id = ?
|
||||
`).run(now, employeeId)
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
transaction()
|
||||
res.json({ success: true, message: 'Organization updated successfully' })
|
||||
} catch (error: any) {
|
||||
if (error.message === 'Unit not found') {
|
||||
return res.status(404).json({ success: false, error: { message: 'Unit not found' } })
|
||||
}
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error updating employee organization:', error)
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
// Get all employees in a unit (for managers)
|
||||
router.get('/unit/:unitId/employees', authenticate, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { unitId } = req.params
|
||||
|
||||
// Check if user is a manager of this unit or admin
|
||||
const isManager = db.prepare(`
|
||||
SELECT 1 FROM employee_unit_assignments
|
||||
WHERE employee_id = ? AND unit_id = ?
|
||||
AND role IN ('leiter', 'stellvertreter')
|
||||
AND (end_date IS NULL OR end_date > datetime('now'))
|
||||
`).get(req.user?.employeeId, unitId)
|
||||
|
||||
if (!isManager && req.user?.role !== 'admin' && req.user?.role !== 'superuser') {
|
||||
return res.status(403).json({ success: false, error: { message: 'Access denied' } })
|
||||
}
|
||||
|
||||
const employees = db.prepare(`
|
||||
SELECT
|
||||
e.id, e.first_name as firstName, e.last_name as lastName,
|
||||
e.position, e.email, e.phone, e.photo,
|
||||
eua.role, eua.is_primary as isPrimary,
|
||||
eua.start_date as startDate
|
||||
FROM employee_unit_assignments eua
|
||||
JOIN employees e ON e.id = eua.employee_id
|
||||
WHERE eua.unit_id = ?
|
||||
AND (eua.end_date IS NULL OR eua.end_date > datetime('now'))
|
||||
ORDER BY
|
||||
CASE eua.role
|
||||
WHEN 'leiter' THEN 1
|
||||
WHEN 'stellvertreter' THEN 2
|
||||
WHEN 'beauftragter' THEN 3
|
||||
ELSE 4
|
||||
END,
|
||||
e.last_name, e.first_name
|
||||
`).all(unitId)
|
||||
|
||||
res.json({ success: true, data: employees })
|
||||
} catch (error) {
|
||||
logger.error('Error fetching unit employees:', error)
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
681
backend/src/routes/organization.ts
Normale Datei
681
backend/src/routes/organization.ts
Normale Datei
@ -0,0 +1,681 @@
|
||||
import { Router } from 'express'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { body, param, validationResult } from 'express-validator'
|
||||
import { db } from '../config/secureDatabase'
|
||||
import { authenticate, AuthRequest } from '../middleware/auth'
|
||||
import {
|
||||
OrganizationalUnit,
|
||||
EmployeeUnitAssignment,
|
||||
SpecialPosition,
|
||||
DeputyAssignment,
|
||||
DeputyDelegation
|
||||
} from '@skillmate/shared'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
const router = Router()
|
||||
|
||||
// Get all organizational units
|
||||
router.get('/units', authenticate, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const units = db.prepare(`
|
||||
SELECT
|
||||
id, code, name, type, level, parent_id as parentId,
|
||||
position_x as positionX, position_y as positionY,
|
||||
color, order_index as orderIndex, description,
|
||||
has_fuehrungsstelle as hasFuehrungsstelle,
|
||||
fuehrungsstelle_name as fuehrungsstelleName,
|
||||
is_active as isActive,
|
||||
created_at as createdAt,
|
||||
updated_at as updatedAt
|
||||
FROM organizational_units
|
||||
WHERE is_active = 1
|
||||
ORDER BY level, order_index, name
|
||||
`).all()
|
||||
|
||||
res.json({ success: true, data: units })
|
||||
} catch (error) {
|
||||
logger.error('Error fetching organizational units:', error)
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
// Get organizational hierarchy (tree structure)
|
||||
router.get('/hierarchy', authenticate, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const units = db.prepare(`
|
||||
SELECT
|
||||
id, code, name, type, level, parent_id as parentId,
|
||||
position_x as positionX, position_y as positionY,
|
||||
color, order_index as orderIndex, description,
|
||||
has_fuehrungsstelle as hasFuehrungsstelle,
|
||||
fuehrungsstelle_name as fuehrungsstelleName,
|
||||
is_active as isActive
|
||||
FROM organizational_units
|
||||
WHERE is_active = 1
|
||||
ORDER BY level, order_index, name
|
||||
`).all() as any[]
|
||||
|
||||
// Build tree structure
|
||||
const unitMap = new Map()
|
||||
const rootUnits: any[] = []
|
||||
|
||||
// First pass: create map
|
||||
units.forEach(unit => {
|
||||
unitMap.set(unit.id, { ...unit, children: [] })
|
||||
})
|
||||
|
||||
// Second pass: build tree
|
||||
units.forEach(unit => {
|
||||
const node = unitMap.get(unit.id)
|
||||
if (unit.parentId && unitMap.has(unit.parentId)) {
|
||||
unitMap.get(unit.parentId).children.push(node)
|
||||
} else {
|
||||
rootUnits.push(node)
|
||||
}
|
||||
})
|
||||
|
||||
res.json({ success: true, data: rootUnits })
|
||||
} catch (error) {
|
||||
logger.error('Error building organizational hierarchy:', error)
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
// Get single unit with employees
|
||||
router.get('/units/:id', authenticate, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const unit = db.prepare(`
|
||||
SELECT
|
||||
id, code, name, type, level, parent_id as parentId,
|
||||
position_x as positionX, position_y as positionY,
|
||||
color, order_index as orderIndex, description,
|
||||
has_fuehrungsstelle as hasFuehrungsstelle,
|
||||
fuehrungsstelle_name as fuehrungsstelleName,
|
||||
is_active as isActive,
|
||||
created_at as createdAt,
|
||||
updated_at as updatedAt
|
||||
FROM organizational_units
|
||||
WHERE id = ?
|
||||
`).get(req.params.id) as any
|
||||
|
||||
if (!unit) {
|
||||
return res.status(404).json({ success: false, error: { message: 'Unit not found' } })
|
||||
}
|
||||
|
||||
// Get employees assigned to this unit
|
||||
const employees = db.prepare(`
|
||||
SELECT
|
||||
e.id, e.first_name as firstName, e.last_name as lastName,
|
||||
e.position, e.department, e.email, e.phone, e.photo,
|
||||
eua.role, eua.is_primary as isPrimary
|
||||
FROM employee_unit_assignments eua
|
||||
JOIN employees e ON e.id = eua.employee_id
|
||||
WHERE eua.unit_id = ? AND (eua.end_date IS NULL OR eua.end_date > datetime('now'))
|
||||
ORDER BY
|
||||
CASE eua.role
|
||||
WHEN 'leiter' THEN 1
|
||||
WHEN 'stellvertreter' THEN 2
|
||||
WHEN 'beauftragter' THEN 3
|
||||
ELSE 4
|
||||
END,
|
||||
e.last_name, e.first_name
|
||||
`).all(req.params.id)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
...unit,
|
||||
employees
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching unit details:', error)
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
// Create new organizational unit (Admin only)
|
||||
router.post('/units',
|
||||
authenticate,
|
||||
[
|
||||
body('code').notEmpty().trim(),
|
||||
body('name').notEmpty().trim(),
|
||||
body('type').isIn(['direktion', 'abteilung', 'dezernat', 'sachgebiet', 'teildezernat', 'fuehrungsstelle', 'stabsstelle', 'sondereinheit']),
|
||||
body('level').isInt({ min: 0, max: 10 }),
|
||||
body('parentId').optional({ checkFalsy: true }).isUUID()
|
||||
],
|
||||
async (req: AuthRequest, res: any, next: any) => {
|
||||
try {
|
||||
const errors = validationResult(req)
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ success: false, error: { message: 'Invalid input', details: errors.array() } })
|
||||
}
|
||||
|
||||
// Check admin permission
|
||||
if (req.user?.role !== 'admin') {
|
||||
return res.status(403).json({ success: false, error: { message: 'Admin access required' } })
|
||||
}
|
||||
|
||||
const { code, name, type, level, parentId, color, description, hasFuehrungsstelle, fuehrungsstelleName } = req.body
|
||||
const now = new Date().toISOString()
|
||||
const unitId = uuidv4()
|
||||
|
||||
// Check if code already exists
|
||||
const existing = db.prepare('SELECT id FROM organizational_units WHERE code = ?').get(code)
|
||||
if (existing) {
|
||||
return res.status(400).json({ success: false, error: { message: 'Unit code already exists' } })
|
||||
}
|
||||
|
||||
// Get max order index for this level
|
||||
const maxOrder = db.prepare('SELECT MAX(order_index) as max FROM organizational_units WHERE level = ?').get(level) as any
|
||||
const orderIndex = (maxOrder?.max || 0) + 1
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO organizational_units (
|
||||
id, code, name, type, level, parent_id,
|
||||
color, order_index, description,
|
||||
has_fuehrungsstelle, fuehrungsstelle_name,
|
||||
is_active, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
unitId, code, name, type, level, parentId || null,
|
||||
color || null, orderIndex, description || null,
|
||||
hasFuehrungsstelle ? 1 : 0, fuehrungsstelleName || null,
|
||||
1, now, now
|
||||
)
|
||||
|
||||
res.json({ success: true, data: { id: unitId } })
|
||||
} catch (error) {
|
||||
logger.error('Error creating organizational unit:', error)
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Update organizational unit (Admin only)
|
||||
router.put('/units/:id',
|
||||
authenticate,
|
||||
[
|
||||
param('id').isUUID(),
|
||||
body('name').optional().notEmpty().trim(),
|
||||
body('description').optional(),
|
||||
body('color').optional(),
|
||||
// allow updating persisted canvas positions from admin editor
|
||||
body('positionX').optional().isNumeric(),
|
||||
body('positionY').optional().isNumeric()
|
||||
],
|
||||
async (req: AuthRequest, res: any, next: any) => {
|
||||
try {
|
||||
if (req.user?.role !== 'admin') {
|
||||
return res.status(403).json({ success: false, error: { message: 'Admin access required' } })
|
||||
}
|
||||
|
||||
const { name, description, color, hasFuehrungsstelle, fuehrungsstelleName, positionX, positionY } = req.body
|
||||
const now = new Date().toISOString()
|
||||
|
||||
const result = db.prepare(`
|
||||
UPDATE organizational_units
|
||||
SET name = COALESCE(?, name),
|
||||
description = COALESCE(?, description),
|
||||
color = COALESCE(?, color),
|
||||
has_fuehrungsstelle = COALESCE(?, has_fuehrungsstelle),
|
||||
fuehrungsstelle_name = COALESCE(?, fuehrungsstelle_name),
|
||||
position_x = COALESCE(?, position_x),
|
||||
position_y = COALESCE(?, position_y),
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
name || null,
|
||||
description !== undefined ? description : null,
|
||||
color || null,
|
||||
hasFuehrungsstelle !== undefined ? (hasFuehrungsstelle ? 1 : 0) : null,
|
||||
fuehrungsstelleName || null,
|
||||
positionX !== undefined ? Math.round(Number(positionX)) : null,
|
||||
positionY !== undefined ? Math.round(Number(positionY)) : null,
|
||||
now,
|
||||
req.params.id
|
||||
)
|
||||
|
||||
if (result.changes === 0) {
|
||||
return res.status(404).json({ success: false, error: { message: 'Unit not found' } })
|
||||
}
|
||||
|
||||
res.json({ success: true })
|
||||
} catch (error) {
|
||||
logger.error('Error updating organizational unit:', error)
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Assign employee to unit
|
||||
router.post('/assignments',
|
||||
authenticate,
|
||||
[
|
||||
body('employeeId').isUUID(),
|
||||
body('unitId').isUUID(),
|
||||
body('role').isIn(['leiter', 'stellvertreter', 'mitarbeiter', 'beauftragter'])
|
||||
],
|
||||
async (req: AuthRequest, res: any, next: any) => {
|
||||
try {
|
||||
const errors = validationResult(req)
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ success: false, error: { message: 'Invalid input', details: errors.array() } })
|
||||
}
|
||||
|
||||
const { employeeId, unitId, role, isPrimary } = req.body
|
||||
const now = new Date().toISOString()
|
||||
const assignmentId = uuidv4()
|
||||
|
||||
// Permission model: users may only assign themselves and only as 'mitarbeiter' or 'beauftragter'
|
||||
const isAdmin = req.user?.role === 'admin' || req.user?.role === 'superuser'
|
||||
if (!isAdmin) {
|
||||
if (!req.user?.employeeId || req.user.employeeId !== employeeId) {
|
||||
return res.status(403).json({ success: false, error: { message: 'Cannot assign other employees' } })
|
||||
}
|
||||
if (!['mitarbeiter', 'beauftragter'].includes(role)) {
|
||||
return res.status(403).json({ success: false, error: { message: 'Insufficient role to set this assignment' } })
|
||||
}
|
||||
}
|
||||
|
||||
// Validate unit exists and is active
|
||||
const unit = db.prepare('SELECT id FROM organizational_units WHERE id = ? AND is_active = 1').get(unitId)
|
||||
if (!unit) {
|
||||
return res.status(404).json({ success: false, error: { message: 'Unit not found' } })
|
||||
}
|
||||
|
||||
// Check if assignment already exists
|
||||
const existing = db.prepare(`
|
||||
SELECT id FROM employee_unit_assignments
|
||||
WHERE employee_id = ? AND unit_id = ? AND role = ? AND end_date IS NULL
|
||||
`).get(employeeId, unitId, role)
|
||||
|
||||
if (existing) {
|
||||
return res.status(400).json({ success: false, error: { message: 'Assignment already exists' } })
|
||||
}
|
||||
|
||||
// If setting as primary, demote all other active assignments for this employee
|
||||
if (isPrimary) {
|
||||
db.prepare(`
|
||||
UPDATE employee_unit_assignments
|
||||
SET is_primary = 0, updated_at = ?
|
||||
WHERE employee_id = ? AND end_date IS NULL
|
||||
`).run(now, employeeId)
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO employee_unit_assignments (
|
||||
id, employee_id, unit_id, role,
|
||||
start_date, is_primary, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
assignmentId, employeeId, unitId, role,
|
||||
now, isPrimary ? 1 : 0, now, now
|
||||
)
|
||||
|
||||
res.json({ success: true, data: { id: assignmentId } })
|
||||
} catch (error) {
|
||||
logger.error('Error assigning employee to unit:', error)
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Get my organizational units
|
||||
router.get('/my-units', authenticate, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
if (!req.user?.employeeId) {
|
||||
return res.json({ success: true, data: [] })
|
||||
}
|
||||
|
||||
const units = db.prepare(`
|
||||
SELECT
|
||||
ou.id, ou.code, ou.name, ou.type, ou.level,
|
||||
eua.role, eua.is_primary as isPrimary
|
||||
FROM employee_unit_assignments eua
|
||||
JOIN organizational_units ou ON ou.id = eua.unit_id
|
||||
WHERE eua.employee_id = ?
|
||||
AND (eua.end_date IS NULL OR eua.end_date > datetime('now'))
|
||||
AND ou.is_active = 1
|
||||
ORDER BY eua.is_primary DESC, ou.level, ou.name
|
||||
`).all(req.user.employeeId)
|
||||
|
||||
res.json({ success: true, data: units })
|
||||
} catch (error) {
|
||||
logger.error('Error fetching user units:', error)
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
// Get my deputy assignments
|
||||
router.get('/deputies/my', authenticate, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
if (!req.user?.employeeId) {
|
||||
return res.json({ success: true, data: { asDeputy: [], asPrincipal: [] } })
|
||||
}
|
||||
|
||||
// Get assignments where I'm the deputy
|
||||
const asDeputy = db.prepare(`
|
||||
SELECT
|
||||
da.id, da.principal_id as principalId, da.deputy_id as deputyId,
|
||||
da.unit_id as unitId, da.valid_from as validFrom, da.valid_until as validUntil,
|
||||
da.reason, da.can_delegate as canDelegate,
|
||||
p.first_name || ' ' || p.last_name as principalName,
|
||||
ou.name as unitName
|
||||
FROM deputy_assignments da
|
||||
JOIN employees p ON p.id = da.principal_id
|
||||
LEFT JOIN organizational_units ou ON ou.id = da.unit_id
|
||||
WHERE da.deputy_id = ?
|
||||
AND da.valid_from <= datetime('now')
|
||||
AND da.valid_until >= datetime('now')
|
||||
ORDER BY da.valid_from DESC
|
||||
`).all(req.user.employeeId)
|
||||
|
||||
// Get assignments where I'm the principal
|
||||
const asPrincipal = db.prepare(`
|
||||
SELECT
|
||||
da.id, da.principal_id as principalId, da.deputy_id as deputyId,
|
||||
da.unit_id as unitId, da.valid_from as validFrom, da.valid_until as validUntil,
|
||||
da.reason,
|
||||
d.first_name || ' ' || d.last_name as deputyName,
|
||||
ou.name as unitName
|
||||
FROM deputy_assignments da
|
||||
JOIN employees d ON d.id = da.deputy_id
|
||||
LEFT JOIN organizational_units ou ON ou.id = da.unit_id
|
||||
WHERE da.principal_id = ?
|
||||
AND da.valid_from <= datetime('now')
|
||||
AND da.valid_until >= datetime('now')
|
||||
ORDER BY da.valid_from DESC
|
||||
`).all(req.user.employeeId)
|
||||
|
||||
res.json({ success: true, data: { asDeputy, asPrincipal } })
|
||||
} catch (error) {
|
||||
logger.error('Error fetching deputy assignments:', error)
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
// Create deputy assignment
|
||||
router.post('/deputies',
|
||||
authenticate,
|
||||
[
|
||||
body('deputyId').isUUID(),
|
||||
body('validFrom').isISO8601(),
|
||||
body('validUntil').isISO8601()
|
||||
],
|
||||
async (req: AuthRequest, res: any, next: any) => {
|
||||
try {
|
||||
const errors = validationResult(req)
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ success: false, error: { message: 'Invalid input', details: errors.array() } })
|
||||
}
|
||||
|
||||
if (!req.user?.employeeId) {
|
||||
return res.status(403).json({ success: false, error: { message: 'No employee linked to user' } })
|
||||
}
|
||||
|
||||
const { deputyId, unitId, validFrom, validUntil, reason, canDelegate } = req.body
|
||||
const now = new Date().toISOString()
|
||||
const assignmentId = uuidv4()
|
||||
|
||||
// Check for conflicts
|
||||
const conflict = db.prepare(`
|
||||
SELECT id FROM deputy_assignments
|
||||
WHERE principal_id = ? AND deputy_id = ?
|
||||
AND ((valid_from BETWEEN ? AND ?) OR (valid_until BETWEEN ? AND ?))
|
||||
`).get(req.user.employeeId, deputyId, validFrom, validUntil, validFrom, validUntil)
|
||||
|
||||
if (conflict) {
|
||||
return res.status(400).json({ success: false, error: { message: 'Conflicting assignment exists' } })
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO deputy_assignments (
|
||||
id, principal_id, deputy_id, unit_id,
|
||||
valid_from, valid_until, reason, can_delegate,
|
||||
created_by, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
assignmentId, req.user.employeeId, deputyId, unitId || null,
|
||||
validFrom, validUntil, reason || null, canDelegate ? 1 : 0,
|
||||
req.user.id, now, now
|
||||
)
|
||||
|
||||
res.json({ success: true, data: { id: assignmentId } })
|
||||
} catch (error) {
|
||||
logger.error('Error creating deputy assignment:', error)
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Alias: Create deputy assignment for current user (same as above, convenient endpoint)
|
||||
router.post('/deputies/my',
|
||||
authenticate,
|
||||
[
|
||||
body('deputyId').isUUID(),
|
||||
body('validFrom').isISO8601(),
|
||||
body('validUntil').optional({ nullable: true }).isISO8601(),
|
||||
body('unitId').optional({ nullable: true }).isUUID()
|
||||
],
|
||||
async (req: AuthRequest, res: any, next: any) => {
|
||||
try {
|
||||
const errors = validationResult(req)
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ success: false, error: { message: 'Invalid input', details: errors.array() } })
|
||||
}
|
||||
|
||||
if (!req.user?.employeeId) {
|
||||
return res.status(403).json({ success: false, error: { message: 'No employee linked to user' } })
|
||||
}
|
||||
|
||||
const { deputyId, unitId, validFrom, validUntil, reason, canDelegate } = req.body
|
||||
const now = new Date().toISOString()
|
||||
const assignmentId = uuidv4()
|
||||
|
||||
// Check for conflicts
|
||||
const conflict = db.prepare(`
|
||||
SELECT id FROM deputy_assignments
|
||||
WHERE principal_id = ? AND deputy_id = ?
|
||||
AND ((valid_from BETWEEN ? AND ?) OR (valid_until BETWEEN ? AND ?))
|
||||
`).get(req.user.employeeId, deputyId, validFrom, validUntil || validFrom, validFrom, validUntil || validFrom)
|
||||
|
||||
if (conflict) {
|
||||
return res.status(400).json({ success: false, error: { message: 'Conflicting assignment exists' } })
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO deputy_assignments (
|
||||
id, principal_id, deputy_id, unit_id,
|
||||
valid_from, valid_until, reason, can_delegate,
|
||||
created_by, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
assignmentId, req.user.employeeId, deputyId, unitId || null,
|
||||
validFrom, validUntil || validFrom, reason || null, canDelegate ? 1 : 0,
|
||||
req.user.id, now, now
|
||||
)
|
||||
|
||||
res.json({ success: true, data: { id: assignmentId } })
|
||||
} catch (error) {
|
||||
logger.error('Error creating deputy assignment (my):', error)
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Delegate deputy assignment
|
||||
router.post('/deputies/delegate',
|
||||
authenticate,
|
||||
[
|
||||
body('assignmentId').isUUID(),
|
||||
body('toDeputyId').isUUID(),
|
||||
body('reason').optional().trim()
|
||||
],
|
||||
async (req: AuthRequest, res: any, next: any) => {
|
||||
try {
|
||||
const errors = validationResult(req)
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ success: false, error: { message: 'Invalid input', details: errors.array() } })
|
||||
}
|
||||
|
||||
if (!req.user?.employeeId) {
|
||||
return res.status(403).json({ success: false, error: { message: 'No employee linked to user' } })
|
||||
}
|
||||
|
||||
const { assignmentId, toDeputyId, reason } = req.body
|
||||
const now = new Date().toISOString()
|
||||
const delegationId = uuidv4()
|
||||
|
||||
// Check if user can delegate this assignment
|
||||
const assignment = db.prepare(`
|
||||
SELECT * FROM deputy_assignments
|
||||
WHERE id = ? AND deputy_id = ? AND can_delegate = 1
|
||||
AND valid_from <= datetime('now')
|
||||
AND valid_until >= datetime('now')
|
||||
`).get(assignmentId, req.user.employeeId) as any
|
||||
|
||||
if (!assignment) {
|
||||
return res.status(403).json({ success: false, error: { message: 'Cannot delegate this assignment' } })
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO deputy_delegations (
|
||||
id, original_assignment_id, from_deputy_id, to_deputy_id,
|
||||
reason, delegated_at, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
delegationId, assignmentId, req.user.employeeId, toDeputyId,
|
||||
reason || null, now, now
|
||||
)
|
||||
|
||||
res.json({ success: true, data: { id: delegationId } })
|
||||
} catch (error) {
|
||||
logger.error('Error delegating deputy assignment:', error)
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Delete deputy assignment (principal or admin)
|
||||
router.delete('/deputies/:id', authenticate, async (req: AuthRequest, res: any, next: any) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
if (!id) {
|
||||
return res.status(400).json({ success: false, error: { message: 'Missing id' } })
|
||||
}
|
||||
|
||||
// Load assignment
|
||||
const assignment = db.prepare(`
|
||||
SELECT id, principal_id as principalId FROM deputy_assignments WHERE id = ?
|
||||
`).get(id) as any
|
||||
|
||||
if (!assignment) {
|
||||
return res.status(404).json({ success: false, error: { message: 'Assignment not found' } })
|
||||
}
|
||||
|
||||
// Permission: principal or admin/superuser
|
||||
const isAdmin = req.user?.role === 'admin' || req.user?.role === 'superuser'
|
||||
if (!isAdmin && req.user?.employeeId !== assignment.principalId) {
|
||||
return res.status(403).json({ success: false, error: { message: 'Access denied' } })
|
||||
}
|
||||
|
||||
// Delete delegations first (FK not ON DELETE CASCADE)
|
||||
db.prepare('DELETE FROM deputy_delegations WHERE original_assignment_id = ?').run(id)
|
||||
// Delete assignment
|
||||
db.prepare('DELETE FROM deputy_assignments WHERE id = ?').run(id)
|
||||
|
||||
return res.json({ success: true })
|
||||
} catch (error) {
|
||||
logger.error('Error deleting deputy assignment:', error)
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
// Get deputy chain for an assignment
|
||||
router.get('/deputies/chain/:id', authenticate, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const chain = []
|
||||
|
||||
// Get original assignment
|
||||
const assignment = db.prepare(`
|
||||
SELECT
|
||||
da.*,
|
||||
p.first_name || ' ' || p.last_name as principalName,
|
||||
d.first_name || ' ' || d.last_name as deputyName
|
||||
FROM deputy_assignments da
|
||||
JOIN employees p ON p.id = da.principal_id
|
||||
JOIN employees d ON d.id = da.deputy_id
|
||||
WHERE da.id = ?
|
||||
`).get(req.params.id) as any
|
||||
|
||||
if (!assignment) {
|
||||
return res.status(404).json({ success: false, error: { message: 'Assignment not found' } })
|
||||
}
|
||||
|
||||
chain.push({
|
||||
type: 'original',
|
||||
from: assignment.principalName,
|
||||
to: assignment.deputyName,
|
||||
reason: assignment.reason
|
||||
})
|
||||
|
||||
// Get all delegations
|
||||
const delegations = db.prepare(`
|
||||
SELECT
|
||||
dd.*,
|
||||
f.first_name || ' ' || f.last_name as fromName,
|
||||
t.first_name || ' ' || t.last_name as toName
|
||||
FROM deputy_delegations dd
|
||||
JOIN employees f ON f.id = dd.from_deputy_id
|
||||
JOIN employees t ON t.id = dd.to_deputy_id
|
||||
WHERE dd.original_assignment_id = ?
|
||||
ORDER BY dd.delegated_at
|
||||
`).all(req.params.id) as any[]
|
||||
|
||||
delegations.forEach(del => {
|
||||
chain.push({
|
||||
type: 'delegation',
|
||||
from: del.fromName,
|
||||
to: del.toName,
|
||||
reason: del.reason,
|
||||
delegatedAt: del.delegated_at
|
||||
})
|
||||
})
|
||||
|
||||
res.json({ success: true, data: chain })
|
||||
} catch (error) {
|
||||
logger.error('Error fetching deputy chain:', error)
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
// Get special positions
|
||||
router.get('/special-positions', authenticate, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const positions = db.prepare(`
|
||||
SELECT
|
||||
sp.id, sp.position_type as positionType,
|
||||
sp.employee_id as employeeId,
|
||||
sp.unit_id as unitId,
|
||||
sp.start_date as startDate,
|
||||
sp.end_date as endDate,
|
||||
e.first_name || ' ' || e.last_name as employeeName,
|
||||
e.photo,
|
||||
ou.name as unitName
|
||||
FROM special_positions sp
|
||||
JOIN employees e ON e.id = sp.employee_id
|
||||
LEFT JOIN organizational_units ou ON ou.id = sp.unit_id
|
||||
WHERE sp.is_active = 1
|
||||
AND (sp.end_date IS NULL OR sp.end_date > datetime('now'))
|
||||
ORDER BY sp.position_type, e.last_name
|
||||
`).all()
|
||||
|
||||
res.json({ success: true, data: positions })
|
||||
} catch (error) {
|
||||
logger.error('Error fetching special positions:', error)
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
399
backend/src/routes/organizationImport.ts
Normale Datei
399
backend/src/routes/organizationImport.ts
Normale Datei
@ -0,0 +1,399 @@
|
||||
import { Router } from 'express'
|
||||
import multer from 'multer'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
const pdfParse = require('pdf-parse')
|
||||
import { db } from '../config/secureDatabase'
|
||||
import { authenticate, authorize, AuthRequest } from '../middleware/auth'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
const router = Router()
|
||||
|
||||
// Configure multer for PDF uploads
|
||||
const upload = multer({
|
||||
dest: 'uploads/temp/',
|
||||
limits: {
|
||||
fileSize: 10 * 1024 * 1024 // 10MB limit
|
||||
},
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (file.mimetype === 'application/pdf') {
|
||||
cb(null, true)
|
||||
} else {
|
||||
cb(new Error('Only PDF files are allowed'))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Helper to parse organizational structure from PDF text
|
||||
function parseOrganizationFromText(text: string) {
|
||||
const units: any[] = []
|
||||
const lines = text.split('\n').map(line => line.trim()).filter(line => line && line.length > 2)
|
||||
|
||||
// Pattern matching for different organizational levels
|
||||
const patterns = {
|
||||
direktor: /(Direktor|Director)\s*(LKA)?/i,
|
||||
abteilung: /^Abteilung\s+(\d+)/i,
|
||||
dezernat: /^Dezernat\s+([\d]+)/i,
|
||||
sachgebiet: /^SG\s+([\d\.]+)/i,
|
||||
teildezernat: /^TD\s+([\d\.]+)/i,
|
||||
stabsstelle: /(Leitungsstab|LStab|Führungsgruppe)/i,
|
||||
fahndung: /Fahndungsgruppe/i,
|
||||
sondereinheit: /(Personalrat|Schwerbehindertenvertretung|beauftragt|Innenrevision|IUK-Lage)/i
|
||||
}
|
||||
|
||||
// Color mapping for departments
|
||||
const colors: Record<string, string> = {
|
||||
'1': '#dc2626',
|
||||
'2': '#ea580c',
|
||||
'3': '#0891b2',
|
||||
'4': '#7c3aed',
|
||||
'5': '#0d9488',
|
||||
'6': '#be185d',
|
||||
'ZA': '#6b7280'
|
||||
}
|
||||
|
||||
let currentAbteilung: any = null
|
||||
let currentDezernat: any = null
|
||||
|
||||
lines.forEach(line => {
|
||||
// Check for Direktor
|
||||
if (patterns.direktor.test(line)) {
|
||||
units.push({
|
||||
code: 'DIR',
|
||||
name: 'Direktor LKA NRW',
|
||||
type: 'direktion',
|
||||
level: 0,
|
||||
parentId: null,
|
||||
color: '#1e3a8a'
|
||||
})
|
||||
}
|
||||
|
||||
// Check for Abteilung
|
||||
const abtMatch = line.match(/Abteilung\s+(\d+|Zentralabteilung)/i)
|
||||
if (abtMatch) {
|
||||
const abtNum = abtMatch[1] === 'Zentralabteilung' ? 'ZA' : abtMatch[1]
|
||||
const abtName = line.replace(/^Abteilung\s+\d+\s*[-–]\s*/, '')
|
||||
currentAbteilung = {
|
||||
code: `Abt ${abtNum}`,
|
||||
name: abtName || `Abteilung ${abtNum}`,
|
||||
type: 'abteilung',
|
||||
level: 1,
|
||||
parentId: 'DIR',
|
||||
color: colors[abtNum] || '#6b7280',
|
||||
hasFuehrungsstelle: abtNum !== 'ZA'
|
||||
}
|
||||
units.push(currentAbteilung)
|
||||
currentDezernat = null
|
||||
}
|
||||
|
||||
// Check for Dezernat
|
||||
const dezMatch = line.match(/(?:Dezernat|Dez)\s+([\d\s]+)/i)
|
||||
if (dezMatch && currentAbteilung) {
|
||||
const dezNum = dezMatch[1].trim()
|
||||
const dezName = line.replace(/^(?:Dezernat|Dez)\s+[\d\s]+\s*[-–]?\s*/, '').trim()
|
||||
currentDezernat = {
|
||||
code: `Dez ${dezNum}`,
|
||||
name: dezName || `Dezernat ${dezNum}`,
|
||||
type: 'dezernat',
|
||||
level: 2,
|
||||
parentId: currentAbteilung.code
|
||||
}
|
||||
units.push(currentDezernat)
|
||||
}
|
||||
|
||||
// Check for Sachgebiet
|
||||
const sgMatch = line.match(/SG\s+([\d\.]+)/i)
|
||||
if (sgMatch && currentDezernat) {
|
||||
const sgNum = sgMatch[1]
|
||||
const sgName = line.replace(/^SG\s+[\d\.]+\s*[-–]?\s*/, '').trim()
|
||||
units.push({
|
||||
code: `SG ${sgNum}`,
|
||||
name: sgName || `Sachgebiet ${sgNum}`,
|
||||
type: 'sachgebiet',
|
||||
level: 3,
|
||||
parentId: currentDezernat.code
|
||||
})
|
||||
}
|
||||
|
||||
// Check for Teildezernat
|
||||
const tdMatch = line.match(/TD\s+([\d\.]+)/i)
|
||||
if (tdMatch && currentDezernat) {
|
||||
const tdNum = tdMatch[1]
|
||||
const tdName = line.replace(/^TD\s+[\d\.]+\s*[-–]?\s*/, '').trim()
|
||||
units.push({
|
||||
code: `TD ${tdNum}`,
|
||||
name: tdName || `Teildezernat ${tdNum}`,
|
||||
type: 'teildezernat',
|
||||
level: 3,
|
||||
parentId: currentDezernat.code
|
||||
})
|
||||
}
|
||||
|
||||
// Check for Stabsstelle
|
||||
if (patterns.stabsstelle.test(line)) {
|
||||
units.push({
|
||||
code: 'LStab',
|
||||
name: 'Leitungsstab',
|
||||
type: 'stabsstelle',
|
||||
level: 1,
|
||||
parentId: 'DIR',
|
||||
color: '#6b7280'
|
||||
})
|
||||
}
|
||||
|
||||
// Check for Sondereinheiten (non-hierarchical)
|
||||
if (patterns.sondereinheit.test(line)) {
|
||||
let code = 'SE'
|
||||
let name = line
|
||||
|
||||
if (line.includes('Personalrat')) {
|
||||
code = 'PR'
|
||||
name = 'Personalrat'
|
||||
} else if (line.includes('Schwerbehindertenvertretung')) {
|
||||
code = 'SBV'
|
||||
name = 'Schwerbehindertenvertretung'
|
||||
} else if (line.includes('Datenschutzbeauftragter')) {
|
||||
code = 'DSB'
|
||||
name = 'Datenschutzbeauftragter'
|
||||
} else if (line.includes('Gleichstellungsbeauftragte')) {
|
||||
code = 'GSB'
|
||||
name = 'Gleichstellungsbeauftragte'
|
||||
} else if (line.includes('Innenrevision')) {
|
||||
code = 'IR'
|
||||
name = 'Innenrevision'
|
||||
}
|
||||
|
||||
units.push({
|
||||
code,
|
||||
name,
|
||||
type: 'sondereinheit',
|
||||
level: 1,
|
||||
parentId: null, // Non-hierarchical
|
||||
color: '#059669'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return units
|
||||
}
|
||||
|
||||
// Import organization from PDF
|
||||
router.post('/import-pdf',
|
||||
authenticate,
|
||||
authorize('admin'),
|
||||
upload.single('pdf'),
|
||||
async (req: AuthRequest, res: any, next: any) => {
|
||||
let tempFilePath: string | null = null
|
||||
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: { message: 'No PDF file uploaded' }
|
||||
})
|
||||
}
|
||||
|
||||
tempFilePath = req.file.path
|
||||
|
||||
// Read PDF file
|
||||
const pdfBuffer = fs.readFileSync(tempFilePath)
|
||||
|
||||
// Parse PDF using pdf-parse
|
||||
let extractedText = ''
|
||||
try {
|
||||
const pdfData = await pdfParse(pdfBuffer)
|
||||
extractedText = pdfData.text
|
||||
|
||||
logger.info(`PDF parsed successfully: ${pdfData.numpages} pages, ${extractedText.length} characters`)
|
||||
} catch (parseError) {
|
||||
logger.error('PDF parsing error:', parseError)
|
||||
// Fallback to simulated data if parsing fails
|
||||
extractedText = `
|
||||
Direktor LKA NRW
|
||||
Leitungsstab
|
||||
Personalrat
|
||||
Schwerbehindertenvertretung
|
||||
Abteilung 1 - Organisierte Kriminalität
|
||||
Dezernat 11 - Ermittlungen OK
|
||||
SG 11.1 - Grundsatzfragen
|
||||
Dezernat 12 - Wirtschaftskriminalität
|
||||
Abteilung 2 - Terrorismusbekämpfung
|
||||
Dezernat 21 - Ermittlungen
|
||||
Abteilung 3 - Strategische Kriminalitätsbekämpfung
|
||||
Abteilung 4 - Cybercrime
|
||||
Abteilung 5 - Kriminalwissenschaftliches Institut
|
||||
Abteilung 6 - Fachaufsicht
|
||||
Zentralabteilung
|
||||
`
|
||||
}
|
||||
|
||||
// Parse the organizational structure
|
||||
const parsedUnits = parseOrganizationFromText(extractedText)
|
||||
|
||||
if (parsedUnits.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: { message: 'No organizational units could be extracted from the PDF' }
|
||||
})
|
||||
}
|
||||
|
||||
// Clear existing organization (optional - could be a parameter)
|
||||
if (req.body.clearExisting === 'true') {
|
||||
db.prepare('DELETE FROM organizational_units').run()
|
||||
}
|
||||
|
||||
// Prepare insert/update with stable parent references and FK-safe order
|
||||
const now = new Date().toISOString()
|
||||
const unitIdMap: Record<string, string> = {}
|
||||
|
||||
// Preload existing IDs by code
|
||||
for (const unit of parsedUnits) {
|
||||
const existing = db.prepare('SELECT id FROM organizational_units WHERE code = ?').get(unit.code) as any
|
||||
unitIdMap[unit.code] = existing?.id || uuidv4()
|
||||
}
|
||||
|
||||
// Sort by level ascending so parents are processed first
|
||||
const sorted = [...parsedUnits].sort((a, b) => (a.level ?? 0) - (b.level ?? 0))
|
||||
|
||||
const tx = db.transaction(() => {
|
||||
sorted.forEach((unit, index) => {
|
||||
const parentId = unit.parentId ? unitIdMap[unit.parentId] : null
|
||||
const existing = db.prepare('SELECT id FROM organizational_units WHERE code = ?').get(unit.code) as any
|
||||
|
||||
if (existing) {
|
||||
db.prepare(`
|
||||
UPDATE organizational_units
|
||||
SET name = ?, type = ?, level = ?, parent_id = ?,
|
||||
color = ?, order_index = ?, has_fuehrungsstelle = ?,
|
||||
is_active = 1, updated_at = ?
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
unit.name,
|
||||
unit.type,
|
||||
unit.level,
|
||||
parentId,
|
||||
unit.color || null,
|
||||
index,
|
||||
unit.hasFuehrungsstelle ? 1 : 0,
|
||||
now,
|
||||
existing.id
|
||||
)
|
||||
} else {
|
||||
db.prepare(`
|
||||
INSERT INTO organizational_units (
|
||||
id, code, name, type, level, parent_id,
|
||||
color, order_index, has_fuehrungsstelle,
|
||||
is_active, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
unitIdMap[unit.code],
|
||||
unit.code,
|
||||
unit.name,
|
||||
unit.type,
|
||||
unit.level,
|
||||
parentId,
|
||||
unit.color || null,
|
||||
index,
|
||||
unit.hasFuehrungsstelle ? 1 : 0,
|
||||
1,
|
||||
now,
|
||||
now
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
tx()
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully imported ${parsedUnits.length} organizational units`,
|
||||
unitsImported: parsedUnits.length,
|
||||
units: parsedUnits.map(u => ({
|
||||
code: u.code,
|
||||
name: u.name,
|
||||
type: u.type
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error importing PDF:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: { message: 'Failed to import PDF: ' + (error as Error).message }
|
||||
})
|
||||
} finally {
|
||||
// Clean up temp file
|
||||
if (tempFilePath && fs.existsSync(tempFilePath)) {
|
||||
fs.unlinkSync(tempFilePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Import from structured JSON
|
||||
router.post('/import-json',
|
||||
authenticate,
|
||||
authorize('admin'),
|
||||
async (req: AuthRequest, res: any, next: any) => {
|
||||
try {
|
||||
const { units, clearExisting } = req.body
|
||||
|
||||
if (!units || !Array.isArray(units)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: { message: 'Invalid units data' }
|
||||
})
|
||||
}
|
||||
|
||||
// Clear existing if requested
|
||||
if (clearExisting) {
|
||||
db.prepare('DELETE FROM organizational_units').run()
|
||||
}
|
||||
|
||||
// Import units
|
||||
const now = new Date().toISOString()
|
||||
units.forEach((unit: any, index: number) => {
|
||||
const id = uuidv4()
|
||||
db.prepare(`
|
||||
INSERT INTO organizational_units (
|
||||
id, code, name, type, level, parent_id,
|
||||
color, order_index, description,
|
||||
has_fuehrungsstelle, is_active, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
unit.code,
|
||||
unit.name,
|
||||
unit.type,
|
||||
unit.level,
|
||||
unit.parentId || null,
|
||||
unit.color || null,
|
||||
index,
|
||||
unit.description || null,
|
||||
unit.hasFuehrungsstelle ? 1 : 0,
|
||||
1,
|
||||
now,
|
||||
now
|
||||
)
|
||||
})
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully imported ${units.length} units`,
|
||||
count: units.length
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error importing JSON:', error)
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export default router
|
||||
@ -4,7 +4,6 @@ import bcrypt from 'bcrypt'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { db, encryptedDb } from '../config/secureDatabase'
|
||||
import { authenticate, authorize, AuthRequest } from '../middleware/auth'
|
||||
import { requirePermission } from '../middleware/roleAuth'
|
||||
import { User, UserRole } from '@skillmate/shared'
|
||||
import { FieldEncryption } from '../services/encryption'
|
||||
import { emailService } from '../services/emailService'
|
||||
@ -13,7 +12,7 @@ import { logger } from '../utils/logger'
|
||||
const router = Router()
|
||||
|
||||
// Get all users (admin only)
|
||||
router.get('/', authenticate, requirePermission('users:read'), async (req: AuthRequest, res, next) => {
|
||||
router.get('/', authenticate, authorize('admin', 'superuser'), async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const users = db.prepare(`
|
||||
SELECT id, username, email, role, employee_id, last_login, is_active, created_at, updated_at
|
||||
@ -57,7 +56,7 @@ router.get('/', authenticate, requirePermission('users:read'), async (req: AuthR
|
||||
// Update user role (admin only)
|
||||
router.put('/:id/role',
|
||||
authenticate,
|
||||
requirePermission('users:update'),
|
||||
authorize('admin'),
|
||||
[
|
||||
body('role').isIn(['admin', 'superuser', 'user'])
|
||||
],
|
||||
@ -109,7 +108,7 @@ router.put('/:id/role',
|
||||
// Bulk create users from employees
|
||||
router.post('/bulk-create-from-employees',
|
||||
authenticate,
|
||||
requirePermission('users:create'),
|
||||
authorize('admin'),
|
||||
[
|
||||
body('employeeIds').isArray({ min: 1 }),
|
||||
body('role').isIn(['admin', 'superuser', 'user'])
|
||||
@ -189,7 +188,7 @@ router.post('/bulk-create-from-employees',
|
||||
// Update user status (admin only)
|
||||
router.put('/:id/status',
|
||||
authenticate,
|
||||
requirePermission('users:update'),
|
||||
authorize('admin'),
|
||||
[
|
||||
body('isActive').isBoolean()
|
||||
],
|
||||
@ -241,7 +240,7 @@ router.put('/:id/status',
|
||||
// Reset user password (admin only)
|
||||
router.post('/:id/reset-password',
|
||||
authenticate,
|
||||
requirePermission('users:update'),
|
||||
authorize('admin'),
|
||||
[
|
||||
body('newPassword').optional().isLength({ min: 8 })
|
||||
],
|
||||
@ -294,7 +293,7 @@ router.post('/:id/reset-password',
|
||||
// Delete user (admin only)
|
||||
router.delete('/:id',
|
||||
authenticate,
|
||||
requirePermission('users:delete'),
|
||||
authorize('admin'),
|
||||
async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
@ -333,7 +332,7 @@ export default router
|
||||
// Create user account from existing employee (admin only)
|
||||
router.post('/create-from-employee',
|
||||
authenticate,
|
||||
requirePermission('users:create'),
|
||||
authorize('admin'),
|
||||
[
|
||||
body('employeeId').notEmpty().isString(),
|
||||
body('username').optional().isString().isLength({ min: 3 }),
|
||||
@ -452,7 +451,7 @@ router.post('/purge',
|
||||
// Send temporary password via email to user's email
|
||||
router.post('/:id/send-temp-password',
|
||||
authenticate,
|
||||
requirePermission('users:update'),
|
||||
authorize('admin'),
|
||||
[
|
||||
body('password').notEmpty().isString().isLength({ min: 6 })
|
||||
],
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { db } from '../../src/config/secureDatabase'
|
||||
import { db } from '../../config/secureDatabase'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { FieldEncryption } from '../../src/services/encryption'
|
||||
import { FieldEncryption } from '../../services/encryption'
|
||||
import { User, LoginResponse } from '@skillmate/shared'
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'
|
||||
|
||||
107
backend/test-organigramm.txt
Normale Datei
107
backend/test-organigramm.txt
Normale Datei
@ -0,0 +1,107 @@
|
||||
LKA NRW Organigramm
|
||||
|
||||
Direktor LKA NRW
|
||||
Führungsstelle Direktor
|
||||
|
||||
Leitungsstab
|
||||
- Presse- und Öffentlichkeitsarbeit
|
||||
- Controlling
|
||||
- Innenrevision
|
||||
|
||||
Personalrat
|
||||
Schwerbehindertenvertretung
|
||||
Datenschutzbeauftragter
|
||||
Gleichstellungsbeauftragte
|
||||
|
||||
Abteilung 1 - Organisierte Kriminalität
|
||||
Führungsstelle Abt. 1
|
||||
|
||||
Dezernat 11 - Ermittlungen OK
|
||||
SG 11.1 - Grundsatzfragen OK
|
||||
SG 11.2 - Operative Auswertung
|
||||
SG 11.3 - Finanzermittlungen
|
||||
|
||||
Dezernat 12 - Wirtschaftskriminalität
|
||||
SG 12.1 - Komplexe Wirtschaftsverfahren
|
||||
SG 12.2 - Korruptionsdelikte
|
||||
SG 12.3 - Vermögensabschöpfung
|
||||
|
||||
Dezernat 13 - Rauschgiftkriminalität
|
||||
SG 13.1 - Internationale Drogenhandel
|
||||
SG 13.2 - Synthetische Drogen
|
||||
TD 13.3 - Darknet-Ermittlungen
|
||||
|
||||
Abteilung 2 - Terrorismusbekämpfung / Staatsschutz
|
||||
Führungsstelle Abt. 2
|
||||
|
||||
Dezernat 21 - Islamistischer Terrorismus
|
||||
SG 21.1 - Operative Ermittlungen
|
||||
SG 21.2 - Gefährderanalyse
|
||||
|
||||
Dezernat 22 - Politisch motivierte Kriminalität
|
||||
SG 22.1 - Rechtsextremismus
|
||||
SG 22.2 - Linksextremismus
|
||||
SG 22.3 - Ausländerextremismus
|
||||
|
||||
Abteilung 3 - Strategische Kriminalitätsbekämpfung
|
||||
Führungsstelle Abt. 3
|
||||
|
||||
Dezernat 31 - Analyse und Auswertung
|
||||
SG 31.1 - Strategische Analyse
|
||||
SG 31.2 - Lagebilderstellung
|
||||
|
||||
Dezernat 32 - Informationsmanagement
|
||||
SG 32.1 - Datenbanken und Systeme
|
||||
SG 32.2 - Informationsaustausch
|
||||
|
||||
Abteilung 4 - Cybercrime
|
||||
Führungsstelle Abt. 4
|
||||
|
||||
Dezernat 41 - Digitale Ermittlungen
|
||||
SG 41.1 - Internetkriminalität
|
||||
SG 41.2 - Digitale Forensik
|
||||
|
||||
Dezernat 42 - Cybersecurity
|
||||
SG 42.1 - Critical Infrastructure
|
||||
SG 42.2 - Incident Response
|
||||
|
||||
Abteilung 5 - Kriminalwissenschaftliches Institut
|
||||
Führungsstelle Abt. 5
|
||||
|
||||
Dezernat 51 - Kriminaltechnik
|
||||
SG 51.1 - DNA-Analytik
|
||||
SG 51.2 - Daktyloskopie
|
||||
SG 51.3 - Ballistik
|
||||
|
||||
Dezernat 52 - Digitale Forensik
|
||||
SG 52.1 - Mobile Forensik
|
||||
SG 52.2 - Computer Forensik
|
||||
|
||||
Abteilung 6 - Fachaufsicht
|
||||
Führungsstelle Abt. 6
|
||||
|
||||
Dezernat 61 - Qualitätsmanagement
|
||||
SG 61.1 - Standards und Richtlinien
|
||||
SG 61.2 - Evaluierung
|
||||
|
||||
Dezernat 62 - Aus- und Fortbildung
|
||||
SG 62.1 - Fachschulungen
|
||||
SG 62.2 - Führungskräfteentwicklung
|
||||
|
||||
Zentralabteilung
|
||||
|
||||
Dezernat ZA 1 - Personal
|
||||
SG ZA 1.1 - Personalverwaltung
|
||||
SG ZA 1.2 - Personalentwicklung
|
||||
|
||||
Dezernat ZA 2 - Haushalt und Finanzen
|
||||
SG ZA 2.1 - Haushaltsplanung
|
||||
SG ZA 2.2 - Beschaffung
|
||||
|
||||
Dezernat ZA 3 - IT und Digitalisierung
|
||||
SG ZA 3.1 - IT-Infrastruktur
|
||||
SG ZA 3.2 - Digitale Transformation
|
||||
|
||||
Dezernat ZA 4 - Organisation und Recht
|
||||
SG ZA 4.1 - Organisationsentwicklung
|
||||
SG ZA 4.2 - Rechtsangelegenheiten
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren