Files
SDS-CRM/server/db.js

349 lines
12 KiB
JavaScript

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 'after_sales' CHECK ("role" IN ('admin', 'viewer', 'after_sales')),
"source" TEXT NOT NULL DEFAULT 'local' CHECK ("source" IN ('local', 'ldap')),
"ldap_dn" TEXT,
"firstname" TEXT,
"lastname" 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 userCols = db.prepare('PRAGMA table_info(users)').all();
if (!userCols.some((c) => c.name === 'firstname')) {
db.exec('ALTER TABLE users ADD COLUMN firstname TEXT');
}
if (!userCols.some((c) => c.name === 'lastname')) {
db.exec('ALTER TABLE users ADD COLUMN lastname TEXT');
}
const usersTableSql = db
.prepare(
"SELECT sql FROM sqlite_master WHERE type='table' AND name='users'",
)
.get()?.sql;
if (usersTableSql && !usersTableSql.includes('after_sales')) {
db.exec('BEGIN');
try {
db.exec(`
CREATE TABLE "users_new" (
"id" TEXT NOT NULL PRIMARY KEY,
"username" TEXT NOT NULL UNIQUE,
"password_hash" TEXT,
"role" TEXT NOT NULL DEFAULT 'after_sales' CHECK ("role" IN ('admin', 'viewer', 'after_sales')),
"source" TEXT NOT NULL DEFAULT 'local' CHECK ("source" IN ('local', 'ldap')),
"ldap_dn" TEXT,
"firstname" TEXT,
"lastname" 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'))
);
`);
db.exec(`
INSERT INTO users_new (
id, username, password_hash, role, source, ldap_dn, firstname, lastname, active, created_at, updated_at
)
SELECT
id,
username,
password_hash,
CASE
WHEN role = 'user' THEN 'after_sales'
WHEN role IN ('admin', 'viewer', 'after_sales') THEN role
ELSE 'after_sales'
END,
source,
ldap_dn,
firstname,
lastname,
active,
created_at,
updated_at
FROM users;
`);
db.exec('DROP TABLE users');
db.exec('ALTER TABLE users_new RENAME TO users');
db.exec('CREATE INDEX IF NOT EXISTS users_username_idx ON "users" ("username")');
db.exec('COMMIT');
} catch (e) {
try {
db.exec('ROLLBACK');
} catch {
/* ignore */
}
throw e;
}
}
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();
}
const ticketCols3 = db.prepare('PRAGMA table_info(tickets)').all();
if (!ticketCols3.some((c) => c.name === 'assigned_user_id')) {
db.exec(
'ALTER TABLE tickets ADD COLUMN assigned_user_id TEXT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE',
);
db.exec(
'CREATE INDEX IF NOT EXISTS tickets_assigned_user_id_idx ON tickets (assigned_user_id)',
);
}
export default db;