Initial commit: IT Nexus Web-App
This commit is contained in:
356
backend/src/models/Asset.js
Normal file
356
backend/src/models/Asset.js
Normal 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;
|
||||
203
backend/src/models/AssetAssignment.js
Normal file
203
backend/src/models/AssetAssignment.js
Normal 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;
|
||||
73
backend/src/models/AssetInspection.js
Normal file
73
backend/src/models/AssetInspection.js
Normal 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;
|
||||
73
backend/src/models/AssetMovement.js
Normal file
73
backend/src/models/AssetMovement.js
Normal 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;
|
||||
124
backend/src/models/AuditLog.js
Normal file
124
backend/src/models/AuditLog.js
Normal 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;
|
||||
71
backend/src/models/EmailDesign.js
Normal file
71
backend/src/models/EmailDesign.js
Normal 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 };
|
||||
91
backend/src/models/EmailTemplate.js
Normal file
91
backend/src/models/EmailTemplate.js
Normal 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 };
|
||||
82
backend/src/models/ExternalAlert.js
Normal file
82
backend/src/models/ExternalAlert.js
Normal 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;
|
||||
219
backend/src/models/FidoKey.js
Normal file
219
backend/src/models/FidoKey.js
Normal 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;
|
||||
105
backend/src/models/IsoTask.js
Normal file
105
backend/src/models/IsoTask.js
Normal 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;
|
||||
81
backend/src/models/ItTopic.js
Normal file
81
backend/src/models/ItTopic.js
Normal 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;
|
||||
98
backend/src/models/License.js
Normal file
98
backend/src/models/License.js
Normal 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;
|
||||
138
backend/src/models/MonitoringAgent.js
Normal file
138
backend/src/models/MonitoringAgent.js
Normal 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;
|
||||
105
backend/src/models/NetworkDevice.js
Normal file
105
backend/src/models/NetworkDevice.js
Normal 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;
|
||||
233
backend/src/models/OffboardingProtocol.js
Normal file
233
backend/src/models/OffboardingProtocol.js
Normal 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;
|
||||
257
backend/src/models/OnboardingProcess.js
Normal file
257
backend/src/models/OnboardingProcess.js
Normal 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;
|
||||
292
backend/src/models/OnboardingProtocol.js
Normal file
292
backend/src/models/OnboardingProtocol.js
Normal 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;
|
||||
79
backend/src/models/PurchaseOrder.js
Normal file
79
backend/src/models/PurchaseOrder.js
Normal 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
142
backend/src/models/Risk.js
Normal 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;
|
||||
32
backend/src/models/Role.js
Normal file
32
backend/src/models/Role.js
Normal 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;
|
||||
54
backend/src/models/StockThreshold.js
Normal file
54
backend/src/models/StockThreshold.js
Normal 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;
|
||||
384
backend/src/models/Ticket.js
Normal file
384
backend/src/models/Ticket.js
Normal 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;
|
||||
50
backend/src/models/TicketComment.js
Normal file
50
backend/src/models/TicketComment.js
Normal 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;
|
||||
137
backend/src/models/TicketSettings.js
Normal file
137
backend/src/models/TicketSettings.js
Normal 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
179
backend/src/models/User.js
Normal 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;
|
||||
37
backend/src/models/WarehouseLocation.js
Normal file
37
backend/src/models/WarehouseLocation.js
Normal 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;
|
||||
Reference in New Issue
Block a user