diff --git a/backend/src/controllers/authController.ts b/backend/src/controllers/authController.ts index 95f3944..e678fcd 100644 --- a/backend/src/controllers/authController.ts +++ b/backend/src/controllers/authController.ts @@ -32,13 +32,19 @@ export interface JWTPayload { } export interface RegisterRequest { - email: string; + // REMOVED: email - will be auto-generated password: string; - firstname: String; - lastname: String; + firstname: string; + lastname: string; role?: string; } +function generateEmail(firstname: string, lastname: string): string { + const cleanFirstname = firstname.toLowerCase().replace(/[^a-z0-9]/g, ''); + const cleanLastname = lastname.toLowerCase().replace(/[^a-z0-9]/g, ''); + return `${cleanFirstname}.${cleanLastname}@sp.de`; +} + export const login = async (req: Request, res: Response) => { try { const { email, password } = req.body as LoginRequest; @@ -162,16 +168,19 @@ export const validateToken = async (req: Request, res: Response) => { export const register = async (req: Request, res: Response) => { try { - const { email, password, firstname, lastname, role = 'user' } = req.body as RegisterRequest; + const { password, firstname, lastname, role = 'user' } = req.body as RegisterRequest; - // Validate required fields - if (!email || !password || !firstname || !lastname) { + // Validate required fields - REMOVED email + if (!password || !firstname || !lastname) { return res.status(400).json({ - error: 'E-Mail, Passwort und Name sind erforderlich' + error: 'Password, firstname und lastname sind erforderlich' }); } - // Check if email already exists + // Generate email automatically + const email = generateEmail(firstname, lastname); + + // Check if generated email already exists const existingUser = await db.get( 'SELECT id FROM employees WHERE email = ?', [email] @@ -179,19 +188,19 @@ export const register = async (req: Request, res: Response) => { if (existingUser) { return res.status(400).json({ - error: 'Ein Benutzer mit dieser E-Mail existiert bereits' + error: `Ein Benutzer mit der E-Mail ${email} existiert bereits` }); } - // Hash password + // Hash password and create user const hashedPassword = await bcrypt.hash(password, 10); - // Insert user - const result = await db.run( - `INSERT INTO employees (id, email, password, firstname, lastname, role, employee_type, contract_type, can_work_alone, is_active) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [uuidv4(), email, hashedPassword, firstname, lastname, role, 'experienced', 'small', false, 1] - ); + // Insert user with generated email + const result = await db.run( + `INSERT INTO employees (id, email, password, firstname, lastname, role, employee_type, contract_type, can_work_alone, is_active) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [uuidv4(), email, hashedPassword, firstname, lastname, role, 'experienced', 'small', false, 1] + ); if (!result.lastID) { throw new Error('Benutzer konnte nicht erstellt werden'); @@ -199,7 +208,7 @@ export const register = async (req: Request, res: Response) => { // Get created user const newUser = await db.get( - 'SELECT id, email, name, role FROM employees WHERE id = ?', + 'SELECT id, email, firstname, lastname, role FROM employees WHERE id = ?', [result.lastID] ); diff --git a/backend/src/controllers/employeeController.ts b/backend/src/controllers/employeeController.ts index fcc99b1..b411e3b 100644 --- a/backend/src/controllers/employeeController.ts +++ b/backend/src/controllers/employeeController.ts @@ -6,6 +6,13 @@ import { db } from '../services/databaseService.js'; import { AuthRequest } from '../middleware/auth.js'; import { CreateEmployeeRequest } from '../models/Employee.js'; +function generateEmail(firstname: string, lastname: string): string { + // Remove special characters and convert to lowercase + const cleanFirstname = firstname.toLowerCase().replace(/[^a-z0-9]/g, ''); + const cleanLastname = lastname.toLowerCase().replace(/[^a-z0-9]/g, ''); + return `${cleanFirstname}.${cleanLastname}@sp.de`; +} + export const getEmployees = async (req: AuthRequest, res: Response): Promise => { try { console.log('πŸ” Fetching employees - User:', req.user); @@ -76,41 +83,40 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise('SELECT id FROM employees WHERE email = ? AND is_active = 1', [email]); + // Check if generated email already exists + const existingUser = await db.get('SELECT id FROM employees WHERE email = ? AND is_active = 1', [email]); - if (existingActiveUser) { - console.log('❌ Email exists for active user:', existingActiveUser); - res.status(409).json({ error: 'Email already exists' }); + if (existingUser) { + console.log('❌ Generated email already exists:', email); + res.status(409).json({ + error: `Employee with email ${email} already exists. Please use different firstname/lastname.` + }); return; } - // Hash password + // Hash password and create employee const hashedPassword = await bcrypt.hash(password, 10); const employeeId = uuidv4(); @@ -123,8 +129,8 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise(` SELECT - id, email, name, role, is_active as isActive, + id, email, firstname, lastname, role, is_active as isActive, employee_type as employeeType, contract_type as contractType, can_work_alone as canWorkAlone, @@ -157,35 +163,58 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise('SELECT * FROM employees WHERE id = ?', [id]); if (!existingEmployee) { res.status(404).json({ error: 'Employee not found' }); return; } - // Update employee + // Generate new email if firstname or lastname changed + let email = existingEmployee.email; + if (firstname || lastname) { + const newFirstname = firstname || existingEmployee.firstname; + const newLastname = lastname || existingEmployee.lastname; + email = generateEmail(newFirstname, newLastname); + + // Check if new email already exists (for another employee) + const emailExists = await db.get( + 'SELECT id FROM employees WHERE email = ? AND id != ? AND is_active = 1', + [email, id] + ); + + if (emailExists) { + res.status(409).json({ + error: `Cannot update name - email ${email} already exists for another employee` + }); + return; + } + } + + // Update employee with potentially new email await db.run( `UPDATE employees - SET firstname = COALESCE(?, firstname), - lastname = COALESCE(?, lastname), - role = COALESCE(?, role), - is_active = COALESCE(?, is_active), - employee_type = COALESCE(?, employee_type), - contract_type = COALESCE(?, contract_type), - can_work_alone = COALESCE(?, can_work_alone) - WHERE id = ?`, - [firstname, lastname, role, isActive, employeeType, contractType, canWorkAlone, id] + SET firstname = COALESCE(?, firstname), + lastname = COALESCE(?, lastname), + email = ?, + role = COALESCE(?, role), + is_active = COALESCE(?, is_active), + employee_type = COALESCE(?, employee_type), + contract_type = COALESCE(?, contract_type), + can_work_alone = COALESCE(?, can_work_alone) + WHERE id = ?`, + [firstname, lastname, email, role, isActive, employeeType, contractType, canWorkAlone, id] ); - console.log('βœ… Employee updated successfully'); + console.log('βœ… Employee updated successfully with email:', email); // Return updated employee const updatedEmployee = await db.get(` SELECT - id, email, name, role, is_active as isActive, + id, email, firstname, lastname, role, is_active as isActive, employee_type as employeeType, contract_type as contractType, can_work_alone as canWorkAlone, diff --git a/backend/src/controllers/setupController.ts b/backend/src/controllers/setupController.ts index 2ed106a..a35db8c 100644 --- a/backend/src/controllers/setupController.ts +++ b/backend/src/controllers/setupController.ts @@ -5,6 +5,25 @@ import { v4 as uuidv4 } from 'uuid'; import { randomUUID } from 'crypto'; import { db } from '../services/databaseService.js'; +// Add the same email generation function +function generateEmail(firstname: string, lastname: string): string { + // Convert German umlauts to their expanded forms + const convertUmlauts = (str: string): string => { + return str + .toLowerCase() + .replace(/ΓΌ/g, 'ue') + .replace(/ΓΆ/g, 'oe') + .replace(/Γ€/g, 'ae') + .replace(/ß/g, 'ss'); + }; + + // Remove any remaining special characters and convert to lowercase + const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, ''); + const cleanLastname = convertUmlauts(lastname).replace(/[^a-z0-9]/g, ''); + + return `${cleanFirstname}.${cleanLastname}@sp.de`; +} + export const checkSetupStatus = async (req: Request, res: Response): Promise => { try { const adminExists = await db.get<{ 'COUNT(*)': number }>( @@ -43,12 +62,11 @@ export const setupAdmin = async (req: Request, res: Response): Promise => return; } - const { password, firstname, lastname } = req.body; - const email = 'admin@instandhaltung.de'; + const { password, firstname, lastname } = req.body; // Changed from name to firstname/lastname - console.log('πŸ‘€ Creating admin with data:', { name, email }); + console.log('πŸ‘€ Creating admin with data:', { firstname, lastname }); - // Validation + // Validation - updated for firstname/lastname if (!password || !firstname || !lastname) { res.status(400).json({ error: 'Passwort, Vorname und Nachname sind erforderlich' }); return; @@ -60,6 +78,10 @@ export const setupAdmin = async (req: Request, res: Response): Promise => return; } + // Generate email automatically using the same pattern + const email = generateEmail(firstname, lastname); + console.log('πŸ“§ Generated admin email:', email); + // Hash password const hashedPassword = await bcrypt.hash(password, 10); const adminId = randomUUID(); @@ -70,18 +92,14 @@ export const setupAdmin = async (req: Request, res: Response): Promise => await db.run('BEGIN TRANSACTION'); try { - // Create admin user + // Create admin user with generated email await db.run( `INSERT INTO employees (id, email, password, firstname, lastname, role, employee_type, contract_type, can_work_alone, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [adminId, email, hashedPassword, firstname, lastname, 'admin', 'manager', 'large', true, 1] ); - console.log('βœ… Admin user created successfully'); - - // Initialize default templates WITHOUT starting a new transaction - //console.log('πŸ”„ Initialisiere Standard-Vorlagen...'); - //await initializeDefaultTemplates(adminId, false); + console.log('βœ… Admin user created successfully with email:', email); // Commit the entire setup transaction await db.run('COMMIT'); @@ -91,7 +109,9 @@ export const setupAdmin = async (req: Request, res: Response): Promise => res.status(201).json({ success: true, message: 'Admin erfolgreich erstellt', - email: email + email: email, + firstname: firstname, + lastname: lastname }); } catch (dbError) { diff --git a/backend/src/models/Employee.ts b/backend/src/models/Employee.ts index 325b1b6..87623ed 100644 --- a/backend/src/models/Employee.ts +++ b/backend/src/models/Employee.ts @@ -14,7 +14,6 @@ export interface Employee { } export interface CreateEmployeeRequest { - email: string; password: string; firstname: string; lastname: string; diff --git a/backend/src/models/ShiftPlan.ts b/backend/src/models/ShiftPlan.ts index 932a2ea..e0eca0f 100644 --- a/backend/src/models/ShiftPlan.ts +++ b/backend/src/models/ShiftPlan.ts @@ -3,15 +3,15 @@ export interface ShiftPlan { id: string; name: string; description?: string; - startDate?: string; // Optional for templates - endDate?: string; // Optional for templates + startDate?: string; + endDate?: string; isTemplate: boolean; status: 'draft' | 'published' | 'archived' | 'template'; createdBy: string; createdAt: string; timeSlots: TimeSlot[]; shifts: Shift[]; - scheduledShifts?: ScheduledShift[]; // Only for non-template plans with dates + scheduledShifts?: ScheduledShift[]; } export interface TimeSlot { @@ -85,7 +85,6 @@ export interface AssignEmployeeRequest { scheduledShiftId: string; } - export interface UpdateRequiredEmployeesRequest { requiredEmployees: number; } \ No newline at end of file diff --git a/backend/src/models/scheduling.ts b/backend/src/models/scheduling.ts index 2f27e17..5943cc1 100644 --- a/backend/src/models/scheduling.ts +++ b/backend/src/models/scheduling.ts @@ -2,15 +2,20 @@ import { Employee } from './Employee.js'; import { ShiftPlan } from './ShiftPlan.js'; -// Add the missing type definitions +// Updated Availability interface to match new schema export interface Availability { id: string; employeeId: string; planId: string; - dayOfWeek: number; // 1=Monday, 7=Sunday - timeSlotId: string; + shiftId: string; // Now references shift_id instead of time_slot_id + day_of_week preferenceLevel: 1 | 2 | 3; // 1:preferred, 2:available, 3:unavailable notes?: string; + // Optional convenience fields (can be joined from shifts and time_slots tables) + dayOfWeek?: number; + timeSlotId?: string; + timeSlotName?: string; + startTime?: string; + endTime?: string; } export interface Constraint { @@ -120,4 +125,77 @@ export interface ShiftRequirement { maxEmployees: number; assignedEmployees: string[]; isPriority: boolean; +} + +// New types for the updated schema +export interface EmployeeWithRoles extends Employee { + roles: string[]; +} + +export interface ShiftWithDetails { + id: string; + planId: string; + timeSlotId: string; + dayOfWeek: number; + requiredEmployees: number; + color?: string; + timeSlot: { + id: string; + name: string; + startTime: string; + endTime: string; + description?: string; + }; +} + +export interface AvailabilityWithDetails extends Availability { + employee?: { + id: string; + firstname: string; + lastname: string; + employeeType: 'manager' | 'trainee' | 'experienced'; + canWorkAlone: boolean; + }; + shift?: { + dayOfWeek: number; + timeSlotId: string; + timeSlot?: { + name: string; + startTime: string; + endTime: string; + }; + }; +} + +// Types for scheduling algorithm input +export interface SchedulingInput { + planId: string; + startDate: string; + endDate: string; + constraints: Constraint[]; + options?: SolverOptions; +} + +// Types for scheduling results with enhanced information +export interface EnhancedAssignment extends Assignment { + employeeName: string; + shiftDetails: { + date: string; + dayOfWeek: number; + timeSlotName: string; + startTime: string; + endTime: string; + }; + preferenceLevel: number; +} + +export interface SchedulingStatistics { + totalShifts: number; + totalAssignments: number; + coverageRate: number; + preferenceSatisfaction: number; + constraintViolations: number; + hardConstraintViolations: number; + softConstraintViolations: number; + processingTime: number; } \ No newline at end of file diff --git a/frontend/src/models/Employee.ts b/frontend/src/models/Employee.ts index 68aa0e1..87623ed 100644 --- a/frontend/src/models/Employee.ts +++ b/frontend/src/models/Employee.ts @@ -2,7 +2,8 @@ export interface Employee { id: string; email: string; - name: string; + firstname: string; + lastname: string; role: 'admin' | 'maintenance' | 'user'; employeeType: 'manager' | 'trainee' | 'experienced'; contractType: 'small' | 'large'; @@ -13,9 +14,9 @@ export interface Employee { } export interface CreateEmployeeRequest { - email: string; password: string; - name: string; + firstname: string; + lastname: string; role: 'admin' | 'maintenance' | 'user'; employeeType: 'manager' | 'trainee' | 'experienced'; contractType: 'small' | 'large'; @@ -23,7 +24,8 @@ export interface CreateEmployeeRequest { } export interface UpdateEmployeeRequest { - name?: string; + firstname?: string; + lastname?: string; role?: 'admin' | 'maintenance' | 'user'; employeeType?: 'manager' | 'trainee' | 'experienced'; contractType?: 'small' | 'large'; @@ -39,8 +41,7 @@ export interface EmployeeAvailability { id: string; employeeId: string; planId: string; - dayOfWeek: number; // 1=Monday, 7=Sunday - timeSlotId: string; + shiftId: string; // Now references shift_id instead of time_slot_id + day_of_week preferenceLevel: 1 | 2 | 3; // 1:preferred, 2:available, 3:unavailable notes?: string; } @@ -72,4 +73,14 @@ export interface ManagerSelfAssignmentRequest { export interface EmployeeWithAvailabilities extends Employee { availabilities: EmployeeAvailability[]; +} + +// Additional types for the new roles system +export interface Role { + role: 'admin' | 'user' | 'maintenance'; +} + +export interface EmployeeRole { + employeeId: string; + role: 'admin' | 'user' | 'maintenance'; } \ No newline at end of file diff --git a/frontend/src/models/ShiftPlan.ts b/frontend/src/models/ShiftPlan.ts index bd2b05e..d44f6f3 100644 --- a/frontend/src/models/ShiftPlan.ts +++ b/frontend/src/models/ShiftPlan.ts @@ -3,14 +3,15 @@ export interface ShiftPlan { id: string; name: string; description?: string; - startDate?: string; // Optional for templates - endDate?: string; // Optional for templates + startDate?: string; + endDate?: string; isTemplate: boolean; status: 'draft' | 'published' | 'archived' | 'template'; createdBy: string; createdAt: string; timeSlots: TimeSlot[]; shifts: Shift[]; + scheduledShifts?: ScheduledShift[]; } export interface TimeSlot { @@ -49,16 +50,6 @@ export interface ShiftAssignment { assignedBy: string; } -export interface EmployeeAvailability { - id: string; - employeeId: string; - planId: string; - dayOfWeek: number; - timeSlotId: string; - preferenceLevel: 1 | 2 | 3; // 1:preferred, 2:available, 3:unavailable - notes?: string; -} - // Request/Response DTOs export interface CreateShiftPlanRequest { name: string; @@ -68,7 +59,6 @@ export interface CreateShiftPlanRequest { isTemplate: boolean; timeSlots: Omit[]; shifts: Omit[]; - templateId?: string; } export interface UpdateShiftPlanRequest { @@ -95,10 +85,6 @@ export interface AssignEmployeeRequest { scheduledShiftId: string; } -export interface UpdateAvailabilityRequest { - planId: string; - availabilities: Omit[]; -} export interface UpdateRequiredEmployeesRequest { requiredEmployees: number; diff --git a/frontend/src/models/scheduling.ts b/frontend/src/models/scheduling.ts index c6427bc..5943cc1 100644 --- a/frontend/src/models/scheduling.ts +++ b/frontend/src/models/scheduling.ts @@ -2,15 +2,20 @@ import { Employee } from './Employee.js'; import { ShiftPlan } from './ShiftPlan.js'; -// Add the missing type definitions +// Updated Availability interface to match new schema export interface Availability { id: string; employeeId: string; planId: string; - dayOfWeek: number; // 1=Monday, 7=Sunday - timeSlotId: string; + shiftId: string; // Now references shift_id instead of time_slot_id + day_of_week preferenceLevel: 1 | 2 | 3; // 1:preferred, 2:available, 3:unavailable notes?: string; + // Optional convenience fields (can be joined from shifts and time_slots tables) + dayOfWeek?: number; + timeSlotId?: string; + timeSlotName?: string; + startTime?: string; + endTime?: string; } export interface Constraint { @@ -75,6 +80,7 @@ export interface Solution { variablesCreated: number; optimal: boolean; }; + variables?: { [key: string]: number }; } // Additional helper types for the scheduling system @@ -119,4 +125,77 @@ export interface ShiftRequirement { maxEmployees: number; assignedEmployees: string[]; isPriority: boolean; +} + +// New types for the updated schema +export interface EmployeeWithRoles extends Employee { + roles: string[]; +} + +export interface ShiftWithDetails { + id: string; + planId: string; + timeSlotId: string; + dayOfWeek: number; + requiredEmployees: number; + color?: string; + timeSlot: { + id: string; + name: string; + startTime: string; + endTime: string; + description?: string; + }; +} + +export interface AvailabilityWithDetails extends Availability { + employee?: { + id: string; + firstname: string; + lastname: string; + employeeType: 'manager' | 'trainee' | 'experienced'; + canWorkAlone: boolean; + }; + shift?: { + dayOfWeek: number; + timeSlotId: string; + timeSlot?: { + name: string; + startTime: string; + endTime: string; + }; + }; +} + +// Types for scheduling algorithm input +export interface SchedulingInput { + planId: string; + startDate: string; + endDate: string; + constraints: Constraint[]; + options?: SolverOptions; +} + +// Types for scheduling results with enhanced information +export interface EnhancedAssignment extends Assignment { + employeeName: string; + shiftDetails: { + date: string; + dayOfWeek: number; + timeSlotName: string; + startTime: string; + endTime: string; + }; + preferenceLevel: number; +} + +export interface SchedulingStatistics { + totalShifts: number; + totalAssignments: number; + coverageRate: number; + preferenceSatisfaction: number; + constraintViolations: number; + hardConstraintViolations: number; + softConstraintViolations: number; + processingTime: number; } \ No newline at end of file