diff --git a/backend/src/controllers/authController.ts b/backend/src/controllers/authController.ts index d233a7d..6f9ed3d 100644 --- a/backend/src/controllers/authController.ts +++ b/backend/src/controllers/authController.ts @@ -1,9 +1,11 @@ // backend/src/controllers/authController.ts +import { v4 as uuidv4 } from 'uuid'; import { Request, Response } from 'express'; import jwt from 'jsonwebtoken'; import bcrypt from 'bcrypt'; import { db } from '../services/databaseService.js'; import { AuthRequest } from '../middleware/auth.js'; +import { Employee, EmployeeWithPassword } from '../models/Employee.js'; export interface User { id: number; @@ -50,8 +52,8 @@ export const login = async (req: Request, res: Response) => { } // Get user from database - const user = await db.get( - 'SELECT id, email, password, name, role FROM users WHERE email = ? AND is_active = 1', + const user = await db.get( + 'SELECT id, email, password, name, role, employee_type as employeeType, contract_type as contractType, can_work_alone as canWorkAlone, is_active as isActive FROM employees WHERE email = ? AND is_active = 1', [email] ); @@ -114,9 +116,9 @@ export const getCurrentUser = async (req: Request, res: Response) => { return res.status(401).json({ error: 'Nicht authentifiziert' }); } - const user = await db.get( - 'SELECT id, email, name, role FROM users WHERE id = ? AND is_active = 1', - [jwtUser.userId] // ← HIER: userId verwenden + const user = await db.get( + 'SELECT id, email, name, role, employee_type as employeeType, contract_type as contractType, can_work_alone as canWorkAlone, is_active as isActive FROM employees WHERE id = ? AND is_active = 1', + [jwtUser.userId] ); console.log('🔍 User found in database:', user ? 'Yes' : 'No'); @@ -171,8 +173,8 @@ export const register = async (req: Request, res: Response) => { } // Check if email already exists - const existingUser = await db.get( - 'SELECT id FROM users WHERE email = ?', + const existingUser = await db.get( + 'SELECT id FROM employees WHERE email = ?', [email] ); @@ -187,9 +189,9 @@ export const register = async (req: Request, res: Response) => { // Insert user const result = await db.run( - `INSERT INTO users (email, password, name, role) - VALUES (?, ?, ?, ?)`, - [email, hashedPassword, name, role] + `INSERT INTO employees (id, email, password, name, role, employee_type, contract_type, can_work_alone, is_active) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [uuidv4(), email, hashedPassword, name, role, 'experienced', 'small', false, 1] ); if (!result.lastID) { @@ -197,8 +199,8 @@ export const register = async (req: Request, res: Response) => { } // Get created user - const newUser = await db.get( - 'SELECT id, email, name, role FROM users WHERE id = ?', + const newUser = await db.get( + 'SELECT id, email, name, role FROM employees WHERE id = ?', [result.lastID] ); diff --git a/backend/src/controllers/shiftPlanController.ts b/backend/src/controllers/shiftPlanController.ts index 1a484c4..1f1d85c 100644 --- a/backend/src/controllers/shiftPlanController.ts +++ b/backend/src/controllers/shiftPlanController.ts @@ -3,34 +3,7 @@ import { Request, Response } from 'express'; import { v4 as uuidv4 } from 'uuid'; import { db } from '../services/databaseService.js'; import { AuthRequest } from '../middleware/auth.js'; - -export interface ShiftPlan { - id: string; - name: string; - startDate: string; - endDate: string; - templateId?: string; - status: 'draft' | 'published'; - createdBy: string; - createdAt: string; -} - -export interface AssignedShift { - id: string; - shiftPlanId: string; - date: string; - startTime: string; - endTime: string; - requiredEmployees: number; - assignedEmployees: string[]; -} - -export interface CreateShiftPlanRequest { - name: string; - startDate: string; - endDate: string; - templateId?: string; -} +import { ShiftPlan, CreateShiftPlanRequest } from '../models/ShiftPlan.js'; export const getShiftPlans = async (req: AuthRequest, res: Response): Promise => { try { diff --git a/backend/src/database/schema.sql b/backend/src/database/schema.sql index f5a19b0..9b1d93c 100644 --- a/backend/src/database/schema.sql +++ b/backend/src/database/schema.sql @@ -1,83 +1,118 @@ --- Tabelle für Benutzer -CREATE TABLE IF NOT EXISTS users ( +-- Employees table +CREATE TABLE IF NOT EXISTS employees ( 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, - employee_type TEXT CHECK(employee_type IN ('chef', 'neuling', 'erfahren')), - is_sufficiently_independent BOOLEAN DEFAULT FALSE, + role TEXT CHECK(role IN ('admin', 'user', 'maintenance')) NOT NULL, + employee_type TEXT CHECK(employee_type IN ('manager', 'trainee', 'experienced')) NOT NULL, + contract_type TEXT CHECK(contract_type IN ('small', 'large')) NOT NULL, + can_work_alone BOOLEAN DEFAULT FALSE, is_active BOOLEAN DEFAULT TRUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_login TEXT DEFAULT NULL ); --- 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, - time_slot_id TEXT NOT NULL, - day_of_week INTEGER NOT NULL CHECK (day_of_week >= 1 AND day_of_week <= 7), - required_employees INTEGER DEFAULT 1, - color TEXT DEFAULT '#3498db', - FOREIGN KEY (template_id) REFERENCES shift_templates(id) ON DELETE CASCADE, - FOREIGN KEY (time_slot_id) REFERENCES template_time_slots(id) ON DELETE CASCADE -); - --- Tabelle für Zeitbereiche in den Vorlagen -CREATE TABLE IF NOT EXISTS template_time_slots ( - id TEXT PRIMARY KEY, - template_id TEXT NOT NULL, - name TEXT NOT NULL, - start_time TEXT NOT NULL, - end_time TEXT NOT NULL, - FOREIGN KEY (template_id) REFERENCES shift_templates(id) ON DELETE CASCADE -); - --- Zusätzliche Tabellen für shift_plans +-- Shift plans table 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', + description TEXT, + start_date TEXT, + end_date TEXT, + is_template BOOLEAN DEFAULT FALSE, + status TEXT CHECK(status IN ('draft', 'published', 'archived', 'template')) DEFAULT 'draft', created_by TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (created_by) REFERENCES users(id), - FOREIGN KEY (template_id) REFERENCES shift_templates(id) + FOREIGN KEY (created_by) REFERENCES employees(id) ); -CREATE TABLE IF NOT EXISTS assigned_shifts ( +-- Time slots within plans +CREATE TABLE IF NOT EXISTS time_slots ( id TEXT PRIMARY KEY, + plan_id TEXT NOT NULL, name TEXT NOT NULL, - 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 '[]', -- JSON array of user IDs - FOREIGN KEY (shift_plan_id) REFERENCES shift_plans(id) ON DELETE CASCADE + description TEXT, + FOREIGN KEY (plan_id) REFERENCES shift_plans(id) ON DELETE CASCADE ); --- Zusätzliche Tabelle für Mitarbeiter-Verfügbarkeiten -CREATE TABLE IF NOT EXISTS employee_availabilities ( +-- Shifts table (defines shifts for each day of week in the plan) +CREATE TABLE IF NOT EXISTS shifts ( + id TEXT PRIMARY KEY, + plan_id TEXT NOT NULL, + time_slot_id TEXT NOT NULL, + day_of_week INTEGER NOT NULL CHECK (day_of_week >= 1 AND day_of_week <= 7), + required_employees INTEGER NOT NULL CHECK (required_employees >= 1 AND required_employees <= 10) DEFAULT 2, + color TEXT DEFAULT '#3498db', + FOREIGN KEY (plan_id) REFERENCES shift_plans(id) ON DELETE CASCADE, + FOREIGN KEY (time_slot_id) REFERENCES time_slots(id) ON DELETE CASCADE, + UNIQUE(plan_id, time_slot_id, day_of_week) +); + +-- Actual scheduled shifts (generated from plan + date range) +CREATE TABLE IF NOT EXISTS scheduled_shifts ( + id TEXT PRIMARY KEY, + plan_id TEXT NOT NULL, + date TEXT NOT NULL, + time_slot_id TEXT NOT NULL, + required_employees INTEGER NOT NULL CHECK (required_employees >= 1 AND required_employees <= 10) DEFAULT 2, + assigned_employees TEXT DEFAULT '[]', -- JSON array of employee IDs + FOREIGN KEY (plan_id) REFERENCES shift_plans(id) ON DELETE CASCADE, + FOREIGN KEY (time_slot_id) REFERENCES time_slots(id), + UNIQUE(plan_id, date, time_slot_id) +); + +-- Employee assignments to specific shifts +CREATE TABLE IF NOT EXISTS shift_assignments ( + id TEXT PRIMARY KEY, + scheduled_shift_id TEXT NOT NULL, + employee_id TEXT NOT NULL, + assignment_status TEXT CHECK(assignment_status IN ('assigned', 'cancelled')) DEFAULT 'assigned', + assigned_at DATETIME DEFAULT CURRENT_TIMESTAMP, + assigned_by TEXT NOT NULL, + FOREIGN KEY (scheduled_shift_id) REFERENCES scheduled_shifts(id) ON DELETE CASCADE, + FOREIGN KEY (employee_id) REFERENCES employees(id), + FOREIGN KEY (assigned_by) REFERENCES employees(id), + UNIQUE(scheduled_shift_id, employee_id) +); + +-- Employee availability preferences for specific shift plans +CREATE TABLE IF NOT EXISTS employee_availability ( id TEXT PRIMARY KEY, employee_id TEXT NOT NULL, - day_of_week INTEGER NOT NULL CHECK (day_of_week >= 0 AND day_of_week <= 6), - start_time TEXT NOT NULL, - end_time TEXT NOT NULL, - is_available BOOLEAN DEFAULT FALSE, - FOREIGN KEY (employee_id) REFERENCES users(id) ON DELETE CASCADE + plan_id TEXT NOT NULL, + day_of_week INTEGER NOT NULL CHECK (day_of_week >= 1 AND day_of_week <= 7), + time_slot_id TEXT NOT NULL, + preference_level INTEGER CHECK(preference_level IN (1, 2, 3)) NOT NULL, + notes TEXT, + FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE, + FOREIGN KEY (plan_id) REFERENCES shift_plans(id) ON DELETE CASCADE, + FOREIGN KEY (time_slot_id) REFERENCES time_slots(id), + UNIQUE(employee_id, plan_id, day_of_week, time_slot_id) ); + +-- Performance indexes +CREATE INDEX IF NOT EXISTS idx_employees_role_active ON employees(role, is_active); +CREATE INDEX IF NOT EXISTS idx_employees_email_active ON employees(email, is_active); +CREATE INDEX IF NOT EXISTS idx_employees_type_active ON employees(employee_type, is_active); + +CREATE INDEX IF NOT EXISTS idx_shift_plans_status_date ON shift_plans(status, start_date, end_date); +CREATE INDEX IF NOT EXISTS idx_shift_plans_created_by ON shift_plans(created_by); +CREATE INDEX IF NOT EXISTS idx_shift_plans_template ON shift_plans(is_template, status); + +CREATE INDEX IF NOT EXISTS idx_time_slots_plan ON time_slots(plan_id); + +CREATE INDEX IF NOT EXISTS idx_shifts_plan_day ON shifts(plan_id, day_of_week); +CREATE INDEX IF NOT EXISTS idx_shifts_required_employees ON shifts(required_employees); +CREATE INDEX IF NOT EXISTS idx_shifts_plan_time ON shifts(plan_id, time_slot_id, day_of_week); + +CREATE INDEX IF NOT EXISTS idx_scheduled_shifts_plan_date ON scheduled_shifts(plan_id, date); +CREATE INDEX IF NOT EXISTS idx_scheduled_shifts_date_time ON scheduled_shifts(date, time_slot_id); +CREATE INDEX IF NOT EXISTS idx_scheduled_shifts_required_employees ON scheduled_shifts(required_employees); + +CREATE INDEX IF NOT EXISTS idx_shift_assignments_employee ON shift_assignments(employee_id); +CREATE INDEX IF NOT EXISTS idx_shift_assignments_shift ON shift_assignments(scheduled_shift_id); + +CREATE INDEX IF NOT EXISTS idx_employee_availability_employee_plan ON employee_availability(employee_id, plan_id); \ No newline at end of file diff --git a/backend/src/models/Employee.ts b/backend/src/models/Employee.ts index 680dbe2..68aa0e1 100644 --- a/backend/src/models/Employee.ts +++ b/backend/src/models/Employee.ts @@ -3,9 +3,10 @@ export interface Employee { id: string; email: string; name: string; - role: 'admin' | 'instandhalter' | 'user'; - employeeType: 'chef' | 'neuling' | 'erfahren'; - isSufficientlyIndependent: boolean; + role: 'admin' | 'maintenance' | 'user'; + employeeType: 'manager' | 'trainee' | 'experienced'; + contractType: 'small' | 'large'; + canWorkAlone: boolean; isActive: boolean; createdAt: string; lastLogin?: string | null; @@ -15,19 +16,60 @@ export interface CreateEmployeeRequest { email: string; password: string; name: string; - role: 'admin' | 'instandhalter' | 'user'; - employeeType: 'chef' | 'neuling' | 'erfahren'; - isSufficientlyIndependent: boolean; + role: 'admin' | 'maintenance' | 'user'; + employeeType: 'manager' | 'trainee' | 'experienced'; + contractType: 'small' | 'large'; + canWorkAlone: boolean; } export interface UpdateEmployeeRequest { name?: string; - role?: 'admin' | 'instandhalter' | 'user'; - employeeType?: 'chef' | 'neuling' | 'erfahren'; - isSufficientlyIndependent?: boolean; + role?: 'admin' | 'maintenance' | 'user'; + employeeType?: 'manager' | 'trainee' | 'experienced'; + contractType?: 'small' | 'large'; + canWorkAlone?: boolean; isActive?: boolean; } export interface EmployeeWithPassword extends Employee { password: string; +} + +export interface EmployeeAvailability { + id: string; + employeeId: string; + planId: string; + dayOfWeek: number; // 1=Monday, 7=Sunday + timeSlotId: string; + preferenceLevel: 1 | 2 | 3; // 1:preferred, 2:available, 3:unavailable + notes?: string; +} + +export interface ManagerAvailability { + id: string; + employeeId: string; + planId: string; + dayOfWeek: number; // 1=Monday, 7=Sunday + timeSlotId: string; + isAvailable: boolean; // Simple available/not available + assignedBy: string; // Always self for manager +} + +export interface CreateAvailabilityRequest { + planId: string; + availabilities: Omit[]; +} + +export interface UpdateAvailabilityRequest { + planId: string; + availabilities: Omit[]; +} + +export interface ManagerSelfAssignmentRequest { + planId: string; + assignments: Omit[]; +} + +export interface EmployeeWithAvailabilities extends Employee { + availabilities: EmployeeAvailability[]; } \ No newline at end of file diff --git a/backend/src/models/Shift.ts b/backend/src/models/Shift.ts deleted file mode 100644 index df2a78f..0000000 --- a/backend/src/models/Shift.ts +++ /dev/null @@ -1,55 +0,0 @@ -// backend/src/models/Shift.ts -export interface Shift { - id: string; - name: string; - description?: string; - isDefault: boolean; - createdBy: string; - createdAt: string; - shifts: ShiftSlot[]; -} - -export interface ShiftSlot { - id: string; - shiftId: string; - dayOfWeek: number; - name: string; - startTime: string; - endTime: string; - requiredEmployees: number; - color?: string; -} - -export interface CreateShiftRequest { - name: string; - description?: string; - isDefault: boolean; - shifts: Omit[]; -} - -export interface UpdateShiftSlotRequest { - name?: string; - description?: string; - isDefault?: boolean; - shifts?: Omit[]; -} - -export interface ShiftPlan { - id: string; - name: string; - startDate: string; - endDate: string; - templateId?: string; - shifts: AssignedShift[]; - status: 'draft' | 'published'; - createdBy: string; -} - -export interface AssignedShift { - id: string; - date: string; - startTime: string; - endTime: string; - requiredEmployees: number; - assignedEmployees: string[]; -} diff --git a/backend/src/models/ShiftPlan.ts b/backend/src/models/ShiftPlan.ts new file mode 100644 index 0000000..5c8a4c4 --- /dev/null +++ b/backend/src/models/ShiftPlan.ts @@ -0,0 +1,219 @@ +// backend/src/models/ShiftPlan.ts +export interface ShiftPlan { + id: string; + name: string; + description?: string; + startDate?: string; // Optional for templates + endDate?: string; // Optional for templates + isTemplate: boolean; + status: 'draft' | 'published' | 'archived' | 'template'; + createdBy: string; + createdAt: string; + timeSlots: TimeSlot[]; + shifts: Shift[]; + scheduledShifts?: ScheduledShift[]; // Only for non-template plans with dates +} + +export interface TimeSlot { + id: string; + planId: string; + name: string; + startTime: string; + endTime: string; + description?: string; +} + +export interface Shift { + id: string; + planId: string; + timeSlotId: string; + dayOfWeek: number; // 1=Monday, 7=Sunday + requiredEmployees: number; + color?: string; +} + +export interface ScheduledShift { + id: string; + planId: string; + date: string; + timeSlotId: string; + requiredEmployees: number; + assignedEmployees: string[]; // employee IDs +} + +export interface ShiftAssignment { + id: string; + scheduledShiftId: string; + employeeId: string; + assignmentStatus: 'assigned' | 'cancelled'; + assignedAt: string; + 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; + description?: string; + startDate?: string; + endDate?: string; + isTemplate: boolean; + timeSlots: Omit[]; + shifts: Omit[]; +} + +export interface UpdateShiftPlanRequest { + name?: string; + description?: string; + startDate?: string; + endDate?: string; + status?: 'draft' | 'published' | 'archived' | 'template'; + timeSlots?: Omit[]; + shifts?: Omit[]; +} + +export interface CreateShiftFromTemplateRequest { + templatePlanId: string; + name: string; + startDate: string; + endDate: string; + description?: string; +} + +export interface AssignEmployeeRequest { + employeeId: string; + scheduledShiftId: string; +} + +export interface UpdateAvailabilityRequest { + planId: string; + availabilities: Omit[]; +} + +// Default time slots for ZEBRA (specific workplace) +export const DEFAULT_ZEBRA_TIME_SLOTS: Omit[] = [ + { + name: 'Vormittag', + startTime: '08:00', + endTime: '12:00', + description: 'Vormittagsschicht' + }, + { + name: 'Nachmittag', + startTime: '11:30', + endTime: '15:30', + description: 'Nachmittagsschicht' + }, +]; + +// Default time slots for general use +export const DEFAULT_TIME_SLOTS: Omit[] = [ + { + name: 'Vormittag', + startTime: '08:00', + endTime: '12:00', + description: 'Vormittagsschicht' + }, + { + name: 'Nachmittag', + startTime: '11:30', + endTime: '15:30', + description: 'Nachmittagsschicht' + }, + { + name: 'Abend', + startTime: '14:00', + endTime: '18:00', + description: 'Abendschicht' + }, +]; + +// Helper functions +export function validateRequiredEmployees(shift: Shift | ScheduledShift): string[] { + const errors: string[] = []; + + if (shift.requiredEmployees < 1) { + errors.push('Required employees must be at least 1'); + } + + if (shift.requiredEmployees > 10) { + errors.push('Required employees cannot exceed 10'); + } + + return errors; +} + + +export function isTemplate(plan: ShiftPlan): boolean { + return plan.isTemplate || plan.status === 'template'; +} + +export function hasDateRange(plan: ShiftPlan): boolean { + return !isTemplate(plan) && !!plan.startDate && !!plan.endDate; +} + +export function validatePlanDates(plan: ShiftPlan): string[] { + const errors: string[] = []; + + if (!isTemplate(plan)) { + if (!plan.startDate) errors.push('Start date is required for non-template plans'); + if (!plan.endDate) errors.push('End date is required for non-template plans'); + if (plan.startDate && plan.endDate && plan.startDate > plan.endDate) { + errors.push('Start date must be before end date'); + } + } + + return errors; +} + +// Type guards +export function isScheduledShift(shift: Shift | ScheduledShift): shift is ScheduledShift { + return 'date' in shift; +} + +// Template presets for quick setup +// Default shifts for ZEBRA standard week template with variable required employees +export const DEFAULT_ZEBRA_SHIFTS: Omit[] = [ + // Monday-Thursday: Morning + Afternoon + ...Array.from({ length: 4 }, (_, i) => i + 1).flatMap(day => [ + { timeSlotId: 'morning', dayOfWeek: day, requiredEmployees: 2, color: '#3498db' }, + { timeSlotId: 'afternoon', dayOfWeek: day, requiredEmployees: 2, color: '#e74c3c' } + ]), + // Friday: Morning only + { timeSlotId: 'morning', dayOfWeek: 5, requiredEmployees: 2, color: '#3498db' } +]; + +// Default shifts for general standard week template with variable required employees +export const DEFAULT_SHIFTS: Omit[] = [ + // Monday-Friday: Morning + Afternoon + Evening + ...Array.from({ length: 5 }, (_, i) => i + 1).flatMap(day => [ + { timeSlotId: 'morning', dayOfWeek: day, requiredEmployees: 2, color: '#3498db' }, + { timeSlotId: 'afternoon', dayOfWeek: day, requiredEmployees: 2, color: '#e74c3c' }, + { timeSlotId: 'evening', dayOfWeek: day, requiredEmployees: 1, color: '#2ecc71' } // Only 1 for evening + ]) +]; + +// Template presets for quick creation +export const TEMPLATE_PRESETS = { + ZEBRA_STANDARD: { + name: 'ZEBRA Standardwoche', + description: 'Standard Vorlage für ZEBRA: Mo-Do Vormittag+Nachmittag, Fr nur Vormittag', + timeSlots: DEFAULT_ZEBRA_TIME_SLOTS, + shifts: DEFAULT_ZEBRA_SHIFTS + }, + GENERAL_STANDARD: { + name: 'Standard Wochenplan', + description: 'Standard Vorlage: Mo-Fr Vormittag+Nachmittag+Abend', + timeSlots: DEFAULT_TIME_SLOTS, + shifts: DEFAULT_SHIFTS + } +} as const; \ No newline at end of file diff --git a/backend/src/models/ShiftTemplate.ts b/backend/src/models/ShiftTemplate.ts deleted file mode 100644 index 4ba74ca..0000000 --- a/backend/src/models/ShiftTemplate.ts +++ /dev/null @@ -1,48 +0,0 @@ -// backend/src/models/ShiftTemplate.ts -export interface TemplateShift { - id: string; - name: string; - description?: string; - isDefault: boolean; - createdBy: string; - createdAt: string; - shifts: TemplateShiftSlot[]; -} - -export interface TemplateShiftSlot { - id: string; - templateId: string; - dayOfWeek: number; - timeSlot: TemplateShiftTimeSlot; - requiredEmployees: number; - color?: string; -} - -export interface TemplateShiftTimeSlot { - id: string; - name: string; // e.g., "Frühschicht", "Spätschicht" - startTime: string; - endTime: string; -} - -export const DEFAULT_TIME_SLOTS: TemplateShiftTimeSlot[] = [ - { id: 'morning', name: 'Vormittag', startTime: '08:00', endTime: '12:00' }, - { id: 'afternoon', name: 'Nachmittag', startTime: '11:30', endTime: '15:30' }, -]; - - -export interface CreateShiftTemplateRequest { - name: string; - description?: string; - isDefault: boolean; - shifts: Omit[]; - timeSlots: TemplateShiftTimeSlot[]; -} - -export interface UpdateShiftTemplateRequest { - name?: string; - description?: string; - isDefault?: boolean; - shifts?: Omit[]; - timeSlots?: TemplateShiftTimeSlot[]; -} \ No newline at end of file diff --git a/backend/src/models/User.ts b/backend/src/models/User.ts deleted file mode 100644 index dea3e74..0000000 --- a/backend/src/models/User.ts +++ /dev/null @@ -1,15 +0,0 @@ -// backend/src/models/User.ts -export interface User { - id: string; - email: string; - password: string; // gehashed - name: string; - role: 'admin' | 'instandhalter' | 'user'; - createdAt: Date; -} - -export interface UserSession { - userId: string; - token: string; - expiresAt: Date; -} \ No newline at end of file diff --git a/backend/src/models/defaults/employeeDefaults.ts b/backend/src/models/defaults/employeeDefaults.ts new file mode 100644 index 0000000..671466d --- /dev/null +++ b/backend/src/models/defaults/employeeDefaults.ts @@ -0,0 +1,85 @@ +// backend/src/models/defaults/employeeDefaults.ts +import { EmployeeAvailability, ManagerAvailability } from '../Employee.js'; + +// Default employee data for quick creation +export const EMPLOYEE_DEFAULTS = { + role: 'user' as const, + employeeType: 'experienced' as const, + contractType: 'small' as const, + canWorkAlone: false, + isActive: true +}; + +// Manager-specific defaults +export const MANAGER_DEFAULTS = { + role: 'admin' as const, + employeeType: 'manager' as const, + contractType: 'large' as const, // Not really used but required by DB + canWorkAlone: true, + isActive: true +}; + +// Contract type descriptions +export const CONTRACT_TYPE_DESCRIPTIONS = { + small: '1 Schicht pro Woche', + large: '2 Schichten pro Woche', + manager: 'Kein Vertragslimit - Immer MO und DI verfügbar' +} as const; + +// Employee type descriptions +export const EMPLOYEE_TYPE_DESCRIPTIONS = { + manager: 'Chef - Immer MO und DI in beiden Schichten, kann eigene Schichten festlegen', + trainee: 'Neuling - Darf nicht alleine sein, benötigt erfahrene Begleitung', + experienced: 'Erfahren - Kann alleine arbeiten (wenn freigegeben)' +} as const; + +// Availability preference descriptions +export const AVAILABILITY_PREFERENCES = { + 1: { label: 'Bevorzugt', color: '#10b981', description: 'Möchte diese Schicht arbeiten' }, + 2: { label: 'Möglich', color: '#f59e0b', description: 'Kann diese Schicht arbeiten' }, + 3: { label: 'Nicht möglich', color: '#ef4444', description: 'Kann diese Schicht nicht arbeiten' } +} as const; + +// Default availability for new employees (all shifts unavailable as level 3) +export function createDefaultAvailabilities(employeeId: string, planId: string, timeSlotIds: string[]): Omit[] { + const availabilities: Omit[] = []; + + // Monday to Friday (1-5) + for (let day = 1; day <= 5; day++) { + for (const timeSlotId of timeSlotIds) { + availabilities.push({ + employeeId, + planId, + dayOfWeek: day, + timeSlotId, + preferenceLevel: 3 // Default to "unavailable" - employees must explicitly set availability + }); + } + } + + return availabilities; +} + +// Create complete manager availability for all days (default: only Mon-Tue available) +export function createManagerDefaultSchedule(managerId: string, planId: string, timeSlotIds: string[]): Omit[] { + const assignments: Omit[] = []; + + // Monday to Sunday (1-7) + for (let dayOfWeek = 1; dayOfWeek <= 7; dayOfWeek++) { + for (const timeSlotId of timeSlotIds) { + // Default: available only on Monday (1) and Tuesday (2) + const isAvailable = dayOfWeek === 1 || dayOfWeek === 2; + + assignments.push({ + employeeId: managerId, + planId, + dayOfWeek, + timeSlotId, + isAvailable, + assignedBy: managerId + }); + } + } + + return assignments; +} \ No newline at end of file diff --git a/backend/src/models/defaults/index.ts b/backend/src/models/defaults/index.ts new file mode 100644 index 0000000..be13429 --- /dev/null +++ b/backend/src/models/defaults/index.ts @@ -0,0 +1,3 @@ +// backend/src/models/defaults/index.ts +export * from './employeeDefaults.js'; +export * from './shiftPlanDefaults.js'; \ No newline at end of file diff --git a/backend/src/models/defaults/shiftPlanDefaults.ts b/backend/src/models/defaults/shiftPlanDefaults.ts new file mode 100644 index 0000000..5deacd5 --- /dev/null +++ b/backend/src/models/defaults/shiftPlanDefaults.ts @@ -0,0 +1,146 @@ +// backend/src/models/defaults/shiftPlanDefaults.ts +import { TimeSlot, Shift } from '../ShiftPlan.js'; + +// Default time slots for ZEBRA (specific workplace) +export const DEFAULT_ZEBRA_TIME_SLOTS: Omit[] = [ + { + name: 'Vormittag', + startTime: '08:00', + endTime: '12:00', + description: 'Vormittagsschicht' + }, + { + name: 'Nachmittag', + startTime: '11:30', + endTime: '15:30', + description: 'Nachmittagsschicht' + }, +]; + +// Default time slots for general use +export const DEFAULT_TIME_SLOTS: Omit[] = [ + { + name: 'Vormittag', + startTime: '08:00', + endTime: '12:00', + description: 'Vormittagsschicht' + }, + { + name: 'Nachmittag', + startTime: '11:30', + endTime: '15:30', + description: 'Nachmittagsschicht' + }, + { + name: 'Abend', + startTime: '14:00', + endTime: '18:00', + description: 'Abendschicht' + }, +]; + +// Default shifts for ZEBRA standard week template with variable required employees +export const DEFAULT_ZEBRA_SHIFTS: Omit[] = [ + // Monday-Thursday: Morning + Afternoon + ...Array.from({ length: 4 }, (_, i) => i + 1).flatMap(day => [ + { timeSlotId: 'morning', dayOfWeek: day, requiredEmployees: 2, color: '#3498db' }, + { timeSlotId: 'afternoon', dayOfWeek: day, requiredEmployees: 2, color: '#e74c3c' } + ]), + // Friday: Morning only + { timeSlotId: 'morning', dayOfWeek: 5, requiredEmployees: 2, color: '#3498db' } +]; + +// Default shifts for general standard week template with variable required employees +export const DEFAULT_SHIFTS: Omit[] = [ + // Monday-Friday: Morning + Afternoon + Evening + ...Array.from({ length: 5 }, (_, i) => i + 1).flatMap(day => [ + { timeSlotId: 'morning', dayOfWeek: day, requiredEmployees: 2, color: '#3498db' }, + { timeSlotId: 'afternoon', dayOfWeek: day, requiredEmployees: 2, color: '#e74c3c' }, + { timeSlotId: 'evening', dayOfWeek: day, requiredEmployees: 1, color: '#2ecc71' } // Only 1 for evening + ]) +]; + +// Template presets for quick creation +export const TEMPLATE_PRESETS = { + ZEBRA_STANDARD: { + name: 'ZEBRA Standardwoche', + description: 'Standard Vorlage für ZEBRA: Mo-Do Vormittag+Nachmittag, Fr nur Vormittag', + timeSlots: DEFAULT_ZEBRA_TIME_SLOTS, + shifts: DEFAULT_ZEBRA_SHIFTS + }, + ZEBRA_MINIMAL: { + name: 'ZEBRA Minimal', + description: 'ZEBRA mit minimaler Besetzung', + timeSlots: DEFAULT_ZEBRA_TIME_SLOTS, + shifts: [ + ...Array.from({ length: 5 }, (_, i) => i + 1).flatMap(day => [ + { timeSlotId: 'morning', dayOfWeek: day, requiredEmployees: 1, color: '#3498db' }, + { timeSlotId: 'afternoon', dayOfWeek: day, requiredEmployees: 1, color: '#e74c3c' } + ]) + ] + }, + ZEBRA_FULL: { + name: 'ZEBRA Vollbesetzung', + description: 'ZEBRA mit voller Besetzung', + timeSlots: DEFAULT_ZEBRA_TIME_SLOTS, + shifts: [ + ...Array.from({ length: 5 }, (_, i) => i + 1).flatMap(day => [ + { timeSlotId: 'morning', dayOfWeek: day, requiredEmployees: 3, color: '#3498db' }, + { timeSlotId: 'afternoon', dayOfWeek: day, requiredEmployees: 3, color: '#e74c3c' } + ]) + ] + }, + GENERAL_STANDARD: { + name: 'Standard Wochenplan', + description: 'Standard Vorlage: Mo-Fr Vormittag+Nachmittag+Abend', + timeSlots: DEFAULT_TIME_SLOTS, + shifts: DEFAULT_SHIFTS + }, + ZEBRA_PART_TIME: { + name: 'ZEBRA Teilzeit', + description: 'ZEBRA Vorlage mit reduzierten Schichten', + timeSlots: DEFAULT_ZEBRA_TIME_SLOTS, + shifts: [ + // Monday-Thursday: Morning only + ...Array.from({ length: 4 }, (_, i) => i + 1).map(day => ({ + timeSlotId: 'morning', dayOfWeek: day, requiredEmployees: 1, color: '#3498db' + })) + ] + } +} as const; + +// Helper function to create plan from preset +export function createPlanFromPreset( + presetName: keyof typeof TEMPLATE_PRESETS, + isTemplate: boolean = true, + startDate?: string, + endDate?: string +) { + const preset = TEMPLATE_PRESETS[presetName]; + return { + name: preset.name, + description: preset.description, + startDate, + endDate, + isTemplate, + timeSlots: preset.timeSlots, + shifts: preset.shifts + }; +} + +// Color schemes for shifts +export const SHIFT_COLORS = { + morning: '#3498db', // Blue + afternoon: '#e74c3c', // Red + evening: '#2ecc71', // Green + night: '#9b59b6', // Purple + default: '#95a5a6' // Gray +} as const; + +// Status descriptions +export const PLAN_STATUS_DESCRIPTIONS = { + draft: 'Entwurf - Kann bearbeitet werden', + published: 'Veröffentlicht - Für alle sichtbar', + archived: 'Archiviert - Nur noch lesbar', + template: 'Vorlage - Kann für neue Pläne verwendet werden' +} as const; \ No newline at end of file diff --git a/backend/src/models/helpers/employeeHelpers.ts b/backend/src/models/helpers/employeeHelpers.ts new file mode 100644 index 0000000..0f9fa35 --- /dev/null +++ b/backend/src/models/helpers/employeeHelpers.ts @@ -0,0 +1,128 @@ +// backend/src/models/helpers/employeeHelpers.ts +import { Employee, CreateEmployeeRequest, EmployeeAvailability, ManagerAvailability } from '../Employee.js'; + +// Validation helpers +export function validateEmployee(employee: CreateEmployeeRequest): string[] { + const errors: string[] = []; + + if (!employee.email || !employee.email.includes('@')) { + errors.push('Valid email is required'); + } + + if (!employee.password || employee.password.length < 6) { + errors.push('Password must be at least 6 characters long'); + } + + if (!employee.name || employee.name.trim().length < 2) { + errors.push('Name is required and must be at least 2 characters long'); + } + + if (!employee.contractType) { + errors.push('Contract type is required'); + } + + return errors; +} + +export function validateAvailability(availability: Omit): string[] { + const errors: string[] = []; + + if (availability.dayOfWeek < 1 || availability.dayOfWeek > 7) { + errors.push('Day of week must be between 1 and 7'); + } + + if (![1, 2, 3].includes(availability.preferenceLevel)) { + errors.push('Preference level must be 1, 2, or 3'); + } + + if (!availability.timeSlotId) { + errors.push('Time slot ID is required'); + } + + if (!availability.planId) { + errors.push('Plan ID is required'); + } + + return errors; +} + +// Employee type guards +export function isManager(employee: Employee): boolean { + return employee.employeeType === 'manager'; +} + +export function isTrainee(employee: Employee): boolean { + return employee.employeeType === 'trainee'; +} + +export function isExperienced(employee: Employee): boolean { + return employee.employeeType === 'experienced'; +} + +export function isAdmin(employee: Employee): boolean { + return employee.role === 'admin'; +} + +// Business logic helpers +export function canEmployeeWorkAlone(employee: Employee): boolean { + return employee.canWorkAlone && employee.employeeType === 'experienced'; +} + +export function getEmployeeWorkHours(employee: Employee): number { + // Manager: no contract limit, others: small=1, large=2 shifts per week + return isManager(employee) ? 999 : (employee.contractType === 'small' ? 1 : 2); +} + +export function requiresAvailabilityPreference(employee: Employee): boolean { + // Only non-managers use the preference system + return !isManager(employee); +} + +export function canSetOwnAvailability(employee: Employee): boolean { + // Manager can set their own specific shift assignments + return isManager(employee); +} + +// Manager availability helpers +export function isManagerAvailable( + managerAssignments: ManagerAvailability[], + dayOfWeek: number, + timeSlotId: string +): boolean { + const assignment = managerAssignments.find(assignment => + assignment.dayOfWeek === dayOfWeek && + assignment.timeSlotId === timeSlotId + ); + + return assignment ? assignment.isAvailable : false; +} + +export function getManagerAvailableShifts(managerAssignments: ManagerAvailability[]): ManagerAvailability[] { + return managerAssignments.filter(assignment => assignment.isAvailable); +} + +export function updateManagerAvailability( + assignments: ManagerAvailability[], + dayOfWeek: number, + timeSlotId: string, + isAvailable: boolean +): ManagerAvailability[] { + return assignments.map(assignment => + assignment.dayOfWeek === dayOfWeek && assignment.timeSlotId === timeSlotId + ? { ...assignment, isAvailable } + : assignment + ); +} + +export function validateManagerMinimumAvailability(managerAssignments: ManagerAvailability[]): boolean { + const requiredShifts = [ + { dayOfWeek: 1, timeSlotId: 'morning' }, + { dayOfWeek: 1, timeSlotId: 'afternoon' }, + { dayOfWeek: 2, timeSlotId: 'morning' }, + { dayOfWeek: 2, timeSlotId: 'afternoon' } + ]; + + return requiredShifts.every(required => + isManagerAvailable(managerAssignments, required.dayOfWeek, required.timeSlotId) + ); +} \ No newline at end of file diff --git a/backend/src/models/helpers/index.ts b/backend/src/models/helpers/index.ts new file mode 100644 index 0000000..f8b345d --- /dev/null +++ b/backend/src/models/helpers/index.ts @@ -0,0 +1,3 @@ +// backend/src/models/helpers/index.ts +export * from './employeeHelpers.js'; +export * from './shiftPlanHelpers.js'; \ No newline at end of file diff --git a/backend/src/models/helpers/shiftPlanHelpers.ts b/backend/src/models/helpers/shiftPlanHelpers.ts new file mode 100644 index 0000000..e8fd378 --- /dev/null +++ b/backend/src/models/helpers/shiftPlanHelpers.ts @@ -0,0 +1,118 @@ +// backend/src/models/helpers/shiftPlanHelpers.ts +import { ShiftPlan, Shift, ScheduledShift, TimeSlot } from '../ShiftPlan.js'; + +// Validation helpers +export function validateRequiredEmployees(shift: Shift | ScheduledShift): string[] { + const errors: string[] = []; + + if (shift.requiredEmployees < 1) { + errors.push('Required employees must be at least 1'); + } + + if (shift.requiredEmployees > 10) { + errors.push('Required employees cannot exceed 10'); + } + + return errors; +} + +export function isTemplate(plan: ShiftPlan): boolean { + return plan.isTemplate || plan.status === 'template'; +} + +export function hasDateRange(plan: ShiftPlan): boolean { + return !isTemplate(plan) && !!plan.startDate && !!plan.endDate; +} + +export function validatePlanDates(plan: ShiftPlan): string[] { + const errors: string[] = []; + + if (!isTemplate(plan)) { + if (!plan.startDate) errors.push('Start date is required for non-template plans'); + if (!plan.endDate) errors.push('End date is required for non-template plans'); + if (plan.startDate && plan.endDate && plan.startDate > plan.endDate) { + errors.push('Start date must be before end date'); + } + } + + return errors; +} + +export function validateTimeSlot(timeSlot: { startTime: string; endTime: string }): string[] { + const errors: string[] = []; + + if (!timeSlot.startTime || !timeSlot.endTime) { + errors.push('Start time and end time are required'); + return errors; + } + + const start = new Date(`2000-01-01T${timeSlot.startTime}`); + const end = new Date(`2000-01-01T${timeSlot.endTime}`); + + if (start >= end) { + errors.push('Start time must be before end time'); + } + + return errors; +} + +// Type guards +export function isScheduledShift(shift: Shift | ScheduledShift): shift is ScheduledShift { + return 'date' in shift; +} + +export function isTemplateShift(shift: Shift | ScheduledShift): shift is Shift { + return 'dayOfWeek' in shift && !('date' in shift); +} + +// Business logic helpers +export function getShiftsForDay(plan: ShiftPlan, dayOfWeek: number): Shift[] { + return plan.shifts.filter(shift => shift.dayOfWeek === dayOfWeek); +} + +export function getTimeSlotById(plan: ShiftPlan, timeSlotId: string): TimeSlot | undefined { + return plan.timeSlots.find(slot => slot.id === timeSlotId); +} + +export function calculateTotalRequiredEmployees(plan: ShiftPlan): number { + return plan.shifts.reduce((total, shift) => total + shift.requiredEmployees, 0); +} + +export function getScheduledShiftByDateAndTime( + plan: ShiftPlan, + date: string, + timeSlotId: string +): ScheduledShift | undefined { + return plan.scheduledShifts?.find(shift => + shift.date === date && shift.timeSlotId === timeSlotId + ); +} + +export function canPublishPlan(plan: ShiftPlan): { canPublish: boolean; errors: string[] } { + const errors: string[] = []; + + if (!hasDateRange(plan)) { + errors.push('Plan must have a date range to be published'); + } + + if (plan.shifts.length === 0) { + errors.push('Plan must have at least one shift'); + } + + if (plan.timeSlots.length === 0) { + errors.push('Plan must have at least one time slot'); + } + + // Validate all shifts + plan.shifts.forEach((shift, index) => { + const shiftErrors = validateRequiredEmployees(shift); + if (shiftErrors.length > 0) { + errors.push(`Shift ${index + 1}: ${shiftErrors.join(', ')}`); + } + }); + + return { + canPublish: errors.length === 0, + errors + }; +} \ No newline at end of file diff --git a/frontend/src/pages/Employees/components/AvailabilityManager.tsx b/frontend/src/pages/Employees/components/AvailabilityManager.tsx index e64b535..9dcc697 100644 --- a/frontend/src/pages/Employees/components/AvailabilityManager.tsx +++ b/frontend/src/pages/Employees/components/AvailabilityManager.tsx @@ -1,8 +1,11 @@ -// frontend/src/pages/Employees/components/AvailabilityManager.tsx +// frontend/src/pages/Employees/components/AvailabilityManager.tsx - KORRIGIERT import React, { useState, useEffect } from 'react'; -import { Employee, Availability } from '../../../types/employee'; +import { Employee, Availability } from '../../../../../backend/src/models/employee'; import { employeeService } from '../../../services/employeeService'; -import { shiftPlanService, ShiftPlan, ShiftPlanShift } from '../../../services/shiftPlanService'; +import { shiftPlanService } from '../../../services/shiftPlanService'; +import { ShiftPlan, TimeSlot } from '../../../../../backend/src/models/shiftPlan'; +import { shiftTemplateService } from '../../../services/shiftTemplateService'; +import { time } from 'console'; interface AvailabilityManagerProps { employee: Employee; @@ -11,7 +14,17 @@ interface AvailabilityManagerProps { } // Verfügbarkeits-Level -export type AvailabilityLevel = 1 | 2 | 3; // 1: bevorzugt, 2: möglich, 3: nicht möglich +export type AvailabilityLevel = 1 | 2 | 3; + +// Interface für Zeit-Slots +interface TimeSlot { + id: string; + name: string; + startTime: string; + endTime: string; + displayName: string; + source: string; +} const AvailabilityManager: React.FC = ({ employee, @@ -22,6 +35,7 @@ const AvailabilityManager: React.FC = ({ const [shiftPlans, setShiftPlans] = useState([]); const [selectedPlanId, setSelectedPlanId] = useState(''); const [selectedPlan, setSelectedPlan] = useState(null); + const [timeSlots, setTimeSlots] = useState([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(''); @@ -36,7 +50,6 @@ const AvailabilityManager: React.FC = ({ { id: 0, name: 'Sonntag' } ]; - // Verfügbarkeits-Level mit Farben und Beschreibungen const availabilityLevels = [ { level: 1 as AvailabilityLevel, label: 'Bevorzugt', color: '#27ae60', bgColor: '#d5f4e6', description: 'Ideale Zeit' }, { level: 2 as AvailabilityLevel, label: 'Möglich', color: '#f39c12', bgColor: '#fef5e7', description: 'Akzeptable Zeit' }, @@ -53,60 +66,191 @@ const AvailabilityManager: React.FC = ({ } }, [selectedPlanId]); + // NEU: Hole Zeit-Slots aus Schichtvorlagen als Hauptquelle + const loadTimeSlotsFromTemplates = async (): Promise => { + try { + console.log('🔄 LADE ZEIT-SLOTS AUS SCHICHTVORLAGEN...'); + const shiftPlan = await shiftPlanService.getShiftPlans(); + console.log('✅ SCHICHTVORLAGEN GELADEN:', shiftPlan); + + const allTimeSlots = new Map(); + + shiftPlan.forEach(plan => { + console.log(`📋 VORLAGE: ${plan.name}`, plan); + + // Extrahiere Zeit-Slots aus den Schicht-Zeitbereichen + if (plan.shifts && plan.shifts.length > 0) { + plan.shifts.forEach(shift => { + const key = `${shift.timeSlot.startTime}-${shift.timeSlot.endTime}`; + if (!allTimeSlots.has(key)) { + allTimeSlots.set(key, { + id: shift.id || `slot-${shift.timeSlot.startTime.replace(/:/g, '')}-${shift.timeSlot.endTime.replace(/:/g, '')}`, + name: shift.timeSlot.name || 'Schicht', + startTime: shift.timeSlot.startTime, + endTime: shift.timeSlot.endTime, + displayName: `${shift.timeSlot.name || 'Schicht'} (${formatTime(shift.timeSlot.startTime)}-${formatTime(shift.timeSlot.endTime)})`, + source: `Vorlage: ${plan.name}` + }); + } + }); + } + }); + + const result = Array.from(allTimeSlots.values()).sort((a, b) => + a.startTime.localeCompare(b.startTime) + ); + + console.log('✅ ZEIT-SLOTS AUS VORLAGEN:', result); + return result; + } catch (error) { + console.error('❌ FEHLER BEIM LADEN DER VORLAGEN:', error); + return getDefaultTimeSlots(); + } + }; + + // NEU: Alternative Methode - Extrahiere aus Schichtplänen + const extractTimeSlotsFromPlans = (plans: ShiftPlan[]): TimeSlot[] => { + console.log('🔄 EXTRAHIERE ZEIT-SLOTS AUS SCHICHTPLÄNEN:', plans); + + const allTimeSlots = new Map(); + + plans.forEach(plan => { + console.log(`📋 ANALYSIERE PLAN: ${plan.name}`, { + id: plan.id, + shifts: plan.shifts + }); + + // Prüfe ob Schichten existieren und ein Array sind + if (plan.shifts && Array.isArray(plan.shifts)) { + plan.shifts.forEach(shift => { + console.log(` 🔍 SCHICHT:`, shift); + + if (shift.timeSlot.startTime && shift.timeSlot.endTime) { + const key = `${shift.timeSlot.startTime}-${shift.timeSlot.endTime}`; + if (!allTimeSlots.has(key)) { + allTimeSlots.set(key, { + id: `slot-${shift.timeSlot.startTime.replace(/:/g, '')}-${shift.timeSlot.endTime.replace(/:/g, '')}`, + name: shift.timeSlot.name || 'Schicht', + startTime: shift.timeSlot.startTime, + endTime: shift.timeSlot.endTime, + displayName: `${shift.timeSlot.name || 'Schicht'} (${formatTime(shift.timeSlot.startTime)}-${formatTime(shift.timeSlot.endTime)})`, + source: `Plan: ${plan.name}` + }); + } + } + }); + } else { + console.log(` ❌ KEINE SCHICHTEN IN PLAN ${plan.name} oder keine Array-Struktur`); + } + }); + + const result = Array.from(allTimeSlots.values()).sort((a, b) => + a.startTime.localeCompare(b.startTime) + ); + + console.log('✅ ZEIT-SLOTS AUS PLÄNEN:', result); + return result; + }; + + const getDefaultTimeSlots = (): TimeSlot[] => { + console.log('⚠️ VERWENDE STANDARD-ZEIT-SLOTS'); + return [ + { + id: 'slot-0800-1200', + name: 'Vormittag', + startTime: '08:00', + endTime: '12:00', + displayName: 'Vormittag (08:00-12:00)', + source: 'Standard' + }, + { + id: 'slot-1200-1600', + name: 'Nachmittag', + startTime: '12:00', + endTime: '16:00', + displayName: 'Nachmittag (12:00-16:00)', + source: 'Standard' + }, + { + id: 'slot-1600-2000', + name: 'Abend', + startTime: '16:00', + endTime: '20:00', + displayName: 'Abend (16:00-20:00)', + source: 'Standard' + } + ]; + }; + + const formatTime = (time: string): string => { + return time.substring(0, 5); + }; + const loadData = async () => { try { setLoading(true); + console.log('🔄 LADE DATEN FÜR MITARBEITER:', employee.id); - // Load availabilities + // 1. Lade Verfügbarkeiten + let existingAvailabilities: Availability[] = []; try { - const availData = await employeeService.getAvailabilities(employee.id); - setAvailabilities(availData); + existingAvailabilities = await employeeService.getAvailabilities(employee.id); + console.log('✅ VERFÜGBARKEITEN GELADEN:', existingAvailabilities.length); } catch (err) { - // Falls keine Verfügbarkeiten existieren, erstelle Standard-Einträge (Level 3: nicht möglich) - const defaultAvailabilities: Availability[] = daysOfWeek.flatMap(day => [ - { - id: `temp-${day.id}-morning`, - employeeId: employee.id, - dayOfWeek: day.id, - startTime: '08:00', - endTime: '12:00', - isAvailable: false, - availabilityLevel: 3 as AvailabilityLevel - }, - { - id: `temp-${day.id}-afternoon`, - employeeId: employee.id, - dayOfWeek: day.id, - startTime: '12:00', - endTime: '16:00', - isAvailable: false, - availabilityLevel: 3 as AvailabilityLevel - }, - { - id: `temp-${day.id}-evening`, - employeeId: employee.id, - dayOfWeek: day.id, - startTime: '16:00', - endTime: '20:00', - isAvailable: false, - availabilityLevel: 3 as AvailabilityLevel - } - ]); - setAvailabilities(defaultAvailabilities); + console.log('⚠️ KEINE VERFÜGBARKEITEN GEFUNDEN'); } - // Load shift plans + // 2. Lade Schichtpläne + console.log('🔄 LADE SCHICHTPLÄNE...'); const plans = await shiftPlanService.getShiftPlans(); + console.log('✅ SCHICHTPLÄNE GELADEN:', plans.length, plans); + + // 3. VERSUCH 1: Lade Zeit-Slots aus Schichtvorlagen (bessere Quelle) + let extractedTimeSlots = await loadTimeSlotsFromTemplates(); + + // VERSUCH 2: Falls keine Zeit-Slots aus Vorlagen, versuche es mit Schichtplänen + if (extractedTimeSlots.length === 0) { + console.log('⚠️ KEINE ZEIT-SLOTS AUS VORLAGEN, VERSUCHE SCHICHTPLÄNE...'); + extractedTimeSlots = extractTimeSlotsFromPlans(plans); + } + + // VERSUCH 3: Falls immer noch keine, verwende Standard-Slots + if (extractedTimeSlots.length === 0) { + console.log('⚠️ KEINE ZEIT-SLOTS GEFUNDEN, VERWENDE STANDARD-SLOTS'); + extractedTimeSlots = getDefaultTimeSlots(); + } + + setTimeSlots(extractedTimeSlots); setShiftPlans(plans); - // Auto-select the first published plan or the first draft + // 4. Erstelle Standard-Verfügbarkeiten falls nötig + if (existingAvailabilities.length === 0) { + const defaultAvailabilities: Availability[] = daysOfWeek.flatMap(day => + extractedTimeSlots.map(slot => ({ + id: `temp-${day.id}-${slot.id}`, + employeeId: employee.id, + dayOfWeek: day.id, + startTime: slot.startTime, + endTime: slot.endTime, + isAvailable: false, + availabilityLevel: 3 as AvailabilityLevel + })) + ); + setAvailabilities(defaultAvailabilities); + console.log('✅ STANDARD-VERFÜGBARKEITEN ERSTELLT:', defaultAvailabilities.length); + } else { + setAvailabilities(existingAvailabilities); + } + + // 5. Wähle ersten Plan aus if (plans.length > 0) { const publishedPlan = plans.find(plan => plan.status === 'published'); const firstPlan = publishedPlan || plans[0]; setSelectedPlanId(firstPlan.id); + console.log('✅ SCHICHTPLAN AUSGEWÄHLT:', firstPlan.name); } } catch (err: any) { - console.error('Error loading data:', err); + console.error('❌ FEHLER BEIM LADEN DER DATEN:', err); setError('Daten konnten nicht geladen werden'); } finally { setLoading(false); @@ -115,33 +259,82 @@ const AvailabilityManager: React.FC = ({ const loadSelectedPlan = async () => { try { + console.log('🔄 LADE AUSGEWÄHLTEN SCHICHTPLAN:', selectedPlanId); const plan = await shiftPlanService.getShiftPlan(selectedPlanId); setSelectedPlan(plan); + console.log('✅ SCHICHTPLAN GELADEN:', { + name: plan.name, + shiftsCount: plan.shifts?.length || 0, + shifts: plan.shifts + }); } catch (err: any) { - console.error('Error loading shift plan:', err); + console.error('❌ FEHLER BEIM LADEN DES SCHICHTPLANS:', err); setError('Schichtplan konnte nicht geladen werden'); } }; - const handleAvailabilityLevelChange = (dayId: number, timeSlot: string, level: AvailabilityLevel) => { - setAvailabilities(prev => - prev.map(avail => - avail.dayOfWeek === dayId && getTimeSlotName(avail.startTime, avail.endTime) === timeSlot - ? { - ...avail, - availabilityLevel: level, - isAvailable: level !== 3 - } - : avail - ) - ); + const handleAvailabilityLevelChange = (dayId: number, timeSlotId: string, level: AvailabilityLevel) => { + console.log(`🔄 ÄNDERE VERFÜGBARKEIT: Tag ${dayId}, Slot ${timeSlotId}, Level ${level}`); + + setAvailabilities(prev => { + const timeSlot = timeSlots.find(s => s.id === timeSlotId); + if (!timeSlot) { + console.log('❌ ZEIT-SLOT NICHT GEFUNDEN:', timeSlotId); + return prev; + } + + const existingIndex = prev.findIndex(avail => + avail.dayOfWeek === dayId && + avail.startTime === timeSlot.startTime && + avail.endTime === timeSlot.endTime + ); + + console.log(`🔍 EXISTIERENDE VERFÜGBARKEIT GEFUNDEN AN INDEX:`, existingIndex); + + if (existingIndex >= 0) { + // Update existing availability + const updated = [...prev]; + updated[existingIndex] = { + ...updated[existingIndex], + availabilityLevel: level, + isAvailable: level !== 3 + }; + console.log('✅ VERFÜGBARKEIT AKTUALISIERT:', updated[existingIndex]); + return updated; + } else { + // Create new availability + const newAvailability: Availability = { + id: `temp-${dayId}-${timeSlotId}-${Date.now()}`, + employeeId: employee.id, + dayOfWeek: dayId, + startTime: timeSlot.startTime, + endTime: timeSlot.endTime, + isAvailable: level !== 3, + availabilityLevel: level + }; + console.log('🆕 NEUE VERFÜGBARKEIT ERSTELLT:', newAvailability); + return [...prev, newAvailability]; + } + }); }; - const getTimeSlotName = (startTime: string, endTime: string): string => { - if (startTime === '08:00' && endTime === '12:00') return 'Vormittag'; - if (startTime === '12:00' && endTime === '16:00') return 'Nachmittag'; - if (startTime === '16:00' && endTime === '20:00') return 'Abend'; - return `${startTime}-${endTime}`; + const getAvailabilityForDayAndSlot = (dayId: number, timeSlotId: string): AvailabilityLevel => { + const timeSlot = timeSlots.find(s => s.id === timeSlotId); + if (!timeSlot) { + console.log('❌ ZEIT-SLOT NICHT GEFUNDEN FÜR ABFRAGE:', timeSlotId); + return 3; + } + + const availability = availabilities.find(avail => + avail.dayOfWeek === dayId && + avail.startTime === timeSlot.startTime && + avail.endTime === timeSlot.endTime + ); + + const result = availability?.availabilityLevel || 3; + console.log(`🔍 ABFRAGE VERFÜGBARKEIT: Tag ${dayId}, Slot ${timeSlotId} = Level ${result}`); + + return result; }; const handleSave = async () => { @@ -150,99 +343,19 @@ const AvailabilityManager: React.FC = ({ setError(''); await employeeService.updateAvailabilities(employee.id, availabilities); + console.log('✅ VERFÜGBARKEITEN ERFOLGREICH GESPEICHERT'); + onSave(); } catch (err: any) { + console.error('❌ FEHLER BEIM SPEICHERN:', err); setError(err.message || 'Fehler beim Speichern der Verfügbarkeiten'); } finally { setSaving(false); } }; - // Get availability level for a specific shift - const getAvailabilityForShift = (shift: ShiftPlanShift): AvailabilityLevel => { - const shiftDate = new Date(shift.date); - const dayOfWeek = shiftDate.getDay(); // 0 = Sunday, 1 = Monday, etc. - - // Find matching availability for this day and time - const matchingAvailabilities = availabilities.filter(avail => - avail.dayOfWeek === dayOfWeek && - avail.availabilityLevel !== 3 && // Nur Level 1 und 2 berücksichtigen - isTimeOverlap(avail.startTime, avail.endTime, shift.startTime, shift.endTime) - ); - - if (matchingAvailabilities.length === 0) { - return 3; // Nicht möglich, wenn keine Übereinstimmung - } - - // Nehme das beste (niedrigste) Verfügbarkeits-Level - const minLevel = Math.min(...matchingAvailabilities.map(avail => avail.availabilityLevel)); - return minLevel as AvailabilityLevel; - }; - - // Helper function to check time overlap - const isTimeOverlap = (availStart: string, availEnd: string, shiftStart: string, shiftEnd: string): boolean => { - const availStartMinutes = timeToMinutes(availStart); - const availEndMinutes = timeToMinutes(availEnd); - const shiftStartMinutes = timeToMinutes(shiftStart); - const shiftEndMinutes = timeToMinutes(shiftEnd); - - return shiftStartMinutes < availEndMinutes && shiftEndMinutes > availStartMinutes; - }; - - const timeToMinutes = (time: string): number => { - const [hours, minutes] = time.split(':').map(Number); - return hours * 60 + minutes; - }; - - // Group shifts by weekday for timetable display - const getTimetableData = () => { - if (!selectedPlan) return { shiftsByDay: {}, weekdays: [] }; - - const shiftsByDay: Record = {}; - - // Initialize empty arrays for each day - daysOfWeek.forEach(day => { - shiftsByDay[day.id] = []; - }); - - // Group shifts by weekday - selectedPlan.shifts.forEach(shift => { - const shiftDate = new Date(shift.date); - const dayOfWeek = shiftDate.getDay(); // 0 = Sunday, 1 = Monday, etc. - shiftsByDay[dayOfWeek].push(shift); - }); - - // Remove duplicate shifts (same name and time on same day) - Object.keys(shiftsByDay).forEach(day => { - const dayNum = parseInt(day); - const uniqueShifts: ShiftPlanShift[] = []; - const seen = new Set(); - - shiftsByDay[dayNum].forEach(shift => { - const key = `${shift.name}|${shift.startTime}|${shift.endTime}`; - if (!seen.has(key)) { - seen.add(key); - uniqueShifts.push(shift); - } - }); - - shiftsByDay[dayNum] = uniqueShifts; - }); - - return { - shiftsByDay, - weekdays: daysOfWeek - }; - }; - - const timetableData = getTimetableData(); - - // Get availability for a specific day and time slot - const getAvailabilityForDayAndSlot = (dayId: number, timeSlot: string): AvailabilityLevel => { - const availability = availabilities.find(avail => - avail.dayOfWeek === dayId && getTimeSlotName(avail.startTime, avail.endTime) === timeSlot - ); - return availability?.availabilityLevel || 3; + const getTimeSlotsForTimetable = (): TimeSlot[] => { + return timeSlots; }; if (loading) { @@ -253,9 +366,11 @@ const AvailabilityManager: React.FC = ({ ); } + const timetableTimeSlots = getTimeSlotsForTimetable(); + return (
= ({ 📅 Verfügbarkeit verwalten + {/* Debug-Info */} +
+

+ {timeSlots.length === 0 ? '❌ PROBLEM: Keine Zeit-Slots gefunden' : '✅ Zeit-Slots geladen'} +

+
+
Zeit-Slots gefunden: {timeSlots.length}
+
Quelle: {timeSlots[0]?.source || 'Unbekannt'}
+
Schichtpläne: {shiftPlans.length}
+
+ + {timeSlots.length > 0 && ( +
+ Gefundene Zeit-Slots: + {timeSlots.map(slot => ( +
+ • {slot.displayName} +
+ ))} +
+ )} +
+ + {/* Rest der Komponente... */}

{employee.name}

- Legen Sie die Verfügbarkeit für {employee.name} fest (1: bevorzugt, 2: möglich, 3: nicht möglich). + Legen Sie die Verfügbarkeit für {employee.name} fest.

@@ -361,22 +509,16 @@ const AvailabilityManager: React.FC = ({ {shiftPlans.map(plan => ( ))}
- - {selectedPlan && ( -
- Zeitraum: {new Date(selectedPlan.startDate).toLocaleDateString('de-DE')} - {new Date(selectedPlan.endDate).toLocaleDateString('de-DE')} -
- )} - {/* Verfügbarkeits-Timetable mit Dropdown-Menüs */} - {selectedPlan && ( + {/* Verfügbarkeits-Timetable */} + {timetableTimeSlots.length > 0 ? (
= ({ padding: '15px 20px', fontWeight: 'bold' }}> - Verfügbarkeit für: {selectedPlan.name} + Verfügbarkeit definieren +
+ {timetableTimeSlots.length} Schichttypen verfügbar +
@@ -405,11 +550,11 @@ const AvailabilityManager: React.FC = ({ textAlign: 'left', border: '1px solid #dee2e6', fontWeight: 'bold', - minWidth: '150px' + minWidth: '200px' }}> - Zeit + Schicht (Zeit) - {timetableData.weekdays.map(weekday => ( + {daysOfWeek.map(weekday => ( = ({ - {['Vormittag', 'Nachmittag', 'Abend'].map((timeSlot, timeIndex) => ( - ( + = ({ fontWeight: '500', backgroundColor: '#f8f9fa' }}> - {timeSlot} -
- {timeSlot === 'Vormittag' ? '08:00-12:00' : - timeSlot === 'Nachmittag' ? '12:00-16:00' : '16:00-20:00'} -
+ {timeSlot.displayName} - {timetableData.weekdays.map(weekday => { - const currentLevel = getAvailabilityForDayAndSlot(weekday.id, timeSlot); + {daysOfWeek.map(weekday => { + const currentLevel = getAvailabilityForDayAndSlot(weekday.id, timeSlot.id); const levelConfig = availabilityLevels.find(l => l.level === currentLevel); return ( @@ -452,7 +593,10 @@ const AvailabilityManager: React.FC = ({ }}> -
- {levelConfig?.description} -
); })} @@ -495,51 +631,25 @@ const AvailabilityManager: React.FC = ({
- - {/* Legende */} -
- Legende: - {availabilityLevels.map(level => ( - -
- {level.level}: {level.label} - - ))} -
+
+ ) : ( +
+
+

Keine Schichttypen konfiguriert

+

Es wurden keine Zeit-Slots in den Schichtvorlagen oder -plänen gefunden.

+

+ Bitte erstellen Sie zuerst Schichtvorlagen mit Zeit-Slots. +

)} - {/* Info Text */} -
-

💡 Information

-

- 1: Bevorzugt - Ideale Zeit für diesen Mitarbeiter
- 2: Möglich - Akzeptable Zeit, falls benötigt
- 3: Nicht möglich - Mitarbeiter ist nicht verfügbar
- Das System priorisiert Mitarbeiter mit Level 1 für Schichtzuweisungen. -

-
- {/* Buttons */}
= ({
))} + + {/* Debug-Anzeige */} +
+ Debug: Ausgewählter Typ: {formData.employeeType} +
{/* Eigenständigkeit */} diff --git a/frontend/src/pages/ShiftPlans/ShiftPlanView.tsx b/frontend/src/pages/ShiftPlans/ShiftPlanView.tsx index c9b580d..0af0f1a 100644 --- a/frontend/src/pages/ShiftPlans/ShiftPlanView.tsx +++ b/frontend/src/pages/ShiftPlans/ShiftPlanView.tsx @@ -2,7 +2,8 @@ import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useAuth } from '../../contexts/AuthContext'; -import { shiftPlanService, ShiftPlan } from '../../services/shiftPlanService'; +import { shiftPlanService } from '../../services/shiftPlanService'; +import { ShiftPlan, Shift, TimeSlot } from '../../../../backend/src/models/shiftPlan.js'; import { useNotification } from '../../contexts/NotificationContext'; const ShiftPlanView: React.FC = () => { @@ -63,7 +64,7 @@ const ShiftPlanView: React.FC = () => { // Get all unique shift types (name + time combination) const shiftTypes = Array.from(new Set( shiftPlan.shifts.map(shift => - `${shift.name}|${shift.startTime}|${shift.endTime}` + `${shift.timeSlot.name}|${shift.timeSlot.startTime}|${shift.timeSlot.endTime}` ) )).map(shiftKey => { const [name, startTime, endTime] = shiftKey.split('|'); @@ -83,15 +84,15 @@ const ShiftPlanView: React.FC = () => { const date = new Date(shift.date); const dayOfWeek = date.getDay() === 0 ? 7 : date.getDay(); // Convert to 1-7 (Mon-Sun) return dayOfWeek === weekday && - shift.name === shiftType.name && - shift.startTime === shiftType.startTime && - shift.endTime === shiftType.endTime; + shift.timeSlot.name === shiftType.name && + shift.timeSlot.startTime === shiftType.startTime && + shift.timeSlot.endTime === shiftType.endTime; }); if (shiftsOnDay.length === 0) { weekdayData[weekday] = ''; } else { - const totalAssigned = shiftsOnDay.reduce((sum, shift) => sum + shift.assignedEmployees.length, 0); + const totalAssigned = shiftsOnDay.reduce((sum, shift) => sum + shift.timeSlot.assignedEmployees.length, 0); const totalRequired = shiftsOnDay.reduce((sum, shift) => sum + shift.requiredEmployees, 0); weekdayData[weekday] = `${totalAssigned}/${totalRequired}`; } diff --git a/frontend/src/services/shiftPlanService.ts b/frontend/src/services/shiftPlanService.ts index 913e64a..1114b9f 100644 --- a/frontend/src/services/shiftPlanService.ts +++ b/frontend/src/services/shiftPlanService.ts @@ -1,38 +1,9 @@ // frontend/src/services/shiftPlanService.ts import { authService } from './authService'; +import { ShiftPlan, CreateShiftPlanRequest, ShiftSlot } from '../types/shiftPlan.js'; const API_BASE = 'http://localhost:3002/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; - name: string; - startTime: string; - endTime: string; - requiredEmployees: number; - assignedEmployees: string[]; -} - export const shiftPlanService = { async getShiftPlans(): Promise { const response = await fetch(API_BASE, { @@ -85,17 +56,17 @@ export const shiftPlanService = { const data = await response.json(); // Convert snake_case to camelCase - return { - id: data.id, - name: data.name, - startDate: data.start_date, // Convert here - endDate: data.end_date, // Convert here - templateId: data.template_id, - status: data.status, - createdBy: data.created_by, - createdAt: data.created_at, - shifts: data.shifts || [] - }; + return data.map((plan: any) => ({ + id: plan.id, + name: plan.name, + startDate: plan.start_date, + endDate: plan.end_date, + templateId: plan.template_id, + status: plan.status, + createdBy: plan.created_by, + createdAt: plan.created_at, + shifts: plan.shifts || [] + })); }, async createShiftPlan(plan: CreateShiftPlanRequest): Promise { @@ -158,7 +129,7 @@ export const shiftPlanService = { } }, - async updateShiftPlanShift(planId: string, shift: ShiftPlanShift): Promise { + async updateShiftPlanShift(planId: string, shift: ShiftSlot): Promise { const response = await fetch(`${API_BASE}/${planId}/shifts/${shift.id}`, { method: 'PUT', headers: { @@ -177,7 +148,7 @@ export const shiftPlanService = { } }, - async addShiftPlanShift(planId: string, shift: Omit): Promise { + async addShiftPlanShift(planId: string, shift: Omit): Promise { const response = await fetch(`${API_BASE}/${planId}/shifts`, { method: 'POST', headers: { diff --git a/frontend/src/services/shiftTemplateService.ts b/frontend/src/services/shiftTemplateService.ts index ac3014b..a64390d 100644 --- a/frontend/src/services/shiftTemplateService.ts +++ b/frontend/src/services/shiftTemplateService.ts @@ -1,11 +1,11 @@ // frontend/src/services/shiftTemplateService.ts -import { TemplateShift } from '../types/shiftTemplate'; +import { ShiftPlan } from '../../../backend/src/models/shiftTemplate.js'; import { authService } from './authService'; const API_BASE = 'http://localhost:3002/api/shift-templates'; export const shiftTemplateService = { - async getTemplates(): Promise { + async getTemplates(): Promise { const response = await fetch(API_BASE, { headers: { 'Content-Type': 'application/json', diff --git a/frontend/src/types/employee.ts b/frontend/src/types/employee.ts deleted file mode 100644 index 2e4a8d0..0000000 --- a/frontend/src/types/employee.ts +++ /dev/null @@ -1,39 +0,0 @@ -// frontend/src/types/employee.ts -export interface Employee { - id: string; - email: string; - name: string; - role: 'admin' | 'instandhalter' | 'user'; - employeeType: 'chef' | 'neuling' | 'erfahren'; - isSufficientlyIndependent: boolean; - isActive: boolean; - createdAt: string; - lastLogin?: string | null; -} - -export interface CreateEmployeeRequest { - email: string; - password: string; - name: string; - role: 'admin' | 'instandhalter' | 'user'; - employeeType: 'chef' | 'neuling' | 'erfahren'; - isSufficientlyIndependent: boolean; -} - -export interface UpdateEmployeeRequest { - name?: string; - role?: 'admin' | 'instandhalter' | 'user'; - employeeType?: 'chef' | 'neuling' | 'erfahren'; - isSufficientlyIndependent?: boolean; - isActive?: boolean; -} - -export interface Availability { - id: string; - employeeId: string; - dayOfWeek: number; - startTime: string; - endTime: string; - isAvailable: boolean; - availabilityLevel: 1 | 2 | 3; // 1: bevorzugt, 2: möglich, 3: nicht möglich -} \ No newline at end of file diff --git a/frontend/src/types/shiftTemplate.ts b/frontend/src/types/shiftTemplate.ts deleted file mode 100644 index 4c226f9..0000000 --- a/frontend/src/types/shiftTemplate.ts +++ /dev/null @@ -1,42 +0,0 @@ -// frontend/src/types/shiftTemplate.ts -export interface TemplateShift { - id: string; - name: string; - description?: string; - isDefault: boolean; - createdBy: string; - createdAt: string; - shifts: TemplateShiftSlot[]; -} - -export interface TemplateShiftSlot { - id: string; - templateId?: string; - dayOfWeek: number; - timeSlot: TemplateShiftTimeSlot; - requiredEmployees: number; - color?: string; -} - -export interface TemplateShiftTimeSlot { - id: string; - name: string; // e.g., "Frühschicht", "Spätschicht" - startTime: string; - endTime: string; -} - -export const DEFAULT_TIME_SLOTS: TemplateShiftTimeSlot[] = [ - { id: 'morning', name: 'Vormittag', startTime: '08:00', endTime: '12:00' }, - { id: 'afternoon', name: 'Nachmittag', startTime: '11:30', endTime: '15:30' }, -]; - - -export const DEFAULT_DAYS = [ - { id: 1, name: 'Montag' }, - { id: 2, name: 'Dienstag' }, - { id: 3, name: 'Donnerstag' }, - { id: 4, name: 'Mittwoch' }, - { id: 5, name: 'Freitag' }, - { id: 6, name: 'Samstag' }, - { id: 7, name: 'Sonntag' } -]; \ No newline at end of file diff --git a/frontend/src/types/user.ts b/frontend/src/types/user.ts deleted file mode 100644 index 05f9f2d..0000000 --- a/frontend/src/types/user.ts +++ /dev/null @@ -1,18 +0,0 @@ -// frontend/src/types/user.ts -export interface User { - id: string; - email: string; - name: string; - role: 'admin' | 'instandhalter' | 'user'; - employeeType: 'chef' | 'neuling' | 'erfahren'; - isSufficientlyIndependent: boolean; - isActive: boolean; - createdAt: string; - lastLogin?: string | null; - notes?: string; -} - -export interface LoginRequest { - email: string; - password: string; -} \ No newline at end of file