mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-12-01 06:55:45 +01:00
updated employee and shift structure
This commit is contained in:
@@ -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]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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[];
|
||||||
|
}
|
||||||
@@ -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[];
|
|
||||||
}
|
|
||||||
219
backend/src/models/ShiftPlan.ts
Normal file
219
backend/src/models/ShiftPlan.ts
Normal 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;
|
||||||
@@ -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[];
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
85
backend/src/models/defaults/employeeDefaults.ts
Normal file
85
backend/src/models/defaults/employeeDefaults.ts
Normal 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;
|
||||||
|
}
|
||||||
3
backend/src/models/defaults/index.ts
Normal file
3
backend/src/models/defaults/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// backend/src/models/defaults/index.ts
|
||||||
|
export * from './employeeDefaults.js';
|
||||||
|
export * from './shiftPlanDefaults.js';
|
||||||
146
backend/src/models/defaults/shiftPlanDefaults.ts
Normal file
146
backend/src/models/defaults/shiftPlanDefaults.ts
Normal 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;
|
||||||
128
backend/src/models/helpers/employeeHelpers.ts
Normal file
128
backend/src/models/helpers/employeeHelpers.ts
Normal 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
3
backend/src/models/helpers/index.ts
Normal file
3
backend/src/models/helpers/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// backend/src/models/helpers/index.ts
|
||||||
|
export * from './employeeHelpers.js';
|
||||||
|
export * from './shiftPlanHelpers.js';
|
||||||
118
backend/src/models/helpers/shiftPlanHelpers.ts
Normal file
118
backend/src/models/helpers/shiftPlanHelpers.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
console.log('❌ ZEIT-SLOT NICHT GEFUNDEN:', timeSlotId);
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingIndex = prev.findIndex(avail =>
|
||||||
|
avail.dayOfWeek === dayId &&
|
||||||
|
avail.startTime === timeSlot.startTime &&
|
||||||
|
avail.endTime === timeSlot.endTime
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`🔍 EXISTIERENDE VERFÜGBARKEIT GEFUNDEN AN INDEX:`, existingIndex);
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
// Update existing availability
|
||||||
|
const updated = [...prev];
|
||||||
|
updated[existingIndex] = {
|
||||||
|
...updated[existingIndex],
|
||||||
availabilityLevel: level,
|
availabilityLevel: level,
|
||||||
isAvailable: level !== 3
|
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];
|
||||||
}
|
}
|
||||||
: avail
|
});
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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,50 +631,24 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Legende */}
|
|
||||||
<div style={{
|
|
||||||
padding: '12px 16px',
|
|
||||||
backgroundColor: '#e8f4fd',
|
|
||||||
borderTop: '1px solid #b8d4f0',
|
|
||||||
fontSize: '14px',
|
|
||||||
color: '#2c3e50'
|
|
||||||
}}>
|
|
||||||
<strong>Legende:</strong>
|
|
||||||
{availabilityLevels.map(level => (
|
|
||||||
<span key={level.level} style={{ marginLeft: '15px', display: 'inline-flex', alignItems: 'center', gap: '5px' }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: '12px',
|
|
||||||
height: '12px',
|
|
||||||
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={{
|
<div style={{
|
||||||
backgroundColor: '#e8f4fd',
|
padding: '40px',
|
||||||
border: '1px solid #b6d7e8',
|
textAlign: 'center',
|
||||||
borderRadius: '6px',
|
backgroundColor: '#f8f9fa',
|
||||||
padding: '15px',
|
color: '#6c757d',
|
||||||
marginBottom: '20px'
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #e9ecef'
|
||||||
}}>
|
}}>
|
||||||
<h4 style={{ margin: '0 0 8px 0', color: '#2c3e50' }}>💡 Information</h4>
|
<div style={{ fontSize: '48px', marginBottom: '20px' }}>❌</div>
|
||||||
<p style={{ margin: 0, color: '#546e7a', fontSize: '14px' }}>
|
<h4>Keine Schichttypen konfiguriert</h4>
|
||||||
<strong>1: Bevorzugt</strong> - Ideale Zeit für diesen Mitarbeiter<br/>
|
<p>Es wurden keine Zeit-Slots in den Schichtvorlagen oder -plänen gefunden.</p>
|
||||||
<strong>2: Möglich</strong> - Akzeptable Zeit, falls benötigt<br/>
|
<p style={{ fontSize: '14px', marginTop: '10px' }}>
|
||||||
<strong>3: Nicht möglich</strong> - Mitarbeiter ist nicht verfügbar<br/>
|
Bitte erstellen Sie zuerst Schichtvorlagen mit Zeit-Slots.
|
||||||
Das System priorisiert Mitarbeiter mit Level 1 für Schichtzuweisungen.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Buttons */}
|
{/* Buttons */}
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -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'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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' }
|
|
||||||
];
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user