Initial commit: IT Nexus Web-App

This commit is contained in:
2026-06-01 20:49:07 +02:00
commit 8023765e6c
387 changed files with 106900 additions and 0 deletions

356
backend/src/models/Asset.js Normal file
View File

@@ -0,0 +1,356 @@
const { getDatabase } = require('../config/database');
class Asset {
/**
* Get all assets with related user information
*/
static getAll({ departments } = {}) {
const db = getDatabase();
const where = departments && departments.length
? `WHERE a.department IN (${departments.map(() => '?').join(',')})`
: '';
const stmt = db.prepare(`
SELECT
a.*,
au.username as assigned_to_username,
au.email as assigned_to_email,
cu.username as created_by_username,
uu.username as updated_by_username
FROM assets a
LEFT JOIN users au ON a.assigned_to_user_id = au.id
LEFT JOIN users cu ON a.created_by_user_id = cu.id
LEFT JOIN users uu ON a.updated_by_user_id = uu.id
${where}
ORDER BY a.created_at DESC
`);
return departments && departments.length ? stmt.all(...departments) : stmt.all();
}
/**
* Get asset by ID
*/
static getById(id) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
a.*,
au.username as assigned_to_username,
au.email as assigned_to_email,
cu.username as created_by_username,
uu.username as updated_by_username,
wl.name as location_name
FROM assets a
LEFT JOIN users au ON a.assigned_to_user_id = au.id
LEFT JOIN users cu ON a.created_by_user_id = cu.id
LEFT JOIN users uu ON a.updated_by_user_id = uu.id
LEFT JOIN warehouse_locations wl ON a.location_id = wl.id
WHERE a.id = ?
`);
return stmt.get(id);
}
/**
* Get asset by name (hostname match)
*/
static getByName(name) {
const db = getDatabase();
return db.prepare('SELECT * FROM assets WHERE LOWER(name) = LOWER(?)').get(name);
}
/**
* Get asset by serial number
*/
static getBySerialNumber(serialNumber) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
a.*,
au.username as assigned_to_username,
cu.username as created_by_username,
wl.name as location_name
FROM assets a
LEFT JOIN users au ON a.assigned_to_user_id = au.id
LEFT JOIN users cu ON a.created_by_user_id = cu.id
LEFT JOIN warehouse_locations wl ON a.location_id = wl.id
WHERE a.serial_number = ?
`);
return stmt.get(serialNumber);
}
/**
* Get assets by status
*/
static getByStatus(status) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
a.*,
au.username as assigned_to_username,
cu.username as created_by_username
FROM assets a
LEFT JOIN users au ON a.assigned_to_user_id = au.id
LEFT JOIN users cu ON a.created_by_user_id = cu.id
WHERE a.status = ?
ORDER BY a.created_at DESC
`);
return stmt.all(status);
}
/**
* Get assets by type
*/
static getByType(type) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
a.*,
au.username as assigned_to_username,
cu.username as created_by_username
FROM assets a
LEFT JOIN users au ON a.assigned_to_user_id = au.id
LEFT JOIN users cu ON a.created_by_user_id = cu.id
WHERE a.type = ?
ORDER BY a.created_at DESC
`);
return stmt.all(type);
}
/**
* Get assets assigned to a specific user
*/
static getByAssignedUser(userId) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
a.*,
cu.username as created_by_username
FROM assets a
LEFT JOIN users cu ON a.created_by_user_id = cu.id
WHERE a.assigned_to_user_id = ?
ORDER BY a.created_at DESC
`);
return stmt.all(userId);
}
/**
* Create a new asset
*/
static create(assetData) {
const db = getDatabase();
const stmt = db.prepare(`
INSERT INTO assets (
name,
type,
serial_number,
model,
status,
purchase_date,
description,
teamviewer_id,
assigned_to_user_id,
created_by_user_id,
last_maintenance_date,
next_maintenance_date,
maintenance_interval_months,
maintenance_notes,
department,
inventory_number,
purchase_price,
useful_life_years,
residual_value
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(
assetData.name,
assetData.type,
assetData.serial_number,
assetData.model || null,
assetData.status,
assetData.purchase_date || null,
assetData.description || null,
assetData.teamviewer_id || null,
assetData.assigned_to_user_id || null,
assetData.created_by_user_id,
assetData.last_maintenance_date || null,
assetData.next_maintenance_date || null,
assetData.maintenance_interval_months ? parseInt(assetData.maintenance_interval_months) : null,
assetData.maintenance_notes || null,
assetData.department || 'IT',
assetData.inventory_number || null,
assetData.purchase_price ? parseFloat(assetData.purchase_price) : null,
assetData.useful_life_years ? parseInt(assetData.useful_life_years) : null,
assetData.residual_value ? parseFloat(assetData.residual_value) : 0
);
return this.getById(result.lastInsertRowid);
}
/**
* Update asset
*/
static update(id, assetData, updatedByUserId) {
const db = getDatabase();
const fields = [];
const values = [];
if (assetData.name !== undefined) {
fields.push('name = ?');
values.push(assetData.name);
}
if (assetData.type !== undefined) {
fields.push('type = ?');
values.push(assetData.type);
}
if (assetData.serial_number !== undefined) {
fields.push('serial_number = ?');
values.push(assetData.serial_number);
}
if (assetData.model !== undefined) {
fields.push('model = ?');
values.push(assetData.model);
}
if (assetData.status !== undefined) {
fields.push('status = ?');
values.push(assetData.status);
}
if (assetData.purchase_date !== undefined) {
fields.push('purchase_date = ?');
values.push(assetData.purchase_date);
}
if (assetData.description !== undefined) {
fields.push('description = ?');
values.push(assetData.description);
}
if (assetData.teamviewer_id !== undefined) {
fields.push('teamviewer_id = ?');
values.push(assetData.teamviewer_id || null);
}
if (assetData.assigned_to_user_id !== undefined) {
fields.push('assigned_to_user_id = ?');
values.push(assetData.assigned_to_user_id);
}
if (assetData.last_maintenance_date !== undefined) {
fields.push('last_maintenance_date = ?');
values.push(assetData.last_maintenance_date || null);
}
if (assetData.next_maintenance_date !== undefined) {
fields.push('next_maintenance_date = ?');
values.push(assetData.next_maintenance_date || null);
}
if (assetData.maintenance_interval_months !== undefined) {
fields.push('maintenance_interval_months = ?');
values.push(assetData.maintenance_interval_months ? parseInt(assetData.maintenance_interval_months) : null);
}
if (assetData.maintenance_notes !== undefined) {
fields.push('maintenance_notes = ?');
values.push(assetData.maintenance_notes || null);
}
if (assetData.department !== undefined) {
fields.push('department = ?');
values.push(assetData.department || 'IT');
}
if (assetData.inventory_number !== undefined) {
fields.push('inventory_number = ?');
values.push(assetData.inventory_number || null);
}
if (assetData.purchase_price !== undefined) {
fields.push('purchase_price = ?');
values.push(assetData.purchase_price ? parseFloat(assetData.purchase_price) : null);
}
if (assetData.useful_life_years !== undefined) {
fields.push('useful_life_years = ?');
values.push(assetData.useful_life_years ? parseInt(assetData.useful_life_years) : null);
}
if (assetData.residual_value !== undefined) {
fields.push('residual_value = ?');
values.push(assetData.residual_value ? parseFloat(assetData.residual_value) : 0);
}
if (assetData.os !== undefined) {
fields.push('os = ?');
values.push(assetData.os || null);
}
if (assetData.ip_address !== undefined) {
fields.push('ip_address = ?');
values.push(assetData.ip_address || null);
}
if (assetData.manufacturer !== undefined) {
fields.push('manufacturer = ?');
values.push(assetData.manufacturer || null);
}
if (assetData.last_agent_sync !== undefined) {
fields.push('last_agent_sync = ?');
values.push(assetData.last_agent_sync || null);
}
fields.push('updated_by_user_id = ?');
fields.push('updated_at = CURRENT_TIMESTAMP');
values.push(updatedByUserId, id);
const stmt = db.prepare(`
UPDATE assets
SET ${fields.join(', ')}
WHERE id = ?
`);
stmt.run(...values);
return this.getById(id);
}
/**
* Update only the status
*/
static updateStatus(id, status, updatedByUserId) {
const db = getDatabase();
const stmt = db.prepare(`
UPDATE assets
SET status = ?,
updated_by_user_id = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`);
stmt.run(status, updatedByUserId, id);
return this.getById(id);
}
/**
* Delete asset
*/
static delete(id) {
const db = getDatabase();
const stmt = db.prepare('DELETE FROM assets WHERE id = ?');
const result = stmt.run(id);
return result.changes > 0;
}
/**
* Get statistics
*/
static getStatistics({ departments } = {}) {
const db = getDatabase();
const dw = departments && departments.length
? `AND department IN (${departments.map(() => '?').join(',')})`
: '';
const dp = departments && departments.length ? departments : [];
const q = (sql) => db.prepare(sql).get(...dp);
return {
total: q(`SELECT COUNT(*) as v FROM assets WHERE 1=1 ${dw}`).v,
verfuegbar: q(`SELECT COUNT(*) as v FROM assets WHERE status = 'verfuegbar' ${dw}`).v,
zugewiesen: q(`SELECT COUNT(*) as v FROM assets WHERE status = 'zugewiesen' ${dw}`).v,
inaktiv: q(`SELECT COUNT(*) as v FROM assets WHERE status = 'inaktiv' ${dw}`).v,
beschaedigt:q(`SELECT COUNT(*) as v FROM assets WHERE status = 'beschaedigt' ${dw}`).v,
byType: {
notebook: q(`SELECT COUNT(*) as v FROM assets WHERE type = 'Notebook' ${dw}`).v,
monitor: q(`SELECT COUNT(*) as v FROM assets WHERE type = 'Monitor' ${dw}`).v,
headset: q(`SELECT COUNT(*) as v FROM assets WHERE type = 'Headset' ${dw}`).v,
other: q(`SELECT COUNT(*) as v FROM assets WHERE type = 'Other' ${dw}`).v,
}
};
}
}
module.exports = Asset;

View File

@@ -0,0 +1,203 @@
const { getDatabase } = require('../config/database');
class AssetAssignment {
/**
* Get all assignments with related information
*/
static getAll() {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
aa.*,
a.name as asset_name,
a.type as asset_type,
a.serial_number as asset_serial_number,
u.username as user_username,
u.email as user_email,
ab.username as assigned_by_username
FROM asset_assignments aa
INNER JOIN assets a ON aa.asset_id = a.id
INNER JOIN users u ON aa.user_id = u.id
INNER JOIN users ab ON aa.assigned_by_user_id = ab.id
ORDER BY aa.assigned_at DESC
`);
return stmt.all();
}
/**
* Get assignment by ID
*/
static getById(id) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
aa.*,
a.name as asset_name,
a.type as asset_type,
a.serial_number as asset_serial_number,
u.username as user_username,
u.email as user_email,
ab.username as assigned_by_username
FROM asset_assignments aa
INNER JOIN assets a ON aa.asset_id = a.id
INNER JOIN users u ON aa.user_id = u.id
INNER JOIN users ab ON aa.assigned_by_user_id = ab.id
WHERE aa.id = ?
`);
return stmt.get(id);
}
/**
* Get assignments for a specific asset
*/
static getByAssetId(assetId) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
aa.*,
u.username as user_username,
u.email as user_email,
ab.username as assigned_by_username
FROM asset_assignments aa
INNER JOIN users u ON aa.user_id = u.id
INNER JOIN users ab ON aa.assigned_by_user_id = ab.id
WHERE aa.asset_id = ?
ORDER BY aa.assigned_at DESC
`);
return stmt.all(assetId);
}
/**
* Get assignments for a specific user
*/
static getByUserId(userId) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
aa.*,
a.name as asset_name,
a.type as asset_type,
a.serial_number as asset_serial_number,
ab.username as assigned_by_username
FROM asset_assignments aa
INNER JOIN assets a ON aa.asset_id = a.id
INNER JOIN users ab ON aa.assigned_by_user_id = ab.id
WHERE aa.user_id = ?
ORDER BY aa.assigned_at DESC
`);
return stmt.all(userId);
}
/**
* Get current assignment for an asset (not returned yet)
*/
static getCurrentAssignment(assetId) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
aa.*,
u.username as user_username,
u.email as user_email,
ab.username as assigned_by_username
FROM asset_assignments aa
INNER JOIN users u ON aa.user_id = u.id
INNER JOIN users ab ON aa.assigned_by_user_id = ab.id
WHERE aa.asset_id = ?
AND aa.returned_at IS NULL
ORDER BY aa.assigned_at DESC
LIMIT 1
`);
return stmt.get(assetId);
}
/**
* Get active assignments for a user (not returned yet)
*/
static getActiveAssignmentsByUser(userId) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
aa.*,
a.name as asset_name,
a.type as asset_type,
a.serial_number as asset_serial_number,
ab.username as assigned_by_username
FROM asset_assignments aa
INNER JOIN assets a ON aa.asset_id = a.id
INNER JOIN users ab ON aa.assigned_by_user_id = ab.id
WHERE aa.user_id = ?
AND aa.returned_at IS NULL
ORDER BY aa.assigned_at DESC
`);
return stmt.all(userId);
}
/**
* Create a new assignment
*/
static create(assignmentData) {
const db = getDatabase();
const stmt = db.prepare(`
INSERT INTO asset_assignments (
asset_id,
user_id,
assigned_by_user_id,
notes,
onboarding_protocol_id
) VALUES (?, ?, ?, ?, ?)
`);
const result = stmt.run(
assignmentData.asset_id,
assignmentData.user_id,
assignmentData.assigned_by_user_id,
assignmentData.notes || null,
assignmentData.onboarding_protocol_id || null
);
return this.getById(result.lastInsertRowid);
}
/**
* Mark assignment as returned
*/
static markAsReturned(id, offboardingProtocolId = null) {
const db = getDatabase();
const stmt = db.prepare(`
UPDATE asset_assignments
SET returned_at = CURRENT_TIMESTAMP,
offboarding_protocol_id = ?
WHERE id = ?
`);
stmt.run(offboardingProtocolId, id);
return this.getById(id);
}
/**
* Update assignment notes
*/
static updateNotes(id, notes) {
const db = getDatabase();
const stmt = db.prepare(`
UPDATE asset_assignments
SET notes = ?
WHERE id = ?
`);
stmt.run(notes, id);
return this.getById(id);
}
/**
* Delete assignment
*/
static delete(id) {
const db = getDatabase();
const stmt = db.prepare('DELETE FROM asset_assignments WHERE id = ?');
const result = stmt.run(id);
return result.changes > 0;
}
}
module.exports = AssetAssignment;

View File

@@ -0,0 +1,73 @@
const { getDatabase } = require('../config/database');
class AssetInspection {
/**
* Create the asset_inspections table if it doesn't exist
*/
static migrate() {
const db = getDatabase();
db.prepare(`
CREATE TABLE IF NOT EXISTS asset_inspections (
id INTEGER PRIMARY KEY AUTOINCREMENT,
asset_id INTEGER NOT NULL REFERENCES assets(id) ON DELETE CASCADE,
inspected_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
inspection_date DATE NOT NULL,
result TEXT CHECK(result IN ('bestanden', 'nicht_bestanden')) NOT NULL,
notes TEXT,
next_due_date DATE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`).run();
}
/**
* Create a new inspection record
*/
static create({ asset_id, inspected_by_user_id, inspection_date, result, notes, next_due_date }) {
const db = getDatabase();
const stmt = db.prepare(`
INSERT INTO asset_inspections
(asset_id, inspected_by_user_id, inspection_date, result, notes, next_due_date)
VALUES
(?, ?, ?, ?, ?, ?)
`);
const info = stmt.run(
asset_id,
inspected_by_user_id || null,
inspection_date,
result,
notes || null,
next_due_date || null
);
return this.getById(info.lastInsertRowid);
}
/**
* Get a single inspection by ID
*/
static getById(id) {
const db = getDatabase();
return db.prepare(`
SELECT ai.*, u.username AS inspector_name
FROM asset_inspections ai
LEFT JOIN users u ON ai.inspected_by_user_id = u.id
WHERE ai.id = ?
`).get(id);
}
/**
* Get all inspections for a given asset, newest first
*/
static getByAsset(assetId) {
const db = getDatabase();
return db.prepare(`
SELECT ai.*, u.username AS inspector_name
FROM asset_inspections ai
LEFT JOIN users u ON ai.inspected_by_user_id = u.id
WHERE ai.asset_id = ?
ORDER BY ai.inspection_date DESC, ai.created_at DESC
`).all(assetId);
}
}
module.exports = AssetInspection;

View File

@@ -0,0 +1,73 @@
const { getDatabase } = require('../config/database');
const AssetMovement = {
getAll({ limit = 200, asset_id } = {}) {
const db = getDatabase();
let q = `
SELECT m.*,
a.name AS asset_name, a.inventory_number, a.serial_number,
u.first_name || ' ' || u.last_name AS performed_by_name,
au.first_name || ' ' || au.last_name AS assigned_user_name,
fl.name AS from_location_name,
tl.name AS to_location_name
FROM asset_movements m
LEFT JOIN assets a ON m.asset_id = a.id
LEFT JOIN users u ON m.performed_by = u.id
LEFT JOIN users au ON m.assigned_user_id = au.id
LEFT JOIN warehouse_locations fl ON m.from_location_id = fl.id
LEFT JOIN warehouse_locations tl ON m.to_location_id = tl.id
`;
const params = [];
if (asset_id) {
q += ` WHERE m.asset_id = ?`;
params.push(asset_id);
}
q += ` ORDER BY m.created_at DESC LIMIT ?`;
params.push(limit);
return db.prepare(q).all(...params);
},
getById(id) {
const db = getDatabase();
return db.prepare(`
SELECT m.*,
a.name AS asset_name, a.inventory_number,
u.first_name || ' ' || u.last_name AS performed_by_name,
fl.name AS from_location_name,
tl.name AS to_location_name
FROM asset_movements m
LEFT JOIN assets a ON m.asset_id = a.id
LEFT JOIN users u ON m.performed_by = u.id
LEFT JOIN warehouse_locations fl ON m.from_location_id = fl.id
LEFT JOIN warehouse_locations tl ON m.to_location_id = tl.id
WHERE m.id = ?
`).get(id);
},
create(data) {
const db = getDatabase();
const result = db.prepare(`
INSERT INTO asset_movements
(asset_id, type, from_location_id, to_location_id, assigned_user_id, ticket_id, reason, notes, performed_by)
VALUES
(@asset_id, @type, @from_location_id, @to_location_id, @assigned_user_id, @ticket_id, @reason, @notes, @performed_by)
`).run({
asset_id: data.asset_id,
type: data.type,
from_location_id: data.from_location_id || null,
to_location_id: data.to_location_id || null,
assigned_user_id: data.assigned_user_id || null,
ticket_id: data.ticket_id || null,
reason: data.reason || '',
notes: data.notes || '',
performed_by: data.performed_by,
});
return this.getById(result.lastInsertRowid);
},
getRecentByAsset(asset_id, limit = 20) {
return this.getAll({ asset_id, limit });
},
};
module.exports = AssetMovement;

View File

@@ -0,0 +1,124 @@
const { getDatabase } = require('../config/database');
class AuditLog {
/**
* Create an audit log entry
*/
static create(logData) {
const db = getDatabase();
const stmt = db.prepare(`
INSERT INTO audit_log (
user_id,
action,
entity_type,
entity_id,
old_value,
new_value,
ip_address
) VALUES (?, ?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(
logData.user_id,
logData.action,
logData.entity_type,
logData.entity_id,
logData.old_value ? JSON.stringify(logData.old_value) : null,
logData.new_value ? JSON.stringify(logData.new_value) : null,
logData.ip_address || null
);
return result.lastInsertRowid;
}
/**
* Get all audit logs with pagination
*/
static getAll(limit = 100, offset = 0) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
al.*,
u.username,
u.email
FROM audit_log al
LEFT JOIN users u ON al.user_id = u.id
ORDER BY al.created_at DESC
LIMIT ? OFFSET ?
`);
return stmt.all(limit, offset);
}
/**
* Get audit logs for a specific user
*/
static getByUserId(userId, limit = 100, offset = 0) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
al.*,
u.username,
u.email
FROM audit_log al
LEFT JOIN users u ON al.user_id = u.id
WHERE al.user_id = ?
ORDER BY al.created_at DESC
LIMIT ? OFFSET ?
`);
return stmt.all(userId, limit, offset);
}
/**
* Get audit logs for a specific entity
*/
static getByEntity(entityType, entityId, limit = 100, offset = 0) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
al.*,
u.username,
u.email
FROM audit_log al
LEFT JOIN users u ON al.user_id = u.id
WHERE al.entity_type = ? AND al.entity_id = ?
ORDER BY al.created_at DESC
LIMIT ? OFFSET ?
`);
return stmt.all(entityType, entityId, limit, offset);
}
/**
* Get recent audit logs
*/
static getRecent(limit = 10) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
al.*,
u.username,
u.email
FROM audit_log al
LEFT JOIN users u ON al.user_id = u.id
ORDER BY al.created_at DESC
LIMIT ?
`);
return stmt.all(limit);
}
/**
* Helper method to log user actions
*/
static logUserAction(userId, action, entityType, entityId, oldValue = null, newValue = null, ipAddress = null) {
return this.create({
user_id: userId,
action,
entity_type: entityType,
entity_id: entityId,
old_value: oldValue,
new_value: newValue,
ip_address: ipAddress
});
}
}
module.exports = AuditLog;

View File

@@ -0,0 +1,71 @@
const { getDatabase } = require('../config/database');
const DEFAULT_DESIGN = {
brand_name: 'CEREDA SYSTEMS',
brand_subtitle:'IT Support',
brand_icon: '💻',
logo_url: '',
primary_color: '#0d9488',
button_color: '#0d9488',
bg_color: '#f1f5f9',
company_name: 'Cereda Systems GmbH',
footer_text: 'Automatisch generiert von IT Nexus · Bitte nicht direkt antworten.',
};
const ALLOWED_FIELDS = Object.keys(DEFAULT_DESIGN);
class EmailDesign {
static migrate() {
const db = getDatabase();
db.exec(`
CREATE TABLE IF NOT EXISTS email_design (
id INTEGER PRIMARY KEY DEFAULT 1,
brand_name TEXT NOT NULL DEFAULT 'CEREDA SYSTEMS',
brand_subtitle TEXT NOT NULL DEFAULT 'IT Support',
brand_icon TEXT NOT NULL DEFAULT '💻',
logo_url TEXT NOT NULL DEFAULT '',
primary_color TEXT NOT NULL DEFAULT '#0d9488',
button_color TEXT NOT NULL DEFAULT '#0d9488',
bg_color TEXT NOT NULL DEFAULT '#f1f5f9',
company_name TEXT NOT NULL DEFAULT 'Cereda Systems GmbH',
footer_text TEXT NOT NULL DEFAULT 'Automatisch generiert von IT Nexus · Bitte nicht direkt antworten.'
);
`);
db.prepare('INSERT OR IGNORE INTO email_design (id) VALUES (1)').run();
}
static getDesign() {
try {
const row = getDatabase().prepare('SELECT * FROM email_design WHERE id = 1').get();
return row ? { ...DEFAULT_DESIGN, ...row } : { ...DEFAULT_DESIGN };
} catch (_) {
return { ...DEFAULT_DESIGN };
}
}
static updateDesign(fields) {
const updates = Object.entries(fields).filter(([k]) => ALLOWED_FIELDS.includes(k));
if (!updates.length) return EmailDesign.getDesign();
const db = getDatabase();
const sql = `UPDATE email_design SET ${updates.map(([k]) => `${k}=?`).join(',')} WHERE id=1`;
db.prepare(sql).run(...updates.map(([, v]) => v));
return EmailDesign.getDesign();
}
static reset() {
const db = getDatabase();
db.prepare(`
UPDATE email_design SET
brand_name=?, brand_subtitle=?, brand_icon=?, logo_url=?,
primary_color=?, button_color=?, bg_color=?, company_name=?, footer_text=?
WHERE id=1
`).run(
DEFAULT_DESIGN.brand_name, DEFAULT_DESIGN.brand_subtitle, DEFAULT_DESIGN.brand_icon,
DEFAULT_DESIGN.logo_url, DEFAULT_DESIGN.primary_color, DEFAULT_DESIGN.button_color,
DEFAULT_DESIGN.bg_color, DEFAULT_DESIGN.company_name, DEFAULT_DESIGN.footer_text
);
return EmailDesign.getDesign();
}
}
module.exports = { EmailDesign, DEFAULT_DESIGN };

View File

@@ -0,0 +1,91 @@
const { getDatabase } = require('../config/database');
const DEFAULTS = [
{
type: 'ticket_created',
label: 'Ticket-Bestätigung',
subject: '[{{ticket_number}}] Ticket erstellt: {{ticket_title}}',
intro: 'Danke, {{requester_name}}! Wir haben Ihre Anfrage erhalten und melden uns so schnell wie möglich.',
},
{
type: 'ticket_assigned',
label: 'Ticket-Zuweisung',
subject: '[{{ticket_number}}] Ticket zugewiesen: {{ticket_title}}',
intro: 'Hallo {{assignee_name}}, dir wurde ein neues Ticket zugewiesen. Bitte bearbeite es zeitnah.',
},
{
type: 'comment_added',
label: 'Neuer Kommentar',
subject: '[{{ticket_number}}] Neue Antwort: {{ticket_title}}',
intro: 'Das Support-Team hat auf Ihr Ticket {{ticket_number}} geantwortet.',
},
{
type: 'status_changed',
label: 'Statusänderung',
subject: '[{{ticket_number}}] Status: {{new_status}} {{ticket_title}}',
intro: 'Der Status Ihres Tickets {{ticket_number}} hat sich geändert.',
},
{
type: 'escalation',
label: 'Eskalation',
subject: '⚠️ Eskalation: {{ticket_number}} {{ticket_title}}',
intro: 'Ein Ticket wartet seit {{age_hours}} Stunden auf Bearbeitung und wurde noch nicht bearbeitet.',
},
{
type: 'satisfaction',
label: 'Zufriedenheits-Feedback',
subject: 'War Ihr Problem gelöst? Ticket {{ticket_number}}',
intro: 'Ihr Ticket {{ticket_number}} {{ticket_title}} wurde soeben geschlossen. War die Lösung hilfreich?',
},
];
class EmailTemplate {
static migrate() {
const db = getDatabase();
db.exec(`
CREATE TABLE IF NOT EXISTS email_templates (
type TEXT PRIMARY KEY,
label TEXT NOT NULL,
subject TEXT NOT NULL,
intro TEXT NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
const stmt = db.prepare(
'INSERT OR IGNORE INTO email_templates (type, label, subject, intro) VALUES (?, ?, ?, ?)'
);
for (const d of DEFAULTS) {
stmt.run(d.type, d.label, d.subject, d.intro);
}
}
static getAll() {
const db = getDatabase();
return db.prepare('SELECT * FROM email_templates ORDER BY type').all();
}
static getByType(type) {
const db = getDatabase();
return db.prepare('SELECT * FROM email_templates WHERE type = ?').get(type);
}
static update(type, { subject, intro }) {
const db = getDatabase();
db.prepare(
'UPDATE email_templates SET subject = ?, intro = ?, updated_at = CURRENT_TIMESTAMP WHERE type = ?'
).run(subject, intro, type);
return db.prepare('SELECT * FROM email_templates WHERE type = ?').get(type);
}
static reset(type) {
const d = DEFAULTS.find(x => x.type === type);
if (!d) return null;
const db = getDatabase();
db.prepare(
'UPDATE email_templates SET subject = ?, intro = ?, updated_at = CURRENT_TIMESTAMP WHERE type = ?'
).run(d.subject, d.intro, type);
return db.prepare('SELECT * FROM email_templates WHERE type = ?').get(type);
}
}
module.exports = { EmailTemplate, EMAIL_TEMPLATE_DEFAULTS: DEFAULTS };

View File

@@ -0,0 +1,82 @@
const { getDatabase } = require('../config/database');
const ExternalAlert = {
getAll({ limit = 100, acknowledged } = {}) {
const db = getDatabase();
let q = `SELECT * FROM external_alerts`;
const params = [];
if (acknowledged !== undefined) {
q += ` WHERE acknowledged = ?`;
params.push(acknowledged ? 1 : 0);
}
q += ` ORDER BY created_at DESC LIMIT ?`;
params.push(limit);
return db.prepare(q).all(...params);
},
getById(id) {
const db = getDatabase();
return db.prepare(`SELECT * FROM external_alerts WHERE id = ?`).get(id);
},
getByEmailMessageId(messageId) {
const db = getDatabase();
return db.prepare(`SELECT id FROM external_alerts WHERE email_message_id = ?`).get(messageId);
},
create(data) {
const db = getDatabase();
const stmt = db.prepare(`
INSERT INTO external_alerts
(source, device, service, state_transition, severity, message, customer, monitored_by, state_time, raw_body, email_message_id)
VALUES
(@source, @device, @service, @state_transition, @severity, @message, @customer, @monitored_by, @state_time, @raw_body, @email_message_id)
`);
const result = stmt.run({
source: data.source || 'netgo',
device: data.device || '',
service: data.service || '',
state_transition: data.state_transition || '',
severity: data.severity || 'UNKNOWN',
message: data.message || '',
customer: data.customer || '',
monitored_by: data.monitored_by || '',
state_time: data.state_time || '',
raw_body: data.raw_body || '',
email_message_id: data.email_message_id || null,
});
return this.getById(result.lastInsertRowid);
},
setAiAnalysis(id, analysis) {
const db = getDatabase();
db.prepare(`UPDATE external_alerts SET ai_analysis = ? WHERE id = ?`).run(
typeof analysis === 'string' ? analysis : JSON.stringify(analysis),
id
);
},
acknowledge(id) {
const db = getDatabase();
db.prepare(`UPDATE external_alerts SET acknowledged = 1 WHERE id = ?`).run(id);
return this.getById(id);
},
setTicket(id, ticketId) {
const db = getDatabase();
db.prepare(`UPDATE external_alerts SET ticket_id = ?, acknowledged = 1 WHERE id = ?`).run(ticketId, id);
return this.getById(id);
},
delete(id) {
const db = getDatabase();
db.prepare(`DELETE FROM external_alerts WHERE id = ?`).run(id);
},
getUnacknowledgedCount() {
const db = getDatabase();
return db.prepare(`SELECT COUNT(*) as count FROM external_alerts WHERE acknowledged = 0`).get().count;
},
};
module.exports = ExternalAlert;

View File

@@ -0,0 +1,219 @@
const { getDatabase } = require('../config/database');
class FidoKey {
/**
* Get all FIDO keys with related user information
*/
static getAll() {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
fk.*,
au.username as assigned_to_username,
au.email as assigned_to_email,
cu.username as created_by_username,
uu.username as updated_by_username
FROM fido_keys fk
LEFT JOIN users au ON fk.assigned_to_user_id = au.id
LEFT JOIN users cu ON fk.created_by_user_id = cu.id
LEFT JOIN users uu ON fk.updated_by_user_id = uu.id
ORDER BY fk.created_at DESC
`);
return stmt.all();
}
/**
* Get FIDO key by ID
*/
static getById(id) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
fk.*,
au.username as assigned_to_username,
au.email as assigned_to_email,
cu.username as created_by_username,
uu.username as updated_by_username
FROM fido_keys fk
LEFT JOIN users au ON fk.assigned_to_user_id = au.id
LEFT JOIN users cu ON fk.created_by_user_id = cu.id
LEFT JOIN users uu ON fk.updated_by_user_id = uu.id
WHERE fk.id = ?
`);
return stmt.get(id);
}
/**
* Get FIDO key by serial number
*/
static getBySerialNumber(serialNumber) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
fk.*,
au.username as assigned_to_username,
cu.username as created_by_username
FROM fido_keys fk
LEFT JOIN users au ON fk.assigned_to_user_id = au.id
LEFT JOIN users cu ON fk.created_by_user_id = cu.id
WHERE fk.serial_number = ?
`);
return stmt.get(serialNumber);
}
/**
* Get FIDO keys by status
*/
static getByStatus(status) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
fk.*,
au.username as assigned_to_username,
cu.username as created_by_username
FROM fido_keys fk
LEFT JOIN users au ON fk.assigned_to_user_id = au.id
LEFT JOIN users cu ON fk.created_by_user_id = cu.id
WHERE fk.status = ?
ORDER BY fk.created_at DESC
`);
return stmt.all(status);
}
/**
* Get FIDO keys assigned to a specific user
*/
static getByAssignedUser(userId) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
fk.*,
cu.username as created_by_username
FROM fido_keys fk
LEFT JOIN users cu ON fk.created_by_user_id = cu.id
WHERE fk.assigned_to_user_id = ?
ORDER BY fk.created_at DESC
`);
return stmt.all(userId);
}
/**
* Create a new FIDO key
*/
static create(keyData) {
const db = getDatabase();
const stmt = db.prepare(`
INSERT INTO fido_keys (
name,
serial_number,
status,
description,
assigned_to_user_id,
created_by_user_id
) VALUES (?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(
keyData.name,
keyData.serial_number,
keyData.status,
keyData.description || null,
keyData.assigned_to_user_id || null,
keyData.created_by_user_id
);
return this.getById(result.lastInsertRowid);
}
/**
* Update FIDO key
*/
static update(id, keyData, updatedByUserId) {
const db = getDatabase();
const fields = [];
const values = [];
if (keyData.name !== undefined) {
fields.push('name = ?');
values.push(keyData.name);
}
if (keyData.serial_number !== undefined) {
fields.push('serial_number = ?');
values.push(keyData.serial_number);
}
if (keyData.status !== undefined) {
fields.push('status = ?');
values.push(keyData.status);
}
if (keyData.description !== undefined) {
fields.push('description = ?');
values.push(keyData.description);
}
if (keyData.assigned_to_user_id !== undefined) {
fields.push('assigned_to_user_id = ?');
values.push(keyData.assigned_to_user_id);
}
fields.push('updated_by_user_id = ?');
fields.push('updated_at = CURRENT_TIMESTAMP');
values.push(updatedByUserId, id);
const stmt = db.prepare(`
UPDATE fido_keys
SET ${fields.join(', ')}
WHERE id = ?
`);
stmt.run(...values);
return this.getById(id);
}
/**
* Update only the status
*/
static updateStatus(id, status, updatedByUserId) {
const db = getDatabase();
const stmt = db.prepare(`
UPDATE fido_keys
SET status = ?,
updated_by_user_id = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`);
stmt.run(status, updatedByUserId, id);
return this.getById(id);
}
/**
* Delete FIDO key
*/
static delete(id) {
const db = getDatabase();
const stmt = db.prepare('DELETE FROM fido_keys WHERE id = ?');
const result = stmt.run(id);
return result.changes > 0;
}
/**
* Get statistics
*/
static getStatistics() {
const db = getDatabase();
const totalStmt = db.prepare('SELECT COUNT(*) as total FROM fido_keys');
const activeStmt = db.prepare("SELECT COUNT(*) as active FROM fido_keys WHERE status = 'aktiv'");
const inactiveStmt = db.prepare("SELECT COUNT(*) as inactive FROM fido_keys WHERE status = 'inaktiv'");
const assignedStmt = db.prepare('SELECT COUNT(*) as assigned FROM fido_keys WHERE assigned_to_user_id IS NOT NULL');
return {
total: totalStmt.get().total,
active: activeStmt.get().active,
inactive: inactiveStmt.get().inactive,
assigned: assignedStmt.get().assigned
};
}
}
module.exports = FidoKey;

View File

@@ -0,0 +1,105 @@
const { getDatabase } = require('../config/database');
const CATEGORIES = [
'Risikomanagement',
'TOM',
'Incident Management',
'Business Continuity',
'Dokumentation',
'Awareness & Schulung',
];
const DEFAULT_TASKS = [
{ category: 'Risikomanagement', title: 'IT-Assets erfassen', description: 'Alle relevanten Systeme, Server, Cloud-Dienste, Schnittstellen und kritischen Prozesse inventarisieren.' },
{ category: 'Risikomanagement', title: 'Risikoanalyse durchführen', description: 'Bedrohungen und Schwachstellen identifizieren und nach Eintrittswahrscheinlichkeit/Schaden bewerten.' },
{ category: 'Risikomanagement', title: 'Maßnahmenplan ableiten', description: 'Für priorisierte Risiken konkrete technische oder organisatorische Maßnahmen definieren und terminieren.' },
{ category: 'TOM', title: 'Zugriffskonzept erstellen', description: 'Regeln, wer auf welche Systeme zugreifen darf (Least Privilege Prinzip) und regelmäßige Rechteprüfung einführen.' },
{ category: 'TOM', title: 'Patchmanagement regeln', description: 'Prozess definieren, wie Sicherheitsupdates erkannt, bewertet und zeitnah eingespielt werden.' },
{ category: 'TOM', title: 'Backup-Konzept implementieren', description: 'Regelmäßige Datensicherung durchführen und mindestens jährlich Wiederherstellung testen.' },
{ category: 'TOM', title: 'Monitoring einführen', description: 'Sicherheitsrelevante Ereignisse protokollieren und automatisiert überwachen (z. B. SOC/SIEM).' },
{ category: 'Incident Management', title: 'Incident-Response-Plan erstellen', description: 'Definieren, wie Sicherheitsvorfälle erkannt, bewertet, eskaliert und dokumentiert werden.' },
{ category: 'Incident Management', title: 'Meldeprozess definieren (24h/72h)', description: 'Interne Abläufe festlegen, um Fristen für Erstmeldung und Detailmeldung einhalten zu können.' },
{ category: 'Incident Management', title: 'Test durchführen', description: 'Einmal jährlich eine simulierte Cyberattacke als Übung durchführen.' },
{ category: 'Business Continuity', title: 'Kritische Prozesse identifizieren', description: 'Festlegen, welche Geschäftsprozesse bei IT-Ausfall zuerst wiederhergestellt werden müssen.' },
{ category: 'Business Continuity', title: 'Wiederanlaufplan definieren', description: 'Schriftlich festhalten, wie Systeme und Betrieb im Notfall wieder hochgefahren werden.' },
{ category: 'Dokumentation', title: 'Sicherheitsrichtlinie erstellen', description: 'Grundlegende IT-Sicherheitsleitlinie verabschieden und veröffentlichen.' },
{ category: 'Dokumentation', title: 'Nachweisführung etablieren', description: 'Alle Maßnahmen, Schulungen, Prüfungen dokumentieren und versionieren.' },
{ category: 'Awareness & Schulung', title: 'Mitarbeiterschulung durchführen', description: 'Regelmäßige Schulungen zu Phishing, Passwortsicherheit und Umgang mit IT-Vorfällen durchführen.' },
{ category: 'Awareness & Schulung', title: 'Onboarding-Prozess erweitern', description: 'Informationssicherheitsregeln verpflichtend ins Onboarding aufnehmen.' },
];
class IsoTask {
static migrate() {
const db = getDatabase();
db.exec(`
CREATE TABLE IF NOT EXISTS iso_tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'Offen'
CHECK(status IN ('Offen','In Bearbeitung','Abgeschlossen')),
responsible TEXT,
notes TEXT,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
`);
// Seed if empty
const count = db.prepare('SELECT COUNT(*) as c FROM iso_tasks').get().c;
if (count === 0) {
const stmt = db.prepare(
'INSERT INTO iso_tasks (category, title, description, sort_order) VALUES (?, ?, ?, ?)'
);
DEFAULT_TASKS.forEach((t, i) => stmt.run(t.category, t.title, t.description, i));
}
}
static getAll({ category, status } = {}) {
const db = getDatabase();
const where = [];
const vals = [];
if (category) { where.push('category = ?'); vals.push(category); }
if (status) { where.push('status = ?'); vals.push(status); }
return db.prepare(
`SELECT * FROM iso_tasks ${where.length ? 'WHERE ' + where.join(' AND ') : ''} ORDER BY sort_order, id`
).all(...vals);
}
static getById(id) {
return getDatabase().prepare('SELECT * FROM iso_tasks WHERE id = ?').get(id);
}
static create({ category, title, description, status = 'Offen', responsible, notes, sort_order = 0 }) {
const db = getDatabase();
const r = db.prepare(
'INSERT INTO iso_tasks (category, title, description, status, responsible, notes, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?)'
).run(category, title, description ?? null, status, responsible ?? null, notes ?? null, sort_order);
return this.getById(r.lastInsertRowid);
}
static update(id, fields) {
const db = getDatabase();
const allowed = ['category', 'title', 'description', 'status', 'responsible', 'notes', 'sort_order'];
const setClauses = [];
const vals = [];
for (const key of allowed) {
if (fields[key] !== undefined) {
setClauses.push(`${key} = ?`);
vals.push(fields[key]);
}
}
if (!setClauses.length) return this.getById(id);
setClauses.push("updated_at = datetime('now')");
vals.push(id);
db.prepare(`UPDATE iso_tasks SET ${setClauses.join(', ')} WHERE id = ?`).run(...vals);
return this.getById(id);
}
static delete(id) {
return getDatabase().prepare('DELETE FROM iso_tasks WHERE id = ?').run(id).changes > 0;
}
}
module.exports = IsoTask;

View File

@@ -0,0 +1,81 @@
const { getDatabase } = require('../config/database');
class ItTopic {
static migrate() {
const db = getDatabase();
db.exec(`
CREATE TABLE IF NOT EXISTS it_topics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
category TEXT NOT NULL DEFAULT 'Infrastruktur & Betrieb'
CHECK(category IN ('Infrastruktur & Betrieb','Benutzer & Arbeitsplätze','Systeme','IT-Governance & Compliance','Archiv')),
status TEXT NOT NULL DEFAULT 'Offen'
CHECK(status IN ('Offen','In Planung','In Umsetzung','Abgeschlossen')),
priority TEXT NOT NULL DEFAULT 'Normal'
CHECK(priority IN ('Niedrig','Normal','Hoch','Kritisch')),
responsible TEXT,
target_date TEXT,
description TEXT,
notes TEXT,
sort_order INTEGER NOT NULL DEFAULT 0,
planner_task_id TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
`);
// Migration: add planner_task_id to existing tables
try { db.exec(`ALTER TABLE it_topics ADD COLUMN planner_task_id TEXT`); } catch {}
}
static getAll({ category, status } = {}) {
const db = getDatabase();
const where = [];
const vals = [];
if (category) { where.push('category = ?'); vals.push(category); }
if (status) { where.push('status = ?'); vals.push(status); }
const sql = `
SELECT * FROM it_topics
${where.length ? 'WHERE ' + where.join(' AND ') : ''}
ORDER BY sort_order, created_at DESC
`;
return db.prepare(sql).all(...vals);
}
static getById(id) {
const db = getDatabase();
return db.prepare('SELECT * FROM it_topics WHERE id = ?').get(id);
}
static create({ title, category = 'Infrastruktur & Betrieb', status = 'Offen', priority = 'Normal', responsible, target_date, description, notes, sort_order = 0 }) {
const db = getDatabase();
const r = db.prepare(
'INSERT INTO it_topics (title, category, status, priority, responsible, target_date, description, notes, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'
).run(title, category, status, priority, responsible ?? null, target_date ?? null, description ?? null, notes ?? null, sort_order);
return this.getById(r.lastInsertRowid);
}
static update(id, fields) {
const db = getDatabase();
const allowed = ['title','category','status','priority','responsible','target_date','description','notes','sort_order','planner_task_id'];
const setClauses = [];
const vals = [];
for (const key of allowed) {
if (fields[key] !== undefined) {
setClauses.push(`${key} = ?`);
vals.push(fields[key]);
}
}
if (!setClauses.length) return this.getById(id);
setClauses.push("updated_at = datetime('now')");
vals.push(id);
db.prepare(`UPDATE it_topics SET ${setClauses.join(', ')} WHERE id = ?`).run(...vals);
return this.getById(id);
}
static delete(id) {
const db = getDatabase();
return db.prepare('DELETE FROM it_topics WHERE id = ?').run(id).changes > 0;
}
}
module.exports = ItTopic;

View File

@@ -0,0 +1,98 @@
const { getDatabase } = require('../config/database');
class License {
static getAll() {
const db = getDatabase();
const stmt = db.prepare(`
SELECT l.*, u.username as created_by_username
FROM licenses l
LEFT JOIN users u ON l.created_by_user_id = u.id
ORDER BY l.name ASC
`);
return stmt.all();
}
static getById(id) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT l.*, u.username as created_by_username
FROM licenses l
LEFT JOIN users u ON l.created_by_user_id = u.id
WHERE l.id = ?
`);
return stmt.get(id);
}
static create(data) {
const db = getDatabase();
const stmt = db.prepare(`
INSERT INTO licenses (
name, vendor, license_type, product_key,
seats, purchase_date, expiry_date, cost, notes,
sku_id, created_by_user_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(
data.name,
data.vendor || null,
data.license_type || 'subscription',
data.product_key || null,
data.seats ? parseInt(data.seats) : null,
data.purchase_date || null,
data.expiry_date || null,
data.cost ? parseFloat(data.cost) : null,
data.notes || null,
data.sku_id || null,
data.created_by_user_id
);
return this.getById(result.lastInsertRowid);
}
static update(id, data) {
const db = getDatabase();
const fields = [];
const values = [];
if (data.name !== undefined) { fields.push('name = ?'); values.push(data.name); }
if (data.vendor !== undefined) { fields.push('vendor = ?'); values.push(data.vendor || null); }
if (data.license_type !== undefined) { fields.push('license_type = ?'); values.push(data.license_type); }
if (data.product_key !== undefined) { fields.push('product_key = ?'); values.push(data.product_key || null); }
if (data.seats !== undefined) { fields.push('seats = ?'); values.push(data.seats ? parseInt(data.seats) : null); }
if (data.purchase_date !== undefined) { fields.push('purchase_date = ?'); values.push(data.purchase_date || null); }
if (data.expiry_date !== undefined) { fields.push('expiry_date = ?'); values.push(data.expiry_date || null); }
if (data.cost !== undefined) { fields.push('cost = ?'); values.push(data.cost ? parseFloat(data.cost) : null); }
if (data.notes !== undefined) { fields.push('notes = ?'); values.push(data.notes || null); }
if (data.sku_id !== undefined) { fields.push('sku_id = ?'); values.push(data.sku_id || null); }
fields.push('updated_at = CURRENT_TIMESTAMP');
values.push(id);
const stmt = db.prepare(`UPDATE licenses SET ${fields.join(', ')} WHERE id = ?`);
stmt.run(...values);
return this.getById(id);
}
static delete(id) {
const db = getDatabase();
const result = db.prepare('DELETE FROM licenses WHERE id = ?').run(id);
return result.changes > 0;
}
static getStatistics() {
const db = getDatabase();
const today = new Date().toISOString().slice(0, 10);
const in30 = new Date(Date.now() + 30 * 86400000).toISOString().slice(0, 10);
const total = db.prepare('SELECT COUNT(*) as count FROM licenses').get().count;
const expired = db.prepare(
`SELECT COUNT(*) as count FROM licenses WHERE expiry_date IS NOT NULL AND expiry_date < ?`
).get(today).count;
const expiringSoon = db.prepare(
`SELECT COUNT(*) as count FROM licenses WHERE expiry_date IS NOT NULL AND expiry_date >= ? AND expiry_date <= ?`
).get(today, in30).count;
return { total, expired, expiringSoon };
}
}
module.exports = License;

View File

@@ -0,0 +1,138 @@
const { getDatabase } = require('../config/database');
class MonitoringAgent {
static getAll() {
const db = getDatabase();
return db.prepare(`
SELECT * FROM monitoring_agents ORDER BY hostname ASC
`).all();
}
static getById(id) {
const db = getDatabase();
return db.prepare('SELECT * FROM monitoring_agents WHERE id = ?').get(id);
}
static getByHostname(hostname) {
const db = getDatabase();
return db.prepare('SELECT * FROM monitoring_agents WHERE hostname = ?').get(hostname);
}
static upsert(data) {
const db = getDatabase();
const existing = this.getByHostname(data.hostname);
if (existing) {
db.prepare(`
UPDATE monitoring_agents SET
ip_address = ?, mac_address = ?, os_name = ?, os_version = ?,
cpu_model = ?, cpu_cores = ?, cpu_usage_percent = ?,
ram_total_gb = ?, ram_used_gb = ?,
disk_total_gb = ?, disk_free_gb = ?,
last_user = ?, uptime_hours = ?, domain = ?,
agent_version = ?, installed_software = ?,
windows_updates_pending = ?,
tpm_present = ?, tpm_version = ?, tpm_v2 = ?,
secure_boot = ?, win11_ready = ?,
bitlocker_status = ?, defender_enabled = ?,
defender_signatures_age = ?, hardware_serial = ?,
status = 'online',
last_checkin = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE hostname = ?
`).run(
data.ip_address || null,
data.mac_address || null,
data.os_name || data.os || null,
data.os_version || null,
data.cpu_model || null,
data.cpu_cores || null,
data.cpu_usage_percent != null ? parseFloat(data.cpu_usage_percent) : (data.cpu_usage != null ? parseFloat(data.cpu_usage) : null),
data.ram_total_gb != null ? parseFloat(data.ram_total_gb) : (data.ram_total != null ? parseFloat(data.ram_total) : null),
data.ram_used_gb != null ? parseFloat(data.ram_used_gb) : (data.ram_used != null ? parseFloat(data.ram_used) : null),
data.disk_total_gb != null ? parseFloat(data.disk_total_gb) : (data.disk_total != null ? parseFloat(data.disk_total) : null),
data.disk_free_gb != null ? parseFloat(data.disk_free_gb) : (data.disk_free != null ? parseFloat(data.disk_free) : null),
data.last_user || null,
data.uptime_hours != null ? parseFloat(data.uptime_hours) : null,
data.domain || null,
data.agent_version || null,
data.installed_software ? JSON.stringify(data.installed_software) : null,
data.windows_updates_pending != null ? parseInt(data.windows_updates_pending) : (data.pending_updates != null ? parseInt(data.pending_updates) : 0),
data.tpm_present ? 1 : 0, data.tpm_version || null, data.tpm_v2 ? 1 : 0,
data.secure_boot ? 1 : 0, data.win11_ready ? 1 : 0,
data.bitlocker_status || 'unknown',
data.defender_enabled ? 1 : 0,
data.defender_signatures_age != null ? parseInt(data.defender_signatures_age) : -1,
data.hardware_serial || null,
data.hostname
);
return this.getByHostname(data.hostname);
} else {
const result = db.prepare(`
INSERT INTO monitoring_agents (
hostname, ip_address, mac_address, os_name, os_version,
cpu_model, cpu_cores, cpu_usage_percent,
ram_total_gb, ram_used_gb, disk_total_gb, disk_free_gb,
last_user, uptime_hours, domain, agent_version,
installed_software, windows_updates_pending,
tpm_present, tpm_version, tpm_v2, secure_boot, win11_ready,
bitlocker_status, defender_enabled, defender_signatures_age, hardware_serial
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
data.hostname,
data.ip_address || null,
data.mac_address || null,
data.os_name || data.os || null,
data.os_version || null,
data.cpu_model || null,
data.cpu_cores || null,
data.cpu_usage_percent != null ? parseFloat(data.cpu_usage_percent) : (data.cpu_usage != null ? parseFloat(data.cpu_usage) : null),
data.ram_total_gb != null ? parseFloat(data.ram_total_gb) : (data.ram_total != null ? parseFloat(data.ram_total) : null),
data.ram_used_gb != null ? parseFloat(data.ram_used_gb) : (data.ram_used != null ? parseFloat(data.ram_used) : null),
data.disk_total_gb != null ? parseFloat(data.disk_total_gb) : (data.disk_total != null ? parseFloat(data.disk_total) : null),
data.disk_free_gb != null ? parseFloat(data.disk_free_gb) : (data.disk_free != null ? parseFloat(data.disk_free) : null),
data.last_user || null,
data.uptime_hours != null ? parseFloat(data.uptime_hours) : null,
data.domain || null,
data.agent_version || null,
data.installed_software ? JSON.stringify(data.installed_software) : null,
data.windows_updates_pending != null ? parseInt(data.windows_updates_pending) : (data.pending_updates != null ? parseInt(data.pending_updates) : 0),
data.tpm_present ? 1 : 0, data.tpm_version || null, data.tpm_v2 ? 1 : 0,
data.secure_boot ? 1 : 0, data.win11_ready ? 1 : 0,
data.bitlocker_status || 'unknown',
data.defender_enabled ? 1 : 0,
data.defender_signatures_age != null ? parseInt(data.defender_signatures_age) : -1,
data.hardware_serial || null
);
return this.getById(result.lastInsertRowid);
}
}
static markOffline() {
const db = getDatabase();
// Mark agents offline if no checkin in last 15 minutes
db.prepare(`
UPDATE monitoring_agents
SET status = 'offline'
WHERE status = 'online'
AND last_checkin < datetime('now', '-15 minutes')
`).run();
}
static delete(id) {
const db = getDatabase();
return db.prepare('DELETE FROM monitoring_agents WHERE id = ?').run(id).changes > 0;
}
static getStatistics() {
const db = getDatabase();
const total = db.prepare('SELECT COUNT(*) as c FROM monitoring_agents').get().c;
const online = db.prepare("SELECT COUNT(*) as c FROM monitoring_agents WHERE status = 'online'").get().c;
const offline = db.prepare("SELECT COUNT(*) as c FROM monitoring_agents WHERE status = 'offline'").get().c;
const highCpu = db.prepare("SELECT COUNT(*) as c FROM monitoring_agents WHERE cpu_usage_percent > 80 AND status = 'online'").get().c;
const lowDisk = db.prepare("SELECT COUNT(*) as c FROM monitoring_agents WHERE disk_free_gb < 10 AND status = 'online'").get().c;
return { total, online, offline, highCpu, lowDisk };
}
}
module.exports = MonitoringAgent;

View File

@@ -0,0 +1,105 @@
const { getDatabase } = require('../config/database');
class NetworkDevice {
static getAll() {
const db = getDatabase();
return db.prepare(`SELECT * FROM network_devices ORDER BY type, name`).all();
}
static getById(id) {
const db = getDatabase();
return db.prepare(`SELECT * FROM network_devices WHERE id = ?`).get(id);
}
static getEnabled() {
const db = getDatabase();
return db.prepare(`SELECT * FROM network_devices WHERE enabled = 1`).all();
}
static create(data) {
const db = getDatabase();
const result = db.prepare(`
INSERT INTO network_devices (name, type, host, check_type, port, http_path, http_keyword,
snmp_community, snmp_version, interval_sec, timeout_sec, enabled, notify_email, location)
VALUES (@name, @type, @host, @check_type, @port, @http_path, @http_keyword,
@snmp_community, @snmp_version, @interval_sec, @timeout_sec, @enabled, @notify_email, @location)
`).run(data);
return this.getById(result.lastInsertRowid);
}
static update(id, data) {
const db = getDatabase();
db.prepare(`
UPDATE network_devices SET
name = @name, type = @type, host = @host, check_type = @check_type,
port = @port, http_path = @http_path, http_keyword = @http_keyword,
snmp_community = @snmp_community, snmp_version = @snmp_version,
interval_sec = @interval_sec, timeout_sec = @timeout_sec,
enabled = @enabled, notify_email = @notify_email, location = @location
WHERE id = @id
`).run({ ...data, id });
return this.getById(id);
}
static updateStatus(id, status, rtt_ms) {
const db = getDatabase();
db.prepare(`
UPDATE network_devices SET last_status = ?, last_checked = datetime('now'), last_rtt_ms = ?
WHERE id = ?
`).run(status, rtt_ms ?? null, id);
}
static delete(id) {
const db = getDatabase();
const result = db.prepare(`DELETE FROM network_devices WHERE id = ?`).run(id);
return result.changes > 0;
}
static addCheck(deviceId, status, rtt_ms, error_msg) {
const db = getDatabase();
db.prepare(`
INSERT INTO device_checks (device_id, status, rtt_ms, error_msg)
VALUES (?, ?, ?, ?)
`).run(deviceId, status, rtt_ms ?? null, error_msg ?? null);
}
static getChecks(deviceId, hoursBack = 24) {
const db = getDatabase();
return db.prepare(`
SELECT status, rtt_ms, error_msg, checked_at
FROM device_checks
WHERE device_id = ? AND checked_at >= datetime('now', '-' || ? || ' hours')
ORDER BY checked_at ASC
`).all(deviceId, hoursBack);
}
static getUptimeStats(deviceId, hoursBack = 24) {
const db = getDatabase();
const row = db.prepare(`
SELECT
COUNT(*) as total,
SUM(CASE WHEN status = 'up' THEN 1 ELSE 0 END) as up_count
FROM device_checks
WHERE device_id = ? AND checked_at >= datetime('now', '-' || ? || ' hours')
`).get(deviceId, hoursBack);
const pct = row.total > 0 ? Math.round((row.up_count / row.total) * 100 * 10) / 10 : null;
return { up: row.up_count, total: row.total, pct };
}
static getStatistics() {
const db = getDatabase();
const all = db.prepare(`SELECT last_status FROM network_devices`).all();
const total = all.length;
const up = all.filter(d => d.last_status === 'up').length;
const down = all.filter(d => d.last_status === 'down').length;
const unknown = all.filter(d => d.last_status === 'unknown').length;
return { total, up, down, unknown };
}
static cleanupOldChecks() {
const db = getDatabase();
db.prepare(`DELETE FROM device_checks WHERE checked_at < datetime('now', '-7 days')`).run();
}
}
module.exports = NetworkDevice;

View File

@@ -0,0 +1,233 @@
const { getDatabase } = require('../config/database');
class OffboardingProtocol {
/**
* Get all offboarding protocols with related information
*/
static getAll() {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
op.*,
e.username as employee_username,
e.first_name as employee_first_name,
e.last_name as employee_last_name,
e.email as employee_email,
r.name as employee_role_name,
cu.username as created_by_username,
uu.username as updated_by_username
FROM offboarding_protocols op
INNER JOIN users e ON op.employee_user_id = e.id
INNER JOIN roles r ON e.role_id = r.id
LEFT JOIN users cu ON op.created_by_user_id = cu.id
LEFT JOIN users uu ON op.updated_by_user_id = uu.id
ORDER BY op.created_at DESC
`);
return stmt.all();
}
/**
* Get offboarding protocol by ID
*/
static getById(id) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
op.*,
e.username as employee_username,
e.first_name as employee_first_name,
e.last_name as employee_last_name,
e.email as employee_email,
r.name as employee_role_name,
cu.username as created_by_username,
uu.username as updated_by_username
FROM offboarding_protocols op
INNER JOIN users e ON op.employee_user_id = e.id
INNER JOIN roles r ON e.role_id = r.id
LEFT JOIN users cu ON op.created_by_user_id = cu.id
LEFT JOIN users uu ON op.updated_by_user_id = uu.id
WHERE op.id = ?
`);
return stmt.get(id);
}
/**
* Get offboarding protocols by status
*/
static getByStatus(status) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
op.*,
e.username as employee_username,
e.first_name as employee_first_name,
e.last_name as employee_last_name,
e.email as employee_email,
cu.username as created_by_username
FROM offboarding_protocols op
INNER JOIN users e ON op.employee_user_id = e.id
LEFT JOIN users cu ON op.created_by_user_id = cu.id
WHERE op.status = ?
ORDER BY op.created_at DESC
`);
return stmt.all(status);
}
/**
* Get offboarding protocols for an employee
*/
static getByEmployee(employeeUserId) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
op.*,
cu.username as created_by_username
FROM offboarding_protocols op
LEFT JOIN users cu ON op.created_by_user_id = cu.id
WHERE op.employee_user_id = ?
ORDER BY op.created_at DESC
`);
return stmt.all(employeeUserId);
}
/**
* Create a new offboarding protocol
*/
static create(protocolData) {
const db = getDatabase();
const stmt = db.prepare(`
INSERT INTO offboarding_protocols (
employee_user_id,
status,
exit_date,
checklist_data,
notes,
created_by_user_id
) VALUES (?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(
protocolData.employee_user_id,
protocolData.status,
protocolData.exit_date,
protocolData.checklist_data || null,
protocolData.notes || null,
protocolData.created_by_user_id
);
return this.getById(result.lastInsertRowid);
}
/**
* Update offboarding protocol
*/
static update(id, protocolData, updatedByUserId) {
const db = getDatabase();
const fields = [];
const values = [];
if (protocolData.status !== undefined) {
fields.push('status = ?');
values.push(protocolData.status);
}
if (protocolData.exit_date !== undefined) {
fields.push('exit_date = ?');
values.push(protocolData.exit_date);
}
if (protocolData.completion_date !== undefined) {
fields.push('completion_date = ?');
values.push(protocolData.completion_date);
}
if (protocolData.checklist_data !== undefined) {
fields.push('checklist_data = ?');
values.push(protocolData.checklist_data);
}
if (protocolData.notes !== undefined) {
fields.push('notes = ?');
values.push(protocolData.notes);
}
if (protocolData.pdf_file_path !== undefined) {
fields.push('pdf_file_path = ?');
values.push(protocolData.pdf_file_path);
}
fields.push('updated_by_user_id = ?');
fields.push('updated_at = CURRENT_TIMESTAMP');
values.push(updatedByUserId, id);
const stmt = db.prepare(`
UPDATE offboarding_protocols
SET ${fields.join(', ')}
WHERE id = ?
`);
stmt.run(...values);
return this.getById(id);
}
/**
* Update checklist data
*/
static updateChecklist(id, checklistData, updatedByUserId) {
const db = getDatabase();
const stmt = db.prepare(`
UPDATE offboarding_protocols
SET checklist_data = ?,
updated_by_user_id = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`);
stmt.run(checklistData, updatedByUserId, id);
return this.getById(id);
}
/**
* Update PDF file path
*/
static updatePdfPath(id, pdfFilePath, updatedByUserId) {
const db = getDatabase();
const stmt = db.prepare(`
UPDATE offboarding_protocols
SET pdf_file_path = ?,
updated_by_user_id = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`);
stmt.run(pdfFilePath, updatedByUserId, id);
return this.getById(id);
}
/**
* Delete offboarding protocol
*/
static delete(id) {
const db = getDatabase();
const stmt = db.prepare('DELETE FROM offboarding_protocols WHERE id = ?');
const result = stmt.run(id);
return result.changes > 0;
}
/**
* Get statistics
*/
static getStatistics() {
const db = getDatabase();
const totalStmt = db.prepare('SELECT COUNT(*) as total FROM offboarding_protocols');
const pendingStmt = db.prepare("SELECT COUNT(*) as pending FROM offboarding_protocols WHERE status = 'pending'");
const inProgressStmt = db.prepare("SELECT COUNT(*) as in_progress FROM offboarding_protocols WHERE status = 'in_progress'");
const completedStmt = db.prepare("SELECT COUNT(*) as completed FROM offboarding_protocols WHERE status = 'completed'");
return {
total: totalStmt.get().total,
pending: pendingStmt.get().pending,
in_progress: inProgressStmt.get().in_progress,
completed: completedStmt.get().completed
};
}
}
module.exports = OffboardingProtocol;

View File

@@ -0,0 +1,257 @@
const { getDatabase } = require('../config/database');
class OnboardingProcess {
// ── Migration ────────────────────────────────────────────────────────────
static migrate() {
const db = getDatabase();
// Detect old schema (IT / HR / Buchhaltung as dept names) → reset
try {
const depts = db.prepare('SELECT name FROM onboarding_departments ORDER BY id LIMIT 5').all();
const oldNames = ['IT', 'HR / Personal', 'Buchhaltung'];
if (depts.length > 0 && depts.some(d => oldNames.includes(d.name))) {
console.log('🔄 Migriere Onboarding-Abteilungen auf neues Schema...');
db.prepare('DROP TABLE IF EXISTS onboarding_processes').run();
db.prepare('DROP TABLE IF EXISTS onboarding_departments').run();
}
} catch (_) { /* tables don't exist yet */ }
db.exec(`
CREATE TABLE IF NOT EXISTS onboarding_departments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
icon TEXT NOT NULL DEFAULT '🏢',
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS onboarding_processes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
department_id INTEGER NOT NULL REFERENCES onboarding_departments(id) ON DELETE CASCADE,
responsible_team TEXT NOT NULL DEFAULT 'it'
CHECK(responsible_team IN ('it','hr','buchhaltung')),
title TEXT NOT NULL,
description TEXT,
applies_to TEXT NOT NULL DEFAULT 'both'
CHECK(applies_to IN ('onboarding','offboarding','both')),
tag TEXT NOT NULL DEFAULT 'normal'
CHECK(tag IN ('normal','important','critical')),
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
`);
// Add responsible_team if missing (in case of partial migration)
try {
db.prepare("ALTER TABLE onboarding_processes ADD COLUMN responsible_team TEXT NOT NULL DEFAULT 'it'").run();
} catch (_) { /* already exists */ }
const deptCount = db.prepare('SELECT COUNT(*) as n FROM onboarding_departments').get().n;
if (deptCount === 0) {
this._seedDefaults(db);
}
}
static _seedDefaults(db) {
const insertDept = db.prepare(
'INSERT INTO onboarding_departments (name, icon, sort_order) VALUES (?, ?, ?)'
);
const insertProc = db.prepare(
'INSERT INTO onboarding_processes (department_id, responsible_team, title, description, applies_to, tag, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?)'
);
// Standard-Prozesse (gleich für alle Firmenabteilungen)
const itOn = [
['AD-Account erstellen', 'Benutzername nach Konvention · Temp-Passwort · Gruppe/Abteilung', 'onboarding', 'critical', 1],
['E-Mail-Adresse einrichten', 'Postfach anlegen · Signatur · Verteiler aufnehmen', 'onboarding', 'critical', 2],
['Hardware prüfen / bestellen', 'Verfügbaren Laptop prüfen · ggf. Neubestellung', 'onboarding', 'critical', 3],
['Hardware aufsetzen', 'OS-Image · Updates · Antivirus · Asset erfassen', 'onboarding', 'important', 4],
['Software installieren', 'Office 365 · VPN · Teams · abteilungsspez. Tools', 'onboarding', 'critical', 5],
['Telefon / Durchwahl einrichten', 'IP-Telefon oder Softphone · Durchwahl zuweisen', 'onboarding', 'important', 6],
['Hardware-Übergabe mit Protokoll', 'Laptop + Zubehör übergeben · Protokoll unterschreiben', 'onboarding', 'critical', 7],
['Ersten Login begleiten', 'Passwort ändern · MFA einrichten · VPN + Teams testen', 'onboarding', 'critical', 8],
];
const itOff = [
['Benutzerkonten deaktivieren', 'AD-Account · E-Mail · alle Systeme deaktivieren', 'offboarding', 'critical', 1],
['Zugriffsrechte entziehen', 'VPN · Cloud-Dienste · Remote-Zugriff widerrufen', 'offboarding', 'critical', 2],
['Hardware zurücknehmen', 'Laptop, Handy, Token · Zustand prüfen · Asset aktualisieren', 'offboarding', 'critical', 3],
['E-Mail-Weiterleitung einrichten', 'Eingehende Mails an Vorgesetzten weiterleiten (mind. 3 Mon.)', 'offboarding', 'important', 4],
];
const hrOn = [
['Personalakte anlegen', 'Digitale Akte · Vertragskopie · Eintrittsdatum', 'onboarding', 'critical', 1],
['Alle Abteilungen informieren', 'IT, Buchhaltung: Name, Position, Startdatum', 'onboarding', 'critical', 2],
['Willkommens-E-Mail senden', 'Startzeit · Ansprechpartner · Parkplatz · Dresscode', 'onboarding', 'important', 3],
['Formulare vorbereiten', 'Datenschutz · IT-Nutzungsordnung · Betriebsordnung', 'onboarding', 'normal', 4],
['Pflichtunterweisungen planen', 'Arbeitssicherheit · Brandschutz · Datenschutz (KW 1)', 'onboarding', 'important', 5],
['Empfang & Begrüßung', 'MA am Empfang abholen · Rundgang · Schlüssel/Badge', 'onboarding', 'critical', 6],
['Alle Formulare unterzeichnen', 'Betriebsordnung · Datenschutz · IT-Nutzungsordnung', 'onboarding', 'critical', 7],
];
const hrOff = [
['Abschlussgespräch führen', 'Exit-Interview · Feedback einholen', 'offboarding', 'important', 1],
['Arbeitszeugnis erstellen', 'Entwurf · vom Vorgesetzten prüfen lassen · ausstellen', 'offboarding', 'important', 2],
['Personalakte abschließen', 'Löschfristen prüfen · DSGVO-konform archivieren', 'offboarding', 'normal', 3],
['Sozialversicherung abmelden', 'SV-Abmeldung bei Krankenkasse einreichen', 'offboarding', 'normal', 4],
];
const bkOn = [
['Mitarbeiter in DATEV anlegen', 'Stammdaten · Eintrittsdatum · Kostenstelle', 'onboarding', 'critical', 1],
['Bankdaten & Steuerklasse anfordern', 'IBAN-Formular · Steuerklasse · SV-Nummer', 'onboarding', 'important', 2],
['Lohnkonto einrichten', 'Gehaltsgruppe · Krankenkasse · Urlaubsanspruch', 'onboarding', 'important', 3],
['Sozialversicherung anmelden', 'SV-Anmeldung bei Krankenkasse einreichen', 'onboarding', 'normal', 4],
['Gehaltsabrechnung vorbereiten', 'Erstes Gehalt anteilig berechnen · Auszahlungstermin', 'onboarding', 'important', 5],
];
const bkOff = [
['Letzte Gehaltsabrechnung', 'Anteiliges Gehalt · Urlaubsabgeltung · Auszahlungsdatum', 'offboarding', 'critical', 1],
['DATEV-Austritt buchen', 'Austrittsdatum eintragen · Kostenstelle abmelden', 'offboarding', 'important', 2],
['Offene Spesen prüfen', 'Reisekostenabrechnungen abschließen · Firmenkarte sperren', 'offboarding', 'normal', 3],
];
const allProcesses = [
...itOn.map(p => ['it', ...p]),
...itOff.map(p => ['it', ...p]),
...hrOn.map(p => ['hr', ...p]),
...hrOff.map(p => ['hr', ...p]),
...bkOn.map(p => ['buchhaltung', ...p]),
...bkOff.map(p => ['buchhaltung', ...p]),
];
const departments = [
['Wartung', '🔧', 1],
['TBO/Service', '🛠️', 2],
['Vertrieb AD', '💼', 3],
['Vertrieb ID', '📊', 4],
['KAW', '📦', 5],
['IT', '💻', 6],
['Verwaltung', '🏢', 7],
['Produktion', '🏭', 8],
];
const seed = db.transaction(() => {
departments.forEach(([name, icon, order]) => {
const deptId = insertDept.run(name, icon, order).lastInsertRowid;
allProcesses.forEach(([team, title, desc, applies, tag, sort]) => {
insertProc.run(deptId, team, title, desc, applies, tag, sort);
});
});
});
seed();
}
// ── Departments ──────────────────────────────────────────────────────────
static getAllDepartments() {
const db = getDatabase();
return db.prepare(
'SELECT * FROM onboarding_departments ORDER BY sort_order, name'
).all();
}
static getDepartmentById(id) {
const db = getDatabase();
return db.prepare('SELECT * FROM onboarding_departments WHERE id = ?').get(id);
}
static createDepartment({ name, icon = '🏢', sort_order = 0 }) {
const db = getDatabase();
const r = db.prepare(
'INSERT INTO onboarding_departments (name, icon, sort_order) VALUES (?, ?, ?)'
).run(name, icon, sort_order);
return this.getDepartmentById(r.lastInsertRowid);
}
static updateDepartment(id, { name, icon, sort_order }) {
const db = getDatabase();
const fields = [];
const vals = [];
if (name !== undefined) { fields.push('name = ?'); vals.push(name); }
if (icon !== undefined) { fields.push('icon = ?'); vals.push(icon); }
if (sort_order !== undefined) { fields.push('sort_order = ?'); vals.push(sort_order); }
if (!fields.length) return this.getDepartmentById(id);
vals.push(id);
db.prepare(`UPDATE onboarding_departments SET ${fields.join(', ')} WHERE id = ?`).run(...vals);
return this.getDepartmentById(id);
}
static deleteDepartment(id) {
const db = getDatabase();
return db.prepare('DELETE FROM onboarding_departments WHERE id = ?').run(id).changes > 0;
}
static reorderDepartments(orderedIds) {
const db = getDatabase();
const stmt = db.prepare('UPDATE onboarding_departments SET sort_order = ? WHERE id = ?');
db.transaction(() => {
orderedIds.forEach((id, idx) => stmt.run(idx, id));
})();
}
// ── Processes ────────────────────────────────────────────────────────────
static getAllProcesses({ departmentId, appliesTo, responsibleTeam } = {}) {
const db = getDatabase();
const where = [];
const vals = [];
if (departmentId) { where.push('p.department_id = ?'); vals.push(departmentId); }
if (appliesTo) { where.push("(p.applies_to = ? OR p.applies_to = 'both')"); vals.push(appliesTo); }
if (responsibleTeam) { where.push('p.responsible_team = ?'); vals.push(responsibleTeam); }
const sql = `
SELECT p.*, d.name as department_name, d.icon as department_icon
FROM onboarding_processes p
JOIN onboarding_departments d ON p.department_id = d.id
${where.length ? 'WHERE ' + where.join(' AND ') : ''}
ORDER BY d.sort_order, d.name, p.responsible_team, p.sort_order, p.title
`;
return db.prepare(sql).all(...vals);
}
static getProcessesForChecklist(appliesTo, departmentId) {
return this.getAllProcesses({ appliesTo, departmentId });
}
static getProcessById(id) {
const db = getDatabase();
return db.prepare(
'SELECT p.*, d.name as department_name FROM onboarding_processes p JOIN onboarding_departments d ON p.department_id = d.id WHERE p.id = ?'
).get(id);
}
static createProcess({ department_id, responsible_team = 'it', title, description = null, applies_to = 'both', tag = 'normal', sort_order = 0 }) {
const db = getDatabase();
const r = db.prepare(
'INSERT INTO onboarding_processes (department_id, responsible_team, title, description, applies_to, tag, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?)'
).run(department_id, responsible_team, title, description, applies_to, tag, sort_order);
return this.getProcessById(r.lastInsertRowid);
}
static updateProcess(id, { title, description, applies_to, tag, sort_order, department_id, responsible_team }) {
const db = getDatabase();
const fields = [];
const vals = [];
if (title !== undefined) { fields.push('title = ?'); vals.push(title); }
if (description !== undefined) { fields.push('description = ?'); vals.push(description); }
if (applies_to !== undefined) { fields.push('applies_to = ?'); vals.push(applies_to); }
if (tag !== undefined) { fields.push('tag = ?'); vals.push(tag); }
if (sort_order !== undefined) { fields.push('sort_order = ?'); vals.push(sort_order); }
if (department_id !== undefined) { fields.push('department_id = ?'); vals.push(department_id); }
if (responsible_team !== undefined) { fields.push('responsible_team = ?'); vals.push(responsible_team); }
if (!fields.length) return this.getProcessById(id);
vals.push(id);
db.prepare(`UPDATE onboarding_processes SET ${fields.join(', ')} WHERE id = ?`).run(...vals);
return this.getProcessById(id);
}
static deleteProcess(id) {
const db = getDatabase();
return db.prepare('DELETE FROM onboarding_processes WHERE id = ?').run(id).changes > 0;
}
static reorderProcesses(departmentId, orderedIds) {
const db = getDatabase();
const stmt = db.prepare('UPDATE onboarding_processes SET sort_order = ? WHERE id = ? AND department_id = ?');
db.transaction(() => {
orderedIds.forEach((id, idx) => stmt.run(idx, id, departmentId));
})();
}
}
module.exports = OnboardingProcess;

View File

@@ -0,0 +1,292 @@
const { getDatabase } = require('../config/database');
class OnboardingProtocol {
/**
* Get all onboarding protocols with related information
*/
static getAll() {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
op.*,
e.username as employee_username,
e.first_name as employee_sys_first_name,
e.last_name as employee_sys_last_name,
e.email as employee_email,
r.name as employee_role_name,
cu.username as created_by_username,
uu.username as updated_by_username
FROM onboarding_protocols op
LEFT JOIN users e ON op.employee_user_id = e.id
LEFT JOIN roles r ON e.role_id = r.id
LEFT JOIN users cu ON op.created_by_user_id = cu.id
LEFT JOIN users uu ON op.updated_by_user_id = uu.id
ORDER BY op.created_at DESC
`);
return stmt.all();
}
/**
* Get onboarding protocol by ID
*/
static getById(id) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
op.*,
e.username as employee_username,
e.first_name as employee_sys_first_name,
e.last_name as employee_sys_last_name,
e.email as employee_email,
r.name as employee_role_name,
cu.username as created_by_username,
uu.username as updated_by_username
FROM onboarding_protocols op
LEFT JOIN users e ON op.employee_user_id = e.id
LEFT JOIN roles r ON e.role_id = r.id
LEFT JOIN users cu ON op.created_by_user_id = cu.id
LEFT JOIN users uu ON op.updated_by_user_id = uu.id
WHERE op.id = ?
`);
return stmt.get(id);
}
/**
* Get onboarding protocols by status
*/
static getByStatus(status) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
op.*,
e.username as employee_username,
e.first_name as employee_first_name,
e.last_name as employee_last_name,
e.email as employee_email,
cu.username as created_by_username
FROM onboarding_protocols op
INNER JOIN users e ON op.employee_user_id = e.id
LEFT JOIN users cu ON op.created_by_user_id = cu.id
WHERE op.status = ?
ORDER BY op.created_at DESC
`);
return stmt.all(status);
}
/**
* Get onboarding protocols for an employee
*/
static getByEmployee(employeeUserId) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
op.*,
cu.username as created_by_username
FROM onboarding_protocols op
LEFT JOIN users cu ON op.created_by_user_id = cu.id
WHERE op.employee_user_id = ?
ORDER BY op.created_at DESC
`);
return stmt.all(employeeUserId);
}
/**
* Create a new onboarding protocol
*/
static create(protocolData) {
const db = getDatabase();
const stmt = db.prepare(`
INSERT INTO onboarding_protocols (
employee_user_id,
emp_first_name,
emp_last_name,
emp_private_email,
emp_phone,
emp_address,
department,
position,
work_location,
hours_model,
vacation_model,
dept_contacts,
status,
start_date,
checklist_data,
notes,
created_by_user_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(
protocolData.employee_user_id || null,
protocolData.emp_first_name || null,
protocolData.emp_last_name || null,
protocolData.emp_private_email || null,
protocolData.emp_phone || null,
protocolData.emp_address || null,
protocolData.department || null,
protocolData.position || null,
protocolData.work_location || null,
protocolData.hours_model || null,
protocolData.vacation_model || null,
protocolData.dept_contacts || null,
protocolData.status,
protocolData.start_date,
protocolData.checklist_data || null,
protocolData.notes || null,
protocolData.created_by_user_id
);
return this.getById(result.lastInsertRowid);
}
/**
* Update onboarding protocol
*/
static update(id, protocolData, updatedByUserId) {
const db = getDatabase();
const fields = [];
const values = [];
const personalFields = ['emp_first_name', 'emp_last_name', 'emp_private_email', 'emp_phone',
'emp_address', 'department', 'position', 'work_location', 'hours_model', 'vacation_model',
'dept_contacts', 'employee_user_id'];
for (const f of personalFields) {
if (protocolData[f] !== undefined) {
fields.push(`${f} = ?`);
values.push(protocolData[f]);
}
}
if (protocolData.status !== undefined) {
fields.push('status = ?');
values.push(protocolData.status);
}
if (protocolData.start_date !== undefined) {
fields.push('start_date = ?');
values.push(protocolData.start_date);
}
if (protocolData.completion_date !== undefined) {
fields.push('completion_date = ?');
values.push(protocolData.completion_date);
}
if (protocolData.checklist_data !== undefined) {
fields.push('checklist_data = ?');
values.push(protocolData.checklist_data);
}
if (protocolData.notes !== undefined) {
fields.push('notes = ?');
values.push(protocolData.notes);
}
if (protocolData.pdf_file_path !== undefined) {
fields.push('pdf_file_path = ?');
values.push(protocolData.pdf_file_path);
}
fields.push('updated_by_user_id = ?');
fields.push('updated_at = CURRENT_TIMESTAMP');
values.push(updatedByUserId, id);
const stmt = db.prepare(`
UPDATE onboarding_protocols
SET ${fields.join(', ')}
WHERE id = ?
`);
stmt.run(...values);
return this.getById(id);
}
/**
* Update checklist data
*/
static updateChecklist(id, checklistData, updatedByUserId) {
const db = getDatabase();
const stmt = db.prepare(`
UPDATE onboarding_protocols
SET checklist_data = ?,
updated_by_user_id = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`);
stmt.run(checklistData, updatedByUserId, id);
return this.getById(id);
}
/**
* Update PDF file path
*/
static updatePdfPath(id, pdfFilePath, updatedByUserId) {
const db = getDatabase();
const stmt = db.prepare(`
UPDATE onboarding_protocols
SET pdf_file_path = ?,
updated_by_user_id = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`);
stmt.run(pdfFilePath, updatedByUserId, id);
return this.getById(id);
}
/**
* Delete onboarding protocol
*/
static delete(id) {
const db = getDatabase();
const stmt = db.prepare('DELETE FROM onboarding_protocols WHERE id = ?');
const result = stmt.run(id);
return result.changes > 0;
}
/**
* Set confirm token for employee confirmation email
*/
static setConfirmToken(id, token) {
const db = getDatabase();
db.prepare('UPDATE onboarding_protocols SET confirm_token = ? WHERE id = ?').run(token, id);
}
/**
* Get protocol by confirm token (public, no auth)
*/
static getByConfirmToken(token) {
const db = getDatabase();
return db.prepare('SELECT * FROM onboarding_protocols WHERE confirm_token = ?').get(token);
}
/**
* Mark employee confirmation
*/
static confirmByToken(token) {
const db = getDatabase();
const result = db.prepare(
`UPDATE onboarding_protocols SET employee_confirmed_at = CURRENT_TIMESTAMP, status = 'completed', confirm_token = NULL WHERE confirm_token = ?`
).run(token);
return result.changes > 0;
}
/**
* Get statistics
*/
static getStatistics() {
const db = getDatabase();
const totalStmt = db.prepare('SELECT COUNT(*) as total FROM onboarding_protocols');
const pendingStmt = db.prepare("SELECT COUNT(*) as pending FROM onboarding_protocols WHERE status = 'pending'");
const inProgressStmt = db.prepare("SELECT COUNT(*) as in_progress FROM onboarding_protocols WHERE status = 'in_progress'");
const completedStmt = db.prepare("SELECT COUNT(*) as completed FROM onboarding_protocols WHERE status = 'completed'");
return {
total: totalStmt.get().total,
pending: pendingStmt.get().pending,
in_progress: inProgressStmt.get().in_progress,
completed: completedStmt.get().completed
};
}
}
module.exports = OnboardingProtocol;

View File

@@ -0,0 +1,79 @@
const { getDatabase } = require('../config/database');
const PurchaseOrder = {
getAll({ status } = {}) {
const db = getDatabase();
let q = `
SELECT p.*, u.first_name || ' ' || u.last_name AS created_by_name
FROM purchase_orders p
LEFT JOIN users u ON p.created_by = u.id
`;
const params = [];
if (status) { q += ` WHERE p.status = ?`; params.push(status); }
q += ` ORDER BY p.created_at DESC`;
return db.prepare(q).all(...params);
},
getById(id) {
const db = getDatabase();
return db.prepare(`
SELECT p.*, u.first_name || ' ' || u.last_name AS created_by_name
FROM purchase_orders p
LEFT JOIN users u ON p.created_by = u.id
WHERE p.id = ?
`).get(id);
},
create(data) {
const db = getDatabase();
const result = db.prepare(`
INSERT INTO purchase_orders (category, item_name, quantity, status, notes, created_by)
VALUES (@category, @item_name, @quantity, @status, @notes, @created_by)
`).run({
category: data.category,
item_name: data.item_name,
quantity: data.quantity || 1,
status: data.status || 'offen',
notes: data.notes || '',
created_by: data.created_by,
});
return this.getById(result.lastInsertRowid);
},
update(id, data) {
const db = getDatabase();
const fields = [];
const params = {};
if (data.status !== undefined) {
fields.push(`status = @status`);
params.status = data.status;
if (data.status === 'bestellt') {
fields.push(`ordered_at = CURRENT_TIMESTAMP`);
} else if (data.status === 'erledigt') {
fields.push(`completed_at = CURRENT_TIMESTAMP`);
}
}
if (data.notes !== undefined) { fields.push(`notes = @notes`); params.notes = data.notes; }
if (data.quantity !== undefined) { fields.push(`quantity = @quantity`); params.quantity = data.quantity; }
if (data.item_name !== undefined) { fields.push(`item_name = @item_name`); params.item_name = data.item_name; }
fields.push(`updated_at = CURRENT_TIMESTAMP`);
params.id = id;
db.prepare(`UPDATE purchase_orders SET ${fields.join(', ')} WHERE id = @id`).run(params);
return this.getById(id);
},
delete(id) {
const db = getDatabase();
return db.prepare(`DELETE FROM purchase_orders WHERE id = ?`).run(id).changes > 0;
},
getOpenCount() {
const db = getDatabase();
return db.prepare(`SELECT COUNT(*) AS count FROM purchase_orders WHERE status = 'offen'`).get().count;
},
};
module.exports = PurchaseOrder;

142
backend/src/models/Risk.js Normal file
View File

@@ -0,0 +1,142 @@
const { getDatabase } = require('../config/database');
class Risk {
static migrate() {
const db = getDatabase();
db.exec(`
CREATE TABLE IF NOT EXISTS risks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT NOT NULL DEFAULT 'manual'
CHECK(source IN ('manual','auto')),
source_type TEXT NOT NULL DEFAULT 'manual',
external_id TEXT,
title TEXT NOT NULL,
description TEXT,
category TEXT NOT NULL DEFAULT 'Sonstiges',
probability INTEGER NOT NULL DEFAULT 3 CHECK(probability BETWEEN 1 AND 5),
impact INTEGER NOT NULL DEFAULT 3 CHECK(impact BETWEEN 1 AND 5),
status TEXT NOT NULL DEFAULT 'offen'
CHECK(status IN ('offen','in_bearbeitung','akzeptiert','behoben')),
responsible TEXT,
mitigation TEXT,
due_date TEXT,
metadata TEXT,
last_synced TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE UNIQUE INDEX IF NOT EXISTS risks_external_id ON risks(source_type, external_id)
WHERE external_id IS NOT NULL;
`);
// Migration: add metadata column to existing tables
try { db.exec(`ALTER TABLE risks ADD COLUMN metadata TEXT`); } catch {}
}
static _parse(row) {
if (!row) return null;
if (row.metadata) {
try { row.metadata = JSON.parse(row.metadata); } catch { row.metadata = null; }
}
return row;
}
static getAll({ category, status, source } = {}) {
const db = getDatabase();
const where = [];
const vals = [];
if (category) { where.push('category = ?'); vals.push(category); }
if (status) { where.push('status = ?'); vals.push(status); }
if (source) { where.push('source = ?'); vals.push(source); }
const rows = db.prepare(
`SELECT *, (probability * impact) as score FROM risks
${where.length ? 'WHERE ' + where.join(' AND ') : ''}
ORDER BY (probability * impact) DESC, created_at DESC`
).all(...vals);
return rows.map(r => this._parse(r));
}
static getById(id) {
return this._parse(getDatabase().prepare(
'SELECT *, (probability * impact) as score FROM risks WHERE id = ?'
).get(id));
}
static create(fields) {
const db = getDatabase();
const r = db.prepare(`
INSERT INTO risks (source, source_type, external_id, title, description, category, probability, impact, status, responsible, mitigation, due_date, metadata, last_synced)
VALUES (@source, @source_type, @external_id, @title, @description, @category, @probability, @impact, @status, @responsible, @mitigation, @due_date, @metadata, @last_synced)
`).run({
source: fields.source ?? 'manual',
source_type: fields.source_type ?? 'manual',
external_id: fields.external_id ?? null,
title: fields.title,
description: fields.description ?? null,
category: fields.category ?? 'Sonstiges',
probability: fields.probability ?? 3,
impact: fields.impact ?? 3,
status: fields.status ?? 'offen',
responsible: fields.responsible ?? null,
mitigation: fields.mitigation ?? null,
due_date: fields.due_date ?? null,
metadata: fields.metadata ? JSON.stringify(fields.metadata) : null,
last_synced: fields.last_synced ?? null,
});
return this.getById(r.lastInsertRowid);
}
static upsertAuto({ source_type, external_id, title, description, category, probability, impact, metadata }) {
const db = getDatabase();
const existing = db.prepare(
'SELECT id FROM risks WHERE source_type = ? AND external_id = ?'
).get(source_type, external_id);
const now = new Date().toISOString();
const metaStr = metadata ? JSON.stringify(metadata) : null;
if (existing) {
db.prepare(`
UPDATE risks SET title=?, description=?, probability=?, impact=?, metadata=?, last_synced=?, updated_at=datetime('now')
WHERE id=?
`).run(title, description, probability, impact, metaStr, now, existing.id);
return this.getById(existing.id);
}
return this.create({ source: 'auto', source_type, external_id, title, description, category, probability, impact, metadata, last_synced: now });
}
static update(id, fields) {
const db = getDatabase();
const allowed = ['title','description','category','probability','impact','status','responsible','mitigation','due_date'];
const setClauses = [];
const vals = [];
for (const key of allowed) {
if (fields[key] !== undefined) {
setClauses.push(`${key} = ?`);
vals.push(fields[key]);
}
}
if (!setClauses.length) return this.getById(id);
setClauses.push("updated_at = datetime('now')");
vals.push(id);
db.prepare(`UPDATE risks SET ${setClauses.join(', ')} WHERE id = ?`).run(...vals);
return this.getById(id);
}
static delete(id) {
return getDatabase().prepare('DELETE FROM risks WHERE id = ?').run(id).changes > 0;
}
static getStats() {
const db = getDatabase();
const all = db.prepare('SELECT probability * impact as score, status, source FROM risks').all();
return {
total: all.length,
kritisch: all.filter(r => r.score >= 20).length,
hoch: all.filter(r => r.score >= 12 && r.score < 20).length,
mittel: all.filter(r => r.score >= 6 && r.score < 12).length,
niedrig: all.filter(r => r.score < 6).length,
behoben: all.filter(r => r.status === 'behoben').length,
auto: all.filter(r => r.source === 'auto').length,
};
}
}
module.exports = Risk;

View File

@@ -0,0 +1,32 @@
const { getDatabase } = require('../config/database');
class Role {
/**
* Get all roles
*/
static getAll() {
const db = getDatabase();
const stmt = db.prepare('SELECT * FROM roles ORDER BY id');
return stmt.all();
}
/**
* Get role by ID
*/
static getById(id) {
const db = getDatabase();
const stmt = db.prepare('SELECT * FROM roles WHERE id = ?');
return stmt.get(id);
}
/**
* Get role by name
*/
static getByName(name) {
const db = getDatabase();
const stmt = db.prepare('SELECT * FROM roles WHERE name = ?');
return stmt.get(name);
}
}
module.exports = Role;

View File

@@ -0,0 +1,54 @@
const { getDatabase } = require('../config/database');
const StockThreshold = {
getAll() {
const db = getDatabase();
return db.prepare(`SELECT * FROM stock_thresholds ORDER BY category`).all();
},
getByCategory(category) {
const db = getDatabase();
return db.prepare(`SELECT * FROM stock_thresholds WHERE category = ?`).get(category);
},
upsert(data) {
const db = getDatabase();
db.prepare(`
INSERT INTO stock_thresholds (category, min_stock, notify_email, updated_at)
VALUES (@category, @min_stock, @notify_email, CURRENT_TIMESTAMP)
ON CONFLICT(category) DO UPDATE SET
min_stock = excluded.min_stock,
notify_email = excluded.notify_email,
updated_at = CURRENT_TIMESTAMP
`).run({
category: data.category,
min_stock: data.min_stock || 1,
notify_email: data.notify_email || null,
});
return this.getByCategory(data.category);
},
delete(category) {
const db = getDatabase();
return db.prepare(`DELETE FROM stock_thresholds WHERE category = ?`).run(category).changes > 0;
},
// Returns categories where current count <= min_stock
getViolations() {
const db = getDatabase();
return db.prepare(`
SELECT
t.category,
t.min_stock,
t.notify_email,
COUNT(a.id) AS current_stock
FROM stock_thresholds t
LEFT JOIN assets a ON a.type = t.category AND a.status = 'verfuegbar'
GROUP BY t.category
HAVING current_stock <= t.min_stock
ORDER BY current_stock ASC
`).all();
},
};
module.exports = StockThreshold;

View File

@@ -0,0 +1,384 @@
const { getDatabase } = require('../config/database');
class Ticket {
/**
* Get all tickets with filters and related data
*/
static getAll(filters = {}) {
const db = getDatabase();
let where = [];
let params = [];
if (filters.status) {
where.push('t.status = ?');
params.push(filters.status);
}
if (filters.priority) {
where.push('t.priority = ?');
params.push(filters.priority);
}
if (filters.category) {
where.push('t.category = ?');
params.push(filters.category);
}
if (filters.assigned_to) {
where.push('t.assigned_to_user_id = ?');
params.push(filters.assigned_to);
}
if (filters.created_by_user_id) {
where.push('t.created_by_user_id = ?');
params.push(filters.created_by_user_id);
}
// User scope: tickets created by user OR where user is the requester (by email)
if (filters.user_scope) {
where.push('(t.created_by_user_id = ? OR LOWER(t.requester_email) = LOWER(?))');
params.push(filters.user_scope.userId, filters.user_scope.email);
}
if (filters.search) {
where.push('(t.title LIKE ? OR t.description LIKE ? OR t.ticket_number LIKE ? OR t.requester_name LIKE ?)');
const s = `%${filters.search}%`;
params.push(s, s, s, s);
}
const whereClause = where.length > 0 ? 'WHERE ' + where.join(' AND ') : '';
const stmt = db.prepare(`
SELECT
t.*,
au.username as assigned_to_username,
au.first_name as assigned_to_first_name,
au.last_name as assigned_to_last_name,
cu.username as created_by_username,
a.name as asset_name,
a.serial_number as asset_serial,
a.teamviewer_id as asset_teamviewer_id
FROM tickets t
LEFT JOIN users au ON t.assigned_to_user_id = au.id
LEFT JOIN users cu ON t.created_by_user_id = cu.id
LEFT JOIN assets a ON t.asset_id = a.id
${whereClause}
ORDER BY
CASE t.priority
WHEN 'kritisch' THEN 1
WHEN 'hoch' THEN 2
WHEN 'mittel' THEN 3
WHEN 'niedrig' THEN 4
END,
t.created_at DESC
`);
return stmt.all(...params);
}
/**
* Get a single ticket by ID with all related data
*/
static getById(id) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
t.*,
au.username as assigned_to_username,
au.first_name as assigned_to_first_name,
au.last_name as assigned_to_last_name,
au.email as assigned_to_email,
cu.username as created_by_username,
cu.first_name as created_by_first_name,
cu.last_name as created_by_last_name,
a.name as asset_name,
a.serial_number as asset_serial,
a.type as asset_type,
a.teamviewer_id as asset_teamviewer_id
FROM tickets t
LEFT JOIN users au ON t.assigned_to_user_id = au.id
LEFT JOIN users cu ON t.created_by_user_id = cu.id
LEFT JOIN assets a ON t.asset_id = a.id
WHERE t.id = ?
`);
const ticket = stmt.get(id);
if (ticket) ticket.assignees = Ticket.getAssignees(id);
return ticket;
}
/**
* Get ticket by email Message-ID (for deduplication)
*/
static getByEmailMessageId(messageId) {
const db = getDatabase();
return db.prepare('SELECT id FROM tickets WHERE email_message_id = ?').get(messageId);
}
static getByTicketNumber(ticketNumber) {
const db = getDatabase();
return db.prepare('SELECT id, status FROM tickets WHERE ticket_number = ?').get(ticketNumber);
}
static getAssignees(ticketId) {
const db = getDatabase();
return db.prepare(`
SELECT u.id, u.username, u.first_name, u.last_name, u.email, ta.assigned_at
FROM ticket_assignees ta
JOIN users u ON ta.user_id = u.id
WHERE ta.ticket_id = ?
ORDER BY ta.assigned_at ASC
`).all(ticketId);
}
static addAssignee(ticketId, userId, assignedByUserId) {
const db = getDatabase();
try {
db.prepare(`INSERT OR IGNORE INTO ticket_assignees (ticket_id, user_id, assigned_by_user_id) VALUES (?, ?, ?)`)
.run(ticketId, userId, assignedByUserId || null);
return true;
} catch { return false; }
}
static removeAssignee(ticketId, userId) {
const db = getDatabase();
return db.prepare('DELETE FROM ticket_assignees WHERE ticket_id = ? AND user_id = ?').run(ticketId, userId);
}
/**
* Generate next ticket number: TK-YYYY-NNNN
*/
static _generateTicketNumber(db) {
const year = new Date().getFullYear();
const prefix = `TK-${year}-`;
const last = db.prepare(`
SELECT ticket_number FROM tickets
WHERE ticket_number LIKE ?
ORDER BY id DESC LIMIT 1
`).get(`${prefix}%`);
let seq = 1;
if (last) {
const parts = last.ticket_number.split('-');
seq = parseInt(parts[parts.length - 1]) + 1;
}
return `${prefix}${String(seq).padStart(4, '0')}`;
}
/**
* Create a new ticket
*/
static create(data) {
const db = getDatabase();
const ticketNumber = this._generateTicketNumber(db);
const stmt = db.prepare(`
INSERT INTO tickets (
ticket_number, title, description, status, priority, category,
source, requester_name, requester_email,
assigned_to_user_id, created_by_user_id, asset_id, email_message_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(
ticketNumber,
data.title,
data.description || null,
data.status || 'offen',
data.priority || 'mittel',
data.category || 'Allgemein',
data.source || 'web',
data.requester_name || null,
data.requester_email || null,
data.assigned_to_user_id || null,
data.created_by_user_id || null,
data.asset_id || null,
data.email_message_id || null
);
return this.getById(result.lastInsertRowid);
}
/**
* Update a ticket
*/
static update(id, data) {
const db = getDatabase();
const fields = [];
const values = [];
const allowed = [
'title', 'description', 'status', 'priority', 'category',
'assigned_to_user_id', 'asset_id', 'requester_name', 'requester_email',
'snoozed_until', 'ai_suggestion', 'ai_active',
'satisfaction_rating', 'satisfaction_comment'
];
for (const key of allowed) {
if (data[key] !== undefined) {
fields.push(`${key} = ?`);
values.push(data[key] === '' ? null : data[key]);
}
}
if (fields.length === 0) return this.getById(id);
// Set resolved_at / closed_at timestamps
if (data.status === 'geschlossen' && !fields.includes('resolved_at')) {
fields.push('resolved_at = CURRENT_TIMESTAMP');
}
if (data.status === 'geschlossen') {
fields.push('closed_at = CURRENT_TIMESTAMP');
}
fields.push('updated_at = CURRENT_TIMESTAMP');
values.push(id);
db.prepare(`UPDATE tickets SET ${fields.join(', ')} WHERE id = ?`).run(...values);
return this.getById(id);
}
/**
* Delete a ticket
*/
static delete(id) {
const db = getDatabase();
return db.prepare('DELETE FROM tickets WHERE id = ?').run(id).changes > 0;
}
/**
* Get ticket links
*/
static getLinks(ticketId) {
const db = getDatabase();
return db.prepare(`
SELECT tl.*, t.ticket_number, t.title, t.status, t.priority
FROM ticket_links tl
JOIN tickets t ON tl.linked_ticket_id = t.id
WHERE tl.ticket_id = ?
ORDER BY tl.created_at DESC
`).all(ticketId);
}
static addLink(ticketId, linkedTicketId, linkType, userId) {
const db = getDatabase();
const result = db.prepare(`
INSERT OR IGNORE INTO ticket_links (ticket_id, linked_ticket_id, link_type, created_by_user_id)
VALUES (?, ?, ?, ?)
`).run(ticketId, linkedTicketId, linkType || 'related', userId || null);
return result.lastInsertRowid;
}
static removeLink(linkId) {
const db = getDatabase();
return db.prepare('DELETE FROM ticket_links WHERE id = ?').run(linkId).changes > 0;
}
/**
* Get detailed metrics for the metrics page
*/
static getMetrics(days = 30) {
const db = getDatabase();
// Tickets pro Tag
const perDay = db.prepare(`
SELECT DATE(created_at) as date, COUNT(*) as created,
COUNT(CASE WHEN status IN ('geschlossen') THEN 1 END) as resolved
FROM tickets
WHERE created_at >= datetime('now', '-${parseInt(days)} days')
GROUP BY DATE(created_at)
ORDER BY date
`).all();
// Avg Lösungszeit in Stunden (nur gelöste/geschlossene)
const avgResolution = db.prepare(`
SELECT
priority,
ROUND(AVG(
(JULIANDAY(COALESCE(resolved_at, closed_at)) - JULIANDAY(created_at)) * 24
), 1) as avg_hours,
COUNT(*) as count
FROM tickets
WHERE status IN ('geschlossen')
AND COALESCE(resolved_at, closed_at) IS NOT NULL
GROUP BY priority
`).all();
// Top-Bearbeiter
const topAssignees = db.prepare(`
SELECT
u.username,
u.first_name,
u.last_name,
COUNT(*) as total,
COUNT(CASE WHEN t.status IN ('geschlossen') THEN 1 END) as resolved
FROM tickets t
JOIN users u ON t.assigned_to_user_id = u.id
GROUP BY t.assigned_to_user_id
ORDER BY resolved DESC
LIMIT 5
`).all();
// Nach Kategorie
const byCategory = db.prepare(`
SELECT category, COUNT(*) as count,
COUNT(CASE WHEN status NOT IN ('geschlossen') THEN 1 END) as open
FROM tickets
GROUP BY category
ORDER BY count DESC
`).all();
// SLA-Verletzungen (offen und über SLA-Grenze)
const slaBreaches = db.prepare(`
SELECT priority, COUNT(*) as count
FROM tickets
WHERE status NOT IN ('geschlossen')
AND (
(priority = 'kritisch' AND created_at <= datetime('now', '-24 hours')) OR
(priority = 'hoch' AND created_at <= datetime('now', '-48 hours')) OR
(priority = 'mittel' AND created_at <= datetime('now', '-168 hours')) OR
(priority = 'niedrig' AND created_at <= datetime('now', '-336 hours'))
)
GROUP BY priority
`).all();
// Kundenfeedback
const satisfactionTotal = db.prepare(`
SELECT
COUNT(CASE WHEN satisfaction_rating = 'gut' THEN 1 END) as gut,
COUNT(CASE WHEN satisfaction_rating = 'schlecht' THEN 1 END) as schlecht,
COUNT(satisfaction_rating) as total
FROM tickets
`).get();
const recentFeedback = db.prepare(`
SELECT ticket_number, title, satisfaction_rating, satisfaction_comment,
requester_name, requester_email, updated_at
FROM tickets
WHERE satisfaction_rating IS NOT NULL
ORDER BY updated_at DESC
LIMIT 10
`).all();
return { perDay, avgResolution, topAssignees, byCategory, slaBreaches, satisfactionTotal, recentFeedback };
}
/**
* Get ticket statistics for dashboard
*/
static getStats() {
const db = getDatabase();
const byStatus = db.prepare(`
SELECT status, COUNT(*) as count FROM tickets GROUP BY status
`).all();
const byPriority = db.prepare(`
SELECT priority, COUNT(*) as count FROM tickets
WHERE status NOT IN ('geschlossen')
GROUP BY priority
`).all();
const total = db.prepare('SELECT COUNT(*) as count FROM tickets').get().count;
const open = db.prepare("SELECT COUNT(*) as count FROM tickets WHERE status = 'offen'").get().count;
const inProgress = db.prepare("SELECT COUNT(*) as count FROM tickets WHERE status = 'in_bearbeitung'").get().count;
const critical = db.prepare("SELECT COUNT(*) as count FROM tickets WHERE priority = 'kritisch' AND status NOT IN ('geschlossen')").get().count;
return { total, open, inProgress, critical, byStatus, byPriority };
}
}
module.exports = Ticket;

View File

@@ -0,0 +1,50 @@
const { getDatabase } = require('../config/database');
class TicketComment {
/**
* Get all comments for a ticket
*/
static getByTicketId(ticketId) {
const db = getDatabase();
return db.prepare(`
SELECT
tc.*,
u.username,
u.first_name,
u.last_name,
u.role_id
FROM ticket_comments tc
LEFT JOIN users u ON tc.user_id = u.id
WHERE tc.ticket_id = ?
ORDER BY tc.created_at ASC
`).all(ticketId);
}
/**
* Create a new comment
*/
static create(ticketId, userId, comment, isInternal = false, isAiComment = false) {
const db = getDatabase();
const result = db.prepare(`
INSERT INTO ticket_comments (ticket_id, user_id, comment, is_internal, is_ai_comment)
VALUES (?, ?, ?, ?, ?)
`).run(ticketId, userId || null, comment, isInternal ? 1 : 0, isAiComment ? 1 : 0);
return db.prepare(`
SELECT tc.*, u.username, u.first_name, u.last_name, u.role_id
FROM ticket_comments tc
LEFT JOIN users u ON tc.user_id = u.id
WHERE tc.id = ?
`).get(result.lastInsertRowid);
}
/**
* Delete a comment
*/
static delete(id) {
const db = getDatabase();
return db.prepare('DELETE FROM ticket_comments WHERE id = ?').run(id).changes > 0;
}
}
module.exports = TicketComment;

View File

@@ -0,0 +1,137 @@
const { getDatabase } = require('../config/database');
class TicketSettings {
static migrate() {
const db = getDatabase();
db.exec(`
CREATE TABLE IF NOT EXISTS ticket_categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
icon TEXT NOT NULL DEFAULT '📁',
sort_order INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS ticket_templates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
label TEXT NOT NULL,
title TEXT NOT NULL DEFAULT '',
description TEXT NOT NULL DEFAULT '',
category TEXT NOT NULL DEFAULT 'Allgemein',
priority TEXT NOT NULL DEFAULT 'mittel',
sort_order INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
// Seed categories if empty
const catCount = db.prepare('SELECT COUNT(*) as c FROM ticket_categories').get();
if (catCount.c === 0) {
const insertCat = db.prepare('INSERT INTO ticket_categories (name, icon, sort_order) VALUES (?, ?, ?)');
[
['Allgemein', '📋', 0],
['Software', '💻', 1],
['Hardware', '🖥️', 2],
['Netzwerk', '🌐', 3],
['SelectLine', '📊', 4],
].forEach(([name, icon, order]) => insertCat.run(name, icon, order));
}
// Seed templates if empty
const tplCount = db.prepare('SELECT COUNT(*) as c FROM ticket_templates').get();
if (tplCount.c === 0) {
const insertTpl = db.prepare(
'INSERT INTO ticket_templates (label, title, description, category, priority, sort_order) VALUES (?, ?, ?, ?, ?, ?)'
);
[
['Passwort-Reset', 'Passwort zurücksetzen', 'Bitte setzen Sie mein Passwort zurück.\n\nBetroffenes Konto: ', 'Software', 'mittel', 0],
['VPN-Probleme', 'VPN verbindet nicht', 'VPN lässt sich nicht verbinden oder trennt sich ständig.\n\nBetriebssystem: \nFehlermeldung: ', 'Software', 'mittel', 1],
['Drucker defekt', 'Drucker funktioniert nicht', 'Der Drucker reagiert nicht / druckt nicht korrekt.\n\nDrucker-Modell: \nStandort: \nFehlermeldung: ', 'Hardware', 'mittel', 2],
['Neues Gerät einrichten', 'Einrichtung neues Endgerät', 'Neues Gerät muss eingerichtet und konfiguriert werden.\n\nGerätetyp: \nFür Mitarbeiter: \nBenötigte Software: ', 'Hardware', 'mittel', 3],
['Software-Installation', 'Software installieren', 'Bitte folgende Software installieren:\n\nSoftware: \nVersion: \nLizenz vorhanden: ', 'Software', 'niedrig', 4],
['SelectLine Fehler', 'SelectLine Fehler/Problem', 'Folgender Fehler tritt in SelectLine auf:\n\nModul: \nFehlermeldung: \nWiederholbar: ', 'SelectLine', 'hoch', 5],
['E-Mail Problem', 'E-Mail funktioniert nicht', 'Problem mit E-Mail-Postfach oder Outlook.\n\nProblem: \nFehlermeldung: ', 'Software', 'mittel', 6],
['Netzwerk-Ausfall', 'Kein Internetzugang', 'Kein Internetzugang oder Netzwerkverbindung unterbrochen.\n\nStandort/Raum: \nBetroffene Geräte: \nSeit wann: ', 'Netzwerk', 'hoch', 7],
].forEach(args => insertTpl.run(...args));
}
}
// ── Categories ─────────────────────────────────────────────────────────────
static getAllCategories() {
return getDatabase()
.prepare('SELECT * FROM ticket_categories ORDER BY sort_order, name')
.all();
}
static createCategory({ name, icon = '📁' }) {
const db = getDatabase();
const maxOrder = db.prepare('SELECT MAX(sort_order) as m FROM ticket_categories').get().m ?? -1;
const stmt = db.prepare('INSERT INTO ticket_categories (name, icon, sort_order) VALUES (?, ?, ?)');
const result = stmt.run(name.trim(), icon, maxOrder + 1);
return db.prepare('SELECT * FROM ticket_categories WHERE id = ?').get(result.lastInsertRowid);
}
static updateCategory(id, { name, icon }) {
const db = getDatabase();
const fields = [];
const params = [];
if (name !== undefined) { fields.push('name = ?'); params.push(name.trim()); }
if (icon !== undefined) { fields.push('icon = ?'); params.push(icon); }
if (!fields.length) return db.prepare('SELECT * FROM ticket_categories WHERE id = ?').get(id);
params.push(id);
db.prepare(`UPDATE ticket_categories SET ${fields.join(', ')} WHERE id = ?`).run(...params);
return db.prepare('SELECT * FROM ticket_categories WHERE id = ?').get(id);
}
static deleteCategory(id) {
return getDatabase().prepare('DELETE FROM ticket_categories WHERE id = ?').run(id);
}
static reorderCategories(orderedIds) {
const db = getDatabase();
const stmt = db.prepare('UPDATE ticket_categories SET sort_order = ? WHERE id = ?');
orderedIds.forEach((id, idx) => stmt.run(idx, id));
}
// ── Templates ──────────────────────────────────────────────────────────────
static getAllTemplates() {
return getDatabase()
.prepare('SELECT * FROM ticket_templates ORDER BY sort_order, label')
.all();
}
static createTemplate({ label, title = '', description = '', category = 'Allgemein', priority = 'mittel' }) {
const db = getDatabase();
const maxOrder = db.prepare('SELECT MAX(sort_order) as m FROM ticket_templates').get().m ?? -1;
const stmt = db.prepare(
'INSERT INTO ticket_templates (label, title, description, category, priority, sort_order) VALUES (?, ?, ?, ?, ?, ?)'
);
const result = stmt.run(label.trim(), title, description, category, priority, maxOrder + 1);
return db.prepare('SELECT * FROM ticket_templates WHERE id = ?').get(result.lastInsertRowid);
}
static updateTemplate(id, { label, title, description, category, priority }) {
const db = getDatabase();
const fields = [];
const params = [];
if (label !== undefined) { fields.push('label = ?'); params.push(label.trim()); }
if (title !== undefined) { fields.push('title = ?'); params.push(title); }
if (description !== undefined) { fields.push('description = ?'); params.push(description); }
if (category !== undefined) { fields.push('category = ?'); params.push(category); }
if (priority !== undefined) { fields.push('priority = ?'); params.push(priority); }
if (!fields.length) return db.prepare('SELECT * FROM ticket_templates WHERE id = ?').get(id);
params.push(id);
db.prepare(`UPDATE ticket_templates SET ${fields.join(', ')} WHERE id = ?`).run(...params);
return db.prepare('SELECT * FROM ticket_templates WHERE id = ?').get(id);
}
static deleteTemplate(id) {
return getDatabase().prepare('DELETE FROM ticket_templates WHERE id = ?').run(id);
}
}
module.exports = TicketSettings;

179
backend/src/models/User.js Normal file
View File

@@ -0,0 +1,179 @@
const { getDatabase } = require('../config/database');
class User {
static getAll() {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
u.id, u.username, u.email, u.first_name, u.last_name,
u.is_active, u.must_change_password, u.email_notifications, u.last_login,
u.created_at, u.updated_at, u.role_id, u.azure_id,
r.name as role_name, r.description as role_description
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
ORDER BY u.created_at DESC
`);
return stmt.all();
}
static getById(id) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT
u.id, u.username, u.email, u.first_name, u.last_name,
u.is_active, u.must_change_password, u.email_notifications, u.last_login,
u.created_at, u.updated_at, u.role_id, u.azure_id,
r.name as role_name, r.description as role_description
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
WHERE u.id = ?
`);
return stmt.get(id);
}
static getByIdWithPassword(id) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT u.*, r.name as role_name, r.description as role_description
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
WHERE u.id = ?
`);
return stmt.get(id);
}
static getByUsername(username) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT u.*, r.name as role_name
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
WHERE u.username = ?
`);
return stmt.get(username);
}
static getByEmail(email) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT u.*, r.name as role_name
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
WHERE u.email = ?
`);
return stmt.get(email);
}
static getByAzureId(azureId) {
const db = getDatabase();
const stmt = db.prepare(`
SELECT u.*, r.name as role_name
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
WHERE u.azure_id = ?
`);
return stmt.get(azureId);
}
static createFromAzure({ azure_id, email, first_name, last_name, username }) {
const db = getDatabase();
const role = db.prepare("SELECT id FROM roles WHERE name = 'benutzer'").get();
const stmt = db.prepare(`
INSERT INTO users (username, email, password_hash, role_id, first_name, last_name, is_active, must_change_password, azure_id)
VALUES (?, ?, 'AZURE_SSO_NO_PASSWORD', ?, ?, ?, 1, 0, ?)
`);
const result = stmt.run(username, email, role.id, first_name || null, last_name || null, azure_id);
return this.getById(result.lastInsertRowid);
}
static create(userData) {
const db = getDatabase();
const stmt = db.prepare(`
INSERT INTO users (
username, email, password_hash, role_id,
first_name, last_name, is_active, must_change_password
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(
userData.username,
userData.email,
userData.password_hash,
userData.role_id,
userData.first_name || null,
userData.last_name || null,
userData.is_active !== undefined ? (userData.is_active ? 1 : 0) : 1,
userData.must_change_password !== undefined ? (userData.must_change_password ? 1 : 0) : 1
);
return this.getById(result.lastInsertRowid);
}
static update(id, userData) {
const db = getDatabase();
const fields = [];
const values = [];
if (userData.username !== undefined) { fields.push('username = ?'); values.push(userData.username); }
if (userData.email !== undefined) { fields.push('email = ?'); values.push(userData.email); }
if (userData.password_hash !== undefined) { fields.push('password_hash = ?'); values.push(userData.password_hash); }
if (userData.role_id !== undefined) { fields.push('role_id = ?'); values.push(userData.role_id); }
if (userData.first_name !== undefined) { fields.push('first_name = ?'); values.push(userData.first_name); }
if (userData.last_name !== undefined) { fields.push('last_name = ?'); values.push(userData.last_name); }
if (userData.is_active !== undefined) { fields.push('is_active = ?'); values.push(userData.is_active ? 1 : 0); }
if (userData.must_change_password !== undefined) { fields.push('must_change_password = ?'); values.push(userData.must_change_password ? 1 : 0); }
if (userData.azure_id !== undefined) { fields.push('azure_id = ?'); values.push(userData.azure_id); }
fields.push('updated_at = CURRENT_TIMESTAMP');
values.push(id);
const stmt = db.prepare(`UPDATE users SET ${fields.join(', ')} WHERE id = ?`);
stmt.run(...values);
return this.getById(id);
}
static delete(id) {
const db = getDatabase();
const result = db.prepare('DELETE FROM users WHERE id = ?').run(id);
return result.changes > 0;
}
static updateLastLogin(id) {
const db = getDatabase();
db.prepare('UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?').run(id);
}
static changePassword(id, passwordHash, mustChange = false) {
const db = getDatabase();
db.prepare(`
UPDATE users SET password_hash = ?, must_change_password = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`).run(passwordHash, mustChange ? 1 : 0, id);
}
static updateEmailNotifications(id, enabled) {
const db = getDatabase();
db.prepare('UPDATE users SET email_notifications = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
.run(enabled ? 1 : 0, id);
return this.getById(id);
}
static updateStaffNotifications(id, prefs) {
const db = getDatabase();
db.prepare(`UPDATE users SET
notif_ticket_created = ?,
notif_ticket_assigned = ?,
notif_new_comment = ?,
notif_weekly_report = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?`
).run(
prefs.notif_ticket_created ? 1 : 0,
prefs.notif_ticket_assigned ? 1 : 0,
prefs.notif_new_comment ? 1 : 0,
prefs.notif_weekly_report ? 1 : 0,
id
);
return this.getById(id);
}
}
module.exports = User;

View File

@@ -0,0 +1,37 @@
const { getDatabase } = require('../config/database');
const WarehouseLocation = {
getAll() {
const db = getDatabase();
return db.prepare(`SELECT * FROM warehouse_locations ORDER BY name`).all();
},
getById(id) {
const db = getDatabase();
return db.prepare(`SELECT * FROM warehouse_locations WHERE id = ?`).get(id);
},
create(data) {
const db = getDatabase();
const result = db.prepare(
`INSERT INTO warehouse_locations (name, description) VALUES (@name, @description)`
).run({ name: data.name, description: data.description || '' });
return this.getById(result.lastInsertRowid);
},
update(id, data) {
const db = getDatabase();
db.prepare(
`UPDATE warehouse_locations SET name = @name, description = @description WHERE id = @id`
).run({ id, name: data.name, description: data.description || '' });
return this.getById(id);
},
delete(id) {
const db = getDatabase();
const result = db.prepare(`DELETE FROM warehouse_locations WHERE id = ?`).run(id);
return result.changes > 0;
},
};
module.exports = WarehouseLocation;