Files
SkillMate/backend/src/config/database.ts
2025-09-24 00:28:00 +02:00

571 Zeilen
18 KiB
TypeScript

import Database from 'better-sqlite3'
import path from 'path'
import { Employee, User, SkillDefinition } from '@skillmate/shared'
import bcrypt from 'bcryptjs'
import { v4 as uuidv4 } from 'uuid'
import { db as secureDb, encryptedDb, initializeSecureDatabase } from './secureDatabase'
export { initializeSecureDatabase } from './secureDatabase'
// Export the secure database instance
export const db = secureDb
export function initializeDatabase() {
// Users table
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('admin', 'superuser', 'user')),
employee_id TEXT,
last_login TEXT,
is_active INTEGER DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
`)
// Profiles table (erweitert für Yellow Pages)
db.exec(`
CREATE TABLE IF NOT EXISTS profiles (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
department TEXT,
location TEXT,
role TEXT,
email TEXT,
phone TEXT,
teams_link TEXT,
job_category TEXT CHECK(job_category IN ('Technik', 'IT & Digitalisierung', 'Verwaltung', 'F&E', 'Kommunikation & HR', 'Produktion', 'Sonstiges')),
job_title TEXT,
job_desc TEXT,
consent_public_profile INTEGER DEFAULT 0,
consent_searchable INTEGER DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
updated_by TEXT NOT NULL,
review_due_at TEXT,
search_vector TEXT
)
`)
// Volltext-Index für Suche
db.exec(`
CREATE INDEX IF NOT EXISTS idx_profiles_search
ON profiles(search_vector);
CREATE INDEX IF NOT EXISTS idx_profiles_department
ON profiles(department);
CREATE INDEX IF NOT EXISTS idx_profiles_location
ON profiles(location);
CREATE INDEX IF NOT EXISTS idx_profiles_job_category
ON profiles(job_category);
CREATE INDEX IF NOT EXISTS idx_profiles_review_due
ON profiles(review_due_at);
`)
// Employees table (für Kompatibilität beibehalten)
db.exec(`
CREATE TABLE IF NOT EXISTS employees (
id TEXT PRIMARY KEY,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
employee_number TEXT UNIQUE NOT NULL,
photo TEXT,
position TEXT NOT NULL,
department TEXT NOT NULL,
email TEXT NOT NULL,
phone TEXT NOT NULL,
mobile TEXT,
office TEXT,
availability TEXT NOT NULL,
clearance_level TEXT,
clearance_valid_until TEXT,
clearance_issued_date TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
created_by TEXT NOT NULL,
updated_by TEXT
)
`)
// Skills table
db.exec(`
CREATE TABLE IF NOT EXISTS skills (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
category TEXT NOT NULL,
description TEXT,
requires_certification INTEGER DEFAULT 0,
expires_after INTEGER
)
`)
// Employee skills junction table
db.exec(`
CREATE TABLE IF NOT EXISTS employee_skills (
employee_id TEXT NOT NULL,
skill_id TEXT NOT NULL,
level TEXT,
verified INTEGER DEFAULT 0,
verified_by TEXT,
verified_date TEXT,
PRIMARY KEY (employee_id, skill_id),
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE,
FOREIGN KEY (skill_id) REFERENCES skills(id) ON DELETE CASCADE
)
`)
// Profile Kompetenzen (Arrays als separate Tabellen)
db.exec(`
CREATE TABLE IF NOT EXISTS profile_domains (
profile_id TEXT NOT NULL,
domain TEXT NOT NULL,
PRIMARY KEY (profile_id, domain),
FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS profile_tools (
profile_id TEXT NOT NULL,
tool TEXT NOT NULL,
PRIMARY KEY (profile_id, tool),
FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS profile_methods (
profile_id TEXT NOT NULL,
method TEXT NOT NULL,
PRIMARY KEY (profile_id, method),
FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS profile_industry_knowledge (
profile_id TEXT NOT NULL,
knowledge TEXT NOT NULL,
PRIMARY KEY (profile_id, knowledge),
FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS profile_regulatory (
profile_id TEXT NOT NULL,
regulation TEXT NOT NULL,
PRIMARY KEY (profile_id, regulation),
FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS profile_networks (
profile_id TEXT NOT NULL,
network TEXT NOT NULL,
PRIMARY KEY (profile_id, network),
FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS profile_digital_skills (
profile_id TEXT NOT NULL,
skill TEXT NOT NULL,
PRIMARY KEY (profile_id, skill),
FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS profile_social_skills (
profile_id TEXT NOT NULL,
skill TEXT NOT NULL,
PRIMARY KEY (profile_id, skill),
FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
);
`)
// Profile Sprachen
db.exec(`
CREATE TABLE IF NOT EXISTS profile_languages (
profile_id TEXT NOT NULL,
code TEXT NOT NULL,
level TEXT NOT NULL CHECK(level IN ('basic', 'fluent', 'native', 'business')),
PRIMARY KEY (profile_id, code),
FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
);
`)
// Profile Projekte
db.exec(`
CREATE TABLE IF NOT EXISTS profile_projects (
id TEXT PRIMARY KEY,
profile_id TEXT NOT NULL,
title TEXT NOT NULL,
role TEXT,
summary TEXT,
created_at TEXT NOT NULL,
FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS project_links (
project_id TEXT NOT NULL,
link TEXT NOT NULL,
PRIMARY KEY (project_id, link),
FOREIGN KEY (project_id) REFERENCES profile_projects(id) ON DELETE CASCADE
);
`)
// Language skills table (für Kompatibilität)
db.exec(`
CREATE TABLE IF NOT EXISTS language_skills (
id TEXT PRIMARY KEY,
employee_id TEXT NOT NULL,
language TEXT NOT NULL,
proficiency TEXT NOT NULL,
certified INTEGER DEFAULT 0,
certificate_type TEXT,
is_native INTEGER DEFAULT 0,
can_interpret INTEGER DEFAULT 0,
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE
)
`)
// Specializations table
db.exec(`
CREATE TABLE IF NOT EXISTS specializations (
id TEXT PRIMARY KEY,
employee_id TEXT NOT NULL,
name TEXT NOT NULL,
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE
)
`)
// Sync log table
db.exec(`
CREATE TABLE IF NOT EXISTS sync_log (
id TEXT PRIMARY KEY,
sync_time TEXT NOT NULL,
success INTEGER NOT NULL,
items_synced INTEGER,
error_message TEXT,
duration INTEGER
)
`)
// Create default admin user if not exists
const adminExists = db.prepare('SELECT id FROM users WHERE username = ?').get('admin')
if (!adminExists) {
const hashedPassword = bcrypt.hashSync('admin123', 10)
const now = new Date().toISOString()
db.prepare(`
INSERT INTO users (id, username, email, password, role, is_active, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).run(
uuidv4(),
'admin',
'admin@skillmate.local',
hashedPassword,
'admin',
1,
now,
now
)
}
// Workspace Management Tables
// Workspaces (Desks, Meeting Rooms, etc.)
db.exec(`
CREATE TABLE IF NOT EXISTS workspaces (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('desk', 'meeting_room', 'phone_booth', 'parking', 'locker')),
floor TEXT NOT NULL,
building TEXT,
capacity INTEGER DEFAULT 1,
equipment TEXT, -- JSON array of equipment
position_x INTEGER, -- For floor plan visualization
position_y INTEGER,
is_active INTEGER DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
`)
// Bookings table
db.exec(`
CREATE TABLE IF NOT EXISTS bookings (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL,
user_id TEXT NOT NULL,
employee_id TEXT NOT NULL,
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
status TEXT NOT NULL CHECK(status IN ('confirmed', 'cancelled', 'completed', 'no_show')),
check_in_time TEXT,
check_out_time TEXT,
notes TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (workspace_id) REFERENCES workspaces(id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (employee_id) REFERENCES employees(id)
)
`)
// Recurring bookings
db.exec(`
CREATE TABLE IF NOT EXISTS recurring_bookings (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL,
user_id TEXT NOT NULL,
employee_id TEXT NOT NULL,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
time_start TEXT NOT NULL,
time_end TEXT NOT NULL,
days_of_week TEXT NOT NULL, -- JSON array of days [1,2,3,4,5] for Mon-Fri
created_at TEXT NOT NULL,
FOREIGN KEY (workspace_id) REFERENCES workspaces(id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (employee_id) REFERENCES employees(id)
)
`)
// Booking rules and restrictions
db.exec(`
CREATE TABLE IF NOT EXISTS booking_rules (
id TEXT PRIMARY KEY,
workspace_type TEXT,
max_duration_hours INTEGER,
max_advance_days INTEGER,
min_advance_hours INTEGER,
max_bookings_per_user_per_day INTEGER,
max_bookings_per_user_per_week INTEGER,
auto_release_minutes INTEGER, -- Auto-release if not checked in
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
`)
// Analytics data
db.exec(`
CREATE TABLE IF NOT EXISTS workspace_analytics (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL,
date TEXT NOT NULL,
total_bookings INTEGER DEFAULT 0,
total_hours_booked REAL DEFAULT 0,
utilization_rate REAL DEFAULT 0,
no_show_count INTEGER DEFAULT 0,
unique_users INTEGER DEFAULT 0,
peak_hour INTEGER,
FOREIGN KEY (workspace_id) REFERENCES workspaces(id),
UNIQUE(workspace_id, date)
)
`)
// Floor plans
db.exec(`
CREATE TABLE IF NOT EXISTS floor_plans (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
floor TEXT NOT NULL,
building TEXT,
image_url TEXT,
svg_data TEXT, -- SVG data for interactive floor plan
width INTEGER,
height INTEGER,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
`)
// Organizational Structure Tables
db.exec(`
CREATE TABLE IF NOT EXISTS organizational_units (
id TEXT PRIMARY KEY,
code TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('direktion', 'abteilung', 'dezernat', 'sachgebiet', 'teildezernat', 'fuehrungsstelle', 'stabsstelle', 'sondereinheit')),
level INTEGER NOT NULL,
parent_id TEXT,
position_x INTEGER,
position_y INTEGER,
color TEXT,
order_index INTEGER DEFAULT 0,
description TEXT,
has_fuehrungsstelle INTEGER DEFAULT 0,
fuehrungsstelle_name TEXT,
is_active INTEGER DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (parent_id) REFERENCES organizational_units(id)
);
CREATE INDEX IF NOT EXISTS idx_org_units_parent ON organizational_units(parent_id);
CREATE INDEX IF NOT EXISTS idx_org_units_type ON organizational_units(type);
CREATE INDEX IF NOT EXISTS idx_org_units_level ON organizational_units(level);
`)
// Import rules for organization parsing (persist admin overrides)
db.exec(`
CREATE TABLE IF NOT EXISTS organization_import_rules (
id TEXT PRIMARY KEY,
target_code TEXT UNIQUE NOT NULL,
parent_code TEXT,
type_override TEXT,
name_override TEXT,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_org_rules_target ON organization_import_rules(target_code);
`)
// Employee Unit Assignments
db.exec(`
CREATE TABLE IF NOT EXISTS employee_unit_assignments (
id TEXT PRIMARY KEY,
employee_id TEXT NOT NULL,
unit_id TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('leiter', 'stellvertreter', 'mitarbeiter', 'beauftragter')),
start_date TEXT NOT NULL,
end_date TEXT,
is_primary INTEGER DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE,
FOREIGN KEY (unit_id) REFERENCES organizational_units(id) ON DELETE CASCADE,
UNIQUE(employee_id, unit_id, role)
);
CREATE INDEX IF NOT EXISTS idx_emp_units_employee ON employee_unit_assignments(employee_id);
CREATE INDEX IF NOT EXISTS idx_emp_units_unit ON employee_unit_assignments(unit_id);
`)
// Special Positions (Personalrat, Beauftragte, etc.)
db.exec(`
CREATE TABLE IF NOT EXISTS special_positions (
id TEXT PRIMARY KEY,
employee_id TEXT NOT NULL,
position_type TEXT NOT NULL CHECK(position_type IN ('personalrat', 'schwerbehindertenvertretung', 'datenschutzbeauftragter', 'gleichstellungsbeauftragter', 'inklusionsbeauftragter', 'informationssicherheitsbeauftragter', 'geheimschutzbeauftragter', 'extremismusbeauftragter')),
unit_id TEXT,
start_date TEXT NOT NULL,
end_date TEXT,
is_active INTEGER DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE,
FOREIGN KEY (unit_id) REFERENCES organizational_units(id)
);
CREATE INDEX IF NOT EXISTS idx_special_pos_employee ON special_positions(employee_id);
CREATE INDEX IF NOT EXISTS idx_special_pos_type ON special_positions(position_type);
`)
// Deputy Assignments (Vertretungen)
db.exec(`
CREATE TABLE IF NOT EXISTS deputy_assignments (
id TEXT PRIMARY KEY,
principal_id TEXT NOT NULL,
deputy_id TEXT NOT NULL,
unit_id TEXT,
valid_from TEXT NOT NULL,
valid_until TEXT NOT NULL,
reason TEXT,
can_delegate INTEGER DEFAULT 1,
created_by TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (principal_id) REFERENCES employees(id),
FOREIGN KEY (deputy_id) REFERENCES employees(id),
FOREIGN KEY (unit_id) REFERENCES organizational_units(id),
FOREIGN KEY (created_by) REFERENCES users(id)
);
CREATE INDEX IF NOT EXISTS idx_deputy_principal ON deputy_assignments(principal_id);
CREATE INDEX IF NOT EXISTS idx_deputy_deputy ON deputy_assignments(deputy_id);
CREATE INDEX IF NOT EXISTS idx_deputy_dates ON deputy_assignments(valid_from, valid_until);
`)
// Deputy Delegations (Vertretungs-Weitergaben)
db.exec(`
CREATE TABLE IF NOT EXISTS deputy_delegations (
id TEXT PRIMARY KEY,
original_assignment_id TEXT NOT NULL,
from_deputy_id TEXT NOT NULL,
to_deputy_id TEXT NOT NULL,
reason TEXT,
delegated_at TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY (original_assignment_id) REFERENCES deputy_assignments(id),
FOREIGN KEY (from_deputy_id) REFERENCES employees(id),
FOREIGN KEY (to_deputy_id) REFERENCES employees(id)
);
CREATE INDEX IF NOT EXISTS idx_delegation_assignment ON deputy_delegations(original_assignment_id);
CREATE INDEX IF NOT EXISTS idx_delegation_from ON deputy_delegations(from_deputy_id);
CREATE INDEX IF NOT EXISTS idx_delegation_to ON deputy_delegations(to_deputy_id);
`)
// Audit Log für Änderungsverfolgung
db.exec(`
CREATE TABLE IF NOT EXISTS audit_log (
id TEXT PRIMARY KEY,
entity_type TEXT NOT NULL,
entity_id TEXT NOT NULL,
action TEXT NOT NULL CHECK(action IN ('create', 'update', 'delete')),
user_id TEXT NOT NULL,
changes TEXT, -- JSON mit Änderungen
timestamp TEXT NOT NULL,
ip_address TEXT,
user_agent TEXT
);
CREATE INDEX IF NOT EXISTS idx_audit_entity ON audit_log(entity_type, entity_id);
CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_log(user_id);
CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp);
`)
// Reminder-System
db.exec(`
CREATE TABLE IF NOT EXISTS reminders (
id TEXT PRIMARY KEY,
profile_id TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('annual_update', 'overdue', 'custom')),
message TEXT,
sent_at TEXT,
acknowledged_at TEXT,
created_at TEXT NOT NULL,
FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_reminders_profile ON reminders(profile_id);
CREATE INDEX IF NOT EXISTS idx_reminders_sent ON reminders(sent_at);
`)
// Kontrollierte Vokabulare/Tags
db.exec(`
CREATE TABLE IF NOT EXISTS controlled_vocabulary (
id TEXT PRIMARY KEY,
category TEXT NOT NULL,
value TEXT NOT NULL,
description TEXT,
is_active INTEGER DEFAULT 1,
created_at TEXT NOT NULL,
UNIQUE(category, value)
);
CREATE INDEX IF NOT EXISTS idx_vocab_category ON controlled_vocabulary(category);
`)
// Create indexes for better performance
db.exec(`
CREATE INDEX IF NOT EXISTS idx_employees_availability ON employees(availability);
CREATE INDEX IF NOT EXISTS idx_employees_department ON employees(department);
CREATE INDEX IF NOT EXISTS idx_employee_skills_employee ON employee_skills(employee_id);
CREATE INDEX IF NOT EXISTS idx_employee_skills_skill ON employee_skills(skill_id);
CREATE INDEX IF NOT EXISTS idx_language_skills_employee ON language_skills(employee_id);
CREATE INDEX IF NOT EXISTS idx_specializations_employee ON specializations(employee_id);
CREATE INDEX IF NOT EXISTS idx_bookings_workspace ON bookings(workspace_id);
CREATE INDEX IF NOT EXISTS idx_bookings_user ON bookings(user_id);
CREATE INDEX IF NOT EXISTS idx_bookings_start_time ON bookings(start_time);
CREATE INDEX IF NOT EXISTS idx_bookings_status ON bookings(status);
CREATE INDEX IF NOT EXISTS idx_workspace_analytics_date ON workspace_analytics(date);
`)
}