diff --git a/backend/src/controllers/shiftPlanController.ts b/backend/src/controllers/shiftPlanController.ts index 5a13753..05a579e 100644 --- a/backend/src/controllers/shiftPlanController.ts +++ b/backend/src/controllers/shiftPlanController.ts @@ -10,93 +10,79 @@ import { import { AuthRequest } from '../middleware/auth.js'; import { createPlanFromPreset, TEMPLATE_PRESETS } from '../models/defaults/shiftPlanDefaults.js'; +async function getPlanWithDetails(planId: string) { + const plan = await db.get(` + SELECT sp.*, e.name as created_by_name + FROM shift_plans sp + LEFT JOIN employees e ON sp.created_by = e.id + WHERE sp.id = ? + `, [planId]); + + if (!plan) return null; + + const [timeSlots, shifts] = await Promise.all([ + db.all(`SELECT * FROM time_slots WHERE plan_id = ? ORDER BY start_time`, [planId]), + db.all(` + SELECT s.*, ts.name as time_slot_name, ts.start_time, ts.end_time + FROM shifts s + LEFT JOIN time_slots ts ON s.time_slot_id = ts.id + WHERE s.plan_id = ? + ORDER BY s.day_of_week, ts.start_time + `, [planId]) + ]); + + return { + plan: { + ...plan, + isTemplate: plan.is_template === 1, + startDate: plan.start_date, + endDate: plan.end_date, + createdBy: plan.created_by, + createdAt: plan.created_at, + }, + timeSlots: timeSlots.map(slot => ({ + id: slot.id, + planId: slot.plan_id, + name: slot.name, + startTime: slot.start_time, + endTime: slot.end_time, + description: slot.description + })), + shifts: shifts.map(shift => ({ + id: shift.id, + planId: shift.plan_id, + timeSlotId: shift.time_slot_id, + dayOfWeek: shift.day_of_week, + requiredEmployees: shift.required_employees, + color: shift.color, + timeSlot: { + id: shift.time_slot_id, + name: shift.time_slot_name, + startTime: shift.start_time, + endTime: shift.end_time + } + })) + }; +} + +// Simplified getShiftPlans using shared helper export const getShiftPlans = async (req: Request, res: Response): Promise => { try { - console.log('🔍 Lade Schichtpläne...'); - - const shiftPlans = await db.all(` + const plans = await db.all(` SELECT sp.*, e.name as created_by_name FROM shift_plans sp LEFT JOIN employees e ON sp.created_by = e.id ORDER BY sp.created_at DESC `); - console.log(`✅ ${shiftPlans.length} Schichtpläne gefunden:`, shiftPlans.map(p => p.name)); - - // Für jeden Plan die Schichten und Zeit-Slots laden const plansWithDetails = await Promise.all( - shiftPlans.map(async (plan) => { - // Lade Zeit-Slots - const timeSlots = await db.all(` - SELECT * FROM time_slots - WHERE plan_id = ? - ORDER BY start_time - `, [plan.id]); - - // Lade Schichten - const shifts = await db.all(` - SELECT s.*, ts.name as time_slot_name, ts.start_time, ts.end_time - FROM shifts s - LEFT JOIN time_slots ts ON s.time_slot_id = ts.id - WHERE s.plan_id = ? - ORDER BY s.day_of_week, ts.start_time - `, [plan.id]); - - // Lade geplante Schichten (nur für nicht-Template Pläne) - let scheduledShifts = []; - if (!plan.is_template) { - scheduledShifts = await db.all(` - SELECT ss.*, ts.name as time_slot_name - FROM scheduled_shifts ss - LEFT JOIN time_slots ts ON ss.time_slot_id = ts.id - WHERE ss.plan_id = ? - ORDER BY ss.date, ts.start_time - `, [plan.id]); - } - - return { - ...plan, - isTemplate: plan.is_template === 1, - startDate: plan.start_date, - endDate: plan.end_date, - createdBy: plan.created_by, - createdAt: plan.created_at, - timeSlots: timeSlots.map(slot => ({ - id: slot.id, - planId: slot.plan_id, - name: slot.name, - startTime: slot.start_time, - endTime: slot.end_time, - description: slot.description - })), - shifts: shifts.map(shift => ({ - id: shift.id, - planId: shift.plan_id, - timeSlotId: shift.time_slot_id, - dayOfWeek: shift.day_of_week, - requiredEmployees: shift.required_employees, - color: shift.color, - timeSlot: { - id: shift.time_slot_id, - name: shift.time_slot_name, - startTime: shift.start_time, - endTime: shift.end_time - } - })), - scheduledShifts: scheduledShifts.map(shift => ({ - id: shift.id, - planId: shift.plan_id, - date: shift.date, - timeSlotId: shift.time_slot_id, - requiredEmployees: shift.required_employees, - assignedEmployees: JSON.parse(shift.assigned_employees || '[]'), - timeSlotName: shift.time_slot_name - })) - }; + plans.map(async (plan) => { + const details = await getPlanWithDetails(plan.id); + return details ? { ...details.plan, timeSlots: details.timeSlots, shifts: details.shifts } : null; }) ); - res.json(plansWithDetails); + res.json(plansWithDetails.filter(Boolean)); } catch (error) { console.error('Error fetching shift plans:', error); res.status(500).json({ error: 'Internal server error' }); diff --git a/backend/src/models/defaults/employeeDefaults.ts b/backend/src/models/defaults/employeeDefaults.ts index 671466d..f6131a0 100644 --- a/backend/src/models/defaults/employeeDefaults.ts +++ b/backend/src/models/defaults/employeeDefaults.ts @@ -19,6 +19,36 @@ export const MANAGER_DEFAULTS = { isActive: true }; +export const EMPLOYEE_TYPE_CONFIG = { + manager: { + value: 'manager' as const, + label: 'Chef/Administrator', + color: '#e74c3c', + independent: true, + description: 'Vollzugriff auf alle Funktionen und Mitarbeiterverwaltung' + }, + experienced: { + value: 'experienced' as const, + label: 'Erfahren', + color: '#3498db', + independent: true, + description: 'Langjährige Erfahrung, kann komplexe Aufgaben übernehmen' + }, + trainee: { + value: 'trainee' as const, + label: 'Neuling', + color: '#27ae60', + independent: false, + description: 'Benötigt Einarbeitung und Unterstützung' + } +} as const; + +export const ROLE_CONFIG = [ + { value: 'user', label: 'Mitarbeiter', description: 'Kann eigene Schichten einsehen', color: '#27ae60' }, + { value: 'instandhalter', label: 'Instandhalter', description: 'Kann Schichtpläne erstellen und Mitarbeiter verwalten', color: '#3498db' }, + { value: 'admin', label: 'Administrator', description: 'Voller Zugriff auf alle Funktionen', color: '#e74c3c' } +] as const; + // Contract type descriptions export const CONTRACT_TYPE_DESCRIPTIONS = { small: '1 Schicht pro Woche', @@ -26,13 +56,6 @@ export const CONTRACT_TYPE_DESCRIPTIONS = { 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' }, diff --git a/backend/src/models/helpers/employeeHelpers.ts b/backend/src/models/helpers/employeeHelpers.ts index 0f9fa35..9d1b1f7 100644 --- a/backend/src/models/helpers/employeeHelpers.ts +++ b/backend/src/models/helpers/employeeHelpers.ts @@ -1,128 +1,40 @@ // backend/src/models/helpers/employeeHelpers.ts -import { Employee, CreateEmployeeRequest, EmployeeAvailability, ManagerAvailability } from '../Employee.js'; +import { Employee, CreateEmployeeRequest, EmployeeAvailability } from '../Employee.js'; -// Validation helpers -export function validateEmployee(employee: CreateEmployeeRequest): string[] { +// Simplified validation - use schema validation instead +export function validateEmployeeData(employee: CreateEmployeeRequest): string[] { const errors: string[] = []; - if (!employee.email || !employee.email.includes('@')) { + if (!employee.email?.includes('@')) { errors.push('Valid email is required'); } - if (!employee.password || employee.password.length < 6) { + if (employee.password?.length < 6) { errors.push('Password must be at least 6 characters long'); } - if (!employee.name || employee.name.trim().length < 2) { + if (!employee.name?.trim() || 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[] = []; +// Simplified business logic helpers +export const isManager = (employee: Employee): boolean => + employee.employeeType === 'manager'; - if (availability.dayOfWeek < 1 || availability.dayOfWeek > 7) { - errors.push('Day of week must be between 1 and 7'); - } +export const isTrainee = (employee: Employee): boolean => + employee.employeeType === 'trainee'; - if (![1, 2, 3].includes(availability.preferenceLevel)) { - errors.push('Preference level must be 1, 2, or 3'); - } +export const isExperienced = (employee: Employee): boolean => + employee.employeeType === 'experienced'; - if (!availability.timeSlotId) { - errors.push('Time slot ID is required'); - } +export const isAdmin = (employee: Employee): boolean => + employee.role === 'admin'; - if (!availability.planId) { - errors.push('Plan ID is required'); - } +export const canEmployeeWorkAlone = (employee: Employee): boolean => + employee.canWorkAlone && isExperienced(employee); - 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 +export const getEmployeeWorkHours = (employee: Employee): number => + isManager(employee) ? 999 : (employee.contractType === 'small' ? 1 : 2); diff --git a/frontend/src/models/Employee.ts b/frontend/src/models/Employee.ts new file mode 100644 index 0000000..68aa0e1 --- /dev/null +++ b/frontend/src/models/Employee.ts @@ -0,0 +1,75 @@ +// backend/src/models/Employee.ts +export interface Employee { + id: string; + email: string; + name: string; + role: 'admin' | 'maintenance' | 'user'; + employeeType: 'manager' | 'trainee' | 'experienced'; + contractType: 'small' | 'large'; + canWorkAlone: boolean; + isActive: boolean; + createdAt: string; + lastLogin?: string | null; +} + +export interface CreateEmployeeRequest { + email: string; + password: string; + name: string; + role: 'admin' | 'maintenance' | 'user'; + employeeType: 'manager' | 'trainee' | 'experienced'; + contractType: 'small' | 'large'; + canWorkAlone: boolean; +} + +export interface UpdateEmployeeRequest { + name?: string; + 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/frontend/src/models/ShiftPlan.ts b/frontend/src/models/ShiftPlan.ts new file mode 100644 index 0000000..50d089d --- /dev/null +++ b/frontend/src/models/ShiftPlan.ts @@ -0,0 +1,105 @@ +// 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; + isTemplate?: boolean; + 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[]; +} + +export interface UpdateRequiredEmployeesRequest { + requiredEmployees: number; +} \ No newline at end of file diff --git a/frontend/src/models/defaults/employeeDefaults.ts b/frontend/src/models/defaults/employeeDefaults.ts new file mode 100644 index 0000000..f6131a0 --- /dev/null +++ b/frontend/src/models/defaults/employeeDefaults.ts @@ -0,0 +1,108 @@ +// 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 +}; + +export const EMPLOYEE_TYPE_CONFIG = { + manager: { + value: 'manager' as const, + label: 'Chef/Administrator', + color: '#e74c3c', + independent: true, + description: 'Vollzugriff auf alle Funktionen und Mitarbeiterverwaltung' + }, + experienced: { + value: 'experienced' as const, + label: 'Erfahren', + color: '#3498db', + independent: true, + description: 'Langjährige Erfahrung, kann komplexe Aufgaben übernehmen' + }, + trainee: { + value: 'trainee' as const, + label: 'Neuling', + color: '#27ae60', + independent: false, + description: 'Benötigt Einarbeitung und Unterstützung' + } +} as const; + +export const ROLE_CONFIG = [ + { value: 'user', label: 'Mitarbeiter', description: 'Kann eigene Schichten einsehen', color: '#27ae60' }, + { value: 'instandhalter', label: 'Instandhalter', description: 'Kann Schichtpläne erstellen und Mitarbeiter verwalten', color: '#3498db' }, + { value: 'admin', label: 'Administrator', description: 'Voller Zugriff auf alle Funktionen', color: '#e74c3c' } +] as const; + +// 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; + +// 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/frontend/src/models/defaults/index.ts b/frontend/src/models/defaults/index.ts new file mode 100644 index 0000000..be13429 --- /dev/null +++ b/frontend/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/frontend/src/models/defaults/shiftPlanDefaults.ts b/frontend/src/models/defaults/shiftPlanDefaults.ts new file mode 100644 index 0000000..5deacd5 --- /dev/null +++ b/frontend/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/frontend/src/models/helpers/employeeHelpers.ts b/frontend/src/models/helpers/employeeHelpers.ts new file mode 100644 index 0000000..9d1b1f7 --- /dev/null +++ b/frontend/src/models/helpers/employeeHelpers.ts @@ -0,0 +1,40 @@ +// backend/src/models/helpers/employeeHelpers.ts +import { Employee, CreateEmployeeRequest, EmployeeAvailability } from '../Employee.js'; + +// Simplified validation - use schema validation instead +export function validateEmployeeData(employee: CreateEmployeeRequest): string[] { + const errors: string[] = []; + + if (!employee.email?.includes('@')) { + errors.push('Valid email is required'); + } + + if (employee.password?.length < 6) { + errors.push('Password must be at least 6 characters long'); + } + + if (!employee.name?.trim() || employee.name.trim().length < 2) { + errors.push('Name is required and must be at least 2 characters long'); + } + + return errors; +} + +// Simplified business logic helpers +export const isManager = (employee: Employee): boolean => + employee.employeeType === 'manager'; + +export const isTrainee = (employee: Employee): boolean => + employee.employeeType === 'trainee'; + +export const isExperienced = (employee: Employee): boolean => + employee.employeeType === 'experienced'; + +export const isAdmin = (employee: Employee): boolean => + employee.role === 'admin'; + +export const canEmployeeWorkAlone = (employee: Employee): boolean => + employee.canWorkAlone && isExperienced(employee); + +export const getEmployeeWorkHours = (employee: Employee): number => + isManager(employee) ? 999 : (employee.contractType === 'small' ? 1 : 2); diff --git a/frontend/src/models/helpers/index.ts b/frontend/src/models/helpers/index.ts new file mode 100644 index 0000000..f8b345d --- /dev/null +++ b/frontend/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/frontend/src/models/helpers/shiftPlanHelpers.ts b/frontend/src/models/helpers/shiftPlanHelpers.ts new file mode 100644 index 0000000..e8fd378 --- /dev/null +++ b/frontend/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/EmployeeManagement.tsx b/frontend/src/pages/Employees/EmployeeManagement.tsx index 48f75fb..11788ca 100644 --- a/frontend/src/pages/Employees/EmployeeManagement.tsx +++ b/frontend/src/pages/Employees/EmployeeManagement.tsx @@ -1,6 +1,6 @@ // frontend/src/pages/Employees/EmployeeManagement.tsx import React, { useState, useEffect } from 'react'; -import { Employee } from '../../../../backend/src/models/employee'; +import { Employee } from '../../models/Employee'; import { employeeService } from '../../services/employeeService'; import EmployeeList from './components/EmployeeList'; import EmployeeForm from './components/EmployeeForm'; @@ -220,7 +220,6 @@ const EmployeeManagement: React.FC = () => { onEdit={handleEditEmployee} onDelete={handleDeleteEmployee} // Jetzt mit Employee-Objekt onManageAvailability={handleManageAvailability} - currentUserRole={hasRole(['admin']) ? 'admin' : 'instandhalter'} /> )} diff --git a/frontend/src/pages/Employees/components/AvailabilityManager.tsx b/frontend/src/pages/Employees/components/AvailabilityManager.tsx index 8317d87..69482b4 100644 --- a/frontend/src/pages/Employees/components/AvailabilityManager.tsx +++ b/frontend/src/pages/Employees/components/AvailabilityManager.tsx @@ -1,9 +1,9 @@ // frontend/src/pages/Employees/components/AvailabilityManager.tsx import React, { useState, useEffect } from 'react'; -import { Employee, EmployeeAvailability } from '../../../../../backend/src/models/employee'; import { employeeService } from '../../../services/employeeService'; import { shiftPlanService } from '../../../services/shiftPlanService'; -import { ShiftPlan, TimeSlot, Shift } from '../../../../../backend/src/models/shiftPlan'; +import { Employee, EmployeeAvailability } from '../../../models/Employee'; +import { ShiftPlan, TimeSlot, Shift } from '../../../models/ShiftPlan'; interface AvailabilityManagerProps { employee: Employee; @@ -180,11 +180,13 @@ const AvailabilityManager: React.FC = ({ // 3. Extract time slots from plans let extractedTimeSlots = extractTimeSlotsFromPlans(plans); - // 4. Fallback to default slots if none found + /* 4. Fallback to default slots if none found if (extractedTimeSlots.length === 0) { console.log('⚠️ KEINE ZEIT-SLOTS GEFUNDEN, VERWENDE STANDARD-SLOTS'); extractedTimeSlots = getDefaultTimeSlots(); - } + }*/ + + console.log('✅ GEFUNDENE ZEIT-SLOTS:', extractedTimeSlots.length, extractedTimeSlots); setTimeSlots(extractedTimeSlots); setShiftPlans(plans); diff --git a/frontend/src/pages/Employees/components/EmployeeForm.tsx b/frontend/src/pages/Employees/components/EmployeeForm.tsx index fe92343..a6bc039 100644 --- a/frontend/src/pages/Employees/components/EmployeeForm.tsx +++ b/frontend/src/pages/Employees/components/EmployeeForm.tsx @@ -1,6 +1,7 @@ // frontend/src/pages/Employees/components/EmployeeForm.tsx - KORRIGIERT import React, { useState, useEffect } from 'react'; -import { Employee, CreateEmployeeRequest, UpdateEmployeeRequest } from '../../../types/employee'; +import { Employee, CreateEmployeeRequest, UpdateEmployeeRequest } from '../../../models/Employee'; +import { ROLE_CONFIG, EMPLOYEE_TYPE_CONFIG } from '../../../models/defaults/employeeDefaults'; import { employeeService } from '../../../services/employeeService'; import { useAuth } from '../../../contexts/AuthContext'; @@ -11,34 +12,6 @@ interface EmployeeFormProps { onCancel: () => void; } -// Rollen Definition -const ROLE_OPTIONS = [ - { value: 'user', label: 'Mitarbeiter', description: 'Kann eigene Schichten einsehen' }, - { value: 'instandhalter', label: 'Instandhalter', description: 'Kann Schichtpläne erstellen und Mitarbeiter verwalten' }, - { value: 'admin', label: 'Administrator', description: 'Voller Zugriff auf alle Funktionen' } -] as const; - -// Mitarbeiter Typen Definition -const EMPLOYEE_TYPE_OPTIONS = [ - { - value: 'chef', - label: '👨‍💼 Chef/Administrator', - description: 'Vollzugriff auf alle Funktionen und Mitarbeiterverwaltung', - color: '#e74c3c' - }, - { - value: 'erfahren', - label: '👴 Erfahren', - description: 'Langjährige Erfahrung, kann komplexe Aufgaben übernehmen', - color: '#3498db' - }, - { - value: 'neuling', - label: '👶 Neuling', - description: 'Benötigt Einarbeitung und Unterstützung', - color: '#27ae60' - } -] as const; const EmployeeForm: React.FC = ({ mode, @@ -50,9 +23,9 @@ const EmployeeForm: React.FC = ({ name: '', email: '', password: '', - role: 'user' as 'admin' | 'instandhalter' | 'user', - employeeType: 'neuling' as 'chef' | 'neuling' | 'erfahren', - isSufficientlyIndependent: false, + role: 'user' as 'admin' | 'maintenance' | 'user', + employeeType: 'trainee' as 'manager' | 'trainee' | 'experienced', + canWorkAlone: false, isActive: true }); const [loading, setLoading] = useState(false); @@ -61,22 +34,20 @@ const EmployeeForm: React.FC = ({ useEffect(() => { if (mode === 'edit' && employee) { - console.log('📝 Lade Mitarbeiter-Daten:', employee); setFormData({ name: employee.name, email: employee.email, password: '', // Passwort wird beim Bearbeiten nicht angezeigt role: employee.role, employeeType: employee.employeeType, - isSufficientlyIndependent: employee.isSufficientlyIndependent, + canWorkAlone: employee.canWorkAlone, isActive: employee.isActive }); } }, [mode, employee]); - const handleChange = (e: React.ChangeEvent) => { + const handleChange = (e: React.ChangeEvent) => { const { name, value, type } = e.target; - console.log(`🔄 Feld geändert: ${name} = ${value}`); setFormData(prev => ({ ...prev, @@ -84,25 +55,14 @@ const EmployeeForm: React.FC = ({ })); }; - const handleRoleChange = (roleValue: 'admin' | 'instandhalter' | 'user') => { - console.log(`🔄 Rolle geändert: ${roleValue}`); - setFormData(prev => ({ - ...prev, - role: roleValue - })); - }; - - const handleEmployeeTypeChange = (employeeType: 'chef' | 'neuling' | 'erfahren') => { - console.log(`🔄 Mitarbeiter-Typ geändert: ${employeeType}`); + const handleEmployeeTypeChange = (employeeType: 'manager' | 'trainee' | 'experienced') => { + // Manager and experienced can work alone, trainee cannot + const canWorkAlone = employeeType === 'manager' || employeeType === 'experienced'; - // Automatische Werte basierend auf Typ - const isSufficientlyIndependent = employeeType === 'chef' ? true : - employeeType === 'erfahren' ? true : false; - setFormData(prev => ({ ...prev, employeeType, - isSufficientlyIndependent + canWorkAlone })); }; @@ -111,8 +71,6 @@ const EmployeeForm: React.FC = ({ setLoading(true); setError(''); - console.log('📤 Sende Formulardaten:', formData); - try { if (mode === 'create') { const createData: CreateEmployeeRequest = { @@ -121,52 +79,37 @@ const EmployeeForm: React.FC = ({ password: formData.password, role: formData.role, employeeType: formData.employeeType, - isSufficientlyIndependent: formData.isSufficientlyIndependent, + contractType: 'small', // Default value + canWorkAlone: formData.canWorkAlone }; - console.log('➕ Erstelle Mitarbeiter:', createData); await employeeService.createEmployee(createData); } else if (employee) { const updateData: UpdateEmployeeRequest = { name: formData.name.trim(), role: formData.role, employeeType: formData.employeeType, - isSufficientlyIndependent: formData.isSufficientlyIndependent, + contractType: employee.contractType, // Keep the existing contract type + canWorkAlone: formData.canWorkAlone, isActive: formData.isActive, }; - console.log('✏️ Aktualisiere Mitarbeiter:', updateData); await employeeService.updateEmployee(employee.id, updateData); } - console.log('✅ Erfolg - rufe onSuccess auf'); onSuccess(); } catch (err: any) { - console.error('❌ Fehler beim Speichern:', err); setError(err.message || `Fehler beim ${mode === 'create' ? 'Erstellen' : 'Aktualisieren'} des Mitarbeiters`); } finally { setLoading(false); } }; - const isFormValid = () => { - if (mode === 'create') { - return formData.name.trim() && - formData.email.trim() && - formData.password.length >= 6; - } - return formData.name.trim() && formData.email.trim(); - }; + const isFormValid = mode === 'create' + ? formData.name.trim() && formData.email.trim() && formData.password.length >= 6 + : formData.name.trim() && formData.email.trim(); - const getAvailableRoles = () => { - if (hasRole(['admin'])) { - return ROLE_OPTIONS; - } - if (hasRole(['instandhalter'])) { - return ROLE_OPTIONS.filter(role => role.value !== 'admin'); - } - return ROLE_OPTIONS.filter(role => role.value === 'user'); - }; - - const availableRoles = getAvailableRoles(); + const availableRoles = hasRole(['admin']) + ? ROLE_CONFIG + : ROLE_CONFIG.filter(role => role.value !== 'admin'); return (
= ({

👥 Mitarbeiter Kategorie

- {EMPLOYEE_TYPE_OPTIONS.map(type => ( + {EMPLOYEE_TYPE_CONFIG.map(type => (
= ({
))}
- - {/* Debug-Anzeige */} -
- Debug: Ausgewählter Typ: {formData.employeeType} -
{/* Eigenständigkeit */} @@ -386,29 +317,29 @@ const EmployeeForm: React.FC = ({ }}>
-
- {formData.employeeType === 'chef' + {formData.employeeType === 'manager' ? 'Chefs sind automatisch als eigenständig markiert.' : 'Dieser Mitarbeiter kann komplexe Aufgaben eigenständig lösen und benötigt keine ständige Betreuung.' } @@ -416,14 +347,14 @@ const EmployeeForm: React.FC = ({
- {formData.isSufficientlyIndependent ? 'EIGENSTÄNDIG' : 'BETREUUNG'} + {formData.canWorkAlone ? 'EIGENSTÄNDIG' : 'BETREUUNG'}
@@ -451,14 +382,14 @@ const EmployeeForm: React.FC = ({ backgroundColor: formData.role === role.value ? '#fef9e7' : 'white', cursor: 'pointer' }} - onClick={() => handleRoleChange(role.value)} + onClick={() => setFormData(prev => ({ ...prev, role: role.value }))} > handleRoleChange(role.value)} + onChange={() => setFormData(prev => ({ ...prev, role: role.value }))} style={{ marginRight: '10px', marginTop: '2px' @@ -537,14 +468,14 @@ const EmployeeForm: React.FC = ({ - - - - - {showPreview ? ( - - ) : ( - <> -
- - setTemplate(prev => ({ ...prev, name: e.target.value }))} - placeholder="z.B. Standard Woche, Teilzeit Modell, etc." - /> -
- -
- -