Roll Back Punkt - Ansicht klappt so semi
Dieser Commit ist enthalten in:
25
backend/scripts/extract-pdf.js
Normale Datei
25
backend/scripts/extract-pdf.js
Normale Datei
@ -0,0 +1,25 @@
|
||||
// Simple helper to extract text from the root PDF for inspection
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const pdfParse = require('pdf-parse')
|
||||
|
||||
async function main() {
|
||||
const pdfPath = path.resolve(__dirname, '..', '..', 'Organigramm_ohne_Namen.pdf')
|
||||
if (!fs.existsSync(pdfPath)) {
|
||||
console.error('PDF not found at', pdfPath)
|
||||
process.exit(1)
|
||||
}
|
||||
const buf = fs.readFileSync(pdfPath)
|
||||
const res = await pdfParse(buf)
|
||||
const outPath = path.resolve(__dirname, '..', '..', 'organigramm_text.txt')
|
||||
fs.writeFileSync(outPath, res.text, 'utf8')
|
||||
console.log('Extracted text length:', res.text.length)
|
||||
console.log('Pages:', res.numpages)
|
||||
console.log('Saved to:', outPath)
|
||||
// Print first 200 lines to stdout for quick view
|
||||
const lines = res.text.split('\n').map(s => s.trim()).filter(Boolean)
|
||||
console.log('--- First lines ---')
|
||||
console.log(lines.slice(0, 200).join('\n'))
|
||||
}
|
||||
|
||||
main().catch(err => { console.error(err); process.exit(1) })
|
||||
@ -403,6 +403,19 @@ export function initializeDatabase() {
|
||||
CREATE INDEX IF NOT EXISTS idx_org_units_level ON organizational_units(level);
|
||||
`)
|
||||
|
||||
// Import rules for organization parsing (persist admin overrides)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS organization_import_rules (
|
||||
id TEXT PRIMARY KEY,
|
||||
target_code TEXT UNIQUE NOT NULL,
|
||||
parent_code TEXT,
|
||||
type_override TEXT,
|
||||
name_override TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_org_rules_target ON organization_import_rules(target_code);
|
||||
`)
|
||||
|
||||
// Employee Unit Assignments
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS employee_unit_assignments (
|
||||
@ -554,4 +567,4 @@ export function initializeDatabase() {
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_status ON bookings(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_workspace_analytics_date ON workspace_analytics(date);
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
||||
@ -200,6 +200,8 @@ router.put('/units/:id',
|
||||
body('name').optional().notEmpty().trim(),
|
||||
body('description').optional(),
|
||||
body('color').optional(),
|
||||
body('parentId').optional({ checkFalsy: true }).isUUID(),
|
||||
body('level').optional().isInt({ min: 0, max: 10 }),
|
||||
// allow updating persisted canvas positions from admin editor
|
||||
body('positionX').optional().isNumeric(),
|
||||
body('positionY').optional().isNumeric()
|
||||
@ -210,9 +212,32 @@ router.put('/units/:id',
|
||||
return res.status(403).json({ success: false, error: { message: 'Admin access required' } })
|
||||
}
|
||||
|
||||
const { name, description, color, hasFuehrungsstelle, fuehrungsstelleName, positionX, positionY } = req.body
|
||||
const { name, description, color, hasFuehrungsstelle, fuehrungsstelleName, positionX, positionY, parentId, level } = req.body
|
||||
const now = new Date().toISOString()
|
||||
|
||||
// Optional: validate parentId and avoid cycles
|
||||
let newParentId: string | null | undefined = undefined
|
||||
if (parentId !== undefined) {
|
||||
if (parentId === null || parentId === '' ) {
|
||||
newParentId = null
|
||||
} else {
|
||||
const parent = db.prepare('SELECT id, parent_id as parentId FROM organizational_units WHERE id = ?').get(parentId)
|
||||
if (!parent) {
|
||||
return res.status(404).json({ success: false, error: { message: 'Parent unit not found' } })
|
||||
}
|
||||
// cycle check: walk up from parent to root; id must not appear
|
||||
const targetId = req.params.id
|
||||
let cursor: any = parent
|
||||
while (cursor && cursor.parentId) {
|
||||
if (cursor.parentId === targetId) {
|
||||
return res.status(400).json({ success: false, error: { message: 'Cyclic parent assignment is not allowed' } })
|
||||
}
|
||||
cursor = db.prepare('SELECT id, parent_id as parentId FROM organizational_units WHERE id = ?').get(cursor.parentId)
|
||||
}
|
||||
newParentId = parentId
|
||||
}
|
||||
}
|
||||
|
||||
const result = db.prepare(`
|
||||
UPDATE organizational_units
|
||||
SET name = COALESCE(?, name),
|
||||
@ -220,6 +245,8 @@ router.put('/units/:id',
|
||||
color = COALESCE(?, color),
|
||||
has_fuehrungsstelle = COALESCE(?, has_fuehrungsstelle),
|
||||
fuehrungsstelle_name = COALESCE(?, fuehrungsstelle_name),
|
||||
parent_id = COALESCE(?, parent_id),
|
||||
level = COALESCE(?, level),
|
||||
position_x = COALESCE(?, position_x),
|
||||
position_y = COALESCE(?, position_y),
|
||||
updated_at = ?
|
||||
@ -230,6 +257,8 @@ router.put('/units/:id',
|
||||
color || null,
|
||||
hasFuehrungsstelle !== undefined ? (hasFuehrungsstelle ? 1 : 0) : null,
|
||||
fuehrungsstelleName || null,
|
||||
newParentId !== undefined ? newParentId : null,
|
||||
level !== undefined ? Number(level) : null,
|
||||
positionX !== undefined ? Math.round(Number(positionX)) : null,
|
||||
positionY !== undefined ? Math.round(Number(positionY)) : null,
|
||||
now,
|
||||
|
||||
@ -28,153 +28,179 @@ const upload = multer({
|
||||
// Helper to parse organizational structure from PDF text
|
||||
function parseOrganizationFromText(text: string) {
|
||||
const units: any[] = []
|
||||
const byCode = new Map<string, 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,
|
||||
abteilung: /^Abteilung\s+(\d+|Zentralabteilung)/i,
|
||||
dezernat: /^(?:Dezernat|Dez)\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
|
||||
sondereinheit: /(Personalrat|Schwerbehindertenvertretung|Datenschutzbeauftrag|Gleichstellungsbeauftrag|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'
|
||||
'1': '#dc2626', '2': '#ea580c', '3': '#0891b2', '4': '#7c3aed', '5': '#0d9488', '6': '#be185d', 'ZA': '#6b7280'
|
||||
}
|
||||
|
||||
|
||||
const ensure = (u: any) => {
|
||||
const existing = byCode.get(u.code)
|
||||
if (existing) {
|
||||
// Update minimal fields if missing
|
||||
existing.name = existing.name || u.name
|
||||
existing.type = existing.type || u.type
|
||||
existing.level = existing.level ?? u.level
|
||||
if (!existing.parentId && u.parentId) existing.parentId = u.parentId
|
||||
if (!existing.color && u.color) existing.color = u.color
|
||||
return existing
|
||||
}
|
||||
byCode.set(u.code, u)
|
||||
units.push(u)
|
||||
return u
|
||||
}
|
||||
|
||||
const inferAbteilungCode = (dezNum: string) => {
|
||||
const digits = (dezNum || '').replace(/\D/g, '')
|
||||
const first = digits.charAt(0)
|
||||
if (['1','2','3','4','5','6'].includes(first)) return `Abt ${first}`
|
||||
if (dezNum.toUpperCase().includes('ZA') || first === '0') return 'Abt ZA'
|
||||
return null
|
||||
}
|
||||
|
||||
let currentAbteilung: any = null
|
||||
let currentDezernat: any = null
|
||||
|
||||
lines.forEach(line => {
|
||||
// Check for Direktor
|
||||
|
||||
// Always ensure root
|
||||
ensure({ code: 'DIR', name: 'Direktor LKA NRW', type: 'direktion', level: 0, parentId: null, color: '#1e3a8a' })
|
||||
|
||||
for (const raw of lines) {
|
||||
const line = raw.replace(/[–—]/g, '-')
|
||||
|
||||
if (patterns.direktor.test(line)) {
|
||||
units.push({
|
||||
code: 'DIR',
|
||||
name: 'Direktor LKA NRW',
|
||||
type: 'direktion',
|
||||
level: 0,
|
||||
parentId: null,
|
||||
color: '#1e3a8a'
|
||||
})
|
||||
ensure({ code: 'DIR', name: 'Direktor LKA NRW', type: 'direktion', level: 0, parentId: null, color: '#1e3a8a' })
|
||||
continue
|
||||
}
|
||||
|
||||
// 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 = {
|
||||
const abtName = line.replace(/^Abteilung\s+(?:\d+|Zentralabteilung)\s*-?\s*/i, '').trim() || `Abteilung ${abtNum}`
|
||||
currentAbteilung = ensure({
|
||||
code: `Abt ${abtNum}`,
|
||||
name: abtName || `Abteilung ${abtNum}`,
|
||||
name: abtName,
|
||||
type: 'abteilung',
|
||||
level: 1,
|
||||
parentId: 'DIR',
|
||||
color: colors[abtNum] || '#6b7280',
|
||||
hasFuehrungsstelle: abtNum !== 'ZA'
|
||||
}
|
||||
units.push(currentAbteilung)
|
||||
})
|
||||
currentDezernat = null
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for Dezernat
|
||||
const dezMatch = line.match(/(?:Dezernat|Dez)\s+([\d\s]+)/i)
|
||||
if (dezMatch && currentAbteilung) {
|
||||
|
||||
const dezMatch = line.match(/^(?:Dezernat|Dez)\s+([\d]+)/i)
|
||||
if (dezMatch) {
|
||||
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
|
||||
const dezName = line.replace(/^(?:Dezernat|Dez)\s+[\d]+\s*-?\s*/i, '').trim() || `Dezernat ${dezNum}`
|
||||
// Strict mapping: first digit of dezNum determines Abteilung
|
||||
const inferredAbt = inferAbteilungCode(dezNum)
|
||||
let parentCode: string | null = null
|
||||
if (inferredAbt) {
|
||||
const abtNum = inferredAbt.split(' ')[1]
|
||||
ensure({ code: inferredAbt, name: inferredAbt.replace('Abt', 'Abteilung'), type: 'abteilung', level: 1, parentId: 'DIR', color: colors[abtNum] || '#6b7280', hasFuehrungsstelle: abtNum !== 'ZA' })
|
||||
parentCode = inferredAbt
|
||||
}
|
||||
units.push(currentDezernat)
|
||||
currentDezernat = ensure({ code: `Dez ${dezNum}`, name: dezName, type: 'dezernat', level: 2, parentId: parentCode })
|
||||
continue
|
||||
}
|
||||
|
||||
// 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
|
||||
})
|
||||
|
||||
const sgMatch = line.match(/^SG\s+([\d\.]+)/i)
|
||||
if (sgMatch) {
|
||||
const sgNum = sgMatch[1].trim()
|
||||
const sgName = line.replace(/^SG\s+[\d\.]+\s*-?\s*/i, '').trim() || `Sachgebiet ${sgNum}`
|
||||
// Strict mapping: SG NN.X -> Dez NN under Abt N
|
||||
const prefix = sgNum.split('.')[0]
|
||||
let dezCode: string | null = null
|
||||
if (prefix) {
|
||||
dezCode = `Dez ${prefix}`
|
||||
const inferredAbt = inferAbteilungCode(prefix)
|
||||
if (inferredAbt) {
|
||||
const abtNum = inferredAbt.split(' ')[1]
|
||||
ensure({ code: inferredAbt, name: inferredAbt.replace('Abt', 'Abteilung'), type: 'abteilung', level: 1, parentId: 'DIR', color: colors[abtNum] || '#6b7280', hasFuehrungsstelle: abtNum !== 'ZA' })
|
||||
}
|
||||
ensure({ code: dezCode, name: `Dezernat ${prefix}`, type: 'dezernat', level: 2, parentId: inferredAbt || null })
|
||||
}
|
||||
ensure({ code: `SG ${sgNum}`, name: sgName, type: 'sachgebiet', level: 3, parentId: dezCode })
|
||||
continue
|
||||
}
|
||||
|
||||
// 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
|
||||
})
|
||||
|
||||
const tdMatch = line.match(/^TD\s+([\d\.]+)/i)
|
||||
if (tdMatch) {
|
||||
const tdNum = tdMatch[1].trim()
|
||||
const tdName = line.replace(/^TD\s+[\d\.]+\s*-?\s*/i, '').trim() || `Teildezernat ${tdNum}`
|
||||
// Strict mapping: TD NN.X -> Dez NN under Abt N
|
||||
const prefix = tdNum.split('.')[0]
|
||||
let dezCode: string | null = null
|
||||
if (prefix) {
|
||||
dezCode = `Dez ${prefix}`
|
||||
const inferredAbt = inferAbteilungCode(prefix)
|
||||
if (inferredAbt) {
|
||||
const abtNum = inferredAbt.split(' ')[1]
|
||||
ensure({ code: inferredAbt, name: inferredAbt.replace('Abt', 'Abteilung'), type: 'abteilung', level: 1, parentId: 'DIR', color: colors[abtNum] || '#6b7280', hasFuehrungsstelle: abtNum !== 'ZA' })
|
||||
}
|
||||
ensure({ code: dezCode, name: `Dezernat ${prefix}`, type: 'dezernat', level: 2, parentId: inferredAbt || null })
|
||||
}
|
||||
ensure({ code: `TD ${tdNum}`, name: tdName, type: 'teildezernat', level: 3, parentId: dezCode })
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for Stabsstelle
|
||||
|
||||
if (patterns.stabsstelle.test(line)) {
|
||||
units.push({
|
||||
code: 'LStab',
|
||||
name: 'Leitungsstab',
|
||||
type: 'stabsstelle',
|
||||
level: 1,
|
||||
parentId: 'DIR',
|
||||
color: '#6b7280'
|
||||
})
|
||||
ensure({ code: 'LStab', name: 'Leitungsstab', type: 'stabsstelle', level: 1, parentId: 'DIR', color: '#6b7280' })
|
||||
continue
|
||||
}
|
||||
|
||||
// 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'
|
||||
})
|
||||
if (/Personalrat/i.test(line)) { code = 'PR'; name = 'Personalrat' }
|
||||
else if (/Schwerbehindertenvertretung/i.test(line)) { code = 'SBV'; name = 'Schwerbehindertenvertretung' }
|
||||
else if (/Datenschutzbeauftrag/i.test(line)) { code = 'DSB'; name = 'Datenschutzbeauftragter' }
|
||||
else if (/Gleichstellungsbeauftrag/i.test(line)) { code = 'GSB'; name = 'Gleichstellungsbeauftragte' }
|
||||
else if (/Innenrevision/i.test(line)) { code = 'IR'; name = 'Innenrevision' }
|
||||
ensure({ code, name, type: 'sondereinheit', level: 1, parentId: null, color: '#059669' })
|
||||
continue
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// Post-processing: infer missing parents
|
||||
const has = (code: string) => byCode.has(code)
|
||||
const get = (code: string) => byCode.get(code)
|
||||
for (const u of units) {
|
||||
if (u.type === 'dezernat' && !u.parentId) {
|
||||
const inferred = inferAbteilungCode(String(u.code).replace(/^Dez\s+/i,''))
|
||||
if (inferred) {
|
||||
if (!has(inferred)) {
|
||||
const abtNum = inferred.split(' ')[1]
|
||||
ensure({ code: inferred, name: inferred.replace('Abt', 'Abteilung'), type: 'abteilung', level: 1, parentId: 'DIR', color: colors[abtNum] || '#6b7280', hasFuehrungsstelle: abtNum !== 'ZA' })
|
||||
}
|
||||
u.parentId = inferred
|
||||
}
|
||||
}
|
||||
if ((u.type === 'sachgebiet' || u.type === 'teildezernat') && !u.parentId) {
|
||||
const num = String(u.code).replace(/^(SG|TD)\s+/i,'')
|
||||
const prefix = num.split('.')[0]
|
||||
if (prefix) {
|
||||
const dezCode = `Dez ${prefix}`
|
||||
if (!has(dezCode)) {
|
||||
ensure({ code: dezCode, name: `Dezernat ${prefix}`, type: 'dezernat', level: 2, parentId: inferAbteilungCode(prefix) })
|
||||
}
|
||||
u.parentId = dezCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return units
|
||||
}
|
||||
|
||||
@ -229,7 +255,24 @@ router.post('/import-pdf',
|
||||
}
|
||||
|
||||
// Parse the organizational structure
|
||||
const parsedUnits = parseOrganizationFromText(extractedText)
|
||||
let parsedUnits = parseOrganizationFromText(extractedText)
|
||||
|
||||
// Apply saved import rules (overrides)
|
||||
try {
|
||||
const rules = db.prepare('SELECT target_code, parent_code, type_override, name_override FROM organization_import_rules').all() as any[]
|
||||
if (rules && rules.length) {
|
||||
const byCode: Record<string, any> = {}
|
||||
parsedUnits.forEach(u => { byCode[u.code] = u })
|
||||
rules.forEach(r => {
|
||||
const u = byCode[r.target_code]
|
||||
if (u) {
|
||||
if (r.parent_code) u.parentId = r.parent_code
|
||||
if (r.type_override) u.type = r.type_override
|
||||
if (r.name_override) u.name = r.name_override
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (parsedUnits.length === 0) {
|
||||
return res.status(400).json({
|
||||
@ -238,29 +281,84 @@ router.post('/import-pdf',
|
||||
})
|
||||
}
|
||||
|
||||
// Clear existing organization (optional - could be a parameter)
|
||||
if (req.body.clearExisting === 'true') {
|
||||
// PREVIEW MODE: if requested, compute diff and return without writing
|
||||
const previewOnly = String((req.body as any).previewOnly ?? (req.query as any).previewOnly) === 'true'
|
||||
const overridesRaw = (req.body as any).overrides ?? (req.query as any).overrides
|
||||
let overrides: Array<{ code: string; parentCode?: string; type?: string; name?: string }> = []
|
||||
try {
|
||||
if (overridesRaw) overrides = typeof overridesRaw === 'string' ? JSON.parse(overridesRaw) : overridesRaw
|
||||
} catch {}
|
||||
|
||||
if (overrides.length) {
|
||||
const byCode: Record<string, any> = {}
|
||||
parsedUnits.forEach(u => { byCode[u.code] = u })
|
||||
overrides.forEach(o => {
|
||||
const u = byCode[o.code]
|
||||
if (u) {
|
||||
if (o.parentCode) u.parentId = o.parentCode
|
||||
if (o.type) u.type = o.type
|
||||
if (o.name) u.name = o.name
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const now = new Date().toISOString()
|
||||
|
||||
const existingMap = new Map<string, any>()
|
||||
const existingRows = db.prepare(`SELECT id, code, name, type, level, parent_id as parentId FROM organizational_units`).all() as any[]
|
||||
existingRows.forEach(r => existingMap.set(r.code, r))
|
||||
|
||||
// Diff classification
|
||||
const orphans: any[] = []
|
||||
const toCreate: any[] = []
|
||||
const toUpdate: any[] = []
|
||||
parsedUnits.forEach(u => {
|
||||
const parentCode = u.parentId || null
|
||||
const exists = existingMap.get(u.code)
|
||||
if (!parentCode) {
|
||||
orphans.push(u)
|
||||
}
|
||||
if (!exists) {
|
||||
toCreate.push(u)
|
||||
} else {
|
||||
const diff = (exists.name !== u.name) || (exists.type !== u.type) || (exists.level !== u.level)
|
||||
// Parent diff: compare codes by looking up parent id -> code
|
||||
let existingParentCode: string | null = null
|
||||
if (exists.parentId) {
|
||||
const parentRow = db.prepare('SELECT code FROM organizational_units WHERE id = ?').get(exists.parentId) as any
|
||||
existingParentCode = parentRow?.code || null
|
||||
}
|
||||
const parentDiff = existingParentCode !== parentCode
|
||||
if (diff || parentDiff) {
|
||||
toUpdate.push({ existing: exists, incoming: u })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (previewOnly) {
|
||||
return res.json({ success: true, data: {
|
||||
message: 'Preview generated',
|
||||
counts: { create: toCreate.length, update: toUpdate.length, orphans: orphans.length },
|
||||
diff: { create: toCreate, update: toUpdate, orphans }
|
||||
}})
|
||||
}
|
||||
|
||||
// Clear existing organization (optional)
|
||||
if (((req.body as any).clearExisting ?? (req.query as any).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
|
||||
@ -303,8 +401,20 @@ router.post('/import-pdf',
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
tx()
|
||||
|
||||
// Remember rules (optional)
|
||||
const rememberRules = String((req.body as any).rememberRules ?? (req.query as any).rememberRules) === 'true'
|
||||
if (rememberRules && overrides.length) {
|
||||
const upsert = db.prepare(`
|
||||
INSERT INTO organization_import_rules (id, target_code, parent_code, type_override, name_override, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(target_code) DO UPDATE SET parent_code=excluded.parent_code, type_override=excluded.type_override, name_override=excluded.name_override
|
||||
`)
|
||||
overrides.forEach(o => {
|
||||
upsert.run(uuidv4(), o.code, o.parentCode || null, o.type || null, o.name || null, now)
|
||||
})
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren