mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-11-30 22:45:46 +01:00
added admin setup
This commit is contained in:
2810
backend/package-lock.json
generated
2810
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "schichtplan-backend",
|
"name": "schichtplan-backend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "commonjs",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node -r ts-node/register src/server.ts",
|
"dev": "node --loader ts-node/esm src/server.ts",
|
||||||
"simple": "node src/server.ts",
|
"simple": "node src/server.ts",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"start": "node dist/server.js"
|
"start": "node dist/server.js"
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { Request, Response } from 'express';
|
|||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { db } from '../services/databaseService';
|
import { db } from '../services/databaseService.js';
|
||||||
import { AuthRequest } from '../middleware/auth';
|
import { AuthRequest } from '../middleware/auth.js';
|
||||||
|
|
||||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
||||||
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
|
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
|
||||||
|
|||||||
87
backend/src/controllers/setupController.ts
Normal file
87
backend/src/controllers/setupController.ts
Normal 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' });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
// backend/src/controllers/shiftPlanController.ts
|
// backend/src/controllers/shiftPlanController.ts
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { db } from '../services/databaseService';
|
import { db } from '../services/databaseService.js';
|
||||||
import { AuthRequest } from '../middleware/auth';
|
import { AuthRequest } from '../middleware/auth.js';
|
||||||
|
|
||||||
export interface ShiftPlan {
|
export interface ShiftPlan {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// backend/src/controllers/shiftTemplateController.ts
|
// backend/src/controllers/shiftTemplateController.ts
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { db } from '../services/databaseService';
|
import { db } from '../services/databaseService.js';
|
||||||
import { ShiftTemplate, CreateShiftTemplateRequest, UpdateShiftTemplateRequest } from '../models/ShiftTemplate';
|
import { ShiftTemplate, CreateShiftTemplateRequest, UpdateShiftTemplateRequest } from '../models/ShiftTemplate.js';
|
||||||
|
|
||||||
export const getTemplates = async (req: Request, res: Response): Promise<void> => {
|
export const getTemplates = async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -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
|
-- Zusätzliche Tabellen für shift_plans
|
||||||
CREATE TABLE IF NOT EXISTS shift_plans (
|
CREATE TABLE IF NOT EXISTS shift_plans (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
@@ -32,9 +68,4 @@ CREATE TABLE IF NOT EXISTS employee_availabilities (
|
|||||||
end_time TEXT NOT NULL,
|
end_time TEXT NOT NULL,
|
||||||
is_available BOOLEAN DEFAULT FALSE,
|
is_available BOOLEAN DEFAULT FALSE,
|
||||||
FOREIGN KEY (employee_id) REFERENCES users(id) ON DELETE CASCADE
|
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;
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// backend/src/routes/auth.ts
|
// backend/src/routes/auth.ts
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { login, register, logout, getCurrentUser } from '../controllers/authController';
|
import { login, register, logout, getCurrentUser } from '../controllers/authController.js';
|
||||||
import { authMiddleware } from '../middleware/auth';
|
import { authMiddleware } from '../middleware/auth.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
|||||||
10
backend/src/routes/setup.ts
Normal file
10
backend/src/routes/setup.ts
Normal 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;
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
// backend/src/routes/shiftPlans.ts
|
// backend/src/routes/shiftPlans.ts
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { authMiddleware, requireRole } from '../middleware/auth';
|
import { authMiddleware, requireRole } from '../middleware/auth.js';
|
||||||
import {
|
import {
|
||||||
getShiftPlans,
|
getShiftPlans,
|
||||||
getShiftPlan,
|
getShiftPlan,
|
||||||
createShiftPlan,
|
createShiftPlan,
|
||||||
updateShiftPlan,
|
updateShiftPlan,
|
||||||
deleteShiftPlan
|
deleteShiftPlan
|
||||||
} from '../controllers/shiftPlanController';
|
} from '../controllers/shiftPlanController.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
// backend/src/routes/shiftTemplates.ts
|
// backend/src/routes/shiftTemplates.ts
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { authMiddleware } from '../middleware/auth';
|
import { authMiddleware } from '../middleware/auth.js';
|
||||||
import {
|
import {
|
||||||
getTemplates,
|
getTemplates,
|
||||||
getTemplate,
|
getTemplate,
|
||||||
createTemplate,
|
createTemplate,
|
||||||
updateTemplate,
|
updateTemplate,
|
||||||
deleteTemplate
|
deleteTemplate
|
||||||
} from '../controllers/shiftTemplateController';
|
} from '../controllers/shiftTemplateController.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
|||||||
130
backend/src/scripts/initializeDatabase.ts
Normal file
130
backend/src/scripts/initializeDatabase.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
98
backend/src/scripts/setupDefaultTemplate.ts
Normal file
98
backend/src/scripts/setupDefaultTemplate.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,63 +1,20 @@
|
|||||||
// backend/src/server.ts - Login für alle Benutzer
|
// backend/src/server.ts - Login für alle Benutzer
|
||||||
const express = require('express');
|
import express from 'express';
|
||||||
const cors = require('cors');
|
import cors from 'cors';
|
||||||
const { v4: uuidv4 } = require('uuid');
|
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 app = express();
|
||||||
const PORT = 3002;
|
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
|
// CORS und Middleware
|
||||||
app.use(cors({
|
app.use(cors({
|
||||||
origin: 'http://localhost:3000',
|
origin: 'http://localhost:3000',
|
||||||
@@ -65,6 +22,13 @@ app.use(cors({
|
|||||||
}));
|
}));
|
||||||
app.use(express.json());
|
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
|
// Health route
|
||||||
app.get('/api/health', (req: any, res: any) => {
|
app.get('/api/health', (req: any, res: any) => {
|
||||||
res.json({
|
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
|
// Start server
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, async () => {
|
||||||
console.log('🎉 BACKEND STARTED SUCCESSFULLY!');
|
console.log('🎉 BACKEND STARTED SUCCESSFULLY!');
|
||||||
console.log(`📍 Port: ${PORT}`);
|
console.log(`📍 Port: ${PORT}`);
|
||||||
console.log(`📍 Health: http://localhost:${PORT}/api/health`);
|
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('');
|
||||||
console.log('🔐 SIMPLE LOGIN READY - Plain text passwords for testing!');
|
console.log('🔧 Setup ready at: http://localhost:${PORT}/api/setup/status');
|
||||||
console.log('');
|
console.log('📝 Create your admin account on first launch');
|
||||||
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');
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
// backend/src/services/databaseService.ts
|
|
||||||
import sqlite3 from 'sqlite3';
|
import sqlite3 from 'sqlite3';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
// __dirname für ES Modules
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
const dbPath = path.join(__dirname, '../../database/schichtplan.db');
|
const dbPath = path.join(__dirname, '../../database/schichtplan.db');
|
||||||
|
|
||||||
export class DatabaseService {
|
class Database {
|
||||||
private db: sqlite3.Database;
|
private db: sqlite3.Database;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -17,127 +15,15 @@ export class DatabaseService {
|
|||||||
console.error('Database connection error:', err);
|
console.error('Database connection error:', err);
|
||||||
} else {
|
} else {
|
||||||
console.log('Connected to SQLite database');
|
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() {
|
async exec(sql: string): Promise<void> {
|
||||||
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> {
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.db.exec(sql, (err) => {
|
this.db.exec(sql, (err) => {
|
||||||
if (err) reject(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) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.db.get(sql, params, (err, row) => {
|
this.db.get(sql, params, (err, row) => {
|
||||||
if (err) reject(err);
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.db.all(sql, params, (err, rows) => {
|
this.db.all(sql, params, (err, rows) => {
|
||||||
if (err) reject(err);
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.db.close((err) => {
|
this.db.close((err) => {
|
||||||
if (err) reject(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();
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
// backend/tsconfig.json
|
// backend/tsconfig.json
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
"target": "ES2022",
|
||||||
"module": "ESNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "NodeNext",
|
||||||
"ignoreDeprecations": "6.0",
|
"ignoreDeprecations": "6.0",
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import ShiftPlanCreate from './pages/ShiftPlans/ShiftPlanCreate';
|
|||||||
import EmployeeManagement from './pages/Employees/EmployeeManagement';
|
import EmployeeManagement from './pages/Employees/EmployeeManagement';
|
||||||
import Settings from './pages/Settings/Settings';
|
import Settings from './pages/Settings/Settings';
|
||||||
import Help from './pages/Help/Help';
|
import Help from './pages/Help/Help';
|
||||||
|
import Setup from './pages/Setup/Setup';
|
||||||
|
|
||||||
// Protected Route Component direkt in App.tsx
|
// Protected Route Component direkt in App.tsx
|
||||||
const ProtectedRoute: React.FC<{ children: React.ReactNode; roles?: string[] }> = ({
|
const ProtectedRoute: React.FC<{ children: React.ReactNode; roles?: string[] }> = ({
|
||||||
@@ -50,51 +51,67 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<Router>
|
<AppContent />
|
||||||
<NotificationContainer />
|
|
||||||
<Routes>
|
|
||||||
<Route path="/login" element={<Login />} />
|
|
||||||
|
|
||||||
<Route path="/" element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<Dashboard />
|
|
||||||
</ProtectedRoute>
|
|
||||||
} />
|
|
||||||
|
|
||||||
<Route path="/shift-plans" element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<ShiftPlanList />
|
|
||||||
</ProtectedRoute>
|
|
||||||
} />
|
|
||||||
|
|
||||||
<Route path="/shift-plans/new" element={
|
|
||||||
<ProtectedRoute roles={['admin', 'instandhalter']}>
|
|
||||||
<ShiftPlanCreate />
|
|
||||||
</ProtectedRoute>
|
|
||||||
} />
|
|
||||||
|
|
||||||
<Route path="/employees" element={
|
|
||||||
<ProtectedRoute roles={['admin', 'instandhalter']}>
|
|
||||||
<EmployeeManagement />
|
|
||||||
</ProtectedRoute>
|
|
||||||
} />
|
|
||||||
|
|
||||||
<Route path="/settings" element={
|
|
||||||
<ProtectedRoute roles={['admin']}>
|
|
||||||
<Settings />
|
|
||||||
</ProtectedRoute>
|
|
||||||
} />
|
|
||||||
|
|
||||||
<Route path="/help" element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<Help />
|
|
||||||
</ProtectedRoute>
|
|
||||||
} />
|
|
||||||
</Routes>
|
|
||||||
</Router>
|
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AppContent() {
|
||||||
|
const { loading, needsSetup } = useAuth();
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||||
|
<div>⏳ Lade Anwendung...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
<NotificationContainer />
|
||||||
|
<Routes>
|
||||||
|
{needsSetup ? (
|
||||||
|
<Route path="*" element={<Setup />} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route path="/" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<Dashboard />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
<Route path="/shift-plans" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<ShiftPlanList />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
<Route path="/shift-plans/new" element={
|
||||||
|
<ProtectedRoute roles={['admin', 'instandhalter']}>
|
||||||
|
<ShiftPlanCreate />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
<Route path="/employees" element={
|
||||||
|
<ProtectedRoute roles={['admin', 'instandhalter']}>
|
||||||
|
<EmployeeManagement />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
<Route path="/settings" element={
|
||||||
|
<ProtectedRoute roles={['admin']}>
|
||||||
|
<Settings />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
<Route path="/help" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<Help />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
@@ -9,6 +9,8 @@ interface AuthContextType {
|
|||||||
hasRole: (roles: string[]) => boolean;
|
hasRole: (roles: string[]) => boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
refreshUser: () => void; // NEU: Force refresh
|
refreshUser: () => void; // NEU: Force refresh
|
||||||
|
needsSetup: boolean;
|
||||||
|
checkSetupStatus: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||||
@@ -16,16 +18,31 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|||||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<User | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [needsSetup, setNeedsSetup] = useState(false);
|
||||||
const [refreshTrigger, setRefreshTrigger] = useState(0); // NEU: Refresh trigger
|
const [refreshTrigger, setRefreshTrigger] = useState(0); // NEU: Refresh trigger
|
||||||
|
|
||||||
// User beim Start laden
|
const checkSetupStatus = async () => {
|
||||||
useEffect(() => {
|
try {
|
||||||
const savedUser = authService.getCurrentUser();
|
const response = await fetch('/api/setup/status');
|
||||||
if (savedUser) {
|
const data = await response.json();
|
||||||
setUser(savedUser);
|
setNeedsSetup(data.needsSetup);
|
||||||
console.log('✅ User from localStorage:', savedUser.email);
|
} catch (error) {
|
||||||
|
console.error('Error checking setup status:', error);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
};
|
||||||
|
|
||||||
|
// Check setup status and load user on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const initializeApp = async () => {
|
||||||
|
await checkSetupStatus();
|
||||||
|
const savedUser = authService.getCurrentUser();
|
||||||
|
if (savedUser) {
|
||||||
|
setUser(savedUser);
|
||||||
|
console.log('✅ User from localStorage:', savedUser.email);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
initializeApp();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// NEU: User vom Server laden wenn nötig
|
// NEU: User vom Server laden wenn nötig
|
||||||
@@ -78,7 +95,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
logout,
|
logout,
|
||||||
hasRole,
|
hasRole,
|
||||||
loading,
|
loading,
|
||||||
refreshUser // NEU
|
refreshUser,
|
||||||
|
needsSetup,
|
||||||
|
checkSetupStatus
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
228
frontend/src/pages/Setup/Setup.tsx
Normal file
228
frontend/src/pages/Setup/Setup.tsx
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
|
|
||||||
|
const Setup: React.FC = () => {
|
||||||
|
const [step, setStep] = useState(1);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
name: '',
|
||||||
|
phone: '',
|
||||||
|
department: ''
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { login } = useAuth();
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateStep1 = () => {
|
||||||
|
if (formData.password.length < 6) {
|
||||||
|
setError('Das Passwort muss mindestens 6 Zeichen lang sein.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (formData.password !== formData.confirmPassword) {
|
||||||
|
setError('Die Passwörter stimmen nicht überein.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateStep2 = () => {
|
||||||
|
if (!formData.name.trim()) {
|
||||||
|
setError('Bitte geben Sie einen Namen ein.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
setError('');
|
||||||
|
if (step === 1 && validateStep1()) {
|
||||||
|
setStep(2);
|
||||||
|
} else if (step === 2 && validateStep2()) {
|
||||||
|
handleSubmit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
setError('');
|
||||||
|
setStep(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const adminEmail = 'admin@instandhaltung.de';
|
||||||
|
const response = await fetch('/api/setup/admin', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: adminEmail,
|
||||||
|
password: formData.password,
|
||||||
|
name: formData.name,
|
||||||
|
phone: formData.phone || undefined,
|
||||||
|
department: formData.department || undefined
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
throw new Error(data.error || 'Setup fehlgeschlagen');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Automatically log in after setup
|
||||||
|
await login({ email: adminEmail, password: formData.password });
|
||||||
|
navigate('/');
|
||||||
|
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
|
||||||
|
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-8">
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h1 className="text-3xl font-bold mb-2">Erstkonfiguration</h1>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Konfigurieren Sie den Administrator-Account
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 1 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Admin E-Mail
|
||||||
|
</label>
|
||||||
|
<div className="p-2 bg-gray-100 border rounded">
|
||||||
|
admin@instandhaltung.de
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Passwort
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Mindestens 6 Zeichen"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Passwort bestätigen
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="confirmPassword"
|
||||||
|
value={formData.confirmPassword}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Passwort wiederholen"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 2 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Vollständiger Name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Telefon (optional)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
name="phone"
|
||||||
|
value={formData.phone}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="+49 123 456789"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Abteilung (optional)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="department"
|
||||||
|
value={formData.department}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="z.B. IT"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-6 flex justify-between">
|
||||||
|
{step === 2 && (
|
||||||
|
<button
|
||||||
|
onClick={handleBack}
|
||||||
|
className="px-4 py-2 text-gray-600 hover:text-gray-800"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Zurück
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={handleNext}
|
||||||
|
disabled={loading}
|
||||||
|
className={`px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 ${
|
||||||
|
step === 1 ? 'ml-auto' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
'⏳ Verarbeite...'
|
||||||
|
) : step === 1 ? (
|
||||||
|
'Weiter'
|
||||||
|
) : (
|
||||||
|
'Setup abschließen'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Setup;
|
||||||
125
frontend/src/pages/ShiftPlans/ShiftPlanCreate.module.css
Normal file
125
frontend/src/pages/ShiftPlans/ShiftPlanCreate.module.css
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
.container {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backButton {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backButton:hover {
|
||||||
|
background-color: #e4e4e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroup {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroup label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #34495e;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroup input,
|
||||||
|
.formGroup select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroup select.empty {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateGroup {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background-color: #fde8e8;
|
||||||
|
color: #c53030;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noTemplates {
|
||||||
|
color: #666;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkButton {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #3498db;
|
||||||
|
padding: 0;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkButton:hover {
|
||||||
|
color: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
margin-top: 30px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.createButton {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #2ecc71;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.createButton:hover {
|
||||||
|
background-color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.createButton:disabled {
|
||||||
|
background-color: #95a5a6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
@@ -1,56 +1,169 @@
|
|||||||
// frontend/src/pages/ShiftPlans/ShiftPlanCreate.tsx
|
// frontend/src/pages/ShiftPlans/ShiftPlanCreate.tsx
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
import { shiftTemplateService } from '../../services/shiftTemplateService';
|
||||||
|
import { shiftPlanService } from '../../services/shiftPlanService';
|
||||||
|
import { ShiftTemplate } from '../../types/shiftTemplate';
|
||||||
|
import styles from './ShiftPlanCreate.module.css';
|
||||||
|
|
||||||
const ShiftPlanCreate: React.FC = () => {
|
const ShiftPlanCreate: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const [planName, setPlanName] = useState('');
|
const [planName, setPlanName] = useState('');
|
||||||
const [startDate, setStartDate] = useState('');
|
const [startDate, setStartDate] = useState('');
|
||||||
const [endDate, setEndDate] = useState('');
|
const [endDate, setEndDate] = useState('');
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState('');
|
const [selectedTemplate, setSelectedTemplate] = useState('');
|
||||||
|
const [templates, setTemplates] = useState<ShiftTemplate[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleCreate = async () => {
|
useEffect(() => {
|
||||||
// API Call zum Erstellen
|
loadTemplates();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Template aus URL-Parameter setzen, falls vorhanden
|
||||||
|
const templateId = searchParams.get('template');
|
||||||
|
if (templateId) {
|
||||||
|
setSelectedTemplate(templateId);
|
||||||
|
}
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
|
const loadTemplates = async () => {
|
||||||
|
try {
|
||||||
|
const data = await shiftTemplateService.getTemplates();
|
||||||
|
setTemplates(data);
|
||||||
|
|
||||||
|
// Wenn keine Template-ID in der URL ist, setze die Standard-Vorlage
|
||||||
|
if (!searchParams.get('template')) {
|
||||||
|
const defaultTemplate = data.find(t => t.isDefault);
|
||||||
|
if (defaultTemplate) {
|
||||||
|
setSelectedTemplate(defaultTemplate.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden der Vorlagen:', error);
|
||||||
|
setError('Vorlagen konnten nicht geladen werden');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
try {
|
||||||
|
if (!planName.trim()) {
|
||||||
|
setError('Bitte geben Sie einen Namen für den Schichtplan ein');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!startDate) {
|
||||||
|
setError('Bitte wählen Sie ein Startdatum');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!endDate) {
|
||||||
|
setError('Bitte wählen Sie ein Enddatum');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (new Date(endDate) < new Date(startDate)) {
|
||||||
|
setError('Das Enddatum muss nach dem Startdatum liegen');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await shiftPlanService.createShiftPlan({
|
||||||
|
name: planName,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
templateId: selectedTemplate || undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
// Nach erfolgreicher Erstellung zur Liste der Schichtpläne navigieren
|
||||||
|
navigate('/shift-plans');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Erstellen des Schichtplans:', error);
|
||||||
|
setError('Der Schichtplan konnte nicht erstellt werden. Bitte versuchen Sie es später erneut.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div>Lade Vorlagen...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={styles.container}>
|
||||||
<h1>Neuen Schichtplan erstellen</h1>
|
<div className={styles.header}>
|
||||||
|
<h1>Neuen Schichtplan erstellen</h1>
|
||||||
|
<button onClick={() => navigate(-1)} className={styles.backButton}>
|
||||||
|
Zurück
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className={styles.error}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div className={styles.form}>
|
||||||
<label>Plan Name:</label>
|
<div className={styles.formGroup}>
|
||||||
<input
|
<label>Plan Name:</label>
|
||||||
type="text"
|
<input
|
||||||
value={planName}
|
type="text"
|
||||||
onChange={(e) => setPlanName(e.target.value)}
|
value={planName}
|
||||||
/>
|
onChange={(e) => setPlanName(e.target.value)}
|
||||||
</div>
|
placeholder="z.B. KW 42 2025"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className={styles.dateGroup}>
|
||||||
<label>Von:</label>
|
<div className={styles.formGroup}>
|
||||||
<input
|
<label>Von:</label>
|
||||||
type="date"
|
<input
|
||||||
value={startDate}
|
type="date"
|
||||||
onChange={(e) => setStartDate(e.target.value)}
|
value={startDate}
|
||||||
/>
|
onChange={(e) => setStartDate(e.target.value)}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className={styles.formGroup}>
|
||||||
<label>Bis:</label>
|
<label>Bis:</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={endDate}
|
value={endDate}
|
||||||
onChange={(e) => setEndDate(e.target.value)}
|
onChange={(e) => setEndDate(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className={styles.formGroup}>
|
||||||
<label>Vorlage verwenden:</label>
|
<label>Vorlage verwenden:</label>
|
||||||
<select value={selectedTemplate} onChange={(e) => setSelectedTemplate(e.target.value)}>
|
<select
|
||||||
<option value="">Keine Vorlage</option>
|
value={selectedTemplate}
|
||||||
{/* Vorlagen laden */}
|
onChange={(e) => setSelectedTemplate(e.target.value)}
|
||||||
</select>
|
className={templates.length === 0 ? styles.empty : ''}
|
||||||
</div>
|
>
|
||||||
|
<option value="">Keine Vorlage</option>
|
||||||
|
{templates.map(template => (
|
||||||
|
<option key={template.id} value={template.id}>
|
||||||
|
{template.name} {template.isDefault ? '(Standard)' : ''}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{templates.length === 0 && (
|
||||||
|
<p className={styles.noTemplates}>
|
||||||
|
Keine Vorlagen verfügbar.
|
||||||
|
<button onClick={() => navigate('/shift-templates/new')} className={styles.linkButton}>
|
||||||
|
Neue Vorlage erstellen
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<button onClick={handleCreate}>Schichtplan erstellen</button>
|
<div className={styles.actions}>
|
||||||
|
<button onClick={handleCreate} className={styles.createButton} disabled={!selectedTemplate}>
|
||||||
|
Schichtplan erstellen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
133
frontend/src/services/shiftPlanService.ts
Normal file
133
frontend/src/services/shiftPlanService.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
// frontend/src/services/shiftPlanService.ts
|
||||||
|
import { authService } from './authService';
|
||||||
|
|
||||||
|
const API_BASE = 'http://localhost:3001/api/shift-plans';
|
||||||
|
|
||||||
|
export interface CreateShiftPlanRequest {
|
||||||
|
name: string;
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
templateId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShiftPlan {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
templateId?: string;
|
||||||
|
status: 'draft' | 'published';
|
||||||
|
createdBy: string;
|
||||||
|
createdAt: string;
|
||||||
|
shifts: ShiftPlanShift[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShiftPlanShift {
|
||||||
|
id: string;
|
||||||
|
shiftPlanId: string;
|
||||||
|
date: string;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
requiredEmployees: number;
|
||||||
|
assignedEmployees: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shiftPlanService = {
|
||||||
|
async getShiftPlans(): Promise<ShiftPlan[]> {
|
||||||
|
const response = await fetch(API_BASE, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...authService.getAuthHeaders()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
authService.logout();
|
||||||
|
throw new Error('Nicht authorisiert - bitte erneut anmelden');
|
||||||
|
}
|
||||||
|
throw new Error('Fehler beim Laden der Schichtpläne');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
async getShiftPlan(id: string): Promise<ShiftPlan> {
|
||||||
|
const response = await fetch(`${API_BASE}/${id}`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...authService.getAuthHeaders()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
authService.logout();
|
||||||
|
throw new Error('Nicht authorisiert - bitte erneut anmelden');
|
||||||
|
}
|
||||||
|
throw new Error('Schichtplan nicht gefunden');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
async createShiftPlan(plan: CreateShiftPlanRequest): Promise<ShiftPlan> {
|
||||||
|
const response = await fetch(API_BASE, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...authService.getAuthHeaders()
|
||||||
|
},
|
||||||
|
body: JSON.stringify(plan)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
authService.logout();
|
||||||
|
throw new Error('Nicht authorisiert - bitte erneut anmelden');
|
||||||
|
}
|
||||||
|
throw new Error('Fehler beim Erstellen des Schichtplans');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateShiftPlan(id: string, plan: Partial<ShiftPlan>): Promise<ShiftPlan> {
|
||||||
|
const response = await fetch(`${API_BASE}/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...authService.getAuthHeaders()
|
||||||
|
},
|
||||||
|
body: JSON.stringify(plan)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
authService.logout();
|
||||||
|
throw new Error('Nicht authorisiert - bitte erneut anmelden');
|
||||||
|
}
|
||||||
|
throw new Error('Fehler beim Aktualisieren des Schichtplans');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteShiftPlan(id: string): Promise<void> {
|
||||||
|
const response = await fetch(`${API_BASE}/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...authService.getAuthHeaders()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
authService.logout();
|
||||||
|
throw new Error('Nicht authorisiert - bitte erneut anmelden');
|
||||||
|
}
|
||||||
|
throw new Error('Fehler beim Löschen des Schichtplans');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user