import fs from 'fs'; import path from 'path'; import { DatabaseSync } from 'node:sqlite'; import { fileURLToPath } from 'url'; import { mergeLegacyAttachmentEventsByDay } from './lib/merge-attachment-events.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const dbPath = process.env.SQLITE_PATH || path.join(__dirname, '..', 'data', 'crm.db'); fs.mkdirSync(path.dirname(dbPath), { recursive: true }); const db = new DatabaseSync(dbPath); db.exec('PRAGMA foreign_keys = ON'); const machineCols = db.prepare('PRAGMA table_info(machines)').all(); if (!machineCols.some((c) => c.name === 'extras')) { db.exec('ALTER TABLE machines ADD COLUMN extras TEXT'); } const machineCols2 = db.prepare('PRAGMA table_info(machines)').all(); if (!machineCols2.some((c) => c.name === 'list_status')) { db.exec( "ALTER TABLE machines ADD COLUMN list_status TEXT NOT NULL DEFAULT ''", ); } const hasCustomerId = machineCols.some((c) => c.name === 'customer_id'); const tables = db .prepare( "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'", ) .all() .map((r) => r.name); const hasCustomersTable = tables.includes('customers'); const eventCols = db.prepare('PRAGMA table_info(events)').all(); if (eventCols.length > 0 && !eventCols.some((c) => c.name === 'remote_duration_seconds')) { db.exec('ALTER TABLE events ADD COLUMN remote_duration_seconds INTEGER'); } if (eventCols.length > 0 && !eventCols.some((c) => c.name === 'teamviewer_notes')) { db.exec('ALTER TABLE events ADD COLUMN teamviewer_notes TEXT'); } const hasEventExtras = eventCols.some((c) => c.name === 'callback_number'); if (eventCols.length > 0 && !hasEventExtras) { db.exec('BEGIN'); try { db.exec(` CREATE TABLE events_new ( "id" TEXT NOT NULL PRIMARY KEY, "ticket_id" TEXT NOT NULL, "type" TEXT NOT NULL CHECK ("type" IN ('NOTE', 'CALL', 'REMOTE', 'PART', 'SYSTEM')), "description" TEXT NOT NULL, "callback_number" TEXT, "teamviewer_id" TEXT, "article_number" TEXT, "remote_duration_seconds" INTEGER, "teamviewer_notes" TEXT, "created_at" TEXT NOT NULL DEFAULT (datetime('now')), FOREIGN KEY ("ticket_id") REFERENCES "tickets" ("id") ON DELETE CASCADE ON UPDATE CASCADE ); INSERT INTO events_new (id, ticket_id, type, description, callback_number, teamviewer_id, article_number, remote_duration_seconds, teamviewer_notes, created_at) SELECT id, ticket_id, CASE WHEN type = 'WORK' THEN 'REMOTE' ELSE type END, description, NULL, NULL, NULL, NULL, NULL, created_at FROM events; DROP TABLE events; ALTER TABLE events_new RENAME TO events; `); db.exec( 'CREATE INDEX IF NOT EXISTS events_ticket_id_idx ON "events" ("ticket_id")', ); db.exec( 'CREATE INDEX IF NOT EXISTS events_created_at_idx ON "events" ("created_at")', ); db.exec('COMMIT'); } catch (e) { db.exec('ROLLBACK'); throw e; } } if (hasCustomerId || hasCustomersTable) { db.exec('BEGIN'); try { db.exec(` CREATE TABLE machines_new ( "id" TEXT NOT NULL PRIMARY KEY, "name" TEXT NOT NULL, "typ" TEXT NOT NULL, "seriennummer" TEXT NOT NULL, "standort" TEXT NOT NULL, "list_status" TEXT NOT NULL DEFAULT '', "extras" TEXT, "created_at" TEXT NOT NULL DEFAULT (datetime('now')), "updated_at" TEXT NOT NULL DEFAULT (datetime('now')) ); INSERT INTO machines_new (id, name, typ, seriennummer, standort, list_status, extras, created_at, updated_at) SELECT id, name, typ, seriennummer, standort, COALESCE(list_status, ''), extras, created_at, updated_at FROM machines; DROP TABLE machines; ALTER TABLE machines_new RENAME TO machines; `); if (hasCustomersTable) { db.exec('DROP TABLE customers'); } db.exec('COMMIT'); } catch (e) { db.exec('ROLLBACK'); throw e; } } const tbl = db .prepare( "SELECT name FROM sqlite_master WHERE type='table' AND name='users'", ) .get(); if (!tbl) { db.exec(` CREATE TABLE "users" ( "id" TEXT NOT NULL PRIMARY KEY, "username" TEXT NOT NULL UNIQUE, "password_hash" TEXT, "role" TEXT NOT NULL DEFAULT 'user' CHECK ("role" IN ('admin', 'user')), "source" TEXT NOT NULL DEFAULT 'local' CHECK ("source" IN ('local', 'ldap')), "ldap_dn" TEXT, "active" INTEGER NOT NULL DEFAULT 1 CHECK ("active" IN (0, 1)), "created_at" TEXT NOT NULL DEFAULT (datetime('now')), "updated_at" TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS users_username_idx ON "users" ("username"); `); } const tblSet = db .prepare( "SELECT name FROM sqlite_master WHERE type='table' AND name='app_settings'", ) .get(); if (!tblSet) { db.exec(` CREATE TABLE "app_settings" ( "key" TEXT NOT NULL PRIMARY KEY, "value" TEXT NOT NULL ); `); } const ldapLogTbl = db .prepare( "SELECT name FROM sqlite_master WHERE type='table' AND name='ldap_sync_log'", ) .get(); if (!ldapLogTbl) { db.exec(` CREATE TABLE "ldap_sync_log" ( "id" TEXT NOT NULL PRIMARY KEY, "started_at" TEXT NOT NULL, "finished_at" TEXT NOT NULL, "trigger_type" TEXT NOT NULL CHECK ("trigger_type" IN ('manual', 'automatic')), "status" TEXT NOT NULL CHECK ("status" IN ('success', 'error')), "users_synced" INTEGER NOT NULL DEFAULT 0, "error_message" TEXT ); CREATE INDEX ldap_sync_log_finished_idx ON "ldap_sync_log" ("finished_at" DESC); `); } const ticketAttachmentsTbl = db .prepare( "SELECT name FROM sqlite_master WHERE type='table' AND name='ticket_attachments'", ) .get(); if (!ticketAttachmentsTbl) { db.exec('BEGIN'); try { db.exec(` CREATE TABLE events_new ( "id" TEXT NOT NULL PRIMARY KEY, "ticket_id" TEXT NOT NULL, "type" TEXT NOT NULL CHECK ("type" IN ('NOTE', 'CALL', 'REMOTE', 'PART', 'SYSTEM', 'ATTACHMENT')), "description" TEXT NOT NULL, "callback_number" TEXT, "teamviewer_id" TEXT, "article_number" TEXT, "remote_duration_seconds" INTEGER, "teamviewer_notes" TEXT, "created_at" TEXT NOT NULL DEFAULT (datetime('now')), FOREIGN KEY ("ticket_id") REFERENCES "tickets" ("id") ON DELETE CASCADE ON UPDATE CASCADE ); INSERT INTO events_new ( id, ticket_id, type, description, callback_number, teamviewer_id, article_number, remote_duration_seconds, teamviewer_notes, created_at ) SELECT id, ticket_id, type, description, callback_number, teamviewer_id, article_number, remote_duration_seconds, teamviewer_notes, created_at FROM events; DROP TABLE events; ALTER TABLE events_new RENAME TO events; `); db.exec( 'CREATE INDEX IF NOT EXISTS events_ticket_id_idx ON "events" ("ticket_id")', ); db.exec( 'CREATE INDEX IF NOT EXISTS events_created_at_idx ON "events" ("created_at")', ); db.exec(` CREATE TABLE "ticket_attachments" ( "id" TEXT NOT NULL PRIMARY KEY, "event_id" TEXT NOT NULL, "original_name" TEXT NOT NULL, "stored_path" TEXT NOT NULL, "mime_type" TEXT, "size_bytes" INTEGER NOT NULL, "created_at" TEXT NOT NULL DEFAULT (datetime('now')), FOREIGN KEY ("event_id") REFERENCES "events" ("id") ON DELETE CASCADE ON UPDATE CASCADE ); CREATE INDEX IF NOT EXISTS ticket_attachments_event_idx ON "ticket_attachments" ("event_id"); `); db.exec('COMMIT'); } catch (e) { db.exec('ROLLBACK'); throw e; } } const hasTicketAttachments = db .prepare( "SELECT name FROM sqlite_master WHERE type='table' AND name='ticket_attachments'", ) .get(); const attachmentMergeDone = db .prepare('SELECT 1 AS ok FROM app_settings WHERE key = ?') .get('attachment_events_merge_day_v1'); if (hasTicketAttachments && !attachmentMergeDone) { try { mergeLegacyAttachmentEventsByDay(db); db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)').run( 'attachment_events_merge_day_v1', '1', ); } catch (e) { console.error('CRM: Zusammenführung Anhang-Events fehlgeschlagen:', e); } } const ticketCols = db.prepare('PRAGMA table_info(tickets)').all(); if (!ticketCols.some((c) => c.name === 'sla_days')) { db.exec('ALTER TABLE tickets ADD COLUMN sla_days INTEGER'); } if (!ticketCols.some((c) => c.name === 'sla_anchor_at')) { db.exec('ALTER TABLE tickets ADD COLUMN sla_anchor_at TEXT'); } const ticketCols2 = db.prepare('PRAGMA table_info(tickets)').all(); if (ticketCols2.some((c) => c.name === 'sla_anchor_at')) { db.prepare( 'UPDATE tickets SET sla_anchor_at = created_at WHERE sla_anchor_at IS NULL', ).run(); } export default db;