updated employee and shift structure

This commit is contained in:
2025-10-11 14:33:50 +02:00
parent eb49c58b2d
commit 5262b999aa
22 changed files with 1252 additions and 607 deletions

View File

@@ -1,9 +1,11 @@
// backend/src/controllers/authController.ts // backend/src/controllers/authController.ts
import { v4 as uuidv4 } from 'uuid';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import { db } from '../services/databaseService.js'; import { db } from '../services/databaseService.js';
import { AuthRequest } from '../middleware/auth.js'; import { AuthRequest } from '../middleware/auth.js';
import { Employee, EmployeeWithPassword } from '../models/Employee.js';
export interface User { export interface User {
id: number; id: number;
@@ -50,8 +52,8 @@ export const login = async (req: Request, res: Response) => {
} }
// Get user from database // Get user from database
const user = await db.get<UserWithPassword>( const user = await db.get<EmployeeWithPassword>(
'SELECT id, email, password, name, role FROM users WHERE email = ? AND is_active = 1', '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] [email]
); );
@@ -114,9 +116,9 @@ export const getCurrentUser = async (req: Request, res: Response) => {
return res.status(401).json({ error: 'Nicht authentifiziert' }); return res.status(401).json({ error: 'Nicht authentifiziert' });
} }
const user = await db.get<User>( const user = await db.get<Employee>(
'SELECT id, email, name, role FROM users WHERE id = ? AND is_active = 1', '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] // ← HIER: userId verwenden [jwtUser.userId]
); );
console.log('🔍 User found in database:', user ? 'Yes' : 'No'); 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 // Check if email already exists
const existingUser = await db.get<User>( const existingUser = await db.get<Employee>(
'SELECT id FROM users WHERE email = ?', 'SELECT id FROM employees WHERE email = ?',
[email] [email]
); );
@@ -187,9 +189,9 @@ export const register = async (req: Request, res: Response) => {
// Insert user // Insert user
const result = await db.run( const result = await db.run(
`INSERT INTO users (email, password, name, role) `INSERT INTO employees (id, email, password, name, role, employee_type, contract_type, can_work_alone, is_active)
VALUES (?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[email, hashedPassword, name, role] [uuidv4(), email, hashedPassword, name, role, 'experienced', 'small', false, 1]
); );
if (!result.lastID) { if (!result.lastID) {
@@ -197,8 +199,8 @@ export const register = async (req: Request, res: Response) => {
} }
// Get created user // Get created user
const newUser = await db.get<User>( const newUser = await db.get<Employee>(
'SELECT id, email, name, role FROM users WHERE id = ?', 'SELECT id, email, name, role FROM employees WHERE id = ?',
[result.lastID] [result.lastID]
); );

View File

@@ -3,34 +3,7 @@ import { Request, Response } from 'express';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { db } from '../services/databaseService.js'; import { db } from '../services/databaseService.js';
import { AuthRequest } from '../middleware/auth.js'; import { AuthRequest } from '../middleware/auth.js';
import { ShiftPlan, CreateShiftPlanRequest } from '../models/ShiftPlan.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;
}
export const getShiftPlans = async (req: AuthRequest, res: Response): Promise<void> => { export const getShiftPlans = async (req: AuthRequest, res: Response): Promise<void> => {
try { try {

View File

@@ -1,83 +1,118 @@
-- Tabelle für Benutzer -- Employees table
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS employees (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL, email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL, password TEXT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
role TEXT CHECK(role IN ('admin', 'user', 'instandhalter')) NOT NULL, role TEXT CHECK(role IN ('admin', 'user', 'maintenance')) NOT NULL,
employee_type TEXT CHECK(employee_type IN ('chef', 'neuling', 'erfahren')), employee_type TEXT CHECK(employee_type IN ('manager', 'trainee', 'experienced')) NOT NULL,
is_sufficiently_independent BOOLEAN DEFAULT FALSE, contract_type TEXT CHECK(contract_type IN ('small', 'large')) NOT NULL,
can_work_alone BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE, is_active BOOLEAN DEFAULT TRUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_login TEXT DEFAULT NULL last_login TEXT DEFAULT NULL
); );
-- Tabelle für Schichtvorlagen -- Shift plans table
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
CREATE TABLE IF NOT EXISTS shift_plans ( CREATE TABLE IF NOT EXISTS shift_plans (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
name TEXT NOT NULL, name TEXT NOT NULL,
start_date TEXT NOT NULL, description TEXT,
end_date TEXT NOT NULL, start_date TEXT,
template_id TEXT, end_date TEXT,
status TEXT CHECK(status IN ('draft', 'published')) DEFAULT 'draft', is_template BOOLEAN DEFAULT FALSE,
status TEXT CHECK(status IN ('draft', 'published', 'archived', 'template')) DEFAULT 'draft',
created_by TEXT NOT NULL, created_by TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (created_by) REFERENCES users(id), FOREIGN KEY (created_by) REFERENCES employees(id)
FOREIGN KEY (template_id) REFERENCES shift_templates(id)
); );
CREATE TABLE IF NOT EXISTS assigned_shifts ( -- Time slots within plans
CREATE TABLE IF NOT EXISTS time_slots (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
plan_id TEXT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
shift_plan_id TEXT NOT NULL,
date TEXT NOT NULL,
start_time TEXT NOT NULL, start_time TEXT NOT NULL,
end_time TEXT NOT NULL, end_time TEXT NOT NULL,
required_employees INTEGER DEFAULT 1, description TEXT,
assigned_employees TEXT DEFAULT '[]', -- JSON array of user IDs FOREIGN KEY (plan_id) REFERENCES shift_plans(id) ON DELETE CASCADE
FOREIGN KEY (shift_plan_id) REFERENCES shift_plans(id) ON DELETE CASCADE
); );
-- Zusätzliche Tabelle für Mitarbeiter-Verfügbarkeiten -- Shifts table (defines shifts for each day of week in the plan)
CREATE TABLE IF NOT EXISTS employee_availabilities ( 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, id TEXT PRIMARY KEY,
employee_id TEXT NOT NULL, employee_id TEXT NOT NULL,
day_of_week INTEGER NOT NULL CHECK (day_of_week >= 0 AND day_of_week <= 6), plan_id TEXT NOT NULL,
start_time TEXT NOT NULL, day_of_week INTEGER NOT NULL CHECK (day_of_week >= 1 AND day_of_week <= 7),
end_time TEXT NOT NULL, time_slot_id TEXT NOT NULL,
is_available BOOLEAN DEFAULT FALSE, preference_level INTEGER CHECK(preference_level IN (1, 2, 3)) NOT NULL,
FOREIGN KEY (employee_id) REFERENCES users(id) ON DELETE CASCADE 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);

View File

@@ -3,9 +3,10 @@ export interface Employee {
id: string; id: string;
email: string; email: string;
name: string; name: string;
role: 'admin' | 'instandhalter' | 'user'; role: 'admin' | 'maintenance' | 'user';
employeeType: 'chef' | 'neuling' | 'erfahren'; employeeType: 'manager' | 'trainee' | 'experienced';
isSufficientlyIndependent: boolean; contractType: 'small' | 'large';
canWorkAlone: boolean;
isActive: boolean; isActive: boolean;
createdAt: string; createdAt: string;
lastLogin?: string | null; lastLogin?: string | null;
@@ -15,19 +16,60 @@ export interface CreateEmployeeRequest {
email: string; email: string;
password: string; password: string;
name: string; name: string;
role: 'admin' | 'instandhalter' | 'user'; role: 'admin' | 'maintenance' | 'user';
employeeType: 'chef' | 'neuling' | 'erfahren'; employeeType: 'manager' | 'trainee' | 'experienced';
isSufficientlyIndependent: boolean; contractType: 'small' | 'large';
canWorkAlone: boolean;
} }
export interface UpdateEmployeeRequest { export interface UpdateEmployeeRequest {
name?: string; name?: string;
role?: 'admin' | 'instandhalter' | 'user'; role?: 'admin' | 'maintenance' | 'user';
employeeType?: 'chef' | 'neuling' | 'erfahren'; employeeType?: 'manager' | 'trainee' | 'experienced';
isSufficientlyIndependent?: boolean; contractType?: 'small' | 'large';
canWorkAlone?: boolean;
isActive?: boolean; isActive?: boolean;
} }
export interface EmployeeWithPassword extends Employee { export interface EmployeeWithPassword extends Employee {
password: string; 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<EmployeeAvailability, 'id' | 'employeeId'>[];
}
export interface UpdateAvailabilityRequest {
planId: string;
availabilities: Omit<EmployeeAvailability, 'id' | 'employeeId'>[];
}
export interface ManagerSelfAssignmentRequest {
planId: string;
assignments: Omit<ManagerAvailability, 'id' | 'employeeId' | 'assignedBy'>[];
}
export interface EmployeeWithAvailabilities extends Employee {
availabilities: EmployeeAvailability[];
} }

View File

@@ -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<ShiftSlot, 'id' | 'shiftId'>[];
}
export interface UpdateShiftSlotRequest {
name?: string;
description?: string;
isDefault?: boolean;
shifts?: Omit<ShiftSlot, 'id' | 'shiftId'>[];
}
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[];
}

View File

@@ -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<TimeSlot, 'id' | 'planId'>[];
shifts: Omit<Shift, 'id' | 'planId'>[];
}
export interface UpdateShiftPlanRequest {
name?: string;
description?: string;
startDate?: string;
endDate?: string;
status?: 'draft' | 'published' | 'archived' | 'template';
timeSlots?: Omit<TimeSlot, 'id' | 'planId'>[];
shifts?: Omit<Shift, 'id' | 'planId'>[];
}
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<EmployeeAvailability, 'id' | 'employeeId'>[];
}
// Default time slots for ZEBRA (specific workplace)
export const DEFAULT_ZEBRA_TIME_SLOTS: Omit<TimeSlot, 'id' | 'planId'>[] = [
{
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<TimeSlot, 'id' | 'planId'>[] = [
{
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<Shift, 'id' | 'planId'>[] = [
// 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<Shift, 'id' | 'planId'>[] = [
// 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;

View File

@@ -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<TemplateShiftSlot, 'id' | 'templateId'>[];
timeSlots: TemplateShiftTimeSlot[];
}
export interface UpdateShiftTemplateRequest {
name?: string;
description?: string;
isDefault?: boolean;
shifts?: Omit<TemplateShiftSlot, 'id' | 'templateId'>[];
timeSlots?: TemplateShiftTimeSlot[];
}

View File

@@ -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;
}

View File

@@ -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<EmployeeAvailability, 'id'>[] {
const availabilities: Omit<EmployeeAvailability, 'id'>[] = [];
// 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<ManagerAvailability, 'id'>[] {
const assignments: Omit<ManagerAvailability, 'id'>[] = [];
// 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;
}

View File

@@ -0,0 +1,3 @@
// backend/src/models/defaults/index.ts
export * from './employeeDefaults.js';
export * from './shiftPlanDefaults.js';

View File

@@ -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<TimeSlot, 'id' | 'planId'>[] = [
{
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<TimeSlot, 'id' | 'planId'>[] = [
{
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<Shift, 'id' | 'planId'>[] = [
// 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<Shift, 'id' | 'planId'>[] = [
// 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;

View File

@@ -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<EmployeeAvailability, 'id' | 'employeeId'>): 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)
);
}

View File

@@ -0,0 +1,3 @@
// backend/src/models/helpers/index.ts
export * from './employeeHelpers.js';
export * from './shiftPlanHelpers.js';

View File

@@ -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
};
}

View File

@@ -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 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 { 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 { interface AvailabilityManagerProps {
employee: Employee; employee: Employee;
@@ -11,7 +14,17 @@ interface AvailabilityManagerProps {
} }
// Verfügbarkeits-Level // 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<AvailabilityManagerProps> = ({ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
employee, employee,
@@ -22,6 +35,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
const [shiftPlans, setShiftPlans] = useState<ShiftPlan[]>([]); const [shiftPlans, setShiftPlans] = useState<ShiftPlan[]>([]);
const [selectedPlanId, setSelectedPlanId] = useState<string>(''); const [selectedPlanId, setSelectedPlanId] = useState<string>('');
const [selectedPlan, setSelectedPlan] = useState<ShiftPlan | null>(null); const [selectedPlan, setSelectedPlan] = useState<ShiftPlan | null>(null);
const [timeSlots, setTimeSlots] = useState<TimeSlot[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
@@ -36,7 +50,6 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
{ id: 0, name: 'Sonntag' } { id: 0, name: 'Sonntag' }
]; ];
// Verfügbarkeits-Level mit Farben und Beschreibungen
const availabilityLevels = [ const availabilityLevels = [
{ level: 1 as AvailabilityLevel, label: 'Bevorzugt', color: '#27ae60', bgColor: '#d5f4e6', description: 'Ideale Zeit' }, { 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' }, { level: 2 as AvailabilityLevel, label: 'Möglich', color: '#f39c12', bgColor: '#fef5e7', description: 'Akzeptable Zeit' },
@@ -53,60 +66,191 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
} }
}, [selectedPlanId]); }, [selectedPlanId]);
// NEU: Hole Zeit-Slots aus Schichtvorlagen als Hauptquelle
const loadTimeSlotsFromTemplates = async (): Promise<TimeSlot[]> => {
try {
console.log('🔄 LADE ZEIT-SLOTS AUS SCHICHTVORLAGEN...');
const shiftPlan = await shiftPlanService.getShiftPlans();
console.log('✅ SCHICHTVORLAGEN GELADEN:', shiftPlan);
const allTimeSlots = new Map<string, TimeSlot>();
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<string, TimeSlot>();
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 () => { const loadData = async () => {
try { try {
setLoading(true); setLoading(true);
console.log('🔄 LADE DATEN FÜR MITARBEITER:', employee.id);
// Load availabilities // 1. Lade Verfügbarkeiten
let existingAvailabilities: Availability[] = [];
try { try {
const availData = await employeeService.getAvailabilities(employee.id); existingAvailabilities = await employeeService.getAvailabilities(employee.id);
setAvailabilities(availData); console.log('✅ VERFÜGBARKEITEN GELADEN:', existingAvailabilities.length);
} catch (err) { } catch (err) {
// Falls keine Verfügbarkeiten existieren, erstelle Standard-Einträge (Level 3: nicht möglich) console.log('⚠️ KEINE VERFÜGBARKEITEN GEFUNDEN');
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);
} }
// Load shift plans // 2. Lade Schichtpläne
console.log('🔄 LADE SCHICHTPLÄNE...');
const plans = await shiftPlanService.getShiftPlans(); 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); 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) { if (plans.length > 0) {
const publishedPlan = plans.find(plan => plan.status === 'published'); const publishedPlan = plans.find(plan => plan.status === 'published');
const firstPlan = publishedPlan || plans[0]; const firstPlan = publishedPlan || plans[0];
setSelectedPlanId(firstPlan.id); setSelectedPlanId(firstPlan.id);
console.log('✅ SCHICHTPLAN AUSGEWÄHLT:', firstPlan.name);
} }
} catch (err: any) { } catch (err: any) {
console.error('Error loading data:', err); console.error('❌ FEHLER BEIM LADEN DER DATEN:', err);
setError('Daten konnten nicht geladen werden'); setError('Daten konnten nicht geladen werden');
} finally { } finally {
setLoading(false); setLoading(false);
@@ -115,33 +259,82 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
const loadSelectedPlan = async () => { const loadSelectedPlan = async () => {
try { try {
console.log('🔄 LADE AUSGEWÄHLTEN SCHICHTPLAN:', selectedPlanId);
const plan = await shiftPlanService.getShiftPlan(selectedPlanId); const plan = await shiftPlanService.getShiftPlan(selectedPlanId);
setSelectedPlan(plan); setSelectedPlan(plan);
console.log('✅ SCHICHTPLAN GELADEN:', {
name: plan.name,
shiftsCount: plan.shifts?.length || 0,
shifts: plan.shifts
});
} catch (err: any) { } catch (err: any) {
console.error('Error loading shift plan:', err); console.error('❌ FEHLER BEIM LADEN DES SCHICHTPLANS:', err);
setError('Schichtplan konnte nicht geladen werden'); setError('Schichtplan konnte nicht geladen werden');
} }
}; };
const handleAvailabilityLevelChange = (dayId: number, timeSlot: string, level: AvailabilityLevel) => { const handleAvailabilityLevelChange = (dayId: number, timeSlotId: string, level: AvailabilityLevel) => {
setAvailabilities(prev => console.log(`🔄 ÄNDERE VERFÜGBARKEIT: Tag ${dayId}, Slot ${timeSlotId}, Level ${level}`);
prev.map(avail =>
avail.dayOfWeek === dayId && getTimeSlotName(avail.startTime, avail.endTime) === timeSlot setAvailabilities(prev => {
? { const timeSlot = timeSlots.find(s => s.id === timeSlotId);
...avail, if (!timeSlot) {
availabilityLevel: level, console.log('❌ ZEIT-SLOT NICHT GEFUNDEN:', timeSlotId);
isAvailable: level !== 3 return prev;
} }
: avail
) 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 => { const getAvailabilityForDayAndSlot = (dayId: number, timeSlotId: string): AvailabilityLevel => {
if (startTime === '08:00' && endTime === '12:00') return 'Vormittag'; const timeSlot = timeSlots.find(s => s.id === timeSlotId);
if (startTime === '12:00' && endTime === '16:00') return 'Nachmittag'; if (!timeSlot) {
if (startTime === '16:00' && endTime === '20:00') return 'Abend'; console.log('❌ ZEIT-SLOT NICHT GEFUNDEN FÜR ABFRAGE:', timeSlotId);
return `${startTime}-${endTime}`; 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 () => { const handleSave = async () => {
@@ -150,99 +343,19 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
setError(''); setError('');
await employeeService.updateAvailabilities(employee.id, availabilities); await employeeService.updateAvailabilities(employee.id, availabilities);
console.log('✅ VERFÜGBARKEITEN ERFOLGREICH GESPEICHERT');
onSave(); onSave();
} catch (err: any) { } catch (err: any) {
console.error('❌ FEHLER BEIM SPEICHERN:', err);
setError(err.message || 'Fehler beim Speichern der Verfügbarkeiten'); setError(err.message || 'Fehler beim Speichern der Verfügbarkeiten');
} finally { } finally {
setSaving(false); setSaving(false);
} }
}; };
// Get availability level for a specific shift const getTimeSlotsForTimetable = (): TimeSlot[] => {
const getAvailabilityForShift = (shift: ShiftPlanShift): AvailabilityLevel => { return timeSlots;
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<number, ShiftPlanShift[]> = {};
// 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;
}; };
if (loading) { if (loading) {
@@ -253,9 +366,11 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
); );
} }
const timetableTimeSlots = getTimeSlotsForTimetable();
return ( return (
<div style={{ <div style={{
maxWidth: '1200px', maxWidth: '1400px',
margin: '0 auto', margin: '0 auto',
backgroundColor: 'white', backgroundColor: 'white',
padding: '30px', padding: '30px',
@@ -272,12 +387,45 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
📅 Verfügbarkeit verwalten 📅 Verfügbarkeit verwalten
</h2> </h2>
{/* Debug-Info */}
<div style={{
backgroundColor: timeSlots.length === 0 ? '#f8d7da' : '#d1ecf1',
border: `1px solid ${timeSlots.length === 0 ? '#f5c6cb' : '#bee5eb'}`,
borderRadius: '6px',
padding: '15px',
marginBottom: '20px'
}}>
<h4 style={{
margin: '0 0 10px 0',
color: timeSlots.length === 0 ? '#721c24' : '#0c5460'
}}>
{timeSlots.length === 0 ? '❌ PROBLEM: Keine Zeit-Slots gefunden' : '✅ Zeit-Slots geladen'}
</h4>
<div style={{ fontSize: '12px', fontFamily: 'monospace' }}>
<div><strong>Zeit-Slots gefunden:</strong> {timeSlots.length}</div>
<div><strong>Quelle:</strong> {timeSlots[0]?.source || 'Unbekannt'}</div>
<div><strong>Schichtpläne:</strong> {shiftPlans.length}</div>
</div>
{timeSlots.length > 0 && (
<div style={{ marginTop: '10px' }}>
<strong>Gefundene Zeit-Slots:</strong>
{timeSlots.map(slot => (
<div key={slot.id} style={{ fontSize: '11px', marginLeft: '10px' }}>
{slot.displayName}
</div>
))}
</div>
)}
</div>
{/* Rest der Komponente... */}
<div style={{ marginBottom: '20px' }}> <div style={{ marginBottom: '20px' }}>
<h3 style={{ margin: '0 0 10px 0', color: '#34495e' }}> <h3 style={{ margin: '0 0 10px 0', color: '#34495e' }}>
{employee.name} {employee.name}
</h3> </h3>
<p style={{ margin: 0, color: '#7f8c8d' }}> <p style={{ margin: 0, color: '#7f8c8d' }}>
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.
</p> </p>
</div> </div>
@@ -361,22 +509,16 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
<option value="">Bitte auswählen...</option> <option value="">Bitte auswählen...</option>
{shiftPlans.map(plan => ( {shiftPlans.map(plan => (
<option key={plan.id} value={plan.id}> <option key={plan.id} value={plan.id}>
{plan.name} ({plan.status === 'published' ? 'Veröffentlicht' : 'Entwurf'}) {plan.name} ({plan.shifts?.length || 0} Schichten)
</option> </option>
))} ))}
</select> </select>
</div> </div>
{selectedPlan && (
<div style={{ fontSize: '14px', color: '#666' }}>
Zeitraum: {new Date(selectedPlan.startDate).toLocaleDateString('de-DE')} - {new Date(selectedPlan.endDate).toLocaleDateString('de-DE')}
</div>
)}
</div> </div>
</div> </div>
{/* Verfügbarkeits-Timetable mit Dropdown-Menüs */} {/* Verfügbarkeits-Timetable */}
{selectedPlan && ( {timetableTimeSlots.length > 0 ? (
<div style={{ <div style={{
marginBottom: '30px', marginBottom: '30px',
border: '1px solid #e0e0e0', border: '1px solid #e0e0e0',
@@ -389,7 +531,10 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
padding: '15px 20px', padding: '15px 20px',
fontWeight: 'bold' fontWeight: 'bold'
}}> }}>
Verfügbarkeit für: {selectedPlan.name} Verfügbarkeit definieren
<div style={{ fontSize: '14px', fontWeight: 'normal', marginTop: '5px' }}>
{timetableTimeSlots.length} Schichttypen verfügbar
</div>
</div> </div>
<div style={{ overflowX: 'auto' }}> <div style={{ overflowX: 'auto' }}>
@@ -405,11 +550,11 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
textAlign: 'left', textAlign: 'left',
border: '1px solid #dee2e6', border: '1px solid #dee2e6',
fontWeight: 'bold', fontWeight: 'bold',
minWidth: '150px' minWidth: '200px'
}}> }}>
Zeit Schicht (Zeit)
</th> </th>
{timetableData.weekdays.map(weekday => ( {daysOfWeek.map(weekday => (
<th key={weekday.id} style={{ <th key={weekday.id} style={{
padding: '12px 16px', padding: '12px 16px',
textAlign: 'center', textAlign: 'center',
@@ -423,8 +568,8 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{['Vormittag', 'Nachmittag', 'Abend'].map((timeSlot, timeIndex) => ( {timetableTimeSlots.map((timeSlot, timeIndex) => (
<tr key={timeSlot} style={{ <tr key={`timeSlot-${timeSlot.id}-timeIndex-${timeIndex}`} style={{
backgroundColor: timeIndex % 2 === 0 ? 'white' : '#f8f9fa' backgroundColor: timeIndex % 2 === 0 ? 'white' : '#f8f9fa'
}}> }}>
<td style={{ <td style={{
@@ -433,14 +578,10 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
fontWeight: '500', fontWeight: '500',
backgroundColor: '#f8f9fa' backgroundColor: '#f8f9fa'
}}> }}>
{timeSlot} {timeSlot.displayName}
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
{timeSlot === 'Vormittag' ? '08:00-12:00' :
timeSlot === 'Nachmittag' ? '12:00-16:00' : '16:00-20:00'}
</div>
</td> </td>
{timetableData.weekdays.map(weekday => { {daysOfWeek.map(weekday => {
const currentLevel = getAvailabilityForDayAndSlot(weekday.id, timeSlot); const currentLevel = getAvailabilityForDayAndSlot(weekday.id, timeSlot.id);
const levelConfig = availabilityLevels.find(l => l.level === currentLevel); const levelConfig = availabilityLevels.find(l => l.level === currentLevel);
return ( return (
@@ -452,7 +593,10 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
}}> }}>
<select <select
value={currentLevel} value={currentLevel}
onChange={(e) => handleAvailabilityLevelChange(weekday.id, timeSlot, parseInt(e.target.value) as AvailabilityLevel)} onChange={(e) => {
const newLevel = parseInt(e.target.value) as AvailabilityLevel;
handleAvailabilityLevelChange(weekday.id, timeSlot.id, newLevel);
}}
style={{ style={{
padding: '8px 12px', padding: '8px 12px',
border: `2px solid ${levelConfig?.color || '#ddd'}`, border: `2px solid ${levelConfig?.color || '#ddd'}`,
@@ -460,7 +604,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
backgroundColor: levelConfig?.bgColor || 'white', backgroundColor: levelConfig?.bgColor || 'white',
color: levelConfig?.color || '#333', color: levelConfig?.color || '#333',
fontWeight: 'bold', fontWeight: 'bold',
minWidth: '120px', minWidth: '140px',
cursor: 'pointer', cursor: 'pointer',
textAlign: 'center' textAlign: 'center'
}} }}
@@ -479,14 +623,6 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
</option> </option>
))} ))}
</select> </select>
<div style={{
fontSize: '11px',
color: levelConfig?.color,
marginTop: '4px',
fontWeight: 'bold'
}}>
{levelConfig?.description}
</div>
</td> </td>
); );
})} })}
@@ -495,51 +631,25 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
{/* Legende */} ) : (
<div style={{ <div style={{
padding: '12px 16px', padding: '40px',
backgroundColor: '#e8f4fd', textAlign: 'center',
borderTop: '1px solid #b8d4f0', backgroundColor: '#f8f9fa',
fontSize: '14px', color: '#6c757d',
color: '#2c3e50' borderRadius: '8px',
}}> border: '1px solid #e9ecef'
<strong>Legende:</strong> }}>
{availabilityLevels.map(level => ( <div style={{ fontSize: '48px', marginBottom: '20px' }}></div>
<span key={level.level} style={{ marginLeft: '15px', display: 'inline-flex', alignItems: 'center', gap: '5px' }}> <h4>Keine Schichttypen konfiguriert</h4>
<div <p>Es wurden keine Zeit-Slots in den Schichtvorlagen oder -plänen gefunden.</p>
style={{ <p style={{ fontSize: '14px', marginTop: '10px' }}>
width: '12px', Bitte erstellen Sie zuerst Schichtvorlagen mit Zeit-Slots.
height: '12px', </p>
backgroundColor: level.bgColor,
border: `1px solid ${level.color}`,
borderRadius: '2px'
}}
/>
<strong style={{ color: level.color }}>{level.level}</strong>: {level.label}
</span>
))}
</div>
</div> </div>
)} )}
{/* Info Text */}
<div style={{
backgroundColor: '#e8f4fd',
border: '1px solid #b6d7e8',
borderRadius: '6px',
padding: '15px',
marginBottom: '20px'
}}>
<h4 style={{ margin: '0 0 8px 0', color: '#2c3e50' }}>💡 Information</h4>
<p style={{ margin: 0, color: '#546e7a', fontSize: '14px' }}>
<strong>1: Bevorzugt</strong> - Ideale Zeit für diesen Mitarbeiter<br/>
<strong>2: Möglich</strong> - Akzeptable Zeit, falls benötigt<br/>
<strong>3: Nicht möglich</strong> - Mitarbeiter ist nicht verfügbar<br/>
Das System priorisiert Mitarbeiter mit Level 1 für Schichtzuweisungen.
</p>
</div>
{/* Buttons */} {/* Buttons */}
<div style={{ <div style={{
display: 'flex', display: 'flex',
@@ -564,14 +674,14 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
<button <button
onClick={handleSave} onClick={handleSave}
disabled={saving} disabled={saving || timeSlots.length === 0}
style={{ style={{
padding: '12px 24px', padding: '12px 24px',
backgroundColor: saving ? '#bdc3c7' : '#3498db', backgroundColor: saving ? '#bdc3c7' : (timeSlots.length === 0 ? '#95a5a6' : '#3498db'),
color: 'white', color: 'white',
border: 'none', border: 'none',
borderRadius: '6px', borderRadius: '6px',
cursor: saving ? 'not-allowed' : 'pointer', cursor: (saving || timeSlots.length === 0) ? 'not-allowed' : 'pointer',
fontWeight: 'bold' fontWeight: 'bold'
}} }}
> >

View File

@@ -1,4 +1,4 @@
// frontend/src/pages/Employees/components/EmployeeForm.tsx - VEREINFACHT // frontend/src/pages/Employees/components/EmployeeForm.tsx - KORRIGIERT
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Employee, CreateEmployeeRequest, UpdateEmployeeRequest } from '../../../types/employee'; import { Employee, CreateEmployeeRequest, UpdateEmployeeRequest } from '../../../types/employee';
import { employeeService } from '../../../services/employeeService'; import { employeeService } from '../../../services/employeeService';
@@ -61,10 +61,11 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
useEffect(() => { useEffect(() => {
if (mode === 'edit' && employee) { if (mode === 'edit' && employee) {
console.log('📝 Lade Mitarbeiter-Daten:', employee);
setFormData({ setFormData({
name: employee.name, name: employee.name,
email: employee.email, email: employee.email,
password: '', password: '', // Passwort wird beim Bearbeiten nicht angezeigt
role: employee.role, role: employee.role,
employeeType: employee.employeeType, employeeType: employee.employeeType,
isSufficientlyIndependent: employee.isSufficientlyIndependent, isSufficientlyIndependent: employee.isSufficientlyIndependent,
@@ -75,6 +76,8 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const { name, value, type } = e.target; const { name, value, type } = e.target;
console.log(`🔄 Feld geändert: ${name} = ${value}`);
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
[name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value [name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value
@@ -82,6 +85,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
}; };
const handleRoleChange = (roleValue: 'admin' | 'instandhalter' | 'user') => { const handleRoleChange = (roleValue: 'admin' | 'instandhalter' | 'user') => {
console.log(`🔄 Rolle geändert: ${roleValue}`);
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
role: roleValue role: roleValue
@@ -89,12 +93,16 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
}; };
const handleEmployeeTypeChange = (employeeType: 'chef' | 'neuling' | 'erfahren') => { const handleEmployeeTypeChange = (employeeType: 'chef' | 'neuling' | 'erfahren') => {
console.log(`🔄 Mitarbeiter-Typ geändert: ${employeeType}`);
// Automatische Werte basierend auf Typ
const isSufficientlyIndependent = employeeType === 'chef' ? true :
employeeType === 'erfahren' ? true : false;
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
employeeType, employeeType,
// Automatische Werte basierend auf Typ isSufficientlyIndependent
isSufficientlyIndependent: employeeType === 'chef' ? true :
employeeType === 'erfahren' ? true : false
})); }));
}; };
@@ -103,30 +111,36 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
setLoading(true); setLoading(true);
setError(''); setError('');
console.log('📤 Sende Formulardaten:', formData);
try { try {
if (mode === 'create') { if (mode === 'create') {
const createData: CreateEmployeeRequest = { const createData: CreateEmployeeRequest = {
name: formData.name, name: formData.name.trim(),
email: formData.email, email: formData.email.trim(),
password: formData.password, password: formData.password,
role: formData.role, role: formData.role,
employeeType: formData.employeeType, employeeType: formData.employeeType,
isSufficientlyIndependent: formData.isSufficientlyIndependent, isSufficientlyIndependent: formData.isSufficientlyIndependent,
}; };
console.log(' Erstelle Mitarbeiter:', createData);
await employeeService.createEmployee(createData); await employeeService.createEmployee(createData);
} else if (employee) { } else if (employee) {
const updateData: UpdateEmployeeRequest = { const updateData: UpdateEmployeeRequest = {
name: formData.name, name: formData.name.trim(),
role: formData.role, role: formData.role,
employeeType: formData.employeeType, employeeType: formData.employeeType,
isSufficientlyIndependent: formData.isSufficientlyIndependent, isSufficientlyIndependent: formData.isSufficientlyIndependent,
isActive: formData.isActive, isActive: formData.isActive,
}; };
console.log('✏️ Aktualisiere Mitarbeiter:', updateData);
await employeeService.updateEmployee(employee.id, updateData); await employeeService.updateEmployee(employee.id, updateData);
} }
console.log('✅ Erfolg - rufe onSuccess auf');
onSuccess(); onSuccess();
} catch (err: any) { } catch (err: any) {
console.error('❌ Fehler beim Speichern:', err);
setError(err.message || `Fehler beim ${mode === 'create' ? 'Erstellen' : 'Aktualisieren'} des Mitarbeiters`); setError(err.message || `Fehler beim ${mode === 'create' ? 'Erstellen' : 'Aktualisieren'} des Mitarbeiters`);
} finally { } finally {
setLoading(false); setLoading(false);
@@ -338,6 +352,18 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
</div> </div>
))} ))}
</div> </div>
{/* Debug-Anzeige */}
<div style={{
marginTop: '15px',
padding: '10px',
backgroundColor: '#e8f4fd',
border: '1px solid #b6d7e8',
borderRadius: '4px',
fontSize: '12px'
}}>
<strong>Debug:</strong> Ausgewählter Typ: <code>{formData.employeeType}</code>
</div>
</div> </div>
{/* Eigenständigkeit */} {/* Eigenständigkeit */}

View File

@@ -2,7 +2,8 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext'; 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'; import { useNotification } from '../../contexts/NotificationContext';
const ShiftPlanView: React.FC = () => { const ShiftPlanView: React.FC = () => {
@@ -63,7 +64,7 @@ const ShiftPlanView: React.FC = () => {
// Get all unique shift types (name + time combination) // Get all unique shift types (name + time combination)
const shiftTypes = Array.from(new Set( const shiftTypes = Array.from(new Set(
shiftPlan.shifts.map(shift => shiftPlan.shifts.map(shift =>
`${shift.name}|${shift.startTime}|${shift.endTime}` `${shift.timeSlot.name}|${shift.timeSlot.startTime}|${shift.timeSlot.endTime}`
) )
)).map(shiftKey => { )).map(shiftKey => {
const [name, startTime, endTime] = shiftKey.split('|'); const [name, startTime, endTime] = shiftKey.split('|');
@@ -83,15 +84,15 @@ const ShiftPlanView: React.FC = () => {
const date = new Date(shift.date); const date = new Date(shift.date);
const dayOfWeek = date.getDay() === 0 ? 7 : date.getDay(); // Convert to 1-7 (Mon-Sun) const dayOfWeek = date.getDay() === 0 ? 7 : date.getDay(); // Convert to 1-7 (Mon-Sun)
return dayOfWeek === weekday && return dayOfWeek === weekday &&
shift.name === shiftType.name && shift.timeSlot.name === shiftType.name &&
shift.startTime === shiftType.startTime && shift.timeSlot.startTime === shiftType.startTime &&
shift.endTime === shiftType.endTime; shift.timeSlot.endTime === shiftType.endTime;
}); });
if (shiftsOnDay.length === 0) { if (shiftsOnDay.length === 0) {
weekdayData[weekday] = ''; weekdayData[weekday] = '';
} else { } 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); const totalRequired = shiftsOnDay.reduce((sum, shift) => sum + shift.requiredEmployees, 0);
weekdayData[weekday] = `${totalAssigned}/${totalRequired}`; weekdayData[weekday] = `${totalAssigned}/${totalRequired}`;
} }

View File

@@ -1,38 +1,9 @@
// frontend/src/services/shiftPlanService.ts // frontend/src/services/shiftPlanService.ts
import { authService } from './authService'; import { authService } from './authService';
import { ShiftPlan, CreateShiftPlanRequest, ShiftSlot } from '../types/shiftPlan.js';
const API_BASE = 'http://localhost:3002/api/shift-plans'; 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 = { export const shiftPlanService = {
async getShiftPlans(): Promise<ShiftPlan[]> { async getShiftPlans(): Promise<ShiftPlan[]> {
const response = await fetch(API_BASE, { const response = await fetch(API_BASE, {
@@ -85,17 +56,17 @@ export const shiftPlanService = {
const data = await response.json(); const data = await response.json();
// Convert snake_case to camelCase // Convert snake_case to camelCase
return { return data.map((plan: any) => ({
id: data.id, id: plan.id,
name: data.name, name: plan.name,
startDate: data.start_date, // Convert here startDate: plan.start_date,
endDate: data.end_date, // Convert here endDate: plan.end_date,
templateId: data.template_id, templateId: plan.template_id,
status: data.status, status: plan.status,
createdBy: data.created_by, createdBy: plan.created_by,
createdAt: data.created_at, createdAt: plan.created_at,
shifts: data.shifts || [] shifts: plan.shifts || []
}; }));
}, },
async createShiftPlan(plan: CreateShiftPlanRequest): Promise<ShiftPlan> { async createShiftPlan(plan: CreateShiftPlanRequest): Promise<ShiftPlan> {
@@ -158,7 +129,7 @@ export const shiftPlanService = {
} }
}, },
async updateShiftPlanShift(planId: string, shift: ShiftPlanShift): Promise<void> { async updateShiftPlanShift(planId: string, shift: ShiftSlot): Promise<void> {
const response = await fetch(`${API_BASE}/${planId}/shifts/${shift.id}`, { const response = await fetch(`${API_BASE}/${planId}/shifts/${shift.id}`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
@@ -177,7 +148,7 @@ export const shiftPlanService = {
} }
}, },
async addShiftPlanShift(planId: string, shift: Omit<ShiftPlanShift, 'id' | 'shiftPlanId' | 'assignedEmployees'>): Promise<void> { async addShiftPlanShift(planId: string, shift: Omit<ShiftSlot, 'id' | 'shiftPlanId' | 'assignedEmployees'>): Promise<void> {
const response = await fetch(`${API_BASE}/${planId}/shifts`, { const response = await fetch(`${API_BASE}/${planId}/shifts`, {
method: 'POST', method: 'POST',
headers: { headers: {

View File

@@ -1,11 +1,11 @@
// frontend/src/services/shiftTemplateService.ts // frontend/src/services/shiftTemplateService.ts
import { TemplateShift } from '../types/shiftTemplate'; import { ShiftPlan } from '../../../backend/src/models/shiftTemplate.js';
import { authService } from './authService'; import { authService } from './authService';
const API_BASE = 'http://localhost:3002/api/shift-templates'; const API_BASE = 'http://localhost:3002/api/shift-templates';
export const shiftTemplateService = { export const shiftTemplateService = {
async getTemplates(): Promise<TemplateShift[]> { async getTemplates(): Promise<ShiftPlan[]> {
const response = await fetch(API_BASE, { const response = await fetch(API_BASE, {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@@ -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
}

View File

@@ -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' }
];

View File

@@ -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;
}