added admin setup

This commit is contained in:
2025-10-09 00:09:20 +02:00
parent ceb7058d0b
commit 79b3658c5e
22 changed files with 1168 additions and 3353 deletions

2810
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
{
"name": "schichtplan-backend",
"version": "1.0.0",
"type": "commonjs",
"type": "module",
"scripts": {
"dev": "node -r ts-node/register src/server.ts",
"dev": "node --loader ts-node/esm src/server.ts",
"simple": "node src/server.ts",
"build": "tsc",
"start": "node dist/server.js"

View File

@@ -3,8 +3,8 @@ import { Request, Response } from 'express';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { v4 as uuidv4 } from 'uuid';
import { db } from '../services/databaseService';
import { AuthRequest } from '../middleware/auth';
import { db } from '../services/databaseService.js';
import { AuthRequest } from '../middleware/auth.js';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';

View File

@@ -0,0 +1,87 @@
// backend/src/controllers/setupController.ts
import { Request, Response } from 'express';
import bcrypt from 'bcryptjs';
import { v4 as uuidv4 } from 'uuid';
import { db } from '../services/databaseService.js';
export const checkSetupStatus = async (req: Request, res: Response): Promise<void> => {
try {
const adminExists = await db.get<{ count: number }>(
'SELECT COUNT(*) as count FROM users WHERE role = ?',
['admin']
);
res.json({
needsSetup: !adminExists || adminExists.count === 0
});
} catch (error) {
console.error('Error checking setup status:', error);
res.status(500).json({ error: 'Internal server error' });
}
};
export const setupAdmin = async (req: Request, res: Response): Promise<void> => {
try {
// Check if admin already exists
const adminExists = await db.get<{ count: number }>(
'SELECT COUNT(*) as count FROM users WHERE role = ?',
['admin']
);
if (adminExists && adminExists.count > 0) {
res.status(400).json({ error: 'Admin user already exists' });
return;
}
const { email, password, name, phone, department } = req.body;
// Validation
if (!email || !password || !name) {
res.status(400).json({ error: 'Email, password, and name are required' });
return;
}
// Email format validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
res.status(400).json({ error: 'Invalid email format' });
return;
}
// Password length validation
if (password.length < 6) {
res.status(400).json({ error: 'Password must be at least 6 characters long' });
return;
}
// Check if email already exists
const existingUser = await db.get<{ id: string }>(
'SELECT id FROM users WHERE email = ?',
[email]
);
if (existingUser) {
res.status(409).json({ error: 'Email already exists' });
return;
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
const adminId = uuidv4();
// Create admin user
await db.run(
`INSERT INTO users (id, email, password, name, role, phone, department, is_active)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[adminId, email, hashedPassword, name, 'admin', phone || null, department || null, true]
);
res.status(201).json({
message: 'Admin user created successfully',
userId: adminId
});
} catch (error) {
console.error('Error in setup:', error);
res.status(500).json({ error: 'Internal server error' });
}
};

View File

@@ -1,8 +1,8 @@
// backend/src/controllers/shiftPlanController.ts
import { Request, Response } from 'express';
import { v4 as uuidv4 } from 'uuid';
import { db } from '../services/databaseService';
import { AuthRequest } from '../middleware/auth';
import { db } from '../services/databaseService.js';
import { AuthRequest } from '../middleware/auth.js';
export interface ShiftPlan {
id: string;

View File

@@ -1,8 +1,8 @@
// backend/src/controllers/shiftTemplateController.ts
import { Request, Response } from 'express';
import { v4 as uuidv4 } from 'uuid';
import { db } from '../services/databaseService';
import { ShiftTemplate, CreateShiftTemplateRequest, UpdateShiftTemplateRequest } from '../models/ShiftTemplate';
import { db } from '../services/databaseService.js';
import { ShiftTemplate, CreateShiftTemplateRequest, UpdateShiftTemplateRequest } from '../models/ShiftTemplate.js';
export const getTemplates = async (req: Request, res: Response): Promise<void> => {
try {

View File

@@ -1,3 +1,39 @@
-- Tabelle für Benutzer
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
name TEXT NOT NULL,
role TEXT CHECK(role IN ('admin', 'user', 'instandhalter')) NOT NULL,
phone TEXT,
department TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Tabelle für Schichtvorlagen
CREATE TABLE IF NOT EXISTS shift_templates (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
is_default BOOLEAN DEFAULT FALSE,
created_by TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (created_by) REFERENCES users(id)
);
-- Tabelle für die Schichten in den Vorlagen
CREATE TABLE IF NOT EXISTS template_shifts (
id TEXT PRIMARY KEY,
template_id TEXT NOT NULL,
day_of_week INTEGER NOT NULL CHECK (day_of_week >= 1 AND day_of_week <= 7),
name TEXT NOT NULL,
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
required_employees INTEGER DEFAULT 1,
FOREIGN KEY (template_id) REFERENCES shift_templates(id) ON DELETE CASCADE
);
-- Zusätzliche Tabellen für shift_plans
CREATE TABLE IF NOT EXISTS shift_plans (
id TEXT PRIMARY KEY,
@@ -32,9 +68,4 @@ CREATE TABLE IF NOT EXISTS employee_availabilities (
end_time TEXT NOT NULL,
is_available BOOLEAN DEFAULT FALSE,
FOREIGN KEY (employee_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Users Tabelle erweitern um zusätzliche Felder
ALTER TABLE users ADD COLUMN phone TEXT;
ALTER TABLE users ADD COLUMN department TEXT;
ALTER TABLE users ADD COLUMN is_active BOOLEAN DEFAULT TRUE;
);

View File

@@ -1,7 +1,7 @@
// backend/src/routes/auth.ts
import express from 'express';
import { login, register, logout, getCurrentUser } from '../controllers/authController';
import { authMiddleware } from '../middleware/auth';
import { login, register, logout, getCurrentUser } from '../controllers/authController.js';
import { authMiddleware } from '../middleware/auth.js';
const router = express.Router();

View File

@@ -0,0 +1,10 @@
// backend/src/routes/setup.ts
import express from 'express';
import { checkSetupStatus, setupAdmin } from '../controllers/setupController.js';
const router = express.Router();
router.get('/status', checkSetupStatus);
router.post('/admin', setupAdmin);
export default router;

View File

@@ -1,13 +1,13 @@
// backend/src/routes/shiftPlans.ts
import express from 'express';
import { authMiddleware, requireRole } from '../middleware/auth';
import { authMiddleware, requireRole } from '../middleware/auth.js';
import {
getShiftPlans,
getShiftPlan,
createShiftPlan,
updateShiftPlan,
deleteShiftPlan
} from '../controllers/shiftPlanController';
} from '../controllers/shiftPlanController.js';
const router = express.Router();

View File

@@ -1,13 +1,13 @@
// backend/src/routes/shiftTemplates.ts
import express from 'express';
import { authMiddleware } from '../middleware/auth';
import { authMiddleware } from '../middleware/auth.js';
import {
getTemplates,
getTemplate,
createTemplate,
updateTemplate,
deleteTemplate
} from '../controllers/shiftTemplateController';
} from '../controllers/shiftTemplateController.js';
const router = express.Router();

View File

@@ -0,0 +1,130 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { db } from '../services/databaseService.js';
import { setupDefaultTemplate } from './setupDefaultTemplate.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export async function initializeDatabase(): Promise<void> {
const schemaPath = path.join(__dirname, '../database/schema.sql');
const schema = fs.readFileSync(schemaPath, 'utf8');
try {
console.log('Starting database initialization...');
// Get list of existing tables
interface TableInfo {
name: string;
}
try {
const existingTables = await db.all<TableInfo>(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
);
console.log('Existing tables found:', existingTables.map(t => t.name).join(', ') || 'none');
// Drop existing tables in reverse order of dependencies
const tablesToDrop = [
'employee_availabilities',
'assigned_shifts',
'shift_plans',
'template_shifts',
'shift_templates',
'users'
];
for (const table of tablesToDrop) {
if (existingTables.some(t => t.name === table)) {
console.log(`Dropping table: ${table}`);
await db.run(`DROP TABLE IF EXISTS ${table}`);
}
}
} catch (error) {
console.error('Error checking/dropping existing tables:', error);
// Continue with schema creation even if table dropping fails
}
// Execute schema creation in a transaction
await db.run('BEGIN EXCLUSIVE TRANSACTION');
// Execute each statement separately for better error reporting
const statements = schema
.split(';')
.map(stmt => stmt.trim())
.filter(stmt => stmt.length > 0)
.map(stmt => {
// Remove any single-line comments
return stmt.split('\n')
.filter(line => !line.trim().startsWith('--'))
.join('\n')
.trim();
})
.filter(stmt => stmt.length > 0);
for (const statement of statements) {
try {
console.log('Executing statement:', statement.substring(0, 50) + '...');
await db.run(statement);
} catch (error) {
console.error('Failed SQL statement:', statement);
console.error('Error details:', error);
await db.run('ROLLBACK');
throw error;
}
}
await db.run('COMMIT');
console.log('✅ Datenbankschema erfolgreich initialisiert');
// Give a small delay to ensure all transactions are properly closed
await new Promise(resolve => setTimeout(resolve, 100));
// Create default template
await setupDefaultTemplate();
} catch (error) {
console.error('Fehler bei der Datenbankinitialisierung:', error);
throw error;
}
}
async function createAdminUser(): Promise<void> {
try {
await db.run('BEGIN TRANSACTION');
try {
// Erstelle Admin-Benutzer, wenn noch keiner existiert
const admin = await db.get('SELECT id FROM users WHERE role = ?', ['admin']);
if (!admin) {
await db.run(
`INSERT INTO users (id, email, password, name, role, phone, department, is_active)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
'admin-' + Math.random().toString(36).substring(2),
'admin@schichtplan.de',
'admin123',
'Administrator',
'admin',
'+49 123 456789',
'IT',
true
]
);
console.log('✅ Admin-Benutzer erstellt');
} else {
console.log(' Admin-Benutzer existiert bereits');
}
await db.run('COMMIT');
} catch (error) {
await db.run('ROLLBACK');
throw error;
}
} catch (error) {
console.error('Fehler beim Erstellen des Admin-Benutzers:', error);
throw error;
}
}

View File

@@ -0,0 +1,98 @@
// backend/src/scripts/setupDefaultTemplate.ts
import { v4 as uuidv4 } from 'uuid';
import { db } from '../services/databaseService.js';
interface AdminUser {
id: string;
}
/**
* Sets up the default shift template if it doesn't exist
* @returns {Promise<void>}
*/
export async function setupDefaultTemplate(): Promise<void> {
try {
// Prüfen ob bereits eine Standard-Vorlage existiert
const existingDefault = await db.get(
'SELECT * FROM shift_templates WHERE is_default = 1'
);
if (existingDefault) {
console.log('Standard-Vorlage existiert bereits');
return;
}
// Admin-Benutzer für die Standard-Vorlage finden
const adminUser = await db.get<AdminUser>(
'SELECT id FROM users WHERE role = ?',
['admin']
);
if (!adminUser) {
console.log('Kein Admin-Benutzer gefunden. Standard-Vorlage kann nicht erstellt werden.');
return;
}
const templateId = uuidv4();
// Transaktion starten
await db.run('BEGIN TRANSACTION');
try {
// Standard-Vorlage erstellen
await db.run(
`INSERT INTO shift_templates (id, name, description, is_default, created_by)
VALUES (?, ?, ?, ?, ?)`,
[
templateId,
'Standard Wochenplan',
'Mo-Do: Vormittags- und Nachmittagsschicht, Fr: nur Vormittagsschicht',
1,
adminUser.id
]
);
console.log('Standard-Vorlage erstellt:', templateId);
// Vormittagsschicht Mo-Do
for (let day = 1; day <= 4; day++) {
await db.run(
`INSERT INTO template_shifts (id, template_id, day_of_week, name, start_time, end_time, required_employees)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[uuidv4(), templateId, day, 'Vormittagsschicht', '08:00', '12:00', 1]
);
}
console.log('Vormittagsschichten Mo-Do erstellt');
// Nachmittagsschicht Mo-Do
for (let day = 1; day <= 4; day++) {
await db.run(
`INSERT INTO template_shifts (id, template_id, day_of_week, name, start_time, end_time, required_employees)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[uuidv4(), templateId, day, 'Nachmittagsschicht', '11:30', '15:30', 1]
);
}
console.log('Nachmittagsschichten Mo-Do erstellt');
// Freitag nur Vormittagsschicht
await db.run(
`INSERT INTO template_shifts (id, template_id, day_of_week, name, start_time, end_time, required_employees)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[uuidv4(), templateId, 5, 'Vormittagsschicht', '08:00', '12:00', 1]
);
console.log('Freitag Vormittagsschicht erstellt');
await db.run('COMMIT');
console.log('Standard-Vorlage erfolgreich initialisiert');
} catch (error) {
await db.run('ROLLBACK');
throw error;
}
} catch (error) {
console.error('Fehler beim Erstellen der Standard-Vorlage:', error);
throw error;
}
}

View File

@@ -1,63 +1,20 @@
// backend/src/server.ts - Login für alle Benutzer
const express = require('express');
const cors = require('cors');
const { v4: uuidv4 } = require('uuid');
import express from 'express';
import cors from 'cors';
import { v4 as uuidv4 } from 'uuid';
import { setupDefaultTemplate } from './scripts/setupDefaultTemplate.js';
import { initializeDatabase } from './scripts/initializeDatabase.js';
// Route imports
import authRoutes from './routes/auth.js';
import employeeRoutes from './routes/employees.js';
import shiftPlanRoutes from './routes/shiftPlans.js';
import shiftTemplateRoutes from './routes/shiftTemplates.js';
import setupRoutes from './routes/setup.js';
const app = express();
const PORT = 3002;
// IN-MEMORY STORE für Mitarbeiter
let employees = [
{
id: '1',
email: 'admin@schichtplan.de',
password: 'admin123', // Klartext für Test
name: 'Admin User',
role: 'admin',
isActive: true,
phone: '+49 123 456789',
department: 'IT',
createdAt: new Date().toISOString(),
lastLogin: new Date().toISOString()
},
{
id: '2',
email: 'instandhalter@schichtplan.de',
password: 'instandhalter123',
name: 'Max Instandhalter',
role: 'instandhalter',
isActive: true,
phone: '+49 123 456790',
department: 'Produktion',
createdAt: new Date().toISOString(),
lastLogin: new Date().toISOString()
},
{
id: '3',
email: 'mitarbeiter1@schichtplan.de',
password: 'user123',
name: 'Anna Müller',
role: 'user',
isActive: true,
phone: '+49 123 456791',
department: 'Logistik',
createdAt: new Date().toISOString(),
lastLogin: new Date().toISOString()
},
{
id: '4',
email: 'mitarbeiter2@schichtplan.de',
password: 'user123',
name: 'Tom Schmidt',
role: 'user',
isActive: true,
phone: '+49 123 456792',
department: 'Produktion',
createdAt: new Date().toISOString(),
lastLogin: new Date().toISOString()
}
];
// CORS und Middleware
app.use(cors({
origin: 'http://localhost:3000',
@@ -65,6 +22,13 @@ app.use(cors({
}));
app.use(express.json());
// API Routes
app.use('/api/setup', setupRoutes);
app.use('/api/auth', authRoutes);
app.use('/api/employees', employeeRoutes);
app.use('/api/shift-plans', shiftPlanRoutes);
app.use('/api/shift-templates', shiftTemplateRoutes);
// Health route
app.get('/api/health', (req: any, res: any) => {
res.json({
@@ -74,263 +38,22 @@ app.get('/api/health', (req: any, res: any) => {
});
});
app.post('/api/auth/login', async (req: any, res: any) => {
try {
const { email, password } = req.body;
console.log('🔐 Login attempt for:', email);
console.log('📧 Email:', email);
console.log('🔑 Password length:', password?.length);
if (!email || !password) {
return res.status(400).json({ error: 'Email and password are required' });
}
// Benutzer suchen
const user = employees.find(emp => emp.email === email && emp.isActive);
if (!user) {
console.log('❌ User not found or inactive:', email);
return res.status(401).json({ error: 'Invalid credentials' });
}
console.log('🔍 User found:', user.email);
console.log('💾 Stored password:', user.password);
console.log('↔️ Password match:', password === user.password);
// Passwort-Überprüfung
const isPasswordValid = password === user.password;
if (!isPasswordValid) {
console.log('❌ Password invalid for:', email);
return res.status(401).json({ error: 'Invalid credentials' });
}
// Last Login aktualisieren
user.lastLogin = new Date().toISOString();
console.log('✅ Login successful for:', email);
// User ohne Passwort zurückgeben
const { password: _, ...userWithoutPassword } = user;
res.json({
user: userWithoutPassword,
token: 'jwt-token-' + Date.now() + '-' + user.id,
expiresIn: '7d'
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// EMPLOYEE ROUTES
app.get('/api/employees', async (req: any, res: any) => {
try {
console.log('📋 Fetching employees - Total:', employees.length);
// Passwörter ausblenden
const employeesWithoutPasswords = employees.map(emp => {
const { password, ...empWithoutPassword } = emp;
return empWithoutPassword;
});
res.json(employeesWithoutPasswords);
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
app.post('/api/employees', async (req: any, res: any) => {
try {
const { email, password, name, role, phone, department } = req.body;
console.log('👤 Creating employee:', { email, name, role });
// Validierung
if (!email || !password || !name || !role) {
return res.status(400).json({ error: 'Email, password, name and role are required' });
}
if (password.length < 6) {
return res.status(400).json({ error: 'Password must be at least 6 characters long' });
}
// Rollen-Validierung
const validRoles = ['admin', 'instandhalter', 'user'];
if (!validRoles.includes(role)) {
return res.status(400).json({ error: 'Ungültige Rolle' });
}
// Check if email already exists
if (employees.find(emp => emp.email === email)) {
return res.status(409).json({ error: 'Email already exists' });
}
// NEUEN Benutzer erstellen
const newEmployee = {
id: uuidv4(),
email,
password: password, // Klartext speichern für einfachen Test
name,
role,
isActive: true,
phone: phone || '',
department: department || '',
createdAt: new Date().toISOString(),
lastLogin: ''
};
employees.push(newEmployee);
console.log('✅ Employee created:', {
email: newEmployee.email,
name: newEmployee.name,
role: newEmployee.role
});
console.log('📊 Total employees:', employees.length);
// Response ohne Passwort
const { password: _, ...employeeWithoutPassword } = newEmployee;
res.status(201).json(employeeWithoutPassword);
} catch (error) {
console.error('Error creating employee:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
app.put('/api/employees/:id', async (req: any, res: any) => {
try {
const { id } = req.params;
const { name, role, isActive, phone, department } = req.body;
console.log('✏️ Updating employee:', id);
// Mitarbeiter finden
const employeeIndex = employees.findIndex(emp => emp.id === id);
if (employeeIndex === -1) {
return res.status(404).json({ error: 'Employee not found' });
}
// Mitarbeiter aktualisieren
employees[employeeIndex] = {
...employees[employeeIndex],
name: name || employees[employeeIndex].name,
role: role || employees[employeeIndex].role,
isActive: isActive !== undefined ? isActive : employees[employeeIndex].isActive,
phone: phone || employees[employeeIndex].phone,
department: department || employees[employeeIndex].department
};
console.log('✅ Employee updated:', employees[employeeIndex].name);
// Response ohne Passwort
const { password, ...employeeWithoutPassword } = employees[employeeIndex];
res.json(employeeWithoutPassword);
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
app.delete('/api/employees/:id', async (req: any, res: any) => {
try {
const { id } = req.params;
console.log('🗑️ Deleting employee:', id);
const employeeIndex = employees.findIndex(emp => emp.id === id);
if (employeeIndex === -1) {
return res.status(404).json({ error: 'Employee not found' });
}
const employeeToDelete = employees[employeeIndex];
// Admin-Check
if (employeeToDelete.role === 'admin') {
const adminCount = employees.filter(emp =>
emp.role === 'admin' && emp.isActive
).length;
if (adminCount <= 1) {
return res.status(400).json({
error: 'Mindestens ein Administrator muss im System verbleiben'
});
}
}
// Perform hard delete
employees.splice(employeeIndex, 1);
console.log('✅ Employee permanently deleted:', employeeToDelete.name);
res.status(204).send();
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
// Availability Routes
app.get('/api/employees/:employeeId/availabilities', async (req: any, res: any) => {
try {
const { employeeId } = req.params;
console.log('📅 Fetching availabilities for:', employeeId);
// Mock Verfügbarkeiten
const daysOfWeek = [0, 1, 2, 3, 4, 5, 6];
const timeSlots = [
{ name: 'Vormittag', start: '08:00', end: '12:00' },
{ name: 'Nachmittag', start: '12:00', end: '16:00' },
{ name: 'Abend', start: '16:00', end: '20:00' }
];
const mockAvailabilities = daysOfWeek.flatMap(day =>
timeSlots.map((slot, index) => ({
id: `avail-${employeeId}-${day}-${index}`,
employeeId,
dayOfWeek: day,
startTime: slot.start,
endTime: slot.end,
isAvailable: day >= 1 && day <= 5
}))
);
res.json(mockAvailabilities);
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
app.put('/api/employees/:employeeId/availabilities', async (req: any, res: any) => {
try {
const { employeeId } = req.params;
const availabilities = req.body;
console.log('💾 Saving availabilities for:', employeeId);
// Mock erfolgreiches Speichern
res.json(availabilities);
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
// Start server
app.listen(PORT, () => {
app.listen(PORT, async () => {
console.log('🎉 BACKEND STARTED SUCCESSFULLY!');
console.log(`📍 Port: ${PORT}`);
console.log(`📍 Health: http://localhost:${PORT}/api/health`);
try {
await initializeDatabase();
await setupDefaultTemplate();
console.log('✅ Standard-Vorlage überprüft/erstellt');
} catch (error) {
console.error('❌ Fehler bei der Initialisierung:', error);
}
console.log('');
console.log('🔐 SIMPLE LOGIN READY - Plain text passwords for testing!');
console.log('');
console.log('📋 TEST ACCOUNTS:');
console.log(' 👑 Admin: admin@schichtplan.de / admin123');
console.log(' 🔧 Instandhalter: instandhalter@schichtplan.de / instandhalter123');
console.log(' 👤 User1: mitarbeiter1@schichtplan.de / user123');
console.log(' 👤 User2: mitarbeiter2@schichtplan.de / user123');
console.log(' 👤 Patrick: patrick@patrick.de / 12345678');
});
console.log('🔧 Setup ready at: http://localhost:${PORT}/api/setup/status');
console.log('📝 Create your admin account on first launch');
});

View File

@@ -1,14 +1,12 @@
// backend/src/services/databaseService.ts
import sqlite3 from 'sqlite3';
import path from 'path';
import { fileURLToPath } from 'url';
// __dirname für ES Modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const dbPath = path.join(__dirname, '../../database/schichtplan.db');
export class DatabaseService {
class Database {
private db: sqlite3.Database;
constructor() {
@@ -17,127 +15,15 @@ export class DatabaseService {
console.error('Database connection error:', err);
} else {
console.log('Connected to SQLite database');
this.enableForeignKeysAndInitialize();
// Enable foreign keys asynchronously
this.enableForeignKeys().catch(err => {
console.error('Error enabling foreign keys:', err);
});
}
});
}
private async enableForeignKeysAndInitialize() {
try {
// First enable foreign keys
await this.run('PRAGMA foreign_keys = ON');
console.log('Foreign keys enabled');
// Then check if it's actually enabled
const pragma = await this.get('PRAGMA foreign_keys');
console.log('Foreign keys status:', pragma);
// Now initialize the database
await this.initializeDatabase();
} catch (error) {
console.error('Error in database initialization:', error);
}
}
private initializeDatabase() {
const dropTables = `
DROP TABLE IF EXISTS employee_availabilities;
DROP TABLE IF EXISTS assigned_shifts;
DROP TABLE IF EXISTS shift_plans;
DROP TABLE IF EXISTS template_shifts;
DROP TABLE IF EXISTS shift_templates;
DROP TABLE IF EXISTS users;
`;
this.db.exec(dropTables, (err) => {
if (err) {
console.error('Error dropping tables:', err);
return;
}
console.log('Existing tables dropped');
});
const schema = `
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT NOT NULL,
password TEXT NOT NULL,
name TEXT NOT NULL,
role TEXT CHECK(role IN ('admin', 'instandhalter', 'user')) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT TRUE,
phone TEXT,
department TEXT,
last_login DATETIME,
CONSTRAINT unique_active_email UNIQUE (email, is_active) WHERE is_active = 1
);
CREATE TABLE IF NOT EXISTS shift_templates (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
is_default BOOLEAN DEFAULT FALSE,
created_by TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (created_by) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS template_shifts (
id TEXT PRIMARY KEY,
template_id TEXT NOT NULL,
day_of_week INTEGER NOT NULL CHECK (day_of_week >= 0 AND day_of_week <= 6),
name TEXT NOT NULL,
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
required_employees INTEGER DEFAULT 1,
color TEXT DEFAULT '#3498db',
FOREIGN KEY (template_id) REFERENCES shift_templates(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS shift_plans (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
template_id TEXT,
status TEXT CHECK(status IN ('draft', 'published')) DEFAULT 'draft',
created_by TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
FOREIGN KEY (template_id) REFERENCES shift_templates(id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS assigned_shifts (
id TEXT PRIMARY KEY,
shift_plan_id TEXT NOT NULL,
date TEXT NOT NULL,
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
required_employees INTEGER DEFAULT 1,
assigned_employees TEXT DEFAULT '[]',
FOREIGN KEY (shift_plan_id) REFERENCES shift_plans(id) ON DELETE CASCADE
);
`;
this.db.exec(schema, (err) => {
if (err) {
console.error('Database initialization error:', err);
} else {
console.log('Database initialized successfully');
}
});
}
run(sql: string, params: any[] = []): Promise<void> {
return new Promise((resolve, reject) => {
this.db.run(sql, params, function(err) {
if (err) reject(err);
else resolve();
});
});
}
exec(sql: string): Promise<void> {
async exec(sql: string): Promise<void> {
return new Promise((resolve, reject) => {
this.db.exec(sql, (err) => {
if (err) reject(err);
@@ -146,7 +32,16 @@ export class DatabaseService {
});
}
get<T>(sql: string, params: any[] = []): Promise<T | undefined> {
async run(sql: string, params: any[] = []): Promise<void> {
return new Promise((resolve, reject) => {
this.db.run(sql, params, (err) => {
if (err) reject(err);
else resolve();
});
});
}
async get<T>(sql: string, params: any[] = []): Promise<T | undefined> {
return new Promise((resolve, reject) => {
this.db.get(sql, params, (err, row) => {
if (err) reject(err);
@@ -155,7 +50,7 @@ export class DatabaseService {
});
}
all<T>(sql: string, params: any[] = []): Promise<T[]> {
async all<T>(sql: string, params: any[] = []): Promise<T[]> {
return new Promise((resolve, reject) => {
this.db.all(sql, params, (err, rows) => {
if (err) reject(err);
@@ -164,7 +59,22 @@ export class DatabaseService {
});
}
close(): Promise<void> {
private async enableForeignKeys(): Promise<void> {
try {
// Enable foreign keys
await this.run('PRAGMA foreign_keys = ON');
console.log('Foreign keys enabled');
// Check if it's actually enabled
const result = await this.get<{ foreign_keys: number }>('PRAGMA foreign_keys');
console.log('Foreign keys status:', result);
} catch (error) {
console.error('Error enabling foreign keys:', error);
throw error;
}
}
async close(): Promise<void> {
return new Promise((resolve, reject) => {
this.db.close((err) => {
if (err) reject(err);
@@ -174,4 +84,5 @@ export class DatabaseService {
}
}
export const db = new DatabaseService();
// Export a single instance
export const db = new Database();

View File

@@ -1,9 +1,9 @@
// backend/tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"ignoreDeprecations": "6.0",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,