import { randomUUID } from 'crypto'; import fs from 'fs'; import path from 'path'; import { DatabaseSync } from 'node:sqlite'; import { fileURLToPath } from 'url'; import XLSX from 'xlsx'; import dotenv from 'dotenv'; dotenv.config(); const __dirname = path.dirname(fileURLToPath(import.meta.url)); const root = path.join(__dirname, '..'); const xlsxPath = path.join(root, 'Anlagenliste ITT.xlsx'); const dbPath = process.env.SQLITE_PATH || path.join(root, 'data', 'crm.db'); /** Zeile 9 (1-basiert): Spaltenbeschriftung / Feldnamen */ const ZEILE_BESCHREIBUNG = 9; /** Zeile 7: Gruppierung / übergeordnete Rubrik pro Spalte (z. B. „Kunde“) */ const ZEILE_GRUPPE = 7; /** Nur Spalten 1–99 importieren; Spalten 100–180 entfallen */ const MAX_SPALTEN = 99; if (!fs.existsSync(xlsxPath)) { console.error('Datei nicht gefunden:', xlsxPath); process.exit(1); } function dedupeHeaders(raw) { const count = {}; return raw.map((h) => { const base = String(h ?? '').trim() || 'Spalte'; count[base] = (count[base] || 0) + 1; return count[base] === 1 ? base : `${base}_${count[base]}`; }); } function padRow(arr, width) { const a = Array.isArray(arr) ? [...arr] : []; while (a.length < width) a.push(''); return a.slice(0, width); } const wb = XLSX.readFile(xlsxPath); const sheet = wb.Sheets.Anlagen || wb.Sheets[wb.SheetNames.find((n) => /anlagen/i.test(n))] || wb.Sheets[wb.SheetNames[0]]; const aoa = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '' }); const beschreibIdx = ZEILE_BESCHREIBUNG - 1; const gruppeIdx = ZEILE_GRUPPE - 1; let width = 0; for (const r of [beschreibIdx, gruppeIdx]) { if (aoa[r] && aoa[r].length > width) width = aoa[r].length; } if (sheet['!ref']) { const range = XLSX.utils.decode_range(sheet['!ref']); width = Math.max(width, range.e.c + 1); } width = Math.min(width, MAX_SPALTEN); const rawRow9 = padRow(aoa[beschreibIdx] || [], width).map((h) => String(h ?? '').trim(), ); const rawRow7 = padRow(aoa[gruppeIdx] || [], width).map((h) => String(h ?? '').trim(), ); const headers = dedupeHeaders(rawRow9); fs.mkdirSync(path.dirname(dbPath), { recursive: true }); const db = new DatabaseSync(dbPath); db.exec('PRAGMA foreign_keys = ON'); const cols = db.prepare('PRAGMA table_info(machines)').all(); if (!cols.some((c) => c.name === 'extras')) { db.exec('ALTER TABLE machines ADD COLUMN extras TEXT'); } const replaceAll = process.argv.includes('--replace'); if (replaceAll) { db.exec('DELETE FROM events'); db.exec('DELETE FROM tickets'); db.exec('DELETE FROM machines'); console.log('Bestehende Maschinen/Tickets/Events gelöscht.'); } const insertMachine = db.prepare( `INSERT INTO machines (id, name, typ, seriennummer, standort, extras, updated_at) VALUES (?, ?, ?, ?, ?, ?, datetime('now'))`, ); let imported = 0; let skipped = 0; db.exec('BEGIN'); try { for (let i = beschreibIdx + 1; i < aoa.length; i++) { const row = aoa[i]; if (!row || !row.length) continue; const padded = padRow(row, width); const sn = String(padded[0] ?? '').trim(); if (!/^ITT#/i.test(sn)) continue; const dup = db .prepare('SELECT id FROM machines WHERE seriennummer = ?') .get(sn); if (dup) { skipped += 1; continue; } const rowObj = {}; headers.forEach((h, j) => { const v = padded[j]; rowObj[h] = v === '' || v === undefined || v === null ? '' : String(v).trim(); }); const typ = rowObj.Typ || '—'; const standort = [rowObj.Stadt, rowObj.Land].filter(Boolean).join(', ') || '—'; const werteAlsListe = padded.map((v) => v === '' || v === undefined || v === null ? '' : String(v).trim(), ); const extrasObj = { _beschriftungZeile9: rawRow9, _gruppeZeile7: rawRow7, _werteAlsListe: werteAlsListe, ...rowObj, }; const extrasJson = JSON.stringify(extrasObj); const id = randomUUID(); insertMachine.run(id, sn, typ, sn, standort, extrasJson); imported += 1; } db.exec('COMMIT'); } catch (e) { db.exec('ROLLBACK'); throw e; } db.close(); console.log( `Anlagenliste: ${imported} Maschinen importiert, ${skipped} übersprungen (Seriennr. schon vorhanden).`, ); if (!replaceAll && skipped > 0 && imported === 0) { console.log( 'Hinweis: Für Neuimport: npm run import:anlagen -- --replace', ); }