mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-11-30 22:45:46 +01:00
frontend with ony errors
This commit is contained in:
@@ -10,93 +10,79 @@ import {
|
|||||||
import { AuthRequest } from '../middleware/auth.js';
|
import { AuthRequest } from '../middleware/auth.js';
|
||||||
import { createPlanFromPreset, TEMPLATE_PRESETS } from '../models/defaults/shiftPlanDefaults.js';
|
import { createPlanFromPreset, TEMPLATE_PRESETS } from '../models/defaults/shiftPlanDefaults.js';
|
||||||
|
|
||||||
|
async function getPlanWithDetails(planId: string) {
|
||||||
|
const plan = await db.get<any>(`
|
||||||
|
SELECT sp.*, e.name as created_by_name
|
||||||
|
FROM shift_plans sp
|
||||||
|
LEFT JOIN employees e ON sp.created_by = e.id
|
||||||
|
WHERE sp.id = ?
|
||||||
|
`, [planId]);
|
||||||
|
|
||||||
|
if (!plan) return null;
|
||||||
|
|
||||||
|
const [timeSlots, shifts] = await Promise.all([
|
||||||
|
db.all<any>(`SELECT * FROM time_slots WHERE plan_id = ? ORDER BY start_time`, [planId]),
|
||||||
|
db.all<any>(`
|
||||||
|
SELECT s.*, ts.name as time_slot_name, ts.start_time, ts.end_time
|
||||||
|
FROM shifts s
|
||||||
|
LEFT JOIN time_slots ts ON s.time_slot_id = ts.id
|
||||||
|
WHERE s.plan_id = ?
|
||||||
|
ORDER BY s.day_of_week, ts.start_time
|
||||||
|
`, [planId])
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
plan: {
|
||||||
|
...plan,
|
||||||
|
isTemplate: plan.is_template === 1,
|
||||||
|
startDate: plan.start_date,
|
||||||
|
endDate: plan.end_date,
|
||||||
|
createdBy: plan.created_by,
|
||||||
|
createdAt: plan.created_at,
|
||||||
|
},
|
||||||
|
timeSlots: timeSlots.map(slot => ({
|
||||||
|
id: slot.id,
|
||||||
|
planId: slot.plan_id,
|
||||||
|
name: slot.name,
|
||||||
|
startTime: slot.start_time,
|
||||||
|
endTime: slot.end_time,
|
||||||
|
description: slot.description
|
||||||
|
})),
|
||||||
|
shifts: shifts.map(shift => ({
|
||||||
|
id: shift.id,
|
||||||
|
planId: shift.plan_id,
|
||||||
|
timeSlotId: shift.time_slot_id,
|
||||||
|
dayOfWeek: shift.day_of_week,
|
||||||
|
requiredEmployees: shift.required_employees,
|
||||||
|
color: shift.color,
|
||||||
|
timeSlot: {
|
||||||
|
id: shift.time_slot_id,
|
||||||
|
name: shift.time_slot_name,
|
||||||
|
startTime: shift.start_time,
|
||||||
|
endTime: shift.end_time
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simplified getShiftPlans using shared helper
|
||||||
export const getShiftPlans = async (req: Request, res: Response): Promise<void> => {
|
export const getShiftPlans = async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
console.log('🔍 Lade Schichtpläne...');
|
const plans = await db.all<any>(`
|
||||||
|
|
||||||
const shiftPlans = await db.all<any>(`
|
|
||||||
SELECT sp.*, e.name as created_by_name
|
SELECT sp.*, e.name as created_by_name
|
||||||
FROM shift_plans sp
|
FROM shift_plans sp
|
||||||
LEFT JOIN employees e ON sp.created_by = e.id
|
LEFT JOIN employees e ON sp.created_by = e.id
|
||||||
ORDER BY sp.created_at DESC
|
ORDER BY sp.created_at DESC
|
||||||
`);
|
`);
|
||||||
|
|
||||||
console.log(`✅ ${shiftPlans.length} Schichtpläne gefunden:`, shiftPlans.map(p => p.name));
|
|
||||||
|
|
||||||
// Für jeden Plan die Schichten und Zeit-Slots laden
|
|
||||||
const plansWithDetails = await Promise.all(
|
const plansWithDetails = await Promise.all(
|
||||||
shiftPlans.map(async (plan) => {
|
plans.map(async (plan) => {
|
||||||
// Lade Zeit-Slots
|
const details = await getPlanWithDetails(plan.id);
|
||||||
const timeSlots = await db.all<any>(`
|
return details ? { ...details.plan, timeSlots: details.timeSlots, shifts: details.shifts } : null;
|
||||||
SELECT * FROM time_slots
|
|
||||||
WHERE plan_id = ?
|
|
||||||
ORDER BY start_time
|
|
||||||
`, [plan.id]);
|
|
||||||
|
|
||||||
// Lade Schichten
|
|
||||||
const shifts = await db.all<any>(`
|
|
||||||
SELECT s.*, ts.name as time_slot_name, ts.start_time, ts.end_time
|
|
||||||
FROM shifts s
|
|
||||||
LEFT JOIN time_slots ts ON s.time_slot_id = ts.id
|
|
||||||
WHERE s.plan_id = ?
|
|
||||||
ORDER BY s.day_of_week, ts.start_time
|
|
||||||
`, [plan.id]);
|
|
||||||
|
|
||||||
// Lade geplante Schichten (nur für nicht-Template Pläne)
|
|
||||||
let scheduledShifts = [];
|
|
||||||
if (!plan.is_template) {
|
|
||||||
scheduledShifts = await db.all<any>(`
|
|
||||||
SELECT ss.*, ts.name as time_slot_name
|
|
||||||
FROM scheduled_shifts ss
|
|
||||||
LEFT JOIN time_slots ts ON ss.time_slot_id = ts.id
|
|
||||||
WHERE ss.plan_id = ?
|
|
||||||
ORDER BY ss.date, ts.start_time
|
|
||||||
`, [plan.id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...plan,
|
|
||||||
isTemplate: plan.is_template === 1,
|
|
||||||
startDate: plan.start_date,
|
|
||||||
endDate: plan.end_date,
|
|
||||||
createdBy: plan.created_by,
|
|
||||||
createdAt: plan.created_at,
|
|
||||||
timeSlots: timeSlots.map(slot => ({
|
|
||||||
id: slot.id,
|
|
||||||
planId: slot.plan_id,
|
|
||||||
name: slot.name,
|
|
||||||
startTime: slot.start_time,
|
|
||||||
endTime: slot.end_time,
|
|
||||||
description: slot.description
|
|
||||||
})),
|
|
||||||
shifts: shifts.map(shift => ({
|
|
||||||
id: shift.id,
|
|
||||||
planId: shift.plan_id,
|
|
||||||
timeSlotId: shift.time_slot_id,
|
|
||||||
dayOfWeek: shift.day_of_week,
|
|
||||||
requiredEmployees: shift.required_employees,
|
|
||||||
color: shift.color,
|
|
||||||
timeSlot: {
|
|
||||||
id: shift.time_slot_id,
|
|
||||||
name: shift.time_slot_name,
|
|
||||||
startTime: shift.start_time,
|
|
||||||
endTime: shift.end_time
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
scheduledShifts: scheduledShifts.map(shift => ({
|
|
||||||
id: shift.id,
|
|
||||||
planId: shift.plan_id,
|
|
||||||
date: shift.date,
|
|
||||||
timeSlotId: shift.time_slot_id,
|
|
||||||
requiredEmployees: shift.required_employees,
|
|
||||||
assignedEmployees: JSON.parse(shift.assigned_employees || '[]'),
|
|
||||||
timeSlotName: shift.time_slot_name
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json(plansWithDetails);
|
res.json(plansWithDetails.filter(Boolean));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching shift plans:', error);
|
console.error('Error fetching shift plans:', error);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
|||||||
@@ -19,6 +19,36 @@ export const MANAGER_DEFAULTS = {
|
|||||||
isActive: true
|
isActive: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const EMPLOYEE_TYPE_CONFIG = {
|
||||||
|
manager: {
|
||||||
|
value: 'manager' as const,
|
||||||
|
label: 'Chef/Administrator',
|
||||||
|
color: '#e74c3c',
|
||||||
|
independent: true,
|
||||||
|
description: 'Vollzugriff auf alle Funktionen und Mitarbeiterverwaltung'
|
||||||
|
},
|
||||||
|
experienced: {
|
||||||
|
value: 'experienced' as const,
|
||||||
|
label: 'Erfahren',
|
||||||
|
color: '#3498db',
|
||||||
|
independent: true,
|
||||||
|
description: 'Langjährige Erfahrung, kann komplexe Aufgaben übernehmen'
|
||||||
|
},
|
||||||
|
trainee: {
|
||||||
|
value: 'trainee' as const,
|
||||||
|
label: 'Neuling',
|
||||||
|
color: '#27ae60',
|
||||||
|
independent: false,
|
||||||
|
description: 'Benötigt Einarbeitung und Unterstützung'
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const ROLE_CONFIG = [
|
||||||
|
{ value: 'user', label: 'Mitarbeiter', description: 'Kann eigene Schichten einsehen', color: '#27ae60' },
|
||||||
|
{ value: 'instandhalter', label: 'Instandhalter', description: 'Kann Schichtpläne erstellen und Mitarbeiter verwalten', color: '#3498db' },
|
||||||
|
{ value: 'admin', label: 'Administrator', description: 'Voller Zugriff auf alle Funktionen', color: '#e74c3c' }
|
||||||
|
] as const;
|
||||||
|
|
||||||
// Contract type descriptions
|
// Contract type descriptions
|
||||||
export const CONTRACT_TYPE_DESCRIPTIONS = {
|
export const CONTRACT_TYPE_DESCRIPTIONS = {
|
||||||
small: '1 Schicht pro Woche',
|
small: '1 Schicht pro Woche',
|
||||||
@@ -26,13 +56,6 @@ export const CONTRACT_TYPE_DESCRIPTIONS = {
|
|||||||
manager: 'Kein Vertragslimit - Immer MO und DI verfügbar'
|
manager: 'Kein Vertragslimit - Immer MO und DI verfügbar'
|
||||||
} as const;
|
} 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
|
// Availability preference descriptions
|
||||||
export const AVAILABILITY_PREFERENCES = {
|
export const AVAILABILITY_PREFERENCES = {
|
||||||
1: { label: 'Bevorzugt', color: '#10b981', description: 'Möchte diese Schicht arbeiten' },
|
1: { label: 'Bevorzugt', color: '#10b981', description: 'Möchte diese Schicht arbeiten' },
|
||||||
|
|||||||
@@ -1,128 +1,40 @@
|
|||||||
// backend/src/models/helpers/employeeHelpers.ts
|
// backend/src/models/helpers/employeeHelpers.ts
|
||||||
import { Employee, CreateEmployeeRequest, EmployeeAvailability, ManagerAvailability } from '../Employee.js';
|
import { Employee, CreateEmployeeRequest, EmployeeAvailability } from '../Employee.js';
|
||||||
|
|
||||||
// Validation helpers
|
// Simplified validation - use schema validation instead
|
||||||
export function validateEmployee(employee: CreateEmployeeRequest): string[] {
|
export function validateEmployeeData(employee: CreateEmployeeRequest): string[] {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
if (!employee.email || !employee.email.includes('@')) {
|
if (!employee.email?.includes('@')) {
|
||||||
errors.push('Valid email is required');
|
errors.push('Valid email is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!employee.password || employee.password.length < 6) {
|
if (employee.password?.length < 6) {
|
||||||
errors.push('Password must be at least 6 characters long');
|
errors.push('Password must be at least 6 characters long');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!employee.name || employee.name.trim().length < 2) {
|
if (!employee.name?.trim() || employee.name.trim().length < 2) {
|
||||||
errors.push('Name is required and must be at least 2 characters long');
|
errors.push('Name is required and must be at least 2 characters long');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!employee.contractType) {
|
|
||||||
errors.push('Contract type is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateAvailability(availability: Omit<EmployeeAvailability, 'id' | 'employeeId'>): string[] {
|
// Simplified business logic helpers
|
||||||
const errors: string[] = [];
|
export const isManager = (employee: Employee): boolean =>
|
||||||
|
employee.employeeType === 'manager';
|
||||||
|
|
||||||
if (availability.dayOfWeek < 1 || availability.dayOfWeek > 7) {
|
export const isTrainee = (employee: Employee): boolean =>
|
||||||
errors.push('Day of week must be between 1 and 7');
|
employee.employeeType === 'trainee';
|
||||||
}
|
|
||||||
|
|
||||||
if (![1, 2, 3].includes(availability.preferenceLevel)) {
|
export const isExperienced = (employee: Employee): boolean =>
|
||||||
errors.push('Preference level must be 1, 2, or 3');
|
employee.employeeType === 'experienced';
|
||||||
}
|
|
||||||
|
|
||||||
if (!availability.timeSlotId) {
|
export const isAdmin = (employee: Employee): boolean =>
|
||||||
errors.push('Time slot ID is required');
|
employee.role === 'admin';
|
||||||
}
|
|
||||||
|
|
||||||
if (!availability.planId) {
|
export const canEmployeeWorkAlone = (employee: Employee): boolean =>
|
||||||
errors.push('Plan ID is required');
|
employee.canWorkAlone && isExperienced(employee);
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
export const getEmployeeWorkHours = (employee: Employee): number =>
|
||||||
}
|
isManager(employee) ? 999 : (employee.contractType === 'small' ? 1 : 2);
|
||||||
|
|
||||||
// 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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
75
frontend/src/models/Employee.ts
Normal file
75
frontend/src/models/Employee.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// backend/src/models/Employee.ts
|
||||||
|
export interface Employee {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
role: 'admin' | 'maintenance' | 'user';
|
||||||
|
employeeType: 'manager' | 'trainee' | 'experienced';
|
||||||
|
contractType: 'small' | 'large';
|
||||||
|
canWorkAlone: boolean;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
lastLogin?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateEmployeeRequest {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
name: string;
|
||||||
|
role: 'admin' | 'maintenance' | 'user';
|
||||||
|
employeeType: 'manager' | 'trainee' | 'experienced';
|
||||||
|
contractType: 'small' | 'large';
|
||||||
|
canWorkAlone: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateEmployeeRequest {
|
||||||
|
name?: string;
|
||||||
|
role?: 'admin' | 'maintenance' | 'user';
|
||||||
|
employeeType?: 'manager' | 'trainee' | 'experienced';
|
||||||
|
contractType?: 'small' | 'large';
|
||||||
|
canWorkAlone?: boolean;
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmployeeWithPassword extends Employee {
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmployeeAvailability {
|
||||||
|
id: string;
|
||||||
|
employeeId: string;
|
||||||
|
planId: string;
|
||||||
|
dayOfWeek: number; // 1=Monday, 7=Sunday
|
||||||
|
timeSlotId: string;
|
||||||
|
preferenceLevel: 1 | 2 | 3; // 1:preferred, 2:available, 3:unavailable
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ManagerAvailability {
|
||||||
|
id: string;
|
||||||
|
employeeId: string;
|
||||||
|
planId: string;
|
||||||
|
dayOfWeek: number; // 1=Monday, 7=Sunday
|
||||||
|
timeSlotId: string;
|
||||||
|
isAvailable: boolean; // Simple available/not available
|
||||||
|
assignedBy: string; // Always self for manager
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateAvailabilityRequest {
|
||||||
|
planId: string;
|
||||||
|
availabilities: Omit<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[];
|
||||||
|
}
|
||||||
105
frontend/src/models/ShiftPlan.ts
Normal file
105
frontend/src/models/ShiftPlan.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
// backend/src/models/ShiftPlan.ts
|
||||||
|
export interface ShiftPlan {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
startDate?: string; // Optional for templates
|
||||||
|
endDate?: string; // Optional for templates
|
||||||
|
isTemplate: boolean;
|
||||||
|
status: 'draft' | 'published' | 'archived' | 'template';
|
||||||
|
createdBy: string;
|
||||||
|
createdAt: string;
|
||||||
|
timeSlots: TimeSlot[];
|
||||||
|
shifts: Shift[];
|
||||||
|
scheduledShifts?: ScheduledShift[]; // Only for non-template plans with dates
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimeSlot {
|
||||||
|
id: string;
|
||||||
|
planId: string;
|
||||||
|
name: string;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Shift {
|
||||||
|
id: string;
|
||||||
|
planId: string;
|
||||||
|
timeSlotId: string;
|
||||||
|
dayOfWeek: number; // 1=Monday, 7=Sunday
|
||||||
|
requiredEmployees: number;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScheduledShift {
|
||||||
|
id: string;
|
||||||
|
planId: string;
|
||||||
|
date: string;
|
||||||
|
timeSlotId: string;
|
||||||
|
requiredEmployees: number;
|
||||||
|
assignedEmployees: string[]; // employee IDs
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShiftAssignment {
|
||||||
|
id: string;
|
||||||
|
scheduledShiftId: string;
|
||||||
|
employeeId: string;
|
||||||
|
assignmentStatus: 'assigned' | 'cancelled';
|
||||||
|
assignedAt: string;
|
||||||
|
assignedBy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmployeeAvailability {
|
||||||
|
id: string;
|
||||||
|
employeeId: string;
|
||||||
|
planId: string;
|
||||||
|
dayOfWeek: number;
|
||||||
|
timeSlotId: string;
|
||||||
|
preferenceLevel: 1 | 2 | 3; // 1:preferred, 2:available, 3:unavailable
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request/Response DTOs
|
||||||
|
export interface CreateShiftPlanRequest {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
isTemplate: boolean;
|
||||||
|
timeSlots: Omit<TimeSlot, 'id' | 'planId'>[];
|
||||||
|
shifts: Omit<Shift, 'id' | 'planId'>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateShiftPlanRequest {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
isTemplate?: boolean;
|
||||||
|
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'>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateRequiredEmployeesRequest {
|
||||||
|
requiredEmployees: number;
|
||||||
|
}
|
||||||
108
frontend/src/models/defaults/employeeDefaults.ts
Normal file
108
frontend/src/models/defaults/employeeDefaults.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
// backend/src/models/defaults/employeeDefaults.ts
|
||||||
|
import { EmployeeAvailability, ManagerAvailability } from '../Employee.js';
|
||||||
|
|
||||||
|
// Default employee data for quick creation
|
||||||
|
export const EMPLOYEE_DEFAULTS = {
|
||||||
|
role: 'user' as const,
|
||||||
|
employeeType: 'experienced' as const,
|
||||||
|
contractType: 'small' as const,
|
||||||
|
canWorkAlone: false,
|
||||||
|
isActive: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Manager-specific defaults
|
||||||
|
export const MANAGER_DEFAULTS = {
|
||||||
|
role: 'admin' as const,
|
||||||
|
employeeType: 'manager' as const,
|
||||||
|
contractType: 'large' as const, // Not really used but required by DB
|
||||||
|
canWorkAlone: true,
|
||||||
|
isActive: true
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EMPLOYEE_TYPE_CONFIG = {
|
||||||
|
manager: {
|
||||||
|
value: 'manager' as const,
|
||||||
|
label: 'Chef/Administrator',
|
||||||
|
color: '#e74c3c',
|
||||||
|
independent: true,
|
||||||
|
description: 'Vollzugriff auf alle Funktionen und Mitarbeiterverwaltung'
|
||||||
|
},
|
||||||
|
experienced: {
|
||||||
|
value: 'experienced' as const,
|
||||||
|
label: 'Erfahren',
|
||||||
|
color: '#3498db',
|
||||||
|
independent: true,
|
||||||
|
description: 'Langjährige Erfahrung, kann komplexe Aufgaben übernehmen'
|
||||||
|
},
|
||||||
|
trainee: {
|
||||||
|
value: 'trainee' as const,
|
||||||
|
label: 'Neuling',
|
||||||
|
color: '#27ae60',
|
||||||
|
independent: false,
|
||||||
|
description: 'Benötigt Einarbeitung und Unterstützung'
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const ROLE_CONFIG = [
|
||||||
|
{ value: 'user', label: 'Mitarbeiter', description: 'Kann eigene Schichten einsehen', color: '#27ae60' },
|
||||||
|
{ value: 'instandhalter', label: 'Instandhalter', description: 'Kann Schichtpläne erstellen und Mitarbeiter verwalten', color: '#3498db' },
|
||||||
|
{ value: 'admin', label: 'Administrator', description: 'Voller Zugriff auf alle Funktionen', color: '#e74c3c' }
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// Contract type descriptions
|
||||||
|
export const CONTRACT_TYPE_DESCRIPTIONS = {
|
||||||
|
small: '1 Schicht pro Woche',
|
||||||
|
large: '2 Schichten pro Woche',
|
||||||
|
manager: 'Kein Vertragslimit - Immer MO und DI verfügbar'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Availability preference descriptions
|
||||||
|
export const AVAILABILITY_PREFERENCES = {
|
||||||
|
1: { label: 'Bevorzugt', color: '#10b981', description: 'Möchte diese Schicht arbeiten' },
|
||||||
|
2: { label: 'Möglich', color: '#f59e0b', description: 'Kann diese Schicht arbeiten' },
|
||||||
|
3: { label: 'Nicht möglich', color: '#ef4444', description: 'Kann diese Schicht nicht arbeiten' }
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Default availability for new employees (all shifts unavailable as level 3)
|
||||||
|
export function createDefaultAvailabilities(employeeId: string, planId: string, timeSlotIds: string[]): Omit<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
frontend/src/models/defaults/index.ts
Normal file
3
frontend/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
frontend/src/models/defaults/shiftPlanDefaults.ts
Normal file
146
frontend/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;
|
||||||
40
frontend/src/models/helpers/employeeHelpers.ts
Normal file
40
frontend/src/models/helpers/employeeHelpers.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// backend/src/models/helpers/employeeHelpers.ts
|
||||||
|
import { Employee, CreateEmployeeRequest, EmployeeAvailability } from '../Employee.js';
|
||||||
|
|
||||||
|
// Simplified validation - use schema validation instead
|
||||||
|
export function validateEmployeeData(employee: CreateEmployeeRequest): string[] {
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
if (!employee.email?.includes('@')) {
|
||||||
|
errors.push('Valid email is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (employee.password?.length < 6) {
|
||||||
|
errors.push('Password must be at least 6 characters long');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!employee.name?.trim() || employee.name.trim().length < 2) {
|
||||||
|
errors.push('Name is required and must be at least 2 characters long');
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simplified business logic helpers
|
||||||
|
export const isManager = (employee: Employee): boolean =>
|
||||||
|
employee.employeeType === 'manager';
|
||||||
|
|
||||||
|
export const isTrainee = (employee: Employee): boolean =>
|
||||||
|
employee.employeeType === 'trainee';
|
||||||
|
|
||||||
|
export const isExperienced = (employee: Employee): boolean =>
|
||||||
|
employee.employeeType === 'experienced';
|
||||||
|
|
||||||
|
export const isAdmin = (employee: Employee): boolean =>
|
||||||
|
employee.role === 'admin';
|
||||||
|
|
||||||
|
export const canEmployeeWorkAlone = (employee: Employee): boolean =>
|
||||||
|
employee.canWorkAlone && isExperienced(employee);
|
||||||
|
|
||||||
|
export const getEmployeeWorkHours = (employee: Employee): number =>
|
||||||
|
isManager(employee) ? 999 : (employee.contractType === 'small' ? 1 : 2);
|
||||||
3
frontend/src/models/helpers/index.ts
Normal file
3
frontend/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
frontend/src/models/helpers/shiftPlanHelpers.ts
Normal file
118
frontend/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,6 +1,6 @@
|
|||||||
// frontend/src/pages/Employees/EmployeeManagement.tsx
|
// frontend/src/pages/Employees/EmployeeManagement.tsx
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Employee } from '../../../../backend/src/models/employee';
|
import { Employee } from '../../models/Employee';
|
||||||
import { employeeService } from '../../services/employeeService';
|
import { employeeService } from '../../services/employeeService';
|
||||||
import EmployeeList from './components/EmployeeList';
|
import EmployeeList from './components/EmployeeList';
|
||||||
import EmployeeForm from './components/EmployeeForm';
|
import EmployeeForm from './components/EmployeeForm';
|
||||||
@@ -220,7 +220,6 @@ const EmployeeManagement: React.FC = () => {
|
|||||||
onEdit={handleEditEmployee}
|
onEdit={handleEditEmployee}
|
||||||
onDelete={handleDeleteEmployee} // Jetzt mit Employee-Objekt
|
onDelete={handleDeleteEmployee} // Jetzt mit Employee-Objekt
|
||||||
onManageAvailability={handleManageAvailability}
|
onManageAvailability={handleManageAvailability}
|
||||||
currentUserRole={hasRole(['admin']) ? 'admin' : 'instandhalter'}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// frontend/src/pages/Employees/components/AvailabilityManager.tsx
|
// frontend/src/pages/Employees/components/AvailabilityManager.tsx
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Employee, EmployeeAvailability } from '../../../../../backend/src/models/employee';
|
|
||||||
import { employeeService } from '../../../services/employeeService';
|
import { employeeService } from '../../../services/employeeService';
|
||||||
import { shiftPlanService } from '../../../services/shiftPlanService';
|
import { shiftPlanService } from '../../../services/shiftPlanService';
|
||||||
import { ShiftPlan, TimeSlot, Shift } from '../../../../../backend/src/models/shiftPlan';
|
import { Employee, EmployeeAvailability } from '../../../models/Employee';
|
||||||
|
import { ShiftPlan, TimeSlot, Shift } from '../../../models/ShiftPlan';
|
||||||
|
|
||||||
interface AvailabilityManagerProps {
|
interface AvailabilityManagerProps {
|
||||||
employee: Employee;
|
employee: Employee;
|
||||||
@@ -180,11 +180,13 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
// 3. Extract time slots from plans
|
// 3. Extract time slots from plans
|
||||||
let extractedTimeSlots = extractTimeSlotsFromPlans(plans);
|
let extractedTimeSlots = extractTimeSlotsFromPlans(plans);
|
||||||
|
|
||||||
// 4. Fallback to default slots if none found
|
/* 4. Fallback to default slots if none found
|
||||||
if (extractedTimeSlots.length === 0) {
|
if (extractedTimeSlots.length === 0) {
|
||||||
console.log('⚠️ KEINE ZEIT-SLOTS GEFUNDEN, VERWENDE STANDARD-SLOTS');
|
console.log('⚠️ KEINE ZEIT-SLOTS GEFUNDEN, VERWENDE STANDARD-SLOTS');
|
||||||
extractedTimeSlots = getDefaultTimeSlots();
|
extractedTimeSlots = getDefaultTimeSlots();
|
||||||
}
|
}*/
|
||||||
|
|
||||||
|
console.log('✅ GEFUNDENE ZEIT-SLOTS:', extractedTimeSlots.length, extractedTimeSlots);
|
||||||
|
|
||||||
setTimeSlots(extractedTimeSlots);
|
setTimeSlots(extractedTimeSlots);
|
||||||
setShiftPlans(plans);
|
setShiftPlans(plans);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// frontend/src/pages/Employees/components/EmployeeForm.tsx - KORRIGIERT
|
// 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 '../../../models/Employee';
|
||||||
|
import { ROLE_CONFIG, EMPLOYEE_TYPE_CONFIG } from '../../../models/defaults/employeeDefaults';
|
||||||
import { employeeService } from '../../../services/employeeService';
|
import { employeeService } from '../../../services/employeeService';
|
||||||
import { useAuth } from '../../../contexts/AuthContext';
|
import { useAuth } from '../../../contexts/AuthContext';
|
||||||
|
|
||||||
@@ -11,34 +12,6 @@ interface EmployeeFormProps {
|
|||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rollen Definition
|
|
||||||
const ROLE_OPTIONS = [
|
|
||||||
{ value: 'user', label: 'Mitarbeiter', description: 'Kann eigene Schichten einsehen' },
|
|
||||||
{ value: 'instandhalter', label: 'Instandhalter', description: 'Kann Schichtpläne erstellen und Mitarbeiter verwalten' },
|
|
||||||
{ value: 'admin', label: 'Administrator', description: 'Voller Zugriff auf alle Funktionen' }
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
// Mitarbeiter Typen Definition
|
|
||||||
const EMPLOYEE_TYPE_OPTIONS = [
|
|
||||||
{
|
|
||||||
value: 'chef',
|
|
||||||
label: '👨💼 Chef/Administrator',
|
|
||||||
description: 'Vollzugriff auf alle Funktionen und Mitarbeiterverwaltung',
|
|
||||||
color: '#e74c3c'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'erfahren',
|
|
||||||
label: '👴 Erfahren',
|
|
||||||
description: 'Langjährige Erfahrung, kann komplexe Aufgaben übernehmen',
|
|
||||||
color: '#3498db'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'neuling',
|
|
||||||
label: '👶 Neuling',
|
|
||||||
description: 'Benötigt Einarbeitung und Unterstützung',
|
|
||||||
color: '#27ae60'
|
|
||||||
}
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||||
mode,
|
mode,
|
||||||
@@ -50,9 +23,9 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
|||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
role: 'user' as 'admin' | 'instandhalter' | 'user',
|
role: 'user' as 'admin' | 'maintenance' | 'user',
|
||||||
employeeType: 'neuling' as 'chef' | 'neuling' | 'erfahren',
|
employeeType: 'trainee' as 'manager' | 'trainee' | 'experienced',
|
||||||
isSufficientlyIndependent: false,
|
canWorkAlone: false,
|
||||||
isActive: true
|
isActive: true
|
||||||
});
|
});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -61,22 +34,20 @@ 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: '', // Passwort wird beim Bearbeiten nicht angezeigt
|
password: '', // Passwort wird beim Bearbeiten nicht angezeigt
|
||||||
role: employee.role,
|
role: employee.role,
|
||||||
employeeType: employee.employeeType,
|
employeeType: employee.employeeType,
|
||||||
isSufficientlyIndependent: employee.isSufficientlyIndependent,
|
canWorkAlone: employee.canWorkAlone,
|
||||||
isActive: employee.isActive
|
isActive: employee.isActive
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [mode, employee]);
|
}, [mode, employee]);
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||||
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,
|
||||||
@@ -84,25 +55,14 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRoleChange = (roleValue: 'admin' | 'instandhalter' | 'user') => {
|
const handleEmployeeTypeChange = (employeeType: 'manager' | 'trainee' | 'experienced') => {
|
||||||
console.log(`🔄 Rolle geändert: ${roleValue}`);
|
// Manager and experienced can work alone, trainee cannot
|
||||||
setFormData(prev => ({
|
const canWorkAlone = employeeType === 'manager' || employeeType === 'experienced';
|
||||||
...prev,
|
|
||||||
role: roleValue
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
||||||
isSufficientlyIndependent
|
canWorkAlone
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,8 +71,6 @@ 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 = {
|
||||||
@@ -121,52 +79,37 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
|||||||
password: formData.password,
|
password: formData.password,
|
||||||
role: formData.role,
|
role: formData.role,
|
||||||
employeeType: formData.employeeType,
|
employeeType: formData.employeeType,
|
||||||
isSufficientlyIndependent: formData.isSufficientlyIndependent,
|
contractType: 'small', // Default value
|
||||||
|
canWorkAlone: formData.canWorkAlone
|
||||||
};
|
};
|
||||||
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.trim(),
|
name: formData.name.trim(),
|
||||||
role: formData.role,
|
role: formData.role,
|
||||||
employeeType: formData.employeeType,
|
employeeType: formData.employeeType,
|
||||||
isSufficientlyIndependent: formData.isSufficientlyIndependent,
|
contractType: employee.contractType, // Keep the existing contract type
|
||||||
|
canWorkAlone: formData.canWorkAlone,
|
||||||
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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isFormValid = () => {
|
const isFormValid = mode === 'create'
|
||||||
if (mode === 'create') {
|
? formData.name.trim() && formData.email.trim() && formData.password.length >= 6
|
||||||
return formData.name.trim() &&
|
: formData.name.trim() && formData.email.trim();
|
||||||
formData.email.trim() &&
|
|
||||||
formData.password.length >= 6;
|
|
||||||
}
|
|
||||||
return formData.name.trim() && formData.email.trim();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAvailableRoles = () => {
|
const availableRoles = hasRole(['admin'])
|
||||||
if (hasRole(['admin'])) {
|
? ROLE_CONFIG
|
||||||
return ROLE_OPTIONS;
|
: ROLE_CONFIG.filter(role => role.value !== 'admin');
|
||||||
}
|
|
||||||
if (hasRole(['instandhalter'])) {
|
|
||||||
return ROLE_OPTIONS.filter(role => role.value !== 'admin');
|
|
||||||
}
|
|
||||||
return ROLE_OPTIONS.filter(role => role.value === 'user');
|
|
||||||
};
|
|
||||||
|
|
||||||
const availableRoles = getAvailableRoles();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -294,7 +237,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
|||||||
<h3 style={{ margin: '0 0 15px 0', color: '#495057' }}>👥 Mitarbeiter Kategorie</h3>
|
<h3 style={{ margin: '0 0 15px 0', color: '#495057' }}>👥 Mitarbeiter Kategorie</h3>
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||||
{EMPLOYEE_TYPE_OPTIONS.map(type => (
|
{EMPLOYEE_TYPE_CONFIG.map(type => (
|
||||||
<div
|
<div
|
||||||
key={type.value}
|
key={type.value}
|
||||||
style={{
|
style={{
|
||||||
@@ -352,18 +295,6 @@ 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 */}
|
||||||
@@ -386,29 +317,29 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
|||||||
}}>
|
}}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="isSufficientlyIndependent"
|
name="canWorkAlone"
|
||||||
id="isSufficientlyIndependent"
|
id="canWorkAlone"
|
||||||
checked={formData.isSufficientlyIndependent}
|
checked={formData.canWorkAlone}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
disabled={formData.employeeType === 'chef'}
|
disabled={formData.employeeType === 'manager'}
|
||||||
style={{
|
style={{
|
||||||
width: '20px',
|
width: '20px',
|
||||||
height: '20px',
|
height: '20px',
|
||||||
opacity: formData.employeeType === 'chef' ? 0.5 : 1
|
opacity: formData.employeeType === 'manager' ? 0.5 : 1
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<label htmlFor="isSufficientlyIndependent" style={{
|
<label htmlFor="canWorkAlone" style={{
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: '#2c3e50',
|
color: '#2c3e50',
|
||||||
display: 'block',
|
display: 'block',
|
||||||
opacity: formData.employeeType === 'chef' ? 0.5 : 1
|
opacity: formData.employeeType === 'manager' ? 0.5 : 1
|
||||||
}}>
|
}}>
|
||||||
Als ausreichend eigenständig markieren
|
Als ausreichend eigenständig markieren
|
||||||
{formData.employeeType === 'chef' && ' (Automatisch für Chefs)'}
|
{formData.employeeType === 'manager' && ' (Automatisch für Chefs)'}
|
||||||
</label>
|
</label>
|
||||||
<div style={{ fontSize: '14px', color: '#7f8c8d' }}>
|
<div style={{ fontSize: '14px', color: '#7f8c8d' }}>
|
||||||
{formData.employeeType === 'chef'
|
{formData.employeeType === 'manager'
|
||||||
? 'Chefs sind automatisch als eigenständig markiert.'
|
? 'Chefs sind automatisch als eigenständig markiert.'
|
||||||
: 'Dieser Mitarbeiter kann komplexe Aufgaben eigenständig lösen und benötigt keine ständige Betreuung.'
|
: 'Dieser Mitarbeiter kann komplexe Aufgaben eigenständig lösen und benötigt keine ständige Betreuung.'
|
||||||
}
|
}
|
||||||
@@ -416,14 +347,14 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '6px 12px',
|
padding: '6px 12px',
|
||||||
backgroundColor: formData.isSufficientlyIndependent ? '#27ae60' : '#e74c3c',
|
backgroundColor: formData.canWorkAlone ? '#27ae60' : '#e74c3c',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
borderRadius: '15px',
|
borderRadius: '15px',
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
opacity: formData.employeeType === 'chef' ? 0.7 : 1
|
opacity: formData.employeeType === 'manager' ? 0.7 : 1
|
||||||
}}>
|
}}>
|
||||||
{formData.isSufficientlyIndependent ? 'EIGENSTÄNDIG' : 'BETREUUNG'}
|
{formData.canWorkAlone ? 'EIGENSTÄNDIG' : 'BETREUUNG'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -451,14 +382,14 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
|||||||
backgroundColor: formData.role === role.value ? '#fef9e7' : 'white',
|
backgroundColor: formData.role === role.value ? '#fef9e7' : 'white',
|
||||||
cursor: 'pointer'
|
cursor: 'pointer'
|
||||||
}}
|
}}
|
||||||
onClick={() => handleRoleChange(role.value)}
|
onClick={() => setFormData(prev => ({ ...prev, role: role.value }))}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name="role"
|
name="role"
|
||||||
value={role.value}
|
value={role.value}
|
||||||
checked={formData.role === role.value}
|
checked={formData.role === role.value}
|
||||||
onChange={() => handleRoleChange(role.value)}
|
onChange={() => setFormData(prev => ({ ...prev, role: role.value }))}
|
||||||
style={{
|
style={{
|
||||||
marginRight: '10px',
|
marginRight: '10px',
|
||||||
marginTop: '2px'
|
marginTop: '2px'
|
||||||
@@ -537,14 +468,14 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading || !isFormValid()}
|
disabled={loading || !isFormValid}
|
||||||
style={{
|
style={{
|
||||||
padding: '12px 24px',
|
padding: '12px 24px',
|
||||||
backgroundColor: loading ? '#bdc3c7' : (isFormValid() ? '#27ae60' : '#95a5a6'),
|
backgroundColor: loading ? '#bdc3c7' : (isFormValid ? '#27ae60' : '#95a5a6'),
|
||||||
color: 'white',
|
color: 'white',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: '6px',
|
borderRadius: '6px',
|
||||||
cursor: (loading || !isFormValid()) ? 'not-allowed' : 'pointer',
|
cursor: (loading || !isFormValid) ? 'not-allowed' : 'pointer',
|
||||||
fontWeight: 'bold'
|
fontWeight: 'bold'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// frontend/src/pages/Employees/components/EmployeeList.tsx - KORRIGIERT
|
// frontend/src/pages/Employees/components/EmployeeList.tsx - KORRIGIERT
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Employee } from '../../../types/employee';
|
import { ROLE_CONFIG, EMPLOYEE_TYPE_CONFIG } from '../../../../../backend/src/models/defaults/employeeDefaults';
|
||||||
|
import { Employee } from '../../../../../backend/src/models/employee';
|
||||||
import { useAuth } from '../../../contexts/AuthContext';
|
import { useAuth } from '../../../contexts/AuthContext';
|
||||||
|
|
||||||
interface EmployeeListProps {
|
interface EmployeeListProps {
|
||||||
@@ -8,26 +9,22 @@ interface EmployeeListProps {
|
|||||||
onEdit: (employee: Employee) => void;
|
onEdit: (employee: Employee) => void;
|
||||||
onDelete: (employee: Employee) => void;
|
onDelete: (employee: Employee) => void;
|
||||||
onManageAvailability: (employee: Employee) => void;
|
onManageAvailability: (employee: Employee) => void;
|
||||||
currentUserRole: 'admin' | 'instandhalter';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const EmployeeList: React.FC<EmployeeListProps> = ({
|
const EmployeeList: React.FC<EmployeeListProps> = ({
|
||||||
employees,
|
employees,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
onManageAvailability,
|
onManageAvailability
|
||||||
currentUserRole
|
|
||||||
}) => {
|
}) => {
|
||||||
const [filter, setFilter] = useState<'all' | 'active' | 'inactive'>('active');
|
const [filter, setFilter] = useState<'all' | 'active' | 'inactive'>('active');
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const { user: currentUser } = useAuth();
|
const { user: currentUser, hasRole } = useAuth();
|
||||||
|
|
||||||
const filteredEmployees = employees.filter(employee => {
|
const filteredEmployees = employees.filter(employee => {
|
||||||
// Status-Filter
|
|
||||||
if (filter === 'active' && !employee.isActive) return false;
|
if (filter === 'active' && !employee.isActive) return false;
|
||||||
if (filter === 'inactive' && employee.isActive) return false;
|
if (filter === 'inactive' && employee.isActive) return false;
|
||||||
|
|
||||||
// Suchfilter
|
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
const term = searchTerm.toLowerCase();
|
const term = searchTerm.toLowerCase();
|
||||||
return (
|
return (
|
||||||
@@ -41,28 +38,25 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const getRoleBadgeColor = (role: string) => {
|
// Simplified permission checks
|
||||||
switch (role) {
|
const canDeleteEmployee = (employee: Employee): boolean => {
|
||||||
case 'admin': return '#e74c3c';
|
if (!hasRole(['admin'])) return false;
|
||||||
case 'instandhalter': return '#3498db';
|
if (employee.id === currentUser?.id) return false;
|
||||||
case 'user': return '#27ae60';
|
if (employee.role === 'admin' && !hasRole(['admin'])) return false;
|
||||||
default: return '#95a5a6';
|
return true;
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEmployeeTypeBadge = (type: string) => {
|
const canEditEmployee = (employee: Employee): boolean => {
|
||||||
switch (type) {
|
if (hasRole(['admin'])) return true;
|
||||||
case 'chef': return { text: '👨💼 CHEF', color: '#e74c3c', bgColor: '#fadbd8' };
|
if (hasRole(['maintenance'])) {
|
||||||
case 'erfahren': return { text: '👴 ERFAHREN', color: '#3498db', bgColor: '#d6eaf8' };
|
return employee.role === 'user' || employee.id === currentUser?.id;
|
||||||
case 'neuling': return { text: '👶 NEULING', color: '#27ae60', bgColor: '#d5f4e6' };
|
|
||||||
default: return { text: 'UNBEKANNT', color: '#95a5a6', bgColor: '#ecf0f1' };
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getIndependenceBadge = (isIndependent: boolean) => {
|
// Using shared configuration for consistent styling
|
||||||
return isIndependent
|
const getEmployeeTypeBadge = (type: keyof typeof EMPLOYEE_TYPE_CONFIG) => {
|
||||||
? { text: '✅ Eigenständig', color: '#27ae60', bgColor: '#d5f4e6' }
|
return EMPLOYEE_TYPE_CONFIG[type] || EMPLOYEE_TYPE_CONFIG.trainee;
|
||||||
: { text: '❌ Betreuung', color: '#e74c3c', bgColor: '#fadbd8' };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusBadge = (isActive: boolean) => {
|
const getStatusBadge = (isActive: boolean) => {
|
||||||
@@ -71,31 +65,10 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
|||||||
: { text: 'Inaktiv', color: '#e74c3c', bgColor: '#fadbd8' };
|
: { text: 'Inaktiv', color: '#e74c3c', bgColor: '#fadbd8' };
|
||||||
};
|
};
|
||||||
|
|
||||||
// Kann Benutzer löschen?
|
const getIndependenceBadge = (canWorkAlone: boolean) => {
|
||||||
const canDeleteEmployee = (employee: Employee): boolean => {
|
return canWorkAlone
|
||||||
// Nur Admins können löschen
|
? { text: '✅ Eigenständig', color: '#27ae60', bgColor: '#d5f4e6' }
|
||||||
if (currentUserRole !== 'admin') return false;
|
: { text: '❌ Betreuung', color: '#e74c3c', bgColor: '#fadbd8' };
|
||||||
|
|
||||||
// Kann sich nicht selbst löschen
|
|
||||||
if (employee.id === currentUser?.id) return false;
|
|
||||||
|
|
||||||
// Admins können nur von Admins gelöscht werden
|
|
||||||
if (employee.role === 'admin' && currentUserRole !== 'admin') return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Kann Benutzer bearbeiten?
|
|
||||||
const canEditEmployee = (employee: Employee): boolean => {
|
|
||||||
// Admins können alle bearbeiten
|
|
||||||
if (currentUserRole === 'admin') return true;
|
|
||||||
|
|
||||||
// Instandhalter können nur User und sich selbst bearbeiten
|
|
||||||
if (currentUserRole === 'instandhalter') {
|
|
||||||
return employee.role === 'user' || employee.id === currentUser?.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (employees.length === 0) {
|
if (employees.length === 0) {
|
||||||
@@ -197,8 +170,8 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
|||||||
|
|
||||||
{filteredEmployees.map(employee => {
|
{filteredEmployees.map(employee => {
|
||||||
const employeeType = getEmployeeTypeBadge(employee.employeeType);
|
const employeeType = getEmployeeTypeBadge(employee.employeeType);
|
||||||
const independence = getIndependenceBadge(employee.isSufficientlyIndependent);
|
const independence = getIndependenceBadge(employee.canWorkAlone);
|
||||||
const roleColor = getRoleBadgeColor(employee.role);
|
const roleColor = '#d5f4e6'; // Default color
|
||||||
const status = getStatusBadge(employee.isActive);
|
const status = getStatusBadge(employee.isActive);
|
||||||
const canEdit = canEditEmployee(employee);
|
const canEdit = canEditEmployee(employee);
|
||||||
const canDelete = canDeleteEmployee(employee);
|
const canDelete = canDeleteEmployee(employee);
|
||||||
@@ -239,7 +212,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
|||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: employeeType.bgColor,
|
backgroundColor: employeeType.color,
|
||||||
color: employeeType.color,
|
color: employeeType.color,
|
||||||
padding: '6px 12px',
|
padding: '6px 12px',
|
||||||
borderRadius: '15px',
|
borderRadius: '15px',
|
||||||
@@ -248,7 +221,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
|||||||
display: 'inline-block'
|
display: 'inline-block'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{employeeType.text}
|
{employeeType.label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -284,7 +257,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{employee.role === 'admin' ? 'ADMIN' :
|
{employee.role === 'admin' ? 'ADMIN' :
|
||||||
employee.role === 'instandhalter' ? 'INSTANDHALTER' : 'MITARBEITER'}
|
employee.role === 'maintenance' ? 'INSTANDHALTER' : 'MITARBEITER'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -322,7 +295,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
|||||||
flexWrap: 'wrap'
|
flexWrap: 'wrap'
|
||||||
}}>
|
}}>
|
||||||
{/* Verfügbarkeit Button */}
|
{/* Verfügbarkeit Button */}
|
||||||
{(currentUserRole === 'admin' || currentUserRole === 'instandhalter') && (
|
{(employee.role === 'admin' || employee.role === 'maintenance') && (
|
||||||
<button
|
<button
|
||||||
onClick={() => onManageAvailability(employee)}
|
onClick={() => onManageAvailability(employee)}
|
||||||
style={{
|
style={{
|
||||||
@@ -385,7 +358,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Platzhalter für Symmetrie */}
|
{/* Platzhalter für Symmetrie */}
|
||||||
{!canEdit && !canDelete && (currentUserRole !== 'admin' && currentUserRole !== 'instandhalter') && (
|
{!canEdit && !canDelete && (employee.role !== 'admin' && employee.role !== 'maintenance') && (
|
||||||
<div style={{ width: '32px', height: '32px' }}></div>
|
<div style={{ width: '32px', height: '32px' }}></div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,9 +3,14 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { shiftTemplateService } from '../../services/shiftTemplateService';
|
import { shiftTemplateService } from '../../services/shiftTemplateService';
|
||||||
import { shiftPlanService } from '../../services/shiftPlanService';
|
import { shiftPlanService } from '../../services/shiftPlanService';
|
||||||
import { TemplateShift } from '../../types/shiftTemplate';
|
|
||||||
import styles from './ShiftPlanCreate.module.css';
|
import styles from './ShiftPlanCreate.module.css';
|
||||||
|
|
||||||
|
export interface TemplateShift {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
isDefault?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const ShiftPlanCreate: React.FC = () => {
|
const ShiftPlanCreate: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
// frontend/src/pages/ShiftPlans/ShiftPlanEdit.tsx
|
// frontend/src/pages/ShiftPlans/ShiftPlanEdit.tsx
|
||||||
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 { shiftPlanService, ShiftPlan, ShiftPlanShift } from '../../services/shiftPlanService';
|
import { shiftPlanService } from '../../services/shiftPlanService';
|
||||||
|
import { ShiftPlan, Shift } from '../../../../backend/src/models/shiftPlan';
|
||||||
import { useNotification } from '../../contexts/NotificationContext';
|
import { useNotification } from '../../contexts/NotificationContext';
|
||||||
|
import { getTimeSlotById } from '../../models/helpers/shiftPlanHelpers';
|
||||||
|
|
||||||
const ShiftPlanEdit: React.FC = () => {
|
const ShiftPlanEdit: React.FC = () => {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
@@ -10,12 +12,10 @@ const ShiftPlanEdit: React.FC = () => {
|
|||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
const [shiftPlan, setShiftPlan] = useState<ShiftPlan | null>(null);
|
const [shiftPlan, setShiftPlan] = useState<ShiftPlan | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [editingShift, setEditingShift] = useState<ShiftPlanShift | null>(null);
|
const [editingShift, setEditingShift] = useState<Shift | null>(null);
|
||||||
const [newShift, setNewShift] = useState<Partial<ShiftPlanShift>>({
|
const [newShift, setNewShift] = useState<Partial<ScheduledShift>>({
|
||||||
date: '',
|
date: '',
|
||||||
name: '',
|
timeSlotId: '',
|
||||||
startTime: '',
|
|
||||||
endTime: '',
|
|
||||||
requiredEmployees: 1
|
requiredEmployees: 1
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -41,16 +41,10 @@ const ShiftPlanEdit: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateShift = async (shift: ShiftPlanShift) => {
|
const handleUpdateShift = async (shift: Shift) => {
|
||||||
if (!shiftPlan || !id) return;
|
if (!shiftPlan || !id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await shiftPlanService.updateShiftPlanShift(id, shift);
|
|
||||||
showNotification({
|
|
||||||
type: 'success',
|
|
||||||
title: 'Erfolg',
|
|
||||||
message: 'Schicht wurde aktualisiert.'
|
|
||||||
});
|
|
||||||
loadShiftPlan();
|
loadShiftPlan();
|
||||||
setEditingShift(null);
|
setEditingShift(null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -66,23 +60,16 @@ const ShiftPlanEdit: React.FC = () => {
|
|||||||
const handleAddShift = async () => {
|
const handleAddShift = async () => {
|
||||||
if (!shiftPlan || !id) return;
|
if (!shiftPlan || !id) return;
|
||||||
|
|
||||||
if (!newShift.date || !newShift.name || !newShift.startTime || !newShift.endTime || !newShift.requiredEmployees) {
|
if (!getTimeSlotById(shiftPlan, newShift.timeSlotId?) || !newShift.name || !newShift.startTime || !newShift.endTime || !newShift.requiredEmployees) {
|
||||||
showNotification({
|
showNotification({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'Fehler',
|
title: 'Fehler',
|
||||||
message: 'Bitte füllen Sie alle Pflichtfelder aus.'
|
message: 'Bitte füllen Sie alle Pflichtfelder aus.'
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await shiftPlanService.addShiftPlanShift(id, {
|
|
||||||
date: newShift.date,
|
|
||||||
name: newShift.name,
|
|
||||||
startTime: newShift.startTime,
|
|
||||||
endTime: newShift.endTime,
|
|
||||||
requiredEmployees: Number(newShift.requiredEmployees)
|
|
||||||
});
|
|
||||||
showNotification({
|
showNotification({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: 'Erfolg',
|
title: 'Erfolg',
|
||||||
@@ -112,12 +99,6 @@ const ShiftPlanEdit: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await shiftPlanService.deleteShiftPlanShift(id!, shiftId);
|
|
||||||
showNotification({
|
|
||||||
type: 'success',
|
|
||||||
title: 'Erfolg',
|
|
||||||
message: 'Schicht wurde gelöscht.'
|
|
||||||
});
|
|
||||||
loadShiftPlan();
|
loadShiftPlan();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting shift:', error);
|
console.error('Error deleting shift:', error);
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, 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 } from '../../../../backend/src/models/shiftPlan';
|
||||||
import { useNotification } from '../../contexts/NotificationContext';
|
import { useNotification } from '../../contexts/NotificationContext';
|
||||||
|
import { formatDate } from '../../utils/foramatters';
|
||||||
|
|
||||||
const ShiftPlanList: React.FC = () => {
|
const ShiftPlanList: React.FC = () => {
|
||||||
const { hasRole } = useAuth();
|
const { hasRole } = useAuth();
|
||||||
@@ -55,14 +57,6 @@ const ShiftPlanList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
|
||||||
return new Date(dateString).toLocaleDateString('de-DE', {
|
|
||||||
day: '2-digit',
|
|
||||||
month: '2-digit',
|
|
||||||
year: 'numeric'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div>Lade Schichtpläne...</div>;
|
return <div>Lade Schichtpläne...</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ 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 } from '../../services/shiftPlanService';
|
import { shiftPlanService } from '../../services/shiftPlanService';
|
||||||
import { ShiftPlan, Shift, TimeSlot } from '../../../../backend/src/models/shiftPlan.js';
|
import { getTimeSlotById } from '../../models/helpers/shiftPlanHelpers';
|
||||||
|
import { ShiftPlan, TimeSlot } from '../../models/ShiftPlan';
|
||||||
import { useNotification } from '../../contexts/NotificationContext';
|
import { useNotification } from '../../contexts/NotificationContext';
|
||||||
|
import { formatDate, formatTime } from '../../utils/foramatters';
|
||||||
|
|
||||||
const ShiftPlanView: React.FC = () => {
|
const ShiftPlanView: React.FC = () => {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
@@ -36,94 +38,44 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (dateString: string | undefined): string => {
|
// Simplified timetable data generation
|
||||||
if (!dateString) return 'Kein Datum';
|
|
||||||
|
|
||||||
const date = new Date(dateString);
|
|
||||||
|
|
||||||
if (isNaN(date.getTime())) {
|
|
||||||
return 'Ungültiges Datum';
|
|
||||||
}
|
|
||||||
|
|
||||||
return date.toLocaleDateString('de-DE', {
|
|
||||||
weekday: 'long',
|
|
||||||
day: '2-digit',
|
|
||||||
month: '2-digit',
|
|
||||||
year: 'numeric'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatTime = (timeString: string) => {
|
|
||||||
return timeString.substring(0, 5);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get unique shift types and their staffing per weekday
|
|
||||||
const getTimetableData = () => {
|
const getTimetableData = () => {
|
||||||
if (!shiftPlan) return { shifts: [], weekdays: [] };
|
if (!shiftPlan) return { shifts: [], weekdays: [] };
|
||||||
|
|
||||||
// Get all unique shift types (name + time combination)
|
// Use timeSlots directly since shifts reference them
|
||||||
const shiftTypes = Array.from(new Set(
|
const timetableShifts = shiftPlan.timeSlots.map(timeSlot => {
|
||||||
shiftPlan.shifts.map(shift =>
|
|
||||||
`${shift.timeSlot.name}|${shift.timeSlot.startTime}|${shift.timeSlot.endTime}`
|
|
||||||
)
|
|
||||||
)).map(shiftKey => {
|
|
||||||
const [name, startTime, endTime] = shiftKey.split('|');
|
|
||||||
return { name, startTime, endTime };
|
|
||||||
});
|
|
||||||
|
|
||||||
// Weekdays (1=Monday, 7=Sunday)
|
|
||||||
const weekdays = [1, 2, 3, 4, 5, 6, 7];
|
|
||||||
|
|
||||||
// For each shift type and weekday, calculate staffing
|
|
||||||
const timetableShifts = shiftTypes.map(shiftType => {
|
|
||||||
const weekdayData: Record<number, string> = {};
|
const weekdayData: Record<number, string> = {};
|
||||||
|
|
||||||
weekdays.forEach(weekday => {
|
weekdays.forEach(weekday => {
|
||||||
// Find all shifts of this type on this weekday
|
const shiftsOnDay = shiftPlan.shifts.filter(shift =>
|
||||||
const shiftsOnDay = shiftPlan.shifts.filter(shift => {
|
shift.dayOfWeek === weekday.id &&
|
||||||
const date = new Date(shift.date);
|
shift.timeSlotId === timeSlot.id
|
||||||
const dayOfWeek = date.getDay() === 0 ? 7 : date.getDay(); // Convert to 1-7 (Mon-Sun)
|
);
|
||||||
return dayOfWeek === weekday &&
|
|
||||||
shift.timeSlot.name === shiftType.name &&
|
|
||||||
shift.timeSlot.startTime === shiftType.startTime &&
|
|
||||||
shift.timeSlot.endTime === shiftType.endTime;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (shiftsOnDay.length === 0) {
|
if (shiftsOnDay.length === 0) {
|
||||||
weekdayData[weekday] = '';
|
weekdayData[weekday.id] = '';
|
||||||
} else {
|
} else {
|
||||||
const totalAssigned = shiftsOnDay.reduce((sum, shift) => sum + shift.timeSlot.assignedEmployees.length, 0);
|
const totalRequired = shiftsOnDay.reduce((sum, shift) =>
|
||||||
const totalRequired = shiftsOnDay.reduce((sum, shift) => sum + shift.requiredEmployees, 0);
|
sum + shift.requiredEmployees, 0);
|
||||||
weekdayData[weekday] = `${totalAssigned}/${totalRequired}`;
|
// For now, show required count since we don't have assigned employees in Shift
|
||||||
|
weekdayData[weekday.id] = `0/${totalRequired}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...shiftType,
|
...timeSlot,
|
||||||
displayName: `${shiftType.name} (${formatTime(shiftType.startTime)}–${formatTime(shiftType.endTime)})`,
|
displayName: `${timeSlot.name} (${formatTime(timeSlot.startTime)}–${formatTime(timeSlot.endTime)})`,
|
||||||
weekdayData
|
weekdayData
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return { shifts: timetableShifts, weekdays };
|
||||||
shifts: timetableShifts,
|
|
||||||
weekdays: weekdays.map(day => ({
|
|
||||||
id: day,
|
|
||||||
name: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'][day === 7 ? 0 : day]
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) return <div>Lade Schichtplan...</div>;
|
||||||
return <div>Lade Schichtplan...</div>;
|
if (!shiftPlan) return <div>Schichtplan nicht gefunden</div>;
|
||||||
}
|
|
||||||
|
|
||||||
if (!shiftPlan) {
|
|
||||||
return <div>Schichtplan nicht gefunden</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timetableData = getTimetableData();
|
const timetableData = getTimetableData();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '20px' }}>
|
<div style={{ padding: '20px' }}>
|
||||||
<div style={{
|
<div style={{
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
.editorContainer {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 24px;
|
|
||||||
color: #2c3e50;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.previewButton {
|
|
||||||
padding: 8px 16px;
|
|
||||||
background-color: #3498db;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.previewButton:hover {
|
|
||||||
background-color: #2980b9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.saveButton {
|
|
||||||
padding: 8px 16px;
|
|
||||||
background-color: #2ecc71;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.saveButton:hover {
|
|
||||||
background-color: #27ae60;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formGroup {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formGroup label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: #34495e;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formGroup input[type="text"],
|
|
||||||
.formGroup textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 1px solid #bdc3c7;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formGroup textarea {
|
|
||||||
min-height: 100px;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.defaultCheckbox {
|
|
||||||
margin-top: 15px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.defaultCheckbox input[type="checkbox"] {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.defaultCheckbox label {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.previewContainer {
|
|
||||||
margin-top: 30px;
|
|
||||||
border-top: 1px solid #ddd;
|
|
||||||
padding-top: 20px;
|
|
||||||
}
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
// frontend/src/pages/ShiftTemplates/ShiftTemplateEditor.tsx
|
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
|
||||||
import { TemplateShiftSlot, TemplateShift, TemplateShiftTimeSlot, DEFAULT_DAYS } from '../../types/shiftTemplate';
|
|
||||||
import { shiftTemplateService } from '../../services/shiftTemplateService';
|
|
||||||
import ShiftDayEditor from './components/ShiftDayEditor';
|
|
||||||
import DefaultTemplateView from './components/DefaultTemplateView';
|
|
||||||
import styles from './ShiftTemplateEditor.module.css';
|
|
||||||
|
|
||||||
interface ExtendedTemplateShift extends Omit<TemplateShiftSlot, 'id'> {
|
|
||||||
id?: string;
|
|
||||||
isPreview?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultShift: ExtendedTemplateShift = {
|
|
||||||
dayOfWeek: 1, // Montag
|
|
||||||
timeSlot: { id: '', name: '', startTime: '', endTime: '' },
|
|
||||||
requiredEmployees: 1,
|
|
||||||
color: '#3498db'
|
|
||||||
};
|
|
||||||
|
|
||||||
const ShiftTemplateEditor: React.FC = () => {
|
|
||||||
const { id } = useParams<{ id: string }>();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const isEditing = !!id;
|
|
||||||
|
|
||||||
const [template, setTemplate] = useState<Omit<TemplateShift, 'id' | 'createdAt' | 'createdBy'>>({
|
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
shifts: [],
|
|
||||||
isDefault: false
|
|
||||||
});
|
|
||||||
const [loading, setLoading] = useState(isEditing);
|
|
||||||
const [saving, setSaving] = useState(false);
|
|
||||||
const [showPreview, setShowPreview] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isEditing) {
|
|
||||||
loadTemplate();
|
|
||||||
}
|
|
||||||
}, [id]);
|
|
||||||
|
|
||||||
const loadTemplate = async () => {
|
|
||||||
try {
|
|
||||||
if (!id) return;
|
|
||||||
const data = await shiftTemplateService.getTemplate(id);
|
|
||||||
setTemplate({
|
|
||||||
name: data.name,
|
|
||||||
description: data.description,
|
|
||||||
shifts: data.shifts,
|
|
||||||
isDefault: data.isDefault
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Laden:', error);
|
|
||||||
alert('Vorlage konnte nicht geladen werden');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
if (!template.name.trim()) {
|
|
||||||
alert('Bitte geben Sie einen Namen für die Vorlage ein');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSaving(true);
|
|
||||||
try {
|
|
||||||
if (isEditing && id) {
|
|
||||||
await shiftTemplateService.updateTemplate(id, template);
|
|
||||||
} else {
|
|
||||||
await shiftTemplateService.createTemplate(template);
|
|
||||||
}
|
|
||||||
navigate('/shift-templates');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Speichern fehlgeschlagen:', error);
|
|
||||||
alert('Fehler beim Speichern der Vorlage');
|
|
||||||
} finally {
|
|
||||||
setSaving(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addShift = (dayOfWeek: number) => {
|
|
||||||
const newShift: TemplateShiftSlot = {
|
|
||||||
...defaultShift,
|
|
||||||
id: Date.now().toString(),
|
|
||||||
dayOfWeek,
|
|
||||||
timeSlot: { ...defaultShift.timeSlot, id: Date.now().toString() },
|
|
||||||
requiredEmployees: defaultShift.requiredEmployees,
|
|
||||||
color: defaultShift.color
|
|
||||||
};
|
|
||||||
|
|
||||||
setTemplate(prev => ({
|
|
||||||
...prev,
|
|
||||||
shifts: [...prev.shifts, newShift]
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateShift = (shiftId: string, updates: Partial<TemplateShift>) => {
|
|
||||||
setTemplate(prev => ({
|
|
||||||
...prev,
|
|
||||||
shifts: prev.shifts.map(shift =>
|
|
||||||
shift.id === shiftId ? { ...shift, ...updates } : shift
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeShift = (shiftId: string) => {
|
|
||||||
setTemplate(prev => ({
|
|
||||||
...prev,
|
|
||||||
shifts: prev.shifts.filter(shift => shift.id !== shiftId)
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Preview-Daten für die DefaultTemplateView vorbereiten
|
|
||||||
const previewTemplate: TemplateShift = {
|
|
||||||
id: 'preview',
|
|
||||||
name: template.name || 'Vorschau',
|
|
||||||
description: template.description,
|
|
||||||
shifts: template.shifts.map(shift => ({
|
|
||||||
...shift,
|
|
||||||
id: shift.id || 'preview-' + Date.now()
|
|
||||||
})),
|
|
||||||
createdBy: 'preview',
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
isDefault: template.isDefault
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) return <div>Lade Vorlage...</div>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.editorContainer}>
|
|
||||||
<div className={styles.header}>
|
|
||||||
<h1 className={styles.title}>{isEditing ? 'Vorlage bearbeiten' : 'Neue Vorlage erstellen'}</h1>
|
|
||||||
<div className={styles.buttons}>
|
|
||||||
<button
|
|
||||||
className={styles.previewButton}
|
|
||||||
onClick={() => setShowPreview(!showPreview)}
|
|
||||||
>
|
|
||||||
{showPreview ? 'Editor anzeigen' : 'Vorschau'}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={styles.saveButton}
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={saving}
|
|
||||||
>
|
|
||||||
{saving ? 'Speichern...' : 'Speichern'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showPreview ? (
|
|
||||||
<DefaultTemplateView template={previewTemplate} />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className={styles.formGroup}>
|
|
||||||
<label>Vorlagenname *</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={template.name}
|
|
||||||
onChange={(e) => setTemplate(prev => ({ ...prev, name: e.target.value }))}
|
|
||||||
placeholder="z.B. Standard Woche, Teilzeit Modell, etc."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.formGroup}>
|
|
||||||
<label>Beschreibung</label>
|
|
||||||
<textarea
|
|
||||||
value={template.description || ''}
|
|
||||||
onChange={(e) => setTemplate(prev => ({ ...prev, description: e.target.value }))}
|
|
||||||
placeholder="Beschreibung der Vorlage (optional)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.defaultCheckbox}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="isDefault"
|
|
||||||
checked={template.isDefault}
|
|
||||||
onChange={(e) => setTemplate(prev => ({ ...prev, isDefault: e.target.checked }))}
|
|
||||||
/>
|
|
||||||
<label htmlFor="isDefault">Als Standardvorlage festlegen</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginTop: '30px' }}>
|
|
||||||
<h2>Schichten pro Wochentag</h2>
|
|
||||||
<div style={{ display: 'grid', gap: '20px', marginTop: '20px' }}>
|
|
||||||
{DEFAULT_DAYS.map(day => (
|
|
||||||
<ShiftDayEditor
|
|
||||||
key={day.id}
|
|
||||||
day={day}
|
|
||||||
shifts={template.shifts.filter(s => s.dayOfWeek === day.id)}
|
|
||||||
onAddShift={() => addShift(day.id)}
|
|
||||||
onUpdateShift={updateShift}
|
|
||||||
onRemoveShift={removeShift}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ShiftTemplateEditor;
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
.templateList {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.createButton {
|
|
||||||
padding: 10px 20px;
|
|
||||||
background-color: #007bff;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.createButton:hover {
|
|
||||||
background-color: #0056b3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.templateGrid {
|
|
||||||
display: grid;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.templateCard {
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.templateHeader {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.templateInfo h3 {
|
|
||||||
margin: 0 0 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.templateInfo p {
|
|
||||||
margin: 0 0 10px 0;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.templateMeta {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
.defaultBadge {
|
|
||||||
color: green;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actionButtons {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.viewButton {
|
|
||||||
padding: 5px 10px;
|
|
||||||
border: 1px solid #007bff;
|
|
||||||
color: #007bff;
|
|
||||||
background: white;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.useButton {
|
|
||||||
padding: 5px 10px;
|
|
||||||
background-color: #28a745;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deleteButton {
|
|
||||||
padding: 5px 10px;
|
|
||||||
background-color: #dc3545;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.viewButton:hover {
|
|
||||||
background-color: #e6f0ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.useButton:hover {
|
|
||||||
background-color: #218838;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deleteButton:hover {
|
|
||||||
background-color: #c82333;
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
// frontend/src/pages/ShiftTemplates/ShiftTemplateList.tsx
|
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { TemplateShift } from '../../types/shiftTemplate';
|
|
||||||
import { shiftTemplateService } from '../../services/shiftTemplateService';
|
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
|
||||||
import DefaultTemplateView from './components/DefaultTemplateView';
|
|
||||||
import styles from './ShiftTemplateList.module.css';
|
|
||||||
|
|
||||||
const ShiftTemplateList: React.FC = () => {
|
|
||||||
const [templates, setTemplates] = useState<TemplateShift[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const { hasRole } = useAuth();
|
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState<TemplateShift | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadTemplates();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loadTemplates = async () => {
|
|
||||||
try {
|
|
||||||
const data = await shiftTemplateService.getTemplates();
|
|
||||||
setTemplates(data);
|
|
||||||
// Setze die Standard-Vorlage als ausgewählt
|
|
||||||
const defaultTemplate = data.find(t => t.isDefault);
|
|
||||||
if (defaultTemplate) {
|
|
||||||
setSelectedTemplate(defaultTemplate);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler:', error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
|
||||||
if (!window.confirm('Vorlage wirklich löschen?')) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await shiftTemplateService.deleteTemplate(id);
|
|
||||||
setTemplates(templates.filter(t => t.id !== id));
|
|
||||||
if (selectedTemplate?.id === id) {
|
|
||||||
setSelectedTemplate(null);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Löschen fehlgeschlagen:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) return <div>Lade Vorlagen...</div>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.templateList}>
|
|
||||||
<div className={styles.header}>
|
|
||||||
<h1>Schichtplan Vorlagen</h1>
|
|
||||||
{hasRole(['admin', 'instandhalter']) && (
|
|
||||||
<Link to="/shift-templates/new">
|
|
||||||
<button className={styles.createButton}>
|
|
||||||
Neue Vorlage
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.templateGrid}>
|
|
||||||
{templates.length === 0 ? (
|
|
||||||
<div style={{ textAlign: 'center', padding: '40px', color: '#666' }}>
|
|
||||||
<p>Noch keine Vorlagen vorhanden.</p>
|
|
||||||
{hasRole(['admin', 'instandhalter']) && (
|
|
||||||
<Link to="/shift-templates/new">
|
|
||||||
<button className={styles.createButton}>
|
|
||||||
Erste Vorlage erstellen
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
templates.map(template => (
|
|
||||||
<div key={template.id} className={styles.templateCard}>
|
|
||||||
<div className={styles.templateHeader}>
|
|
||||||
<div className={styles.templateInfo}>
|
|
||||||
<h3>{template.name}</h3>
|
|
||||||
{template.description && (
|
|
||||||
<p>{template.description}</p>
|
|
||||||
)}
|
|
||||||
<div className={styles.templateMeta}>
|
|
||||||
{template.shifts.length} Schichttypen • Erstellt am {new Date(template.createdAt).toLocaleDateString('de-DE')}
|
|
||||||
{template.isDefault && <span className={styles.defaultBadge}>• Standard</span>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.actionButtons}>
|
|
||||||
<button
|
|
||||||
className={styles.viewButton}
|
|
||||||
onClick={() => setSelectedTemplate(template)}
|
|
||||||
>
|
|
||||||
Vorschau
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{hasRole(['admin', 'instandhalter']) && (
|
|
||||||
<>
|
|
||||||
<Link to={`/shift-templates/${template.id}`}>
|
|
||||||
<button className={styles.viewButton}>
|
|
||||||
Bearbeiten
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
<Link to={`/shift-plans/new?template=${template.id}`}>
|
|
||||||
<button className={styles.useButton}>
|
|
||||||
Verwenden
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDelete(template.id)}
|
|
||||||
className={styles.deleteButton}
|
|
||||||
>
|
|
||||||
Löschen
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedTemplate && (
|
|
||||||
<div style={{ marginTop: '30px' }}>
|
|
||||||
<DefaultTemplateView template={selectedTemplate} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ShiftTemplateList;
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
.defaultTemplateView {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.weekView {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
margin-top: 20px;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dayColumn {
|
|
||||||
min-width: 200px;
|
|
||||||
background: #f5f6fa;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dayColumn h3 {
|
|
||||||
margin: 0 0 15px 0;
|
|
||||||
color: #2c3e50;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shiftsContainer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shiftCard {
|
|
||||||
background: white;
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.shiftCard h4 {
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
color: #34495e;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shiftCard p {
|
|
||||||
margin: 0;
|
|
||||||
color: #7f8c8d;
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// frontend/src/pages/ShiftTemplates/components/DefaultTemplateView.tsx
|
|
||||||
import React from 'react';
|
|
||||||
import { TemplateShift } from '../../../types/shiftTemplate';
|
|
||||||
import styles from './DefaultTemplateView.module.css';
|
|
||||||
|
|
||||||
interface DefaultTemplateViewProps {
|
|
||||||
template: TemplateShift;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DefaultTemplateView: React.FC<DefaultTemplateViewProps> = ({ template }) => {
|
|
||||||
// Gruppiere Schichten nach Wochentag
|
|
||||||
const shiftsByDay = template.shifts.reduce((acc, shift) => {
|
|
||||||
const day = shift.dayOfWeek;
|
|
||||||
if (!acc[day]) {
|
|
||||||
acc[day] = [];
|
|
||||||
}
|
|
||||||
acc[day].push(shift);
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<number, typeof template.shifts>);
|
|
||||||
|
|
||||||
// Funktion zum Formatieren der Zeit
|
|
||||||
const formatTime = (time: string) => {
|
|
||||||
return time.substring(0, 5); // Zeigt nur HH:MM
|
|
||||||
};
|
|
||||||
|
|
||||||
// Wochentagsnamen
|
|
||||||
const dayNames = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.defaultTemplateView}>
|
|
||||||
<h2>{template.name}</h2>
|
|
||||||
{template.description && <p>{template.description}</p>}
|
|
||||||
|
|
||||||
<div className={styles.weekView}>
|
|
||||||
{[1, 2, 3, 4, 5].map(dayIndex => (
|
|
||||||
<div key={dayIndex} className={styles.dayColumn}>
|
|
||||||
<h3>{dayNames[dayIndex]}</h3>
|
|
||||||
<div className={styles.shiftsContainer}>
|
|
||||||
{shiftsByDay[dayIndex]?.map(shift => (
|
|
||||||
<div key={shift.id} className={styles.shiftCard}>
|
|
||||||
<h4>{shift.timeSlot.name}</h4>
|
|
||||||
<p>
|
|
||||||
{formatTime(shift.timeSlot.startTime)} - {formatTime(shift.timeSlot.endTime)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DefaultTemplateView;
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
.dayEditor {
|
|
||||||
background: #f8f9fa;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dayHeader {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dayName {
|
|
||||||
font-size: 18px;
|
|
||||||
color: #2c3e50;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.addButton {
|
|
||||||
padding: 6px 12px;
|
|
||||||
background-color: #3498db;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.addButton:hover {
|
|
||||||
background-color: #2980b9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.addButton svg {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shiftsGrid {
|
|
||||||
display: grid;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shiftCard {
|
|
||||||
background: white;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.shiftHeader {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shiftTitle {
|
|
||||||
color: #2c3e50;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deleteButton {
|
|
||||||
padding: 4px 8px;
|
|
||||||
background-color: #e74c3c;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deleteButton:hover {
|
|
||||||
background-color: #c0392b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formGroup {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formGroup label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
color: #34495e;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formGroup input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 6px 8px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formGroup input[type="time"] {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeInputs {
|
|
||||||
display: flex;
|
|
||||||
gap: 15px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.colorPicker {
|
|
||||||
width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.requiredEmployees {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.requiredEmployees input {
|
|
||||||
width: 60px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.requiredEmployees button {
|
|
||||||
padding: 4px 8px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
background: white;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.requiredEmployees button:hover {
|
|
||||||
background: #f5f6fa;
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
// frontend/src/pages/ShiftTemplates/components/ShiftDayEditor.tsx
|
|
||||||
import React from 'react';
|
|
||||||
import { TemplateShiftSlot } from '../../../types/shiftTemplate';
|
|
||||||
import styles from './ShiftDayEditor.module.css';
|
|
||||||
|
|
||||||
interface ShiftDayEditorProps {
|
|
||||||
day: { id: number; name: string };
|
|
||||||
shifts: TemplateShiftSlot[];
|
|
||||||
onAddShift: () => void;
|
|
||||||
onUpdateShift: (shiftId: string, updates: Partial<TemplateShiftSlot>) => void;
|
|
||||||
onRemoveShift: (shiftId: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ShiftDayEditor: React.FC<ShiftDayEditorProps> = ({
|
|
||||||
day,
|
|
||||||
shifts,
|
|
||||||
onAddShift,
|
|
||||||
onUpdateShift,
|
|
||||||
onRemoveShift
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className={styles.dayEditor}>
|
|
||||||
<div className={styles.dayHeader}>
|
|
||||||
<h3 className={styles.dayName}>{day.name}</h3>
|
|
||||||
<button className={styles.addButton} onClick={onAddShift}>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" width="16" height="16">
|
|
||||||
<path fillRule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
Schicht hinzufügen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{shifts.length === 0 ? (
|
|
||||||
<div style={{ textAlign: 'center', padding: '20px', color: '#999', fontStyle: 'italic' }}>
|
|
||||||
Keine Schichten für {day.name}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className={styles.shiftsGrid}>
|
|
||||||
{shifts.map(shift => (
|
|
||||||
<div key={shift.id} className={styles.shiftCard}>
|
|
||||||
<div className={styles.shiftHeader}>
|
|
||||||
<h4 className={styles.shiftTitle}>Schicht bearbeiten</h4>
|
|
||||||
<button
|
|
||||||
className={styles.deleteButton}
|
|
||||||
onClick={() => onRemoveShift(shift.id)}
|
|
||||||
title="Schicht löschen"
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" width="16" height="16">
|
|
||||||
<path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
Löschen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.formGroup}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={shift.timeSlot.name}
|
|
||||||
onChange={(e) => onUpdateShift(shift.id, { timeSlot: { ...shift.timeSlot, name: e.target.value } })}
|
|
||||||
placeholder="Schichtname"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.timeInputs}>
|
|
||||||
<div className={styles.formGroup}>
|
|
||||||
<label>Start</label>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
value={shift.timeSlot.startTime}
|
|
||||||
onChange={(e) => onUpdateShift(shift.id, { timeSlot: { ...shift.timeSlot, startTime: e.target.value } })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.formGroup}>
|
|
||||||
<label>Ende</label>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
value={shift.timeSlot.endTime}
|
|
||||||
onChange={(e) => onUpdateShift(shift.id, { timeSlot: { ...shift.timeSlot, endTime: e.target.value } })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.formGroup}>
|
|
||||||
<label>Benötigte Mitarbeiter</label>
|
|
||||||
<div className={styles.requiredEmployees}>
|
|
||||||
<button
|
|
||||||
onClick={() => onUpdateShift(shift.id, { requiredEmployees: Math.max(1, shift.requiredEmployees - 1) })}
|
|
||||||
>
|
|
||||||
-
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
value={shift.requiredEmployees}
|
|
||||||
onChange={(e) => onUpdateShift(shift.id, { requiredEmployees: parseInt(e.target.value) || 1 })}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={() => onUpdateShift(shift.id, { requiredEmployees: shift.requiredEmployees + 1 })}
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{shift.color && (
|
|
||||||
<div className={styles.formGroup}>
|
|
||||||
<label>Farbe</label>
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
value={shift.color}
|
|
||||||
onChange={(e) => onUpdateShift(shift.id, { color: e.target.value })}
|
|
||||||
className={styles.colorPicker}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ShiftDayEditor;
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
// frontend/src/services/authService.ts
|
// frontend/src/services/authService.ts
|
||||||
|
import { Employee } from '../../../backend/src/models/employee';
|
||||||
const API_BASE = 'http://localhost:3002/api';
|
const API_BASE = 'http://localhost:3002/api';
|
||||||
|
|
||||||
export interface LoginRequest {
|
export interface LoginRequest {
|
||||||
@@ -11,28 +12,14 @@ export interface RegisterRequest {
|
|||||||
password: string;
|
password: string;
|
||||||
name: string;
|
name: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
phone?: string;
|
|
||||||
department?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthResponse {
|
export interface AuthResponse {
|
||||||
user: User;
|
employee: Employee;
|
||||||
token: string;
|
token: string;
|
||||||
expiresIn: string;
|
expiresIn: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface User {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
name: string;
|
|
||||||
role: 'admin' | 'instandhalter' | 'user';
|
|
||||||
createdAt: string;
|
|
||||||
lastLogin?: string;
|
|
||||||
phone?: string;
|
|
||||||
department?: string;
|
|
||||||
isActive?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
class AuthService {
|
class AuthService {
|
||||||
private token: string | null = null;
|
private token: string | null = null;
|
||||||
|
|
||||||
@@ -51,12 +38,11 @@ class AuthService {
|
|||||||
const data: AuthResponse = await response.json();
|
const data: AuthResponse = await response.json();
|
||||||
this.token = data.token;
|
this.token = data.token;
|
||||||
localStorage.setItem('token', data.token);
|
localStorage.setItem('token', data.token);
|
||||||
localStorage.setItem('user', JSON.stringify(data.user));
|
localStorage.setItem('employee', JSON.stringify(data.employee));
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register Methode hinzufügen
|
|
||||||
async register(userData: RegisterRequest): Promise<AuthResponse> {
|
async register(userData: RegisterRequest): Promise<AuthResponse> {
|
||||||
const response = await fetch(`${API_BASE}/employees`, {
|
const response = await fetch(`${API_BASE}/employees`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -69,21 +55,18 @@ class AuthService {
|
|||||||
throw new Error(errorData.error || 'Registrierung fehlgeschlagen');
|
throw new Error(errorData.error || 'Registrierung fehlgeschlagen');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nach der Erstellung automatisch einloggen
|
|
||||||
return this.login({
|
return this.login({
|
||||||
email: userData.email,
|
email: userData.email,
|
||||||
password: userData.password
|
password: userData.password
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// getCurrentUser als SYNCHRON machen
|
getCurrentEmployee(): Employee | null {
|
||||||
getCurrentUser(): User | null {
|
const employeeStr = localStorage.getItem('employee');
|
||||||
const userStr = localStorage.getItem('user');
|
return employeeStr ? JSON.parse(employeeStr) : null;
|
||||||
return userStr ? JSON.parse(userStr) : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Asynchrone Methode für Server-Abfrage
|
async fetchCurrentEmployee(): Promise<Employee | null> {
|
||||||
async fetchCurrentUser(): Promise<User | null> {
|
|
||||||
const token = this.getToken();
|
const token = this.getToken();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return null;
|
return null;
|
||||||
@@ -97,7 +80,8 @@ class AuthService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const user = await response.json();
|
const data = await response.json();
|
||||||
|
const user = data.user;
|
||||||
localStorage.setItem('user', JSON.stringify(user));
|
localStorage.setItem('user', JSON.stringify(user));
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
@@ -125,7 +109,6 @@ class AuthService {
|
|||||||
return this.getToken() !== null;
|
return this.getToken() !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Für API Calls mit Authentication
|
|
||||||
getAuthHeaders(): HeadersInit {
|
getAuthHeaders(): HeadersInit {
|
||||||
const token = this.getToken();
|
const token = this.getToken();
|
||||||
return token ? { 'Authorization': `Bearer ${token}` } : {};
|
return token ? { 'Authorization': `Bearer ${token}` } : {};
|
||||||
|
|||||||
@@ -118,5 +118,4 @@ export class EmployeeService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Exportiere eine Instanz der Klasse
|
|
||||||
export const employeeService = new EmployeeService();
|
export const employeeService = new EmployeeService();
|
||||||
@@ -21,20 +21,7 @@ export const shiftPlanService = {
|
|||||||
throw new Error('Fehler beim Laden der Schichtpläne');
|
throw new Error('Fehler beim Laden der Schichtpläne');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
return await response.json();
|
||||||
|
|
||||||
// Convert snake_case to camelCase
|
|
||||||
return data.map((plan: any) => ({
|
|
||||||
id: plan.id,
|
|
||||||
name: plan.name,
|
|
||||||
startDate: plan.start_date, // Convert here
|
|
||||||
endDate: plan.end_date, // Convert here
|
|
||||||
templateId: plan.template_id,
|
|
||||||
status: plan.status,
|
|
||||||
createdBy: plan.created_by,
|
|
||||||
createdAt: plan.created_at,
|
|
||||||
shifts: plan.shifts || []
|
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async getShiftPlan(id: string): Promise<ShiftPlan> {
|
async getShiftPlan(id: string): Promise<ShiftPlan> {
|
||||||
@@ -53,20 +40,7 @@ export const shiftPlanService = {
|
|||||||
throw new Error('Schichtplan nicht gefunden');
|
throw new Error('Schichtplan nicht gefunden');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
return await response.json();
|
||||||
|
|
||||||
// Convert snake_case to camelCase
|
|
||||||
return data.map((plan: any) => ({
|
|
||||||
id: plan.id,
|
|
||||||
name: plan.name,
|
|
||||||
startDate: plan.start_date,
|
|
||||||
endDate: plan.end_date,
|
|
||||||
templateId: plan.template_id,
|
|
||||||
status: plan.status,
|
|
||||||
createdBy: plan.created_by,
|
|
||||||
createdAt: plan.created_at,
|
|
||||||
shifts: plan.shifts || []
|
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async createShiftPlan(plan: CreateShiftPlanRequest): Promise<ShiftPlan> {
|
async createShiftPlan(plan: CreateShiftPlanRequest): Promise<ShiftPlan> {
|
||||||
@@ -127,60 +101,5 @@ export const shiftPlanService = {
|
|||||||
}
|
}
|
||||||
throw new Error('Fehler beim Löschen des Schichtplans');
|
throw new Error('Fehler beim Löschen des Schichtplans');
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
async updateShiftPlanShift(planId: string, shift: Shift): Promise<void> {
|
|
||||||
const response = await fetch(`${API_BASE}/${planId}/shifts/${shift.id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...authService.getAuthHeaders()
|
|
||||||
},
|
|
||||||
body: JSON.stringify(shift)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
if (response.status === 401) {
|
|
||||||
authService.logout();
|
|
||||||
throw new Error('Nicht authorisiert - bitte erneut anmelden');
|
|
||||||
}
|
|
||||||
throw new Error('Fehler beim Aktualisieren der Schicht');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async addShiftPlanShift(planId: string, shift: Omit<Shift, 'id' | 'shiftPlanId' | 'assignedEmployees'>): Promise<void> {
|
|
||||||
const response = await fetch(`${API_BASE}/${planId}/shifts`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...authService.getAuthHeaders()
|
|
||||||
},
|
|
||||||
body: JSON.stringify(shift)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
if (response.status === 401) {
|
|
||||||
authService.logout();
|
|
||||||
throw new Error('Nicht authorisiert - bitte erneut anmelden');
|
|
||||||
}
|
|
||||||
throw new Error('Fehler beim Hinzufügen der Schicht');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async deleteShiftPlanShift(planId: string, shiftId: string): Promise<void> {
|
|
||||||
const response = await fetch(`${API_BASE}/${planId}/shifts/${shiftId}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
...authService.getAuthHeaders()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
if (response.status === 401) {
|
|
||||||
authService.logout();
|
|
||||||
throw new Error('Nicht authorisiert - bitte erneut anmelden');
|
|
||||||
}
|
|
||||||
throw new Error('Fehler beim Löschen der Schicht');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
36
frontend/src/utils/foramatters.ts
Normal file
36
frontend/src/utils/foramatters.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// frontend/src/shared/utils.ts
|
||||||
|
// import { ScheduledShift } from '../../../backend/src/models/shiftPlan.js';
|
||||||
|
|
||||||
|
// Shared date and time formatting utilities
|
||||||
|
export const formatDate = (dateString: string | undefined): string => {
|
||||||
|
if (!dateString) return 'Kein Datum';
|
||||||
|
|
||||||
|
const date = new Date(dateString);
|
||||||
|
if (isNaN(date.getTime())) return 'Ungültiges Datum';
|
||||||
|
|
||||||
|
return date.toLocaleDateString('de-DE', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatTime = (timeString: string): string => {
|
||||||
|
return timeString?.substring(0, 5) || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatDateTime = (dateString: string): string => {
|
||||||
|
if (!dateString) return 'Kein Datum';
|
||||||
|
|
||||||
|
const date = new Date(dateString);
|
||||||
|
if (isNaN(date.getTime())) return 'Ungültiges Datum';
|
||||||
|
|
||||||
|
return date.toLocaleDateString('de-DE', {
|
||||||
|
weekday: 'long',
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user