updated every file for database changes; starting scheduling debugging

This commit is contained in:
2025-10-21 00:51:23 +02:00
parent 3c4fbc0798
commit 3127692d29
27 changed files with 1861 additions and 866 deletions

View File

@@ -1,141 +0,0 @@
import React from 'react';
import { useScheduling } from '../services/scheduling/useScheduling';
import { ScheduleRequest } from '../models/scheduling';
interface SchedulerProps {
scheduleRequest: ScheduleRequest;
onScheduleGenerated?: (result: any) => void;
}
export const Scheduler: React.FC<SchedulerProps> = ({
scheduleRequest,
onScheduleGenerated
}) => {
const { generateSchedule, loading, error, result } = useScheduling();
const handleGenerateSchedule = async () => {
try {
const scheduleResult = await generateSchedule(scheduleRequest);
if (onScheduleGenerated) {
onScheduleGenerated(scheduleResult);
}
} catch (err) {
console.error('Scheduling failed:', err);
}
};
return (
<div style={{ padding: '20px', border: '1px solid #e0e0e0', borderRadius: '8px' }}>
<h3>Automatic Schedule Generation</h3>
<button
onClick={handleGenerateSchedule}
disabled={loading}
style={{
padding: '12px 24px',
backgroundColor: loading ? '#95a5a6' : '#3498db',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: loading ? 'not-allowed' : 'pointer',
fontSize: '16px',
fontWeight: 'bold'
}}
>
{loading ? '🔄 Generating Schedule...' : '🚀 Generate Optimal Schedule'}
</button>
{loading && (
<div style={{ marginTop: '15px' }}>
<div style={{
width: '100%',
height: '8px',
backgroundColor: '#ecf0f1',
borderRadius: '4px',
overflow: 'hidden'
}}>
<div style={{
width: '70%',
height: '100%',
backgroundColor: '#3498db',
animation: 'pulse 2s infinite',
borderRadius: '4px'
}} />
</div>
<p style={{ color: '#7f8c8d', fontSize: '14px', marginTop: '8px' }}>
Optimizing schedule... (max 2 minutes)
</p>
</div>
)}
{error && (
<div style={{
marginTop: '15px',
padding: '12px',
backgroundColor: '#f8d7da',
border: '1px solid #f5c6cb',
borderRadius: '4px',
color: '#721c24'
}}>
<strong>Error:</strong> {error}
</div>
)}
{result && (
<div style={{ marginTop: '20px' }}>
<ScheduleResultView result={result} />
</div>
)}
</div>
);
};
const ScheduleResultView: React.FC<{ result: any }> = ({ result }) => {
return (
<div style={{
padding: '15px',
backgroundColor: result.success ? '#d4edda' : '#f8d7da',
border: `1px solid ${result.success ? '#c3e6cb' : '#f5c6cb'}`,
borderRadius: '4px'
}}>
<h4 style={{
color: result.success ? '#155724' : '#721c24',
marginTop: 0
}}>
{result.success ? '✅ Schedule Generated Successfully' : '❌ Schedule Generation Failed'}
</h4>
<div style={{ marginBottom: '10px' }}>
<strong>Assignments:</strong> {Object.keys(result.assignments || {}).length} shifts assigned
</div>
<div style={{ marginBottom: '10px' }}>
<strong>Violations:</strong> {result.violations?.length || 0}
</div>
{result.resolution_report && result.resolution_report.length > 0 && (
<details style={{ marginTop: '10px' }}>
<summary style={{ cursor: 'pointer', fontWeight: 'bold' }}>
Resolution Report
</summary>
<div style={{
marginTop: '10px',
maxHeight: '200px',
overflow: 'auto',
fontSize: '12px',
fontFamily: 'monospace',
backgroundColor: 'rgba(0,0,0,0.05)',
padding: '10px',
borderRadius: '4px'
}}>
{result.resolution_report.map((line: string, index: number) => (
<div key={index} style={{ marginBottom: '2px' }}>{line}</div>
))}
</div>
</details>
)}
</div>
);
};
export default Scheduler;

View File

@@ -165,7 +165,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
refreshUser,
needsSetup: needsSetup === null ? true : needsSetup,
checkSetupStatus,
updateUser, // Add this to the context value
updateUser,
};
return (

View File

@@ -4,10 +4,11 @@ export interface Employee {
email: string;
firstname: string;
lastname: string;
employeeType: 'manager' | 'trainee' | 'experienced';
contractType: 'small' | 'large';
employeeType: 'manager' | 'personell' | 'apprentice' | 'guest';
contractType?: 'small' | 'large' | 'flexible';
canWorkAlone: boolean;
isActive: boolean;
isTrainee: boolean;
createdAt: string;
lastLogin?: string | null;
roles?: string[];
@@ -17,20 +18,22 @@ export interface CreateEmployeeRequest {
password: string;
firstname: string;
lastname: string;
roles: string[];
employeeType: 'manager' | 'trainee' | 'experienced';
contractType: 'small' | 'large';
roles?: string[];
employeeType: 'manager' | 'personell' | 'apprentice' | 'guest';
contractType?: 'small' | 'large' | 'flexible';
canWorkAlone: boolean;
isTrainee?: boolean;
}
export interface UpdateEmployeeRequest {
firstname?: string;
lastname?: string;
roles?: string[];
employeeType?: 'manager' | 'trainee' | 'experienced';
contractType?: 'small' | 'large';
employeeType?: 'manager' | 'personell' | 'apprentice' | 'guest';
contractType?: 'small' | 'large' | 'flexible';
canWorkAlone?: boolean;
isActive?: boolean;
isTrainee?: boolean;
}
export interface EmployeeWithPassword extends Employee {
@@ -41,7 +44,7 @@ export interface EmployeeAvailability {
id: string;
employeeId: string;
planId: string;
shiftId: string; // Now references shift_id instead of time_slot_id + day_of_week
shiftId: string;
preferenceLevel: 1 | 2 | 3; // 1:preferred, 2:available, 3:unavailable
notes?: string;
}
@@ -83,4 +86,11 @@ export interface Role {
export interface EmployeeRole {
employeeId: string;
role: 'admin' | 'user' | 'maintenance';
}
// Employee type configuration
export interface EmployeeType {
type: 'manager' | 'personell' | 'apprentice' | 'guest';
category: 'internal' | 'external';
has_contract_type: boolean;
}

View File

@@ -4,19 +4,41 @@ import { EmployeeAvailability, ManagerAvailability } from '../Employee.js';
// Default employee data for quick creation
export const EMPLOYEE_DEFAULTS = {
role: 'user' as const,
employeeType: 'experienced' as const,
employeeType: 'personell' as const,
contractType: 'small' as const,
canWorkAlone: false,
isActive: true
isActive: true,
isTrainee: false
};
// Manager-specific defaults
export const MANAGER_DEFAULTS = {
role: 'admin' as const,
employeeType: 'manager' as const,
contractType: 'large' as const,
contractType: 'flexible' as const,
canWorkAlone: true,
isActive: true
isActive: true,
isTrainee: false
};
// Apprentice defaults
export const APPRENTICE_DEFAULTS = {
role: 'user' as const,
employeeType: 'apprentice' as const,
contractType: 'flexible' as const,
canWorkAlone: false,
isActive: true,
isTrainee: false
};
// Guest defaults
export const GUEST_DEFAULTS = {
role: 'user' as const,
employeeType: 'guest' as const,
contractType: undefined,
canWorkAlone: false,
isActive: true,
isTrainee: false
};
export const EMPLOYEE_TYPE_CONFIG = {
@@ -24,22 +46,37 @@ export const EMPLOYEE_TYPE_CONFIG = {
value: 'manager' as const,
label: 'Chef/Administrator',
color: '#e74c3c',
category: 'internal' as const,
hasContractType: true,
independent: true,
description: 'Vollzugriff auf alle Funktionen und Mitarbeiterverwaltung'
},
experienced: {
value: 'experienced' as const,
label: 'Erfahren',
personell: {
value: 'personell' as const,
label: 'Personal',
color: '#3498db',
category: 'internal' as const,
hasContractType: true,
independent: true,
description: 'Langjährige Erfahrung, kann komplexe Aufgaben übernehmen'
description: 'Reguläre Mitarbeiter mit Vertrag'
},
trainee: {
value: 'trainee' as const,
label: 'Neuling',
color: '#27ae60',
apprentice: {
value: 'apprentice' as const,
label: 'Auszubildender',
color: '#9b59b6',
category: 'internal' as const,
hasContractType: true,
independent: false,
description: 'Benötigt Einarbeitung und Unterstützung'
description: 'Auszubildende mit flexiblem Vertrag'
},
guest: {
value: 'guest' as const,
label: 'Gast',
color: '#95a5a6',
category: 'external' as const,
hasContractType: false,
independent: false,
description: 'Externe Mitarbeiter ohne Vertrag'
}
} as const;
@@ -53,9 +90,10 @@ export const ROLE_CONFIG = [
export const CONTRACT_TYPE_DESCRIPTIONS = {
small: '1 Schicht pro Woche',
large: '2 Schichten pro Woche',
manager: 'Kein Vertragslimit - Immer MO und DI verfügbar'
flexible: 'Flexible Arbeitszeiten'
} as const;
// Availability preference descriptions
export const AVAILABILITY_PREFERENCES = {
1: { label: 'Bevorzugt', color: '#10b981', description: 'Möchte diese Schicht arbeiten' },
@@ -104,4 +142,17 @@ export function createManagerDefaultSchedule(managerId: string, planId: string,
}
return assignments;
}
export function getDefaultsByEmployeeType(employeeType: string) {
switch (employeeType) {
case 'manager':
return MANAGER_DEFAULTS;
case 'apprentice':
return APPRENTICE_DEFAULTS;
case 'guest':
return GUEST_DEFAULTS;
default:
return EMPLOYEE_DEFAULTS;
}
}

View File

@@ -1,9 +1,8 @@
// backend/src/models/helpers/employeeHelpers.ts
import { Employee, CreateEmployeeRequest, EmployeeAvailability } from '../Employee.js';
// Email generation function (same as in controllers)
// Email generation function
function generateEmail(firstname: string, lastname: string): string {
// Convert German umlauts to their expanded forms
const convertUmlauts = (str: string): string => {
return str
.toLowerCase()
@@ -13,19 +12,16 @@ function generateEmail(firstname: string, lastname: string): string {
.replace(/ß/g, 'ss');
};
// Remove any remaining special characters and convert to lowercase
const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, '');
const cleanLastname = convertUmlauts(lastname).replace(/[^a-z0-9]/g, '');
return `${cleanFirstname}.${cleanLastname}@sp.de`;
}
// UPDATED: Validation for new employee model
// UPDATED: Validation for new employee model with employee types
export function validateEmployeeData(employee: CreateEmployeeRequest): string[] {
const errors: string[] = [];
// Email is now auto-generated, so no email validation needed
if (employee.password?.length < 6) {
errors.push('Password must be at least 6 characters long');
}
@@ -38,24 +34,70 @@ export function validateEmployeeData(employee: CreateEmployeeRequest): string[]
errors.push('Last name is required and must be at least 2 characters long');
}
// Validate employee type
const validEmployeeTypes = ['manager', 'personell', 'apprentice', 'guest'];
if (!employee.employeeType || !validEmployeeTypes.includes(employee.employeeType)) {
errors.push(`Employee type must be one of: ${validEmployeeTypes.join(', ')}`);
}
// Validate contract type based on employee type
if (employee.employeeType !== 'guest') {
// Internal types require contract type
if (!employee.contractType) {
errors.push(`Contract type is required for employee type: ${employee.employeeType}`);
} else {
const validContractTypes = ['small', 'large', 'flexible'];
if (!validContractTypes.includes(employee.contractType)) {
errors.push(`Contract type must be one of: ${validContractTypes.join(', ')}`);
}
}
} else {
// External types (guest) should not have contract type
if (employee.contractType) {
errors.push('Contract type is not allowed for guest employees');
}
}
// Validate isTrainee - only applicable for personell type
if (employee.isTrainee && employee.employeeType !== 'personell') {
errors.push('isTrainee is only allowed for personell employee type');
}
return errors;
}
// Generate email for employee (new helper function)
// Generate email for employee
export function generateEmployeeEmail(firstname: string, lastname: string): string {
return generateEmail(firstname, lastname);
}
// Simplified business logic helpers
// UPDATED: Business logic helpers for new employee types
export const isManager = (employee: Employee): boolean =>
employee.employeeType === 'manager';
export const isPersonell = (employee: Employee): boolean =>
employee.employeeType === 'personell';
export const isApprentice = (employee: Employee): boolean =>
employee.employeeType === 'apprentice';
export const isGuest = (employee: Employee): boolean =>
employee.employeeType === 'guest';
export const isInternal = (employee: Employee): boolean =>
['manager', 'personell', 'apprentice'].includes(employee.employeeType);
export const isExternal = (employee: Employee): boolean =>
employee.employeeType === 'guest';
// UPDATED: Trainee logic - now based on isTrainee field for personell type
export const isTrainee = (employee: Employee): boolean =>
employee.employeeType === 'trainee';
employee.employeeType === 'personell' && employee.isTrainee;
export const isExperienced = (employee: Employee): boolean =>
employee.employeeType === 'experienced';
employee.employeeType === 'personell' && !employee.isTrainee;
// Role-based helpers
export const isAdmin = (employee: Employee): boolean =>
employee.roles?.includes('admin') || false;
@@ -64,13 +106,12 @@ export const isMaintenance = (employee: Employee): boolean =>
export const isUser = (employee: Employee): boolean =>
employee.roles?.includes('user') || false;
// UPDATED: Work alone permission - managers and experienced personell can work alone
export const canEmployeeWorkAlone = (employee: Employee): boolean =>
employee.canWorkAlone && isExperienced(employee);
employee.canWorkAlone && (isManager(employee) || isExperienced(employee));
export const getEmployeeWorkHours = (employee: Employee): number =>
isManager(employee) ? 999 : (employee.contractType === 'small' ? 1 : 2);
// New helper for full name display
// Helper for full name display
export const getFullName = (employee: { firstname: string; lastname: string }): string =>
`${employee.firstname} ${employee.lastname}`;
@@ -91,4 +132,14 @@ export function validateAvailabilityData(availability: Omit<EmployeeAvailability
}
return errors;
}
}
// UPDATED: Helper to get employee type category
export const getEmployeeCategory = (employee: Employee): 'internal' | 'external' => {
return isInternal(employee) ? 'internal' : 'external';
};
// Helper to check if employee requires contract type
export const requiresContractType = (employee: Employee): boolean => {
return isInternal(employee);
};

View File

@@ -7,10 +7,10 @@ export interface Availability {
id: string;
employeeId: string;
planId: string;
shiftId: string; // Now references shift_id instead of time_slot_id + day_of_week
preferenceLevel: 1 | 2 | 3; // 1:preferred, 2:available, 3:unavailable
shiftId: string;
preferenceLevel: 1 | 2 | 3;
notes?: string;
// Optional convenience fields (can be joined from shifts and time_slots tables)
// Optional convenience fields
dayOfWeek?: number;
timeSlotId?: string;
timeSlotName?: string;
@@ -30,7 +30,7 @@ export interface Constraint {
maxHoursPerWeek?: number;
[key: string]: any;
};
weight?: number; // For soft constraints
weight?: number;
}
export interface ScheduleRequest {
@@ -153,8 +153,9 @@ export interface AvailabilityWithDetails extends Availability {
id: string;
firstname: string;
lastname: string;
employeeType: 'manager' | 'trainee' | 'experienced';
employeeType: 'manager' | 'personell' | 'apprentice' | 'guest';
canWorkAlone: boolean;
isTrainee: boolean;
};
shift?: {
dayOfWeek: number;

View File

@@ -3,6 +3,7 @@ import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
import { useNotification } from '../../contexts/NotificationContext';
import { employeeService } from '../../services/employeeService';
const Login: React.FC = () => {
const [email, setEmail] = useState('');
@@ -12,7 +13,6 @@ const Login: React.FC = () => {
const { showNotification } = useNotification();
const navigate = useNavigate();
// 🔥 NEU: Redirect wenn bereits eingeloggt
useEffect(() => {
if (user) {
console.log('✅ User already logged in, redirecting to dashboard');
@@ -27,8 +27,7 @@ const Login: React.FC = () => {
try {
console.log('🔐 Attempting login for:', email);
await login({ email, password });
// 🔥 WICHTIG: Erfolgsmeldung und Redirect
console.log('✅ Login successful, redirecting to dashboard');
showNotification({
type: 'success',

View File

@@ -20,6 +20,7 @@ interface DashboardData {
}>;
teamStats: {
totalEmployees: number;
personell: number;
manager: number;
trainee: number;
experienced: number;
@@ -36,6 +37,7 @@ const Dashboard: React.FC = () => {
upcomingShifts: [],
teamStats: {
totalEmployees: 0,
personell: 0,
manager: 0,
trainee: 0,
experienced: 0
@@ -208,11 +210,13 @@ const Dashboard: React.FC = () => {
// Count by type
const managerCount = employees.filter(e => e.employeeType === 'manager').length;
const traineeCount = employees.filter(e => e.employeeType === 'trainee').length;
const experiencedCount = employees.filter(e => e.employeeType === 'experienced').length;
const personellCount = employees.filter(e => e.employeeType === 'personell').length;
const traineeCount = employees.filter(e => e.isTrainee === true).length;
const experiencedCount = employees.filter(e => e.isTrainee === false).length;
return {
totalEmployees,
personell: personellCount,
manager: managerCount,
trainee: traineeCount,
experienced: experiencedCount,
@@ -538,7 +542,7 @@ const Dashboard: React.FC = () => {
<h3 style={{ margin: '0 0 15px 0', color: '#2c3e50' }}>👥 Team-Übersicht</h3>
<div style={{ display: 'grid', gap: '12px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>Mitarbeiter:</span>
<span>Gesamte Belegschaft:</span>
<span style={{ fontWeight: 'bold', fontSize: '18px' }}>
{data.teamStats.totalEmployees}
</span>
@@ -550,15 +554,9 @@ const Dashboard: React.FC = () => {
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>Erfahrene:</span>
<span>Personal:</span>
<span style={{ fontWeight: 'bold', color: '#f39c12' }}>
{data.teamStats.experienced}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>Neue:</span>
<span style={{ fontWeight: 'bold', color: '#e74c3c' }}>
{data.teamStats.trainee}
{data.teamStats.personell}
</span>
</div>
</div>

View File

@@ -54,15 +54,122 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
{ level: 3 as AvailabilityLevel, label: 'Nicht möglich', color: '#e74c3c', bgColor: '#fadbd8', description: 'Nicht verfügbar' }
];
// Lade initial die Schichtpläne
useEffect(() => {
loadData();
const loadInitialData = async () => {
try {
setLoading(true);
console.log('🔄 LADE INITIALDATEN FÜR MITARBEITER:', employee.id);
// 1. Lade alle Schichtpläne
const plans = await shiftPlanService.getShiftPlans();
console.log('✅ SCHICHTPLÄNE GELADEN:', plans.length);
setShiftPlans(plans);
// 2. Wähle ersten verfügbaren Plan aus
if (plans.length > 0) {
const planWithShifts = plans.find(plan =>
plan.shifts && plan.shifts.length > 0 &&
plan.timeSlots && plan.timeSlots.length > 0
) || plans[0];
console.log('✅ ERSTER PLAN AUSGEWÄHLT:', planWithShifts.name);
setSelectedPlanId(planWithShifts.id);
} else {
setLoading(false);
}
} catch (err: any) {
console.error('❌ FEHLER BEIM LADEN DER INITIALDATEN:', err);
setError('Daten konnten nicht geladen werden: ' + (err.message || 'Unbekannter Fehler'));
setLoading(false);
}
};
loadInitialData();
}, [employee.id]);
// Lade Plan-Details und Verfügbarkeiten wenn selectedPlanId sich ändert
useEffect(() => {
if (selectedPlanId) {
loadSelectedPlan();
}
}, [selectedPlanId]);
const loadPlanData = async () => {
if (!selectedPlanId) {
setLoading(false);
return;
}
try {
setLoading(true);
console.log('🔄 LADE PLAN-DATEN FÜR:', selectedPlanId);
// 1. Lade Schichtplan Details
const plan = await shiftPlanService.getShiftPlan(selectedPlanId);
setSelectedPlan(plan);
console.log('✅ SCHICHTPLAN DETAILS GELADEN:', {
name: plan.name,
timeSlotsCount: plan.timeSlots?.length || 0,
shiftsCount: plan.shifts?.length || 0,
usedDays: Array.from(new Set(plan.shifts?.map(s => s.dayOfWeek) || [])).sort()
});
// 2. Lade Verfügbarkeiten für DIESEN Mitarbeiter und DIESEN Plan
console.log('🔄 LADE VERFÜGBARKEITEN FÜR:', {
employeeId: employee.id,
planId: selectedPlanId
});
try {
const allAvailabilities = await employeeService.getAvailabilities(employee.id);
console.log('📋 ALLE VERFÜGBARKEITEN DES MITARBEITERS:', allAvailabilities.length);
// Filtere nach dem aktuellen Plan UND stelle sicher, dass shiftId vorhanden ist
const planAvailabilities = allAvailabilities.filter(
avail => avail.planId === selectedPlanId && avail.shiftId
);
console.log('✅ VERFÜGBARKEITEN FÜR DIESEN PLAN (MIT SHIFT-ID):', planAvailabilities.length);
// Debug: Zeige auch ungültige Einträge
const invalidAvailabilities = allAvailabilities.filter(
avail => avail.planId === selectedPlanId && !avail.shiftId
);
if (invalidAvailabilities.length > 0) {
console.warn('⚠️ UNGÜLTIGE VERFÜGBARKEITEN (OHNE SHIFT-ID):', invalidAvailabilities.length);
invalidAvailabilities.forEach(invalid => {
console.warn(' - Ungültiger Eintrag:', invalid);
});
}
// Transformiere die Daten
const transformedAvailabilities: Availability[] = planAvailabilities.map(avail => ({
...avail,
isAvailable: avail.preferenceLevel !== 3
}));
setAvailabilities(transformedAvailabilities);
// Debug: Zeige vorhandene Präferenzen
if (planAvailabilities.length > 0) {
console.log('🎯 VORHANDENE PRÄFERENZEN:');
planAvailabilities.forEach(avail => {
const shift = plan.shifts?.find(s => s.id === avail.shiftId);
console.log(` - Shift: ${avail.shiftId} (Day: ${shift?.dayOfWeek}), Level: ${avail.preferenceLevel}`);
});
}
} catch (availError) {
console.error('❌ FEHLER BEIM LADEN DER VERFÜGBARKEITEN:', availError);
setAvailabilities([]);
}
} catch (err: any) {
console.error('❌ FEHLER BEIM LADEN DES SCHICHTPLANS:', err);
setError('Schichtplan konnte nicht geladen werden: ' + (err.message || 'Unbekannter Fehler'));
} finally {
setLoading(false);
}
};
loadPlanData();
}, [selectedPlanId, employee.id]);
const formatTime = (time: string): string => {
if (!time) return '--:--';
@@ -116,73 +223,12 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
return { days, shiftsByDay };
};
const loadData = async () => {
try {
setLoading(true);
console.log('🔄 LADE DATEN FÜR MITARBEITER:', employee.id);
// 1. Load availabilities
let existingAvailabilities: Availability[] = [];
try {
const availabilitiesData = await employeeService.getAvailabilities(employee.id);
existingAvailabilities = availabilitiesData.map(avail => ({
...avail,
isAvailable: avail.preferenceLevel !== 3
}));
console.log('✅ VERFÜGBARKEITEN GELADEN:', existingAvailabilities.length);
} catch (err) {
console.log('⚠️ KEINE VERFÜGBARKEITEN GEFUNDEN ODER FEHLER:', err);
}
// 2. Load shift plans
console.log('🔄 LADE SCHICHTPLÄNE...');
const plans = await shiftPlanService.getShiftPlans();
console.log('✅ SCHICHTPLÄNE GELADEN:', plans.length);
setShiftPlans(plans);
// 3. Select first plan with actual shifts if available
if (plans.length > 0) {
const planWithShifts = plans.find(plan =>
plan.shifts && plan.shifts.length > 0 &&
plan.timeSlots && plan.timeSlots.length > 0
) || plans[0];
setSelectedPlanId(planWithShifts.id);
console.log('✅ SCHICHTPLAN AUSGEWÄHLT:', planWithShifts.name);
await loadSelectedPlan();
}
// 4. Set existing availabilities
setAvailabilities(existingAvailabilities);
} catch (err: any) {
console.error('❌ FEHLER BEIM LADEN DER DATEN:', err);
setError('Daten konnten nicht geladen werden: ' + (err.message || 'Unbekannter Fehler'));
} finally {
setLoading(false);
}
};
const loadSelectedPlan = async () => {
try {
console.log('🔄 LADE AUSGEWÄHLTEN SCHICHTPLAN:', selectedPlanId);
const plan = await shiftPlanService.getShiftPlan(selectedPlanId);
setSelectedPlan(plan);
console.log('✅ SCHICHTPLAN GELADEN:', {
name: plan.name,
timeSlotsCount: plan.timeSlots?.length || 0,
shiftsCount: plan.shifts?.length || 0,
usedDays: Array.from(new Set(plan.shifts?.map(s => s.dayOfWeek) || [])).sort()
});
} catch (err: any) {
console.error('❌ FEHLER BEIM LADEN DES SCHICHTPLANS:', err);
setError('Schichtplan konnte nicht geladen werden: ' + (err.message || 'Unbekannter Fehler'));
}
};
const handleAvailabilityLevelChange = (shiftId: string, level: AvailabilityLevel) => {
if (!shiftId) {
console.error('❌ Versuch, Verfügbarkeit ohne Shift-ID zu ändern');
return;
}
console.log(`🔄 ÄNDERE VERFÜGBARKEIT: Shift ${shiftId}, Level ${level}`);
setAvailabilities(prev => {
@@ -238,24 +284,56 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
);
}
// Get all unique shifts across all days for row headers
const allShifts: ExtendedShift[] = [];
const shiftIds = new Set<string>();
// Create a map for quick time slot lookups
const timeSlotMap = new Map(selectedPlan?.timeSlots?.map(ts => [ts.id, ts]) || []);
// Get all unique time slots (rows) by collecting from all shifts
const allTimeSlots = new Map();
days.forEach(day => {
shiftsByDay[day.id]?.forEach(shift => {
if (!shiftIds.has(shift.id)) {
shiftIds.add(shift.id);
allShifts.push(shift);
const timeSlot = timeSlotMap.get(shift.timeSlotId);
if (timeSlot && !allTimeSlots.has(timeSlot.id)) {
allTimeSlots.set(timeSlot.id, {
...timeSlot,
shiftsByDay: {} // Initialize empty object to store shifts by day
});
}
});
});
// Sort shifts by time slot start time
allShifts.sort((a, b) => {
const timeA = a.startTime || '';
const timeB = b.startTime || '';
return timeA.localeCompare(timeB);
// Populate shifts for each time slot by day
days.forEach(day => {
shiftsByDay[day.id]?.forEach(shift => {
const timeSlot = allTimeSlots.get(shift.timeSlotId);
if (timeSlot) {
timeSlot.shiftsByDay[day.id] = shift;
}
});
});
// Convert to array and sort by start time
const sortedTimeSlots = Array.from(allTimeSlots.values()).sort((a, b) => {
return (a.startTime || '').localeCompare(b.startTime || '');
});
// Validation: Check if shifts are correctly placed
const validationErrors: string[] = [];
// Check for missing time slots
const usedTimeSlotIds = new Set(selectedPlan?.shifts?.map(s => s.timeSlotId) || []);
const availableTimeSlotIds = new Set(selectedPlan?.timeSlots?.map(ts => ts.id) || []);
usedTimeSlotIds.forEach(timeSlotId => {
if (!availableTimeSlotIds.has(timeSlotId)) {
validationErrors.push(`Zeitslot ${timeSlotId} wird verwendet, existiert aber nicht in timeSlots`);
}
});
// Check for shifts with invalid day numbers
selectedPlan?.shifts?.forEach(shift => {
if (shift.dayOfWeek < 1 || shift.dayOfWeek > 7) {
validationErrors.push(`Shift ${shift.id} hat ungültigen Wochentag: ${shift.dayOfWeek}`);
}
});
return (
@@ -273,10 +351,39 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
}}>
Verfügbarkeit definieren
<div style={{ fontSize: '14px', fontWeight: 'normal', marginTop: '5px' }}>
{allShifts.length} Schichten {days.length} Tage Direkte Shift-ID Zuordnung
{sortedTimeSlots.length} Zeitslots {days.length} Tage Zeitbasierte Darstellung
</div>
</div>
{/* Validation Warnings */}
{validationErrors.length > 0 && (
<div style={{
backgroundColor: '#fff3cd',
border: '1px solid #ffeaa7',
padding: '15px',
margin: '10px'
}}>
<h4 style={{ margin: '0 0 10px 0', color: '#856404' }}> Validierungswarnungen:</h4>
<ul style={{ margin: 0, paddingLeft: '20px', fontSize: '12px' }}>
{validationErrors.map((error, index) => (
<li key={index}>{error}</li>
))}
</ul>
</div>
)}
{/* Timetable Structure Info */}
<div style={{
backgroundColor: '#d1ecf1',
border: '1px solid #bee5eb',
padding: '10px 15px',
margin: '10px',
borderRadius: '4px',
fontSize: '12px'
}}>
<strong>Struktur-Info:</strong> {sortedTimeSlots.length} Zeitslots × {days.length} Tage = {sortedTimeSlots.length * days.length} Zellen
</div>
<div style={{ overflowX: 'auto' }}>
<table style={{
width: '100%',
@@ -290,9 +397,9 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
textAlign: 'left',
border: '1px solid #dee2e6',
fontWeight: 'bold',
minWidth: '150px'
minWidth: '200px'
}}>
Schicht (Zeit)
Zeitslot
</th>
{days.map(weekday => (
<th key={weekday.id} style={{
@@ -300,7 +407,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
textAlign: 'center',
border: '1px solid #dee2e6',
fontWeight: 'bold',
minWidth: '90px'
minWidth: '120px'
}}>
{weekday.name}
</th>
@@ -308,26 +415,32 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
</tr>
</thead>
<tbody>
{allShifts.map((shift, shiftIndex) => (
<tr key={shift.id} style={{
backgroundColor: shiftIndex % 2 === 0 ? 'white' : '#f8f9fa'
{sortedTimeSlots.map((timeSlot, timeSlotIndex) => (
<tr key={timeSlot.id} style={{
backgroundColor: timeSlotIndex % 2 === 0 ? 'white' : '#f8f9fa'
}}>
<td style={{
padding: '12px 16px',
border: '1px solid #dee2e6',
fontWeight: '500',
backgroundColor: '#f8f9fa'
backgroundColor: '#f8f9fa',
position: 'sticky',
left: 0
}}>
{shift.displayName}
<div style={{ fontSize: '11px', color: '#666', marginTop: '4px' }}>
Shift-ID: {shift.id.substring(0, 8)}...
<div style={{ fontWeight: 'bold' }}>
{timeSlot.name}
</div>
<div style={{ fontSize: '14px', color: '#666' }}>
{formatTime(timeSlot.startTime)} - {formatTime(timeSlot.endTime)}
</div>
<div style={{ fontSize: '11px', color: '#999', marginTop: '4px' }}>
ID: {timeSlot.id.substring(0, 8)}...
</div>
</td>
{days.map(weekday => {
// Check if this shift exists for this day
const shiftForDay = shiftsByDay[weekday.id]?.find(s => s.id === shift.id);
const shift = timeSlot.shiftsByDay[weekday.id];
if (!shiftForDay) {
if (!shift) {
return (
<td key={weekday.id} style={{
padding: '12px 16px',
@@ -337,11 +450,14 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
color: '#ccc',
fontStyle: 'italic'
}}>
-
Kein Shift
</td>
);
}
// Validation: Check if shift has correct timeSlotId and dayOfWeek
const isValidShift = shift.timeSlotId === timeSlot.id && shift.dayOfWeek === weekday.id;
const currentLevel = getAvailabilityForShift(shift.id);
const levelConfig = availabilityLevels.find(l => l.level === currentLevel);
@@ -350,8 +466,31 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
padding: '12px 16px',
border: '1px solid #dee2e6',
textAlign: 'center',
backgroundColor: levelConfig?.bgColor
backgroundColor: !isValidShift ? '#fff3cd' : (levelConfig?.bgColor || 'white'),
position: 'relative'
}}>
{/* Validation indicator */}
{!isValidShift && (
<div style={{
position: 'absolute',
top: '2px',
right: '2px',
backgroundColor: '#f39c12',
color: 'white',
borderRadius: '50%',
width: '16px',
height: '16px',
fontSize: '10px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
title={`Shift Validierung: timeSlotId=${shift.timeSlotId}, dayOfWeek=${shift.dayOfWeek}`}
>
</div>
)}
<select
value={currentLevel}
onChange={(e) => {
@@ -360,10 +499,10 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
}}
style={{
padding: '8px 12px',
border: `2px solid ${levelConfig?.color || '#ddd'}`,
border: `2px solid ${!isValidShift ? '#f39c12' : (levelConfig?.color || '#ddd')}`,
borderRadius: '6px',
backgroundColor: levelConfig?.bgColor || 'white',
color: levelConfig?.color || '#333',
backgroundColor: !isValidShift ? '#fff3cd' : (levelConfig?.bgColor || 'white'),
color: !isValidShift ? '#856404' : (levelConfig?.color || '#333'),
fontWeight: 'bold',
minWidth: '140px',
cursor: 'pointer',
@@ -384,6 +523,23 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
</option>
))}
</select>
{/* Shift debug info */}
<div style={{
fontSize: '10px',
color: '#666',
marginTop: '4px',
textAlign: 'left',
fontFamily: 'monospace'
}}>
<div>Shift: {shift.id.substring(0, 6)}...</div>
<div>Day: {shift.dayOfWeek}</div>
{!isValidShift && (
<div style={{ color: '#e74c3c', fontWeight: 'bold' }}>
VALIDATION ERROR
</div>
)}
</div>
</td>
);
})}
@@ -392,6 +548,27 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
</tbody>
</table>
</div>
{/* Summary Statistics */}
<div style={{
backgroundColor: '#f8f9fa',
padding: '15px',
borderTop: '1px solid #dee2e6',
fontSize: '12px',
color: '#666'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<strong>Zusammenfassung:</strong> {sortedTimeSlots.length} Zeitslots × {days.length} Tage = {sortedTimeSlots.length * days.length} mögliche Shifts
</div>
<div>
<strong>Aktive Verfügbarkeiten:</strong> {availabilities.filter(a => a.preferenceLevel !== 3).length}
</div>
<div>
<strong>Validierungsfehler:</strong> {validationErrors.length}
</div>
</div>
</div>
</div>
);
};
@@ -406,14 +583,24 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
return;
}
const { days, shiftsByDay } = getTimetableData();
// Filter availabilities to only include those with actual shifts
// Filter availabilities to only include those with actual shifts AND valid shiftIds
const validAvailabilities = availabilities.filter(avail => {
// Check if this shiftId exists and is valid
if (!avail.shiftId) {
console.warn('⚠️ Überspringe ungültige Verfügbarkeit ohne Shift-ID:', avail);
return false;
}
// Check if this shiftId exists in the current plan
return selectedPlan?.shifts?.some(shift => shift.id === avail.shiftId);
});
console.log('💾 SPEICHERE VERFÜGBARKEITEN:', {
total: availabilities.length,
valid: validAvailabilities.length,
invalid: availabilities.length - validAvailabilities.length
});
if (validAvailabilities.length === 0) {
setError('Keine gültigen Verfügbarkeiten zum Speichern gefunden');
return;
@@ -485,25 +672,63 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
{/* Debug-Info */}
<div style={{
backgroundColor: shiftsCount === 0 ? '#f8d7da' : '#d1ecf1',
border: `1px solid ${shiftsCount === 0 ? '#f5c6cb' : '#bee5eb'}`,
backgroundColor: !selectedPlan ? '#f8d7da' : (shiftsCount === 0 ? '#fff3cd' : '#d1ecf1'),
border: `1px solid ${!selectedPlan ? '#f5c6cb' : (shiftsCount === 0 ? '#ffeaa7' : '#bee5eb')}`,
borderRadius: '6px',
padding: '15px',
marginBottom: '20px'
}}>
<h4 style={{
margin: '0 0 10px 0',
color: shiftsCount === 0 ? '#721c24' : '#0c5460'
color: !selectedPlan ? '#721c24' : (shiftsCount === 0 ? '#856404' : '#0c5460')
}}>
{shiftsCount === 0 ? '❌ PROBLEM: Keine Shifts gefunden' : '✅ Plan-Daten geladen'}
{!selectedPlan ? '❌ KEIN PLAN AUSGEWÄHLT' :
shiftsCount === 0 ? '⚠️ KEINE SHIFTS GEFUNDEN' : '✅ PLAN-DATEN GELADEN'}
</h4>
<div style={{ fontSize: '12px', fontFamily: 'monospace' }}>
<div><strong>Ausgewählter Plan:</strong> {selectedPlan?.name || 'Keiner'}</div>
<div><strong>Plan ID:</strong> {selectedPlanId || 'Nicht gesetzt'}</div>
<div><strong>Geladene Pläne:</strong> {shiftPlans.length}</div>
<div><strong>Einzigartige Shifts:</strong> {shiftsCount}</div>
<div><strong>Verwendete Tage:</strong> {days.length} ({days.map(d => d.name).join(', ')})</div>
<div><strong>Gesamte Shifts im Plan:</strong> {selectedPlan?.shifts?.length || 0}</div>
<div><strong>Methode:</strong> Direkte Shift-ID Zuordnung</div>
<div><strong>Geladene Verfügbarkeiten:</strong> {availabilities.length}</div>
{selectedPlan && (
<>
<div><strong>Verwendete Tage:</strong> {days.length} ({days.map(d => d.name).join(', ')})</div>
<div><strong>Gesamte Shifts im Plan:</strong> {selectedPlan.shifts?.length || 0}</div>
</>
)}
</div>
{/* Show existing preferences */}
{availabilities.length > 0 && (
<div style={{ marginTop: '10px', paddingTop: '10px', borderTop: '1px solid #bee5eb' }}>
<strong>Vorhandene Präferenzen:</strong>
{availabilities.slice(0, 5).map(avail => {
// SICHERHEITSCHECK: Stelle sicher, dass shiftId existiert
if (!avail.shiftId) {
return (
<div key={avail.id} style={{ fontSize: '11px', color: 'red' }}>
UNGÜLTIG: Keine Shift-ID
</div>
);
}
const shift = selectedPlan?.shifts?.find(s => s.id === avail.shiftId);
const shiftIdDisplay = avail.shiftId ? avail.shiftId.substring(0, 8) + '...' : 'KEINE ID';
return (
<div key={avail.id} style={{ fontSize: '11px' }}>
Shift {shiftIdDisplay} (Day {shift?.dayOfWeek || '?'}): Level {avail.preferenceLevel}
</div>
);
})}
{availabilities.length > 5 && (
<div style={{ fontSize: '11px', fontStyle: 'italic' }}>
... und {availabilities.length - 5} weitere
</div>
)}
</div>
)}
</div>
{/* Employee Info */}
@@ -588,7 +813,12 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
</label>
<select
value={selectedPlanId}
onChange={(e) => setSelectedPlanId(e.target.value)}
onChange={(e) => {
const newPlanId = e.target.value;
console.log('🔄 PLAN WECHSELN ZU:', newPlanId);
setSelectedPlanId(newPlanId);
// Der useEffect wird automatisch ausgelöst
}}
style={{
padding: '8px 12px',
border: '1px solid #ddd',
@@ -609,10 +839,25 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
<div style={{ fontSize: '14px', color: '#666' }}>
<div><strong>Plan:</strong> {selectedPlan.name}</div>
<div><strong>Shifts:</strong> {selectedPlan.shifts?.length || 0}</div>
<div><strong>Zeitslots:</strong> {selectedPlan.timeSlots?.length || 0}</div>
<div><strong>Status:</strong> {selectedPlan.status}</div>
</div>
)}
</div>
{/* Debug Info für Plan Loading */}
{!selectedPlanId && shiftPlans.length > 0 && (
<div style={{
marginTop: '10px',
padding: '10px',
backgroundColor: '#fff3cd',
border: '1px solid #ffeaa7',
borderRadius: '4px',
fontSize: '12px'
}}>
Bitte wählen Sie einen Schichtplan aus
</div>
)}
</div>
{/* Availability Timetable */}

View File

@@ -11,6 +11,9 @@ interface EmployeeFormProps {
onCancel: () => void;
}
type EmployeeType = 'manager' | 'personell' | 'apprentice' | 'guest';
type ContractType = 'small' | 'large' | 'flexible';
const EmployeeForm: React.FC<EmployeeFormProps> = ({
mode,
employee,
@@ -20,13 +23,14 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
const [formData, setFormData] = useState({
firstname: '',
lastname: '',
email: '', // Will be auto-generated and display only
email: '',
password: '',
roles: ['user'] as string[], // Changed from single role to array
employeeType: 'trainee' as 'manager' | 'trainee' | 'experienced',
contractType: 'small' as 'small' | 'large',
roles: ['user'] as string[],
employeeType: 'personell' as EmployeeType,
contractType: 'small' as ContractType | undefined,
canWorkAlone: false,
isActive: true
isActive: true,
isTrainee: false
});
const [passwordForm, setPasswordForm] = useState({
newPassword: '',
@@ -62,12 +66,13 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
firstname: employee.firstname,
lastname: employee.lastname,
email: employee.email,
password: '', // Password wird beim Bearbeiten nicht angezeigt
roles: employee.roles || ['user'], // Use roles array
password: '',
roles: employee.roles || ['user'],
employeeType: employee.employeeType,
contractType: employee.contractType,
canWorkAlone: employee.canWorkAlone,
isActive: employee.isActive
isActive: employee.isActive,
isTrainee: employee.isTrainee || false
});
}
}, [mode, employee]);
@@ -92,13 +97,11 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
const handleRoleChange = (role: string, checked: boolean) => {
setFormData(prev => {
if (checked) {
// Add role if checked
return {
...prev,
roles: [...prev.roles, role]
};
} else {
// Remove role if unchecked
return {
...prev,
roles: prev.roles.filter(r => r !== role)
@@ -107,18 +110,36 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
});
};
const handleEmployeeTypeChange = (employeeType: 'manager' | 'trainee' | 'experienced') => {
// Manager and experienced can work alone, trainee cannot
const canWorkAlone = employeeType === 'manager' || employeeType === 'experienced';
const handleEmployeeTypeChange = (employeeType: EmployeeType) => {
// Determine if contract type should be shown and set default
const requiresContract = employeeType !== 'guest';
const defaultContractType = requiresContract ? 'small' as ContractType : undefined;
// Determine if can work alone based on employee type
const canWorkAlone = employeeType === 'manager' ||
(employeeType === 'personell' && !formData.isTrainee);
// Reset isTrainee if not personell
const isTrainee = employeeType === 'personell' ? formData.isTrainee : false;
setFormData(prev => ({
...prev,
employeeType,
canWorkAlone
contractType: defaultContractType,
canWorkAlone,
isTrainee
}));
};
const handleContractTypeChange = (contractType: 'small' | 'large') => {
const handleTraineeChange = (isTrainee: boolean) => {
setFormData(prev => ({
...prev,
isTrainee,
canWorkAlone: prev.employeeType === 'personell' ? !isTrainee : prev.canWorkAlone
}));
};
const handleContractTypeChange = (contractType: ContractType) => {
setFormData(prev => ({
...prev,
contractType
@@ -136,25 +157,27 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
firstname: formData.firstname.trim(),
lastname: formData.lastname.trim(),
password: formData.password,
roles: formData.roles, // Use roles array
roles: formData.roles,
employeeType: formData.employeeType,
contractType: formData.contractType,
canWorkAlone: formData.canWorkAlone
contractType: formData.employeeType !== 'guest' ? formData.contractType : undefined,
canWorkAlone: formData.canWorkAlone,
isTrainee: formData.isTrainee
};
await employeeService.createEmployee(createData);
} else if (employee) {
const updateData: UpdateEmployeeRequest = {
firstname: formData.firstname.trim(),
lastname: formData.lastname.trim(),
roles: formData.roles, // Use roles array
roles: formData.roles,
employeeType: formData.employeeType,
contractType: formData.contractType,
contractType: formData.employeeType !== 'guest' ? formData.contractType : undefined,
canWorkAlone: formData.canWorkAlone,
isActive: formData.isActive,
isTrainee: formData.isTrainee
};
await employeeService.updateEmployee(employee.id, updateData);
// If password change is requested and user is admin
// Password change logic remains the same
if (showPasswordSection && passwordForm.newPassword && hasRole(['admin'])) {
if (passwordForm.newPassword.length < 6) {
throw new Error('Das neue Passwort muss mindestens 6 Zeichen lang sein');
@@ -163,9 +186,8 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
throw new Error('Die Passwörter stimmen nicht überein');
}
// Use the password change endpoint
await employeeService.changePassword(employee.id, {
currentPassword: '', // Empty for admin reset - backend should handle this
currentPassword: '',
newPassword: passwordForm.newPassword
});
}
@@ -189,9 +211,12 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
const contractTypeOptions = [
{ value: 'small' as const, label: 'Kleiner Vertrag', description: '1 Schicht pro Woche' },
{ value: 'large' as const, label: 'Großer Vertrag', description: '2 Schichten pro Woche' }
{ value: 'large' as const, label: 'Großer Vertrag', description: '2 Schichten pro Woche' },
{ value: 'flexible' as const, label: 'Flexibler Vertrag', description: 'Flexible Arbeitszeiten' }
];
const showContractType = formData.employeeType !== 'guest';
return (
<div style={{
maxWidth: '700px',
@@ -330,8 +355,8 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
)}
</div>
{/* Vertragstyp (nur für Admins) */}
{hasRole(['admin']) && (
{/* Vertragstyp (nur für Admins und interne Mitarbeiter) */}
{hasRole(['admin']) && showContractType && (
<div style={{
padding: '20px',
backgroundColor: '#e8f4fd',
@@ -470,6 +495,37 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
</div>
))}
</div>
{/* FIXED: Trainee checkbox for personell type */}
{formData.employeeType === 'personell' && (
<div style={{
marginTop: '15px',
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '15px',
border: '1px solid #e0e0e0',
borderRadius: '6px',
backgroundColor: '#fff'
}}>
<input
type="checkbox"
name="isTrainee"
id="isTrainee"
checked={formData.isTrainee}
onChange={(e) => handleTraineeChange(e.target.checked)}
style={{ width: '18px', height: '18px' }}
/>
<div>
<label htmlFor="isTrainee" style={{ fontWeight: 'bold', color: '#2c3e50', display: 'block' }}>
Als Neuling markieren
</label>
<div style={{ fontSize: '12px', color: '#7f8c8d' }}>
Neulinge benötigen zusätzliche Betreuung und können nicht eigenständig arbeiten.
</div>
</div>
</div>
)}
</div>
{/* Eigenständigkeit */}
@@ -496,11 +552,11 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
id="canWorkAlone"
checked={formData.canWorkAlone}
onChange={handleChange}
disabled={formData.employeeType === 'manager'}
disabled={formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)}
style={{
width: '20px',
height: '20px',
opacity: formData.employeeType === 'manager' ? 0.5 : 1
opacity: (formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) ? 0.5 : 1
}}
/>
<div style={{ flex: 1 }}>
@@ -508,14 +564,16 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
fontWeight: 'bold',
color: '#2c3e50',
display: 'block',
opacity: formData.employeeType === 'manager' ? 0.5 : 1
opacity: (formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) ? 0.5 : 1
}}>
Als ausreichend eigenständig markieren
{formData.employeeType === 'manager' && ' (Automatisch für Chefs)'}
{(formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) && ' (Automatisch festgelegt)'}
</label>
<div style={{ fontSize: '14px', color: '#7f8c8d' }}>
{formData.employeeType === 'manager'
? 'Chefs sind automatisch als eigenständig markiert.'
: formData.employeeType === 'personell' && formData.isTrainee
? 'Auszubildende können nicht als eigenständig markiert werden.'
: 'Dieser Mitarbeiter kann komplexe Aufgaben eigenständig lösen und benötigt keine ständige Betreuung.'
}
</div>
@@ -527,7 +585,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
borderRadius: '15px',
fontSize: '12px',
fontWeight: 'bold',
opacity: formData.employeeType === 'manager' ? 0.7 : 1
opacity: (formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) ? 0.7 : 1
}}>
{formData.canWorkAlone ? 'EIGENSTÄNDIG' : 'BETREUUNG'}
</div>
@@ -632,7 +690,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
</div>
)}
{/* Systemrollen (nur für Admins) */}
{/* Systemrollen (nur für Admins) - AKTUALISIERT FÜR MEHRFACHE ROLLEN */}
{hasRole(['admin']) && (
<div style={{
padding: '20px',
@@ -714,6 +772,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
</div>
</div>
)}
</div>
{/* Buttons */}

View File

@@ -1,3 +1,4 @@
// EmployeeList.tsx
import React, { useState } from 'react';
import { ROLE_CONFIG, EMPLOYEE_TYPE_CONFIG } from '../../../models/defaults/employeeDefaults';
import { Employee } from '../../../models/Employee';
@@ -13,6 +14,9 @@ interface EmployeeListProps {
type SortField = 'name' | 'employeeType' | 'canWorkAlone' | 'role' | 'lastLogin';
type SortDirection = 'asc' | 'desc';
// FIXED: Use the actual employee types from the Employee interface
type EmployeeType = 'manager' | 'personell' | 'apprentice' | 'guest';
const EmployeeList: React.FC<EmployeeListProps> = ({
employees,
onEdit,
@@ -122,18 +126,18 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
return false;
};
// Using shared configuration for consistent styling
type EmployeeType = 'manager' | 'trainee' | 'experienced';
const getEmployeeTypeBadge = (type: EmployeeType) => {
const getEmployeeTypeBadge = (type: EmployeeType, isTrainee: boolean = false) => {
const config = EMPLOYEE_TYPE_CONFIG[type];
// FIXED: Updated color mapping for actual employee types
const bgColor =
type === 'manager'
? '#fadbd8'
: type === 'trainee'
? '#d5f4e6'
: '#d6eaf8'; // experienced
? '#fadbd8' // light red
: type === 'personell'
? isTrainee ? '#d5f4e6' : '#d6eaf8' // light green for trainee, light blue for experienced
: type === 'apprentice'
? '#e8d7f7' // light purple for apprentice
: '#f8f9fa'; // light gray for guest
return { text: config.label, color: config.color, bgColor };
};
@@ -296,7 +300,8 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
</div>
{sortedEmployees.map(employee => {
const employeeType = getEmployeeTypeBadge(employee.employeeType);
// FIXED: Type assertion to ensure type safety
const employeeType = getEmployeeTypeBadge(employee.employeeType as EmployeeType, employee.isTrainee);
const independence = getIndependenceBadge(employee.canWorkAlone);
const roleInfo = getRoleBadge(employee.roles);
const status = getStatusBadge(employee.isActive);
@@ -541,19 +546,30 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
borderRadius: '12px',
fontSize: '11px',
fontWeight: 'bold'
}}>👴 ERFAHREN</span>
<span style={{ fontSize: '12px', color: '#666' }}>Langjährige Erfahrung</span>
}}>👨🏭 PERSONAL</span>
<span style={{ fontSize: '12px', color: '#666' }}>Reguläre Mitarbeiter</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<span style={{
backgroundColor: '#d5f4e6',
color: '#27ae60',
backgroundColor: '#e8d7f7',
color: '#9b59b6',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '11px',
fontWeight: 'bold'
}}>👶 NEULING</span>
<span style={{ fontSize: '12px', color: '#666' }}>Benötigt Einarbeitung</span>
}}>👨🎓 AUSZUBILDENDER</span>
<span style={{ fontSize: '12px', color: '#666' }}>Auszubildende</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<span style={{
backgroundColor: '#f8f9fa',
color: '#95a5a6',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '11px',
fontWeight: 'bold'
}}>👤 GAST</span>
<span style={{ fontSize: '12px', color: '#666' }}>Externe Mitarbeiter</span>
</div>
</div>
</div>

View File

@@ -16,14 +16,27 @@ interface ExtendedTimeSlot extends TimeSlot {
displayName?: string;
}
interface ExtendedShift {
id: string;
planId: string;
timeSlotId: string;
dayOfWeek: number;
requiredEmployees: number;
color?: string;
timeSlotName?: string;
startTime?: string;
endTime?: string;
displayName?: string;
}
const weekdays = [
{ id: 1, name: 'Mo' },
{ id: 2, name: 'Di' },
{ id: 3, name: 'Mi' },
{ id: 4, name: 'Do' },
{ id: 5, name: 'Fr' },
{ id: 6, name: 'Sa' },
{ id: 7, name: 'So' }
{ id: 1, name: 'Montag' },
{ id: 2, name: 'Dienstag' },
{ id: 3, name: 'Mittwoch' },
{ id: 4, name: 'Donnerstag' },
{ id: 5, name: 'Freitag' },
{ id: 6, name: 'Samstag' },
{ id: 7, name: 'Sonntag' }
];
const ShiftPlanView: React.FC = () => {
@@ -35,14 +48,13 @@ const ShiftPlanView: React.FC = () => {
const [shiftPlan, setShiftPlan] = useState<ShiftPlan | null>(null);
const [employees, setEmployees] = useState<Employee[]>([]);
const [availabilities, setAvailabilities] = useState<EmployeeAvailability[]>([]);
const [assignmentResult, setAssignmentResult] = useState<AssignmentResult | null>(null); // Add this line
const [assignmentResult, setAssignmentResult] = useState<AssignmentResult | null>(null);
const [loading, setLoading] = useState(true);
const [publishing, setPublishing] = useState(false);
const [scheduledShifts, setScheduledShifts] = useState<ScheduledShift[]>([]);
const [showAssignmentPreview, setShowAssignmentPreview] = useState(false);
const [recreating, setRecreating] = useState(false);
useEffect(() => {
loadShiftPlanData();
@@ -76,31 +88,133 @@ const ShiftPlanView: React.FC = () => {
};
}, []);
// Add this useEffect to debug state changes
useEffect(() => {
if (assignmentResult) {
console.log("🔄 assignmentResult UPDATED:", {
success: assignmentResult.success,
assignmentsCount: Object.keys(assignmentResult.assignments).length,
assignmentKeys: Object.keys(assignmentResult.assignments).slice(0, 5), // First 5 keys
violations: assignmentResult.violations.length
});
// Log all assignments with their keys
Object.entries(assignmentResult.assignments).forEach(([key, empIds]) => {
console.log(` 🗂️ Assignment Key: ${key}`);
console.log(` Employees: ${empIds.join(', ')}`);
// Try to identify what this key represents
const isUuid = key.length === 36; // UUID format
console.log(` Type: ${isUuid ? 'UUID (likely scheduled shift)' : 'Pattern (likely shift pattern)'}`);
});
}
}, [assignmentResult]);
console.log('🔍 STATE DEBUG - showAssignmentPreview:', showAssignmentPreview);
console.log('🔍 STATE DEBUG - assignmentResult:', assignmentResult ? 'EXISTS' : 'NULL');
console.log('🔍 STATE DEBUG - publishing:', publishing);
}, [showAssignmentPreview, assignmentResult, publishing]);
useEffect(() => {
(window as any).debugRenderLogic = debugRenderLogic;
return () => { (window as any).debugRenderLogic = undefined; };
}, [shiftPlan, scheduledShifts]);
// Create a data structure that maps days to their shifts with time slot info - SAME AS AVAILABILITYMANAGER
const getTimetableData = () => {
if (!shiftPlan || !shiftPlan.shifts || !shiftPlan.timeSlots) {
return { days: [], shiftsByDay: {}, allTimeSlots: [] };
}
// Create a map for quick time slot lookups
const timeSlotMap = new Map(shiftPlan.timeSlots.map(ts => [ts.id, ts]));
// Group shifts by day and enhance with time slot info - SAME LOGIC AS AVAILABILITYMANAGER
const shiftsByDay = shiftPlan.shifts.reduce((acc, shift) => {
if (!acc[shift.dayOfWeek]) {
acc[shift.dayOfWeek] = [];
}
const timeSlot = timeSlotMap.get(shift.timeSlotId);
const enhancedShift: ExtendedShift = {
...shift,
timeSlotName: timeSlot?.name,
startTime: timeSlot?.startTime,
endTime: timeSlot?.endTime,
displayName: timeSlot ? `${timeSlot.name} (${formatTime(timeSlot.startTime)}-${formatTime(timeSlot.endTime)})` : shift.id
};
acc[shift.dayOfWeek].push(enhancedShift);
return acc;
}, {} as Record<number, ExtendedShift[]>);
// Sort shifts within each day by start time - SAME LOGIC AS AVAILABILITYMANAGER
Object.keys(shiftsByDay).forEach(day => {
shiftsByDay[parseInt(day)].sort((a, b) => {
const timeA = a.startTime || '';
const timeB = b.startTime || '';
return timeA.localeCompare(timeB);
});
});
// Get unique days that have shifts - SAME LOGIC AS AVAILABILITYMANAGER
const days = Array.from(new Set(shiftPlan.shifts.map(shift => shift.dayOfWeek)))
.sort()
.map(dayId => {
return weekdays.find(day => day.id === dayId) || { id: dayId, name: `Tag ${dayId}` };
});
// Get all unique time slots (rows) by collecting from all shifts - SAME LOGIC AS AVAILABILITYMANAGER
const allTimeSlotsMap = new Map();
days.forEach(day => {
shiftsByDay[day.id]?.forEach(shift => {
const timeSlot = timeSlotMap.get(shift.timeSlotId);
if (timeSlot && !allTimeSlotsMap.has(timeSlot.id)) {
allTimeSlotsMap.set(timeSlot.id, {
...timeSlot,
shiftsByDay: {} // Initialize empty object to store shifts by day
});
}
});
});
// Populate shifts for each time slot by day - SAME LOGIC AS AVAILABILITYMANAGER
days.forEach(day => {
shiftsByDay[day.id]?.forEach(shift => {
const timeSlot = allTimeSlotsMap.get(shift.timeSlotId);
if (timeSlot) {
timeSlot.shiftsByDay[day.id] = shift;
}
});
});
// Convert to array and sort by start time - SAME LOGIC AS AVAILABILITYMANAGER
const allTimeSlots = Array.from(allTimeSlotsMap.values()).sort((a, b) => {
return (a.startTime || '').localeCompare(b.startTime || '');
});
return { days, shiftsByDay, allTimeSlots };
};
// VALIDATION FUNCTION - Check if shifts are correctly placed (like in AvailabilityManager)
const validateTimetableStructure = () => {
if (!shiftPlan || !shiftPlan.shifts || !shiftPlan.timeSlots) {
return { isValid: false, errors: ['No shift plan data available'] };
}
const validationErrors: string[] = [];
// Check for missing time slots - SAME VALIDATION AS AVAILABILITYMANAGER
const usedTimeSlotIds = new Set(shiftPlan.shifts.map(s => s.timeSlotId));
const availableTimeSlotIds = new Set(shiftPlan.timeSlots.map(ts => ts.id));
usedTimeSlotIds.forEach(timeSlotId => {
if (!availableTimeSlotIds.has(timeSlotId)) {
validationErrors.push(`Zeitslot ${timeSlotId} wird verwendet, existiert aber nicht in timeSlots`);
}
});
// Check for shifts with invalid day numbers - SAME VALIDATION AS AVAILABILITYMANAGER
shiftPlan.shifts.forEach(shift => {
if (shift.dayOfWeek < 1 || shift.dayOfWeek > 7) {
validationErrors.push(`Shift ${shift.id} hat ungültigen Wochentag: ${shift.dayOfWeek}`);
}
// Check if shift timeSlotId exists in timeSlots
const timeSlotExists = shiftPlan.timeSlots.some(ts => ts.id === shift.timeSlotId);
if (!timeSlotExists) {
validationErrors.push(`Shift ${shift.id} verweist auf nicht existierenden Zeitslot: ${shift.timeSlotId}`);
}
});
// Check for scheduled shifts consistency
scheduledShifts.forEach(scheduledShift => {
const timeSlotExists = shiftPlan.timeSlots.some(ts => ts.id === scheduledShift.timeSlotId);
if (!timeSlotExists) {
validationErrors.push(`Scheduled Shift ${scheduledShift.id} verweist auf nicht existierenden Zeitslot: ${scheduledShift.timeSlotId}`);
}
});
return {
isValid: validationErrors.length === 0,
errors: validationErrors
};
};
const loadShiftPlanData = async () => {
if (!id) return;
@@ -132,7 +246,9 @@ const ShiftPlanView: React.FC = () => {
setScheduledShifts(shiftsData);
// Load availabilities
// Load availabilities - USING THE SAME LOGIC AS AVAILABILITYMANAGER
console.log('🔄 LADE VERFÜGBARKEITEN FÜR PLAN:', id);
const availabilityPromises = employeesData
.filter(emp => emp.isActive)
.map(emp => employeeService.getAvailabilities(emp.id));
@@ -140,12 +256,21 @@ const ShiftPlanView: React.FC = () => {
const allAvailabilities = await Promise.all(availabilityPromises);
const flattenedAvailabilities = allAvailabilities.flat();
// Filter to only include availabilities for the current plan - SAME LOGIC AS AVAILABILITYMANAGER
const planAvailabilities = flattenedAvailabilities.filter(
availability => availability.planId === id
);
console.log('✅ VERFÜGBARKEITEN FÜR DIESEN PLAN:', planAvailabilities.length);
setAvailabilities(planAvailabilities);
// Run validation
const validation = validateTimetableStructure();
if (!validation.isValid) {
console.warn('⚠️ TIMETABLE VALIDATION ERRORS:', validation.errors);
}
} catch (error) {
console.error('Error loading shift plan data:', error);
showNotification({
@@ -222,143 +347,6 @@ const ShiftPlanView: React.FC = () => {
}
};
const debugRenderLogic = () => {
if (!shiftPlan) return;
console.log('🔍 RENDER LOGIC DEBUG:');
console.log('=====================');
const { days, allTimeSlots, timeSlotsByDay } = getTimetableData();
console.log('📊 TABLE STRUCTURE:');
console.log('- Days in table:', days.length);
console.log('- TimeSlots in table:', allTimeSlots.length);
console.log('- Days with data:', Object.keys(timeSlotsByDay).length);
// Zeige die tatsächliche Struktur der Tabelle
console.log('\n📅 ACTUAL TABLE DAYS:');
days.forEach(day => {
const slotsForDay = timeSlotsByDay[day.id] || [];
console.log(`- ${day.name}: ${slotsForDay.length} time slots`);
});
console.log('\n⏰ ACTUAL TIME SLOTS:');
allTimeSlots.forEach(slot => {
console.log(`- ${slot.name} (${slot.startTime}-${slot.endTime})`);
});
// Prüfe wie viele Scheduled Shifts tatsächlich gerendert werden
console.log('\n🔍 SCHEDULED SHIFTS RENDER ANALYSIS:');
let totalRenderedShifts = 0;
let shiftsWithAssignments = 0;
days.forEach(day => {
const slotsForDay = timeSlotsByDay[day.id] || [];
slotsForDay.forEach(timeSlot => {
totalRenderedShifts++;
// Finde den entsprechenden Scheduled Shift
const scheduledShift = scheduledShifts.find(scheduled => {
const scheduledDayOfWeek = getDayOfWeek(scheduled.date);
return scheduledDayOfWeek === day.id &&
scheduled.timeSlotId === timeSlot.id;
});
if (scheduledShift && scheduledShift.assignedEmployees && scheduledShift.assignedEmployees.length > 0) {
shiftsWithAssignments++;
}
});
});
console.log(`- Total shifts in table: ${totalRenderedShifts}`);
console.log(`- Shifts with assignments: ${shiftsWithAssignments}`);
console.log(`- Total scheduled shifts: ${scheduledShifts.length}`);
console.log(`- Coverage: ${Math.round((totalRenderedShifts / scheduledShifts.length) * 100)}%`);
// Problem-Analyse
if (totalRenderedShifts < scheduledShifts.length) {
console.log('\n🚨 PROBLEM: Table is not showing all scheduled shifts!');
console.log('💡 The table structure (days × timeSlots) is smaller than actual scheduled shifts');
// Zeige die fehlenden Shifts
const missingShifts = scheduledShifts.filter(scheduled => {
const dayOfWeek = getDayOfWeek(scheduled.date);
const timeSlotExists = allTimeSlots.some(ts => ts.id === scheduled.timeSlotId);
const dayExists = days.some(day => day.id === dayOfWeek);
return !(timeSlotExists && dayExists);
});
if (missingShifts.length > 0) {
console.log(`${missingShifts.length} shifts cannot be rendered in table:`);
missingShifts.slice(0, 5).forEach(shift => {
const dayOfWeek = getDayOfWeek(shift.date);
const timeSlot = shiftPlan.timeSlots?.find(ts => ts.id === shift.timeSlotId);
console.log(` - ${shift.date} (Day ${dayOfWeek}): ${timeSlot?.name || 'Unknown'} - ${shift.assignedEmployees?.length || 0} assignments`);
});
}
}
};
// Extract plan-specific shifts using the same logic as AvailabilityManager
const getTimetableData = () => {
if (!shiftPlan || !shiftPlan.shifts || !shiftPlan.timeSlots) {
return { days: [], timeSlotsByDay: {}, allTimeSlots: [] };
}
// Group shifts by day
const shiftsByDay = shiftPlan.shifts.reduce((acc, shift) => {
if (!acc[shift.dayOfWeek]) {
acc[shift.dayOfWeek] = [];
}
acc[shift.dayOfWeek].push(shift);
return acc;
}, {} as Record<number, typeof shiftPlan.shifts>);
// Get unique days that have shifts
const days = Array.from(new Set(shiftPlan.shifts.map(shift => shift.dayOfWeek)))
.sort()
.map(dayId => {
return weekdays.find(day => day.id === dayId) || { id: dayId, name: `Tag ${dayId}` };
});
// For each day, get the time slots that actually have shifts
const timeSlotsByDay: Record<number, ExtendedTimeSlot[]> = {};
days.forEach(day => {
const shiftsForDay = shiftsByDay[day.id] || [];
const timeSlotIdsForDay = new Set(shiftsForDay.map(shift => shift.timeSlotId));
timeSlotsByDay[day.id] = shiftPlan.timeSlots
.filter(timeSlot => timeSlotIdsForDay.has(timeSlot.id))
.map(timeSlot => ({
...timeSlot,
displayName: `${timeSlot.name} (${formatTime(timeSlot.startTime)}-${formatTime(timeSlot.endTime)})`
}))
.sort((a, b) => a.startTime.localeCompare(b.startTime));
});
// Get all unique time slots across all days for row headers
const allTimeSlotIds = new Set<string>();
days.forEach(day => {
timeSlotsByDay[day.id]?.forEach(timeSlot => {
allTimeSlotIds.add(timeSlot.id);
});
});
const allTimeSlots = Array.from(allTimeSlotIds)
.map(timeSlotId => shiftPlan.timeSlots.find(ts => ts.id === timeSlotId))
.filter(Boolean)
.map(timeSlot => ({
...timeSlot!,
displayName: `${timeSlot!.name} (${formatTime(timeSlot!.startTime)}-${formatTime(timeSlot!.endTime)})`
}))
.sort((a, b) => a.startTime.localeCompare(b.startTime));
return { days, timeSlotsByDay, allTimeSlots };
};
const getDayOfWeek = (dateString: string): number => {
const date = new Date(dateString);
return date.getDay() === 0 ? 7 : date.getDay();
@@ -369,20 +357,31 @@ const ShiftPlanView: React.FC = () => {
try {
setPublishing(true);
setAssignmentResult(null); // Reset previous results
setShowAssignmentPreview(false); // Reset preview
console.log('🔄 STARTING ASSIGNMENT PREVIEW...');
// FORCE COMPLETE REFRESH - don't rely on cached state
const [refreshedEmployees, refreshedAvailabilities] = await Promise.all([
// Reload employees fresh
employeeService.getEmployees().then(emps => emps.filter(emp => emp.isActive)),
// Reload availabilities fresh
refreshAllAvailabilities()
]);
console.log('🔄 USING FRESH DATA:');
console.log('- Employees:', refreshedEmployees.length);
console.log('- Availabilities:', refreshedAvailabilities.length);
console.log('- Shift Patterns:', shiftPlan.shifts?.length || 0);
console.log('- Scheduled Shifts:', scheduledShifts.length);
// DEBUG: Show shift pattern IDs
if (shiftPlan.shifts) {
console.log('📋 SHIFT PATTERN IDs:');
shiftPlan.shifts.forEach((shift, index) => {
console.log(` ${index + 1}. ${shift.id} (Day ${shift.dayOfWeek}, TimeSlot ${shift.timeSlotId})`);
});
}
// ADD THIS: Define constraints object
const constraints = {
enforceNoTraineeAlone: true,
enforceExperiencedWithChef: true,
@@ -390,6 +389,8 @@ const ShiftPlanView: React.FC = () => {
targetEmployeesPerShift: 2
};
console.log('🧠 Calling shift assignment service...');
// Use the freshly loaded data, not the state
const result = await shiftAssignmentService.assignShifts(
shiftPlan,
@@ -398,7 +399,6 @@ const ShiftPlanView: React.FC = () => {
constraints
);
// COMPREHENSIVE DEBUGGING
console.log("🎯 RAW ASSIGNMENT RESULT FROM API:", {
success: result.success,
assignmentsCount: Object.keys(result.assignments).length,
@@ -407,31 +407,31 @@ const ShiftPlanView: React.FC = () => {
resolutionReport: result.resolutionReport?.length || 0
});
// Log the actual assignments with more context
// Log assignments with shift pattern context
console.log('🔍 ASSIGNMENTS BY SHIFT PATTERN:');
Object.entries(result.assignments).forEach(([shiftId, empIds]) => {
console.log(` 📅 Assignment Key: ${shiftId}`);
console.log(` Employees: ${empIds.join(', ')}`);
const shiftPattern = shiftPlan.shifts?.find(s => s.id === shiftId);
// Try to identify what type of ID this is
const isUuid = shiftId.length === 36; // UUID format
console.log(` Type: ${isUuid ? 'UUID (likely scheduled shift)' : 'Pattern (likely shift pattern)'}`);
// If it's a UUID, check if it matches any scheduled shift
if (isUuid) {
const matchingScheduledShift = scheduledShifts.find(s => s.id === shiftId);
if (matchingScheduledShift) {
console.log(` ✅ Matches scheduled shift: ${matchingScheduledShift.date} - TimeSlot: ${matchingScheduledShift.timeSlotId}`);
} else {
console.log(` ❌ No matching scheduled shift found for UUID`);
}
if (shiftPattern) {
console.log(` ✅ Shift Pattern: ${shiftId}`);
console.log(` - Day: ${shiftPattern.dayOfWeek}, TimeSlot: ${shiftPattern.timeSlotId}`);
console.log(` - Employees: ${empIds.join(', ')}`);
} else {
console.log(` ❌ UNKNOWN ID: ${shiftId}`);
console.log(` - Employees: ${empIds.join(', ')}`);
console.log(` - This ID does not match any shift pattern!`);
}
});
// CRITICAL: Update state and show preview
console.log('🔄 Setting assignment result and showing preview...');
setAssignmentResult(result);
setShowAssignmentPreview(true);
console.log('✅ Assignment preview ready, modal should be visible');
} catch (error) {
console.error('Error during assignment:', error);
console.error('Error during assignment:', error);
showNotification({
type: 'error',
title: 'Fehler',
@@ -458,13 +458,13 @@ const ShiftPlanView: React.FC = () => {
}
console.log(`📊 Found ${updatedShifts.length} scheduled shifts to update`);
console.log('🎯 Assignment keys from algorithm:', Object.keys(assignmentResult.assignments));
const updatePromises = updatedShifts.map(async (scheduledShift) => {
// ✅ FIX: Map scheduled shift to shift pattern to find assignments
const dayOfWeek = getDayOfWeek(scheduledShift.date);
// Find the corresponding shift pattern for this day and time slot
const shiftPattern = shiftPlan.shifts?.find(shift =>
const shiftPattern = shiftPlan?.shifts?.find(shift =>
shift.dayOfWeek === dayOfWeek &&
shift.timeSlotId === scheduledShift.timeSlotId
);
@@ -472,9 +472,13 @@ const ShiftPlanView: React.FC = () => {
let assignedEmployees: string[] = [];
if (shiftPattern) {
// Look for assignments using the shift pattern ID (what scheduler uses)
assignedEmployees = assignmentResult.assignments[shiftPattern.id] || [];
console.log(`📝 Updating scheduled shift ${scheduledShift.id} (Day ${dayOfWeek}, TimeSlot ${scheduledShift.timeSlotId}) with`, assignedEmployees, 'employees');
if (assignedEmployees.length === 0) {
console.warn(`⚠️ No assignments found for shift pattern ${shiftPattern.id}`);
console.log('🔍 Available assignment keys:', Object.keys(assignmentResult.assignments));
}
} else {
console.warn(`⚠️ No shift pattern found for scheduled shift ${scheduledShift.id} (Day ${dayOfWeek}, TimeSlot ${scheduledShift.timeSlotId})`);
}
@@ -509,14 +513,17 @@ const ShiftPlanView: React.FC = () => {
setShiftPlan(reloadedPlan);
setScheduledShifts(reloadedShifts);
setShowAssignmentPreview(false);
setAssignmentResult(null);
console.log('✅ Publishing completed, modal closed');
showNotification({
type: 'success',
title: 'Erfolg',
message: 'Schichtplan wurde erfolgreich veröffentlicht!'
});
setShowAssignmentPreview(false);
} catch (error) {
console.error('❌ Error publishing shift plan:', error);
@@ -576,37 +583,37 @@ const ShiftPlanView: React.FC = () => {
}
};
const validateSchedulingData = (): boolean => {
console.log('🔍 Validating scheduling data...');
const debugShiftMatching = () => {
if (!shiftPlan || !scheduledShifts.length) return;
const totalEmployees = employees.length;
const employeesWithAvailabilities = new Set(
availabilities.map(avail => avail.employeeId)
).size;
console.log('🔍 DEBUG: Shift Pattern to Scheduled Shift Matching');
console.log('==================================================');
const availabilityStatus = {
totalEmployees,
employeesWithAvailabilities,
coverage: Math.round((employeesWithAvailabilities / totalEmployees) * 100)
};
console.log('📊 Availability Coverage:', availabilityStatus);
// Check if we have ALL employee availabilities
if (employeesWithAvailabilities < totalEmployees) {
const missingEmployees = employees.filter(emp =>
!availabilities.some(avail => avail.employeeId === emp.id)
);
shiftPlan.shifts?.forEach(shiftPattern => {
const matchingScheduledShifts = scheduledShifts.filter(scheduled => {
const dayOfWeek = getDayOfWeek(scheduled.date);
return dayOfWeek === shiftPattern.dayOfWeek &&
scheduled.timeSlotId === shiftPattern.timeSlotId;
});
console.warn('⚠️ Missing availabilities for employees:',
missingEmployees.map(emp => emp.email));
console.log(`📅 Shift Pattern: ${shiftPattern.id}`);
console.log(` - Day: ${shiftPattern.dayOfWeek}, TimeSlot: ${shiftPattern.timeSlotId}`);
console.log(` - Matching scheduled shifts: ${matchingScheduledShifts.length}`);
return false;
}
return true;
if (assignmentResult) {
const assignments = assignmentResult.assignments[shiftPattern.id] || [];
console.log(` - Assignments: ${assignments.length} employees`);
}
});
};
// Rufe die Debug-Funktion auf, wenn Assignment-Ergebnisse geladen werden
useEffect(() => {
if (assignmentResult && shiftPlan) {
debugShiftMatching();
}
}, [assignmentResult, shiftPlan]);
const canPublish = () => {
if (!shiftPlan || shiftPlan.status === 'published') return false;
@@ -660,32 +667,33 @@ const ShiftPlanView: React.FC = () => {
const getAssignmentsForScheduledShift = (scheduledShift: ScheduledShift): string[] => {
if (!assignmentResult) return [];
// First try direct match with scheduled shift ID
if (assignmentResult.assignments[scheduledShift.id]) {
return assignmentResult.assignments[scheduledShift.id];
}
// If no direct match, try to find by day and timeSlot pattern
const dayOfWeek = getDayOfWeek(scheduledShift.date);
// Find the corresponding shift pattern for this day and time slot
const shiftPattern = shiftPlan?.shifts?.find(shift =>
shift.dayOfWeek === dayOfWeek &&
shift.timeSlotId === scheduledShift.timeSlotId
);
if (shiftPattern && assignmentResult.assignments[shiftPattern.id]) {
console.log(`✅ Found assignments for shift pattern ${shiftPattern.id}:`, assignmentResult.assignments[shiftPattern.id]);
return assignmentResult.assignments[shiftPattern.id];
}
// Fallback: Check if there's a direct match with scheduled shift ID (unlikely)
if (assignmentResult.assignments[scheduledShift.id]) {
console.log(`⚠️ Using direct scheduled shift assignment for ${scheduledShift.id}`);
return assignmentResult.assignments[scheduledShift.id];
}
console.warn(`❌ No assignments found for scheduled shift ${scheduledShift.id} (Day ${dayOfWeek}, TimeSlot ${scheduledShift.timeSlotId})`);
return [];
};
// Render timetable using the same structure as AvailabilityManager
const renderTimetable = () => {
const { days, allTimeSlots, timeSlotsByDay } = getTimetableData();
if (!shiftPlan?.id) {
console.warn("Shift plan ID is missing");
return null;
}
const { days, allTimeSlots } = getTimetableData();
const validation = validateTimetableStructure();
if (days.length === 0 || allTimeSlots.length === 0) {
return (
@@ -719,10 +727,39 @@ const ShiftPlanView: React.FC = () => {
}}>
Schichtplan
<div style={{ fontSize: '14px', fontWeight: 'normal', marginTop: '5px' }}>
{allTimeSlots.length} Schichttypen {days.length} Tage Nur tatsächlich im Plan verwendete Schichten
{allTimeSlots.length} Zeitslots {days.length} Tage Zeitbasierte Darstellung
</div>
</div>
{/* Validation Warnings - SAME AS AVAILABILITYMANAGER */}
{!validation.isValid && (
<div style={{
backgroundColor: '#fff3cd',
border: '1px solid #ffeaa7',
padding: '15px',
margin: '10px'
}}>
<h4 style={{ margin: '0 0 10px 0', color: '#856404' }}> Validierungswarnungen:</h4>
<ul style={{ margin: 0, paddingLeft: '20px', fontSize: '12px' }}>
{validation.errors.map((error, index) => (
<li key={index}>{error}</li>
))}
</ul>
</div>
)}
{/* Timetable Structure Info - SAME AS AVAILABILITYMANAGER */}
<div style={{
backgroundColor: '#d1ecf1',
border: '1px solid #bee5eb',
padding: '10px 15px',
margin: '10px',
borderRadius: '4px',
fontSize: '12px'
}}>
<strong>Struktur-Info:</strong> {allTimeSlots.length} Zeitslots × {days.length} Tage = {allTimeSlots.length * days.length} Zellen
</div>
<div style={{ overflowX: 'auto' }}>
<table style={{
width: '100%',
@@ -754,23 +791,32 @@ const ShiftPlanView: React.FC = () => {
</tr>
</thead>
<tbody>
{allTimeSlots.map((timeSlot, timeIndex) => (
{allTimeSlots.map((timeSlot, timeSlotIndex) => (
<tr key={timeSlot.id} style={{
backgroundColor: timeIndex % 2 === 0 ? 'white' : '#f8f9fa'
backgroundColor: timeSlotIndex % 2 === 0 ? 'white' : '#f8f9fa'
}}>
<td style={{
padding: '12px 16px',
border: '1px solid #dee2e6',
fontWeight: '500',
backgroundColor: '#f8f9fa'
backgroundColor: '#f8f9fa',
position: 'sticky',
left: 0
}}>
{timeSlot.displayName}
<div style={{ fontWeight: 'bold' }}>
{timeSlot.name}
</div>
<div style={{ fontSize: '14px', color: '#666' }}>
{formatTime(timeSlot.startTime)} - {formatTime(timeSlot.endTime)}
</div>
<div style={{ fontSize: '11px', color: '#999', marginTop: '4px' }}>
ID: {timeSlot.id.substring(0, 8)}...
</div>
</td>
{days.map(weekday => {
// Check if this time slot exists for this day
const timeSlotForDay = timeSlotsByDay[weekday.id]?.find(ts => ts.id === timeSlot.id);
const shift = timeSlot.shiftsByDay[weekday.id];
if (!timeSlotForDay) {
if (!shift) {
return (
<td key={weekday.id} style={{
padding: '12px 16px',
@@ -780,11 +826,14 @@ const ShiftPlanView: React.FC = () => {
color: '#ccc',
fontStyle: 'italic'
}}>
-
Kein Shift
</td>
);
}
// Validation: Check if shift has correct timeSlotId and dayOfWeek - SAME AS AVAILABILITYMANAGER
const isValidShift = shift.timeSlotId === timeSlot.id && shift.dayOfWeek === weekday.id;
let assignedEmployees: string[] = [];
let displayText = '';
@@ -806,7 +855,7 @@ const ShiftPlanView: React.FC = () => {
displayText = assignedEmployees.map(empId => {
const employee = employees.find(emp => emp.id === empId);
return employee ? employee.email : 'Unbekannt';
return employee ? `${employee.firstname} ${employee.lastname}` : 'Unbekannt';
}).join(', ');
}
} else if (assignmentResult) {
@@ -821,20 +870,20 @@ const ShiftPlanView: React.FC = () => {
assignedEmployees = getAssignmentsForScheduledShift(scheduledShift);
displayText = assignedEmployees.map(empId => {
const employee = employees.find(emp => emp.id === empId);
return employee ? employee.email : 'Unbekannt';
return employee ? `${employee.firstname} ${employee.lastname}` : 'Unbekannt';
}).join(', ');
}
}
// If no assignments yet, show empty or required count
if (!displayText) {
const shiftsForSlot = shiftPlan?.shifts?.filter(shift =>
shift.dayOfWeek === weekday.id &&
shift.timeSlotId === timeSlot.id
const shiftsForSlot = shiftPlan?.shifts?.filter(s =>
s.dayOfWeek === weekday.id &&
s.timeSlotId === timeSlot.id
) || [];
const totalRequired = shiftsForSlot.reduce((sum, shift) =>
sum + shift.requiredEmployees, 0);
const totalRequired = shiftsForSlot.reduce((sum, s) =>
sum + s.requiredEmployees, 0);
// Show "0/2" instead of just "0" to indicate it's empty
displayText = `0/${totalRequired}`;
@@ -850,11 +899,51 @@ const ShiftPlanView: React.FC = () => {
padding: '12px 16px',
border: '1px solid #dee2e6',
textAlign: 'center',
backgroundColor: assignedEmployees.length > 0 ? '#e8f5e8' : 'transparent',
backgroundColor: !isValidShift ? '#fff3cd' : (assignedEmployees.length > 0 ? '#e8f5e8' : 'transparent'),
color: assignedEmployees.length > 0 ? '#2c3e50' : '#666',
fontSize: assignedEmployees.length > 0 ? '14px' : 'inherit'
fontSize: assignedEmployees.length > 0 ? '14px' : 'inherit',
position: 'relative'
}}>
{/* Validation indicator - SAME AS AVAILABILITYMANAGER */}
{!isValidShift && (
<div style={{
position: 'absolute',
top: '2px',
right: '2px',
backgroundColor: '#f39c12',
color: 'white',
borderRadius: '50%',
width: '16px',
height: '16px',
fontSize: '10px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
title={`Shift Validierung: timeSlotId=${shift.timeSlotId}, dayOfWeek=${shift.dayOfWeek}`}
>
</div>
)}
{displayText}
{/* Shift debug info - SAME AS AVAILABILITYMANAGER */}
<div style={{
fontSize: '10px',
color: '#666',
marginTop: '4px',
textAlign: 'left',
fontFamily: 'monospace'
}}>
<div>Shift: {shift.id.substring(0, 6)}...</div>
<div>Day: {shift.dayOfWeek}</div>
{!isValidShift && (
<div style={{ color: '#e74c3c', fontWeight: 'bold' }}>
VALIDATION ERROR
</div>
)}
</div>
</td>
);
})}
@@ -863,6 +952,24 @@ const ShiftPlanView: React.FC = () => {
</tbody>
</table>
</div>
{/* Summary Statistics - SAME AS AVAILABILITYMANAGER */}
<div style={{
backgroundColor: '#f8f9fa',
padding: '15px',
borderTop: '1px solid #dee2e6',
fontSize: '12px',
color: '#666'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<strong>Zusammenfassung:</strong> {allTimeSlots.length} Zeitslots × {days.length} Tage = {allTimeSlots.length * days.length} mögliche Shifts
</div>
<div>
<strong>Validierungsfehler:</strong> {validation.errors.length}
</div>
</div>
</div>
</div>
);
};
@@ -872,7 +979,7 @@ const ShiftPlanView: React.FC = () => {
const { days, allTimeSlots } = getTimetableData();
const availabilityStatus = getAvailabilityStatus();
const validation = validateTimetableStructure();
return (
<div style={{ padding: '20px' }}>
@@ -938,6 +1045,50 @@ const ShiftPlanView: React.FC = () => {
</div>
</div>
{/* Debug Info - Enhanced */}
<div style={{
backgroundColor: validation.errors.length > 0 ? '#fff3cd' : (allTimeSlots.length === 0 ? '#f8d7da' : '#d1ecf1'),
border: `1px solid ${validation.errors.length > 0 ? '#ffeaa7' : (allTimeSlots.length === 0 ? '#f5c6cb' : '#bee5eb')}`,
borderRadius: '6px',
padding: '15px',
marginBottom: '20px'
}}>
<h4 style={{
margin: '0 0 10px 0',
color: validation.errors.length > 0 ? '#856404' : (allTimeSlots.length === 0 ? '#721c24' : '#0c5460')
}}>
{validation.errors.length > 0 ? '⚠️ VALIDIERUNGSPROBLEME' :
allTimeSlots.length === 0 ? '❌ KEINE SHIFTS GEFUNDEN' : '✅ PLAN-DATEN GELADEN'}
</h4>
<div style={{ fontSize: '12px', fontFamily: 'monospace' }}>
<div><strong>Ausgewählter Plan:</strong> {shiftPlan.name}</div>
<div><strong>Plan ID:</strong> {shiftPlan.id}</div>
<div><strong>Einzigartige Zeitslots:</strong> {allTimeSlots.length}</div>
<div><strong>Verwendete Tage:</strong> {days.length} ({days.map(d => d.name).join(', ')})</div>
<div><strong>Shift Patterns:</strong> {shiftPlan.shifts?.length || 0}</div>
<div><strong>Scheduled Shifts:</strong> {scheduledShifts.length}</div>
<div><strong>Geladene Verfügbarkeiten:</strong> {availabilities.length}</div>
<div><strong>Aktive Mitarbeiter:</strong> {employees.length}</div>
{assignmentResult && (
<div><strong>Assignment Keys:</strong> {Object.keys(assignmentResult.assignments).length}</div>
)}
</div>
{/* Show shift pattern vs scheduled shift matching */}
{shiftPlan.shifts && scheduledShifts.length > 0 && (
<div style={{ marginTop: '10px', paddingTop: '10px', borderTop: '1px solid #bee5eb' }}>
<strong>Shift Matching:</strong>
<div style={{ fontSize: '11px' }}>
{shiftPlan.shifts.length} Patterns {scheduledShifts.length} Scheduled Shifts
{assignmentResult && (
<div> {Object.keys(assignmentResult.assignments).length} Assignment Keys</div>
)}
</div>
</div>
)}
</div>
{/* Rest of the component remains the same... */}
{/* Availability Status - only show for drafts */}
{shiftPlan.status === 'draft' && (
<div style={{
@@ -1017,8 +1168,8 @@ const ShiftPlanView: React.FC = () => {
</div>
)}
{/* Assignment Preview Modal */}
{showAssignmentPreview && assignmentResult && (
{/* Assignment Preview Modal - FIXED CONDITION */}
{(showAssignmentPreview || assignmentResult) && (
<div style={{
position: 'fixed',
top: 0,
@@ -1037,12 +1188,13 @@ const ShiftPlanView: React.FC = () => {
padding: '30px',
maxWidth: '800px',
maxHeight: '80vh',
overflow: 'auto'
overflow: 'auto',
width: '90%'
}}>
<h2>Wochenmuster-Zuordnung</h2>
{/* Detaillierter Reparatur-Bericht anzeigen */}
{assignmentResult.resolutionReport && (
{assignmentResult?.resolutionReport && (
<div style={{
backgroundColor: '#f8f9fa',
border: '1px solid #e9ecef',
@@ -1170,7 +1322,10 @@ const ShiftPlanView: React.FC = () => {
<div style={{ display: 'flex', gap: '10px', justifyContent: 'flex-end' }}>
<button
onClick={() => setShowAssignmentPreview(false)}
onClick={() => {
setShowAssignmentPreview(false);
setAssignmentResult(null);
}}
style={{
padding: '8px 16px',
backgroundColor: '#95a5a6',
@@ -1183,38 +1338,41 @@ const ShiftPlanView: React.FC = () => {
Abbrechen
</button>
{/* KORRIGIERTER BUTTON MIT TYPESCRIPT-FIX */}
<button
onClick={handlePublish}
disabled={publishing || assignmentResult.violations.filter(v =>
disabled={publishing || (assignmentResult ? assignmentResult.violations.filter(v =>
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
).length > 0}
).length > 0 : true)}
style={{
padding: '10px 20px',
backgroundColor: assignmentResult.violations.filter(v =>
backgroundColor: assignmentResult ? (assignmentResult.violations.filter(v =>
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
).length === 0 ? '#2ecc71' : '#95a5a6',
).length === 0 ? '#2ecc71' : '#95a5a6') : '#95a5a6',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: assignmentResult.violations.filter(v =>
cursor: assignmentResult ? (assignmentResult.violations.filter(v =>
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
).length === 0 ? 'pointer' : 'not-allowed',
).length === 0 ? 'pointer' : 'not-allowed') : 'not-allowed',
fontWeight: 'bold',
fontSize: '16px'
}}
>
{publishing ? 'Veröffentliche...' : (
assignmentResult.violations.filter(v =>
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
).length === 0
? 'Schichtplan veröffentlichen'
: 'Kritische Probleme müssen behoben werden'
assignmentResult ? (
assignmentResult.violations.filter(v =>
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
).length === 0
? 'Schichtplan veröffentlichen'
: 'Kritische Probleme müssen behoben werden'
) : 'Lade Zuordnungen...'
)}
</button>
</div>
</div>
</div>
)}
)}
{/* Timetable */}
<div style={{

View File

@@ -130,6 +130,22 @@ export class EmployeeService {
throw new Error(error.error || 'Failed to change password');
}
}
async updateLastLogin(employeeId: string): Promise<void> {
try {
const response = await fetch(`${API_BASE_URL}/employees/${employeeId}/last-login`, {
method: 'PATCH',
headers: getAuthHeaders(),
});
if (!response.ok) {
throw new Error('Failed to update last login');
}
} catch (error) {
console.error('Error updating last login:', error);
throw error;
}
}
}
export const employeeService = new EmployeeService();