reworked scheduling

This commit is contained in:
2025-10-18 01:55:04 +02:00
parent b86040dc04
commit f705a83cd4
26 changed files with 3222 additions and 3193 deletions

View File

@@ -295,6 +295,8 @@ export const getAvailabilities = async (req: AuthRequest, res: Response): Promis
ORDER BY day_of_week, time_slot_id
`, [employeeId]);
//console.log('✅ Successfully got availabilities from employee:', availabilities);
res.json(availabilities.map(avail => ({
id: avail.id,
employeeId: avail.employee_id,
@@ -348,6 +350,9 @@ export const updateAvailabilities = async (req: AuthRequest, res: Response): Pro
await db.run('COMMIT');
console.log('✅ Successfully updated availablities employee:', );
// Return updated availabilities
const updatedAvailabilities = await db.all<any>(`
SELECT * FROM employee_availability
@@ -365,6 +370,8 @@ export const updateAvailabilities = async (req: AuthRequest, res: Response): Pro
notes: avail.notes
})));
console.log('✅ Successfully updated employee:', updateAvailabilities);
} catch (error) {
await db.run('ROLLBACK');
throw error;

View File

@@ -740,53 +740,6 @@ export const generateScheduledShiftsForPlan = async (req: Request, res: Response
}
};
export const revertToDraft = async (req: Request, res: Response): Promise<void> => {
try {
const { id } = req.params;
const userId = (req as AuthRequest).user?.userId;
if (!userId) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
// Check if plan exists
const existingPlan = await getShiftPlanById(id);
//const existingPlan: ShiftPlan = await db.get('SELECT * FROM shift_plans WHERE id = ?', [id]);
if (!existingPlan) {
res.status(404).json({ error: 'Shift plan not found' });
return;
}
// Only allow reverting from published to draft
if (existingPlan.status !== 'published') {
res.status(400).json({ error: 'Can only revert published plans to draft' });
return;
}
// Update plan status to draft
await db.run(
'UPDATE shift_plans SET status = ? WHERE id = ?',
['draft', id]
);
// Clear all assigned employees from scheduled shifts
await db.run(
'UPDATE scheduled_shifts SET assigned_employees = ? WHERE plan_id = ?',
[JSON.stringify([]), id]
);
console.log(`✅ Plan ${id} reverted to draft status`);
// Return updated plan
const updatedPlan = await getShiftPlanById(id);
res.json(updatedPlan);
} catch (error) {
console.error('Error reverting plan to draft:', error);
res.status(500).json({ error: 'Internal server error' });
}
};
export const regenerateScheduledShifts = async (req: Request, res: Response): Promise<void> => {
try {
@@ -921,4 +874,63 @@ export const updateScheduledShift = async (req: AuthRequest, res: Response): Pro
console.error('❌ Error updating scheduled shift:', error);
res.status(500).json({ error: 'Internal server error: ' + error.message });
}
};
export const clearAssignments = async (req: Request, res: Response): Promise<void> => {
try {
const { id } = req.params;
console.log('🔄 Clearing assignments for plan:', id);
// Check if plan exists
const existingPlan = await db.get('SELECT * FROM shift_plans WHERE id = ?', [id]);
if (!existingPlan) {
res.status(404).json({ error: 'Shift plan not found' });
return;
}
await db.run('BEGIN TRANSACTION');
try {
// Get all scheduled shifts for this plan
const scheduledShifts = await db.all<any>(
'SELECT id FROM scheduled_shifts WHERE plan_id = ?',
[id]
);
console.log(`📋 Found ${scheduledShifts.length} scheduled shifts to clear`);
// Clear all assignments (set assigned_employees to empty array)
for (const shift of scheduledShifts) {
await db.run(
'UPDATE scheduled_shifts SET assigned_employees = ? WHERE id = ?',
[JSON.stringify([]), shift.id]
);
console.log(`✅ Cleared assignments for shift: ${shift.id}`);
}
// Update plan status back to draft
await db.run(
'UPDATE shift_plans SET status = ? WHERE id = ?',
['draft', id]
);
await db.run('COMMIT');
console.log(`✅ Successfully cleared all assignments for plan ${id}`);
res.json({
message: 'Assignments cleared successfully',
clearedShifts: scheduledShifts.length
});
} catch (error) {
await db.run('ROLLBACK');
throw error;
}
} catch (error) {
console.error('❌ Error clearing assignments:', error);
res.status(500).json({ error: 'Internal server error' });
}
};

View File

@@ -18,7 +18,7 @@ const router = express.Router();
router.use(authMiddleware);
// Employee CRUD Routes
router.get('/', requireRole(['admin', 'instandhalter']), getEmployees);
router.get('/', authMiddleware, getEmployees);
router.get('/:id', requireRole(['admin', 'instandhalter']), getEmployee);
router.post('/', requireRole(['admin']), createEmployee);
router.put('/:id', requireRole(['admin']), updateEmployee);

View File

@@ -19,12 +19,12 @@ router.post('/:id/generate-shifts', requireRole(['admin', 'instandhalter']), gen
router.post('/:id/regenerate-shifts', requireRole(['admin', 'instandhalter']), regenerateScheduledShifts);
// GET all scheduled shifts for a plan
router.get('/plan/:planId', requireRole(['admin']), getScheduledShiftsFromPlan);
router.get('/plan/:planId', authMiddleware, getScheduledShiftsFromPlan);
// GET specific scheduled shift
router.get('/:id', requireRole(['admin']), getScheduledShift);
router.get('/:id', authMiddleware, getScheduledShift);
// UPDATE scheduled shift
router.put('/:id', requireRole(['admin']), updateScheduledShift);
router.put('/:id', authMiddleware, updateScheduledShift);
export default router;

View File

@@ -8,7 +8,7 @@ import {
updateShiftPlan,
deleteShiftPlan,
createFromPreset,
revertToDraft,
clearAssignments
} from '../controllers/shiftPlanController.js';
const router = express.Router();
@@ -18,13 +18,13 @@ router.use(authMiddleware);
// Combined routes for both shift plans and templates
// GET all shift plans (including templates)
router.get('/', getShiftPlans);
router.get('/' , authMiddleware, getShiftPlans);
// GET templates only
//router.get('/templates', getTemplates);
// GET specific shift plan or template
router.get('/:id', getShiftPlan);
router.get('/:id', authMiddleware, getShiftPlan);
// POST create new shift plan
router.post('/', requireRole(['admin', 'instandhalter']), createShiftPlan);
@@ -41,7 +41,7 @@ router.put('/:id', requireRole(['admin', 'instandhalter']), updateShiftPlan);
// DELETE shift plan or template
router.delete('/:id', requireRole(['admin', 'instandhalter']), deleteShiftPlan);
// PUT revert published plan to draft
router.put('/:id/revert-to-draft', requireRole(['admin', 'instandhalter']), revertToDraft);
// POST clear assignments and reset to draft
router.post('/:id/clear-assignments', requireRole(['admin', 'instandhalter']), clearAssignments);
export default router;

View File

@@ -5,7 +5,6 @@ import { AuthProvider, useAuth } from './contexts/AuthContext';
import { NotificationProvider } from './contexts/NotificationContext';
import NotificationContainer from './components/Notification/NotificationContainer';
import Layout from './components/Layout/Layout';
import { DesignSystemProvider, GlobalStyles } from './design/DesignSystem';
import Login from './pages/Auth/Login';
import Dashboard from './pages/Dashboard/Dashboard';
import ShiftPlanList from './pages/ShiftPlans/ShiftPlanList';
@@ -133,17 +132,14 @@ const AppContent: React.FC = () => {
function App() {
return (
<DesignSystemProvider>
<GlobalStyles />
<NotificationProvider>
<AuthProvider>
<Router>
<NotificationContainer />
<AppContent />
</Router>
</AuthProvider>
</NotificationProvider>
</DesignSystemProvider>
<NotificationProvider>
<AuthProvider>
<Router>
<NotificationContainer />
<AppContent />
</Router>
</AuthProvider>
</NotificationProvider>
);
}

View File

@@ -4,7 +4,7 @@ import React from 'react';
const Footer: React.FC = () => {
const styles = {
footer: {
background: 'linear-gradient(135deg, #1a1325 0%, #24163a 100%)',
background: 'linear-gradient(0deg, #161718 0%, #24163a 100%)',
color: 'white',
marginTop: 'auto',
borderTop: '1px solid rgba(251, 250, 246, 0.1)',
@@ -44,7 +44,7 @@ const Footer: React.FC = () => {
borderTop: '1px solid rgba(251, 250, 246, 0.1)',
padding: '1.5rem 2rem',
textAlign: 'center' as const,
color: 'rgba(251, 250, 246, 0.6)',
color: '#FBFAF6',
fontSize: '0.9rem',
},
};
@@ -201,8 +201,8 @@ const Footer: React.FC = () => {
<div style={styles.footerBottom}>
<p style={{margin: 0}}>
&copy; 2025 Schichtenplaner. Alle Rechte vorbehalten. |
Made with for efficient team management
&copy; 2025 Schichtenplaner |
Made with <span style={{ color: '#854eca' }}></span> for efficient team management
</p>
</div>
</footer>

View File

@@ -1,11 +1,9 @@
// frontend/src/design/DesignSystem.tsx
import React, { createContext, useContext, ReactNode } from 'react';
// Design Tokens
export const designTokens = {
colors: {
// Primary Colors
white: '#FBFAF6',
default_white: '#ddd', //boxes
white: '#FBFAF6', //background, fonts
black: '#161718',
// Purple Gradients
@@ -122,275 +120,4 @@ export const designTokens = {
xl: '1280px',
'2xl': '1536px',
},
} as const;
// Context for Design System
interface DesignSystemContextType {
tokens: typeof designTokens;
getColor: (path: string) => string;
getSpacing: (size: keyof typeof designTokens.spacing) => string;
}
const DesignSystemContext = createContext<DesignSystemContextType | undefined>(undefined);
// Design System Provider
interface DesignSystemProviderProps {
children: ReactNode;
}
export const DesignSystemProvider: React.FC<DesignSystemProviderProps> = ({ children }) => {
const getColor = (path: string): string => {
const parts = path.split('.');
let current: any = designTokens.colors;
for (const part of parts) {
if (current[part] === undefined) {
console.warn(`Color path "${path}" not found in design tokens`);
return designTokens.colors.primary;
}
current = current[part];
}
return current;
};
const getSpacing = (size: keyof typeof designTokens.spacing): string => {
return designTokens.spacing[size];
};
const value: DesignSystemContextType = {
tokens: designTokens,
getColor,
getSpacing,
};
return (
<DesignSystemContext.Provider value={value}>
{children}
</DesignSystemContext.Provider>
);
};
// Hook to use Design System
export const useDesignSystem = (): DesignSystemContextType => {
const context = useContext(DesignSystemContext);
if (context === undefined) {
throw new Error('useDesignSystem must be used within a DesignSystemProvider');
}
return context;
};
// Utility Components
export interface BoxProps {
children?: ReactNode;
className?: string;
style?: React.CSSProperties;
p?: keyof typeof designTokens.spacing;
px?: keyof typeof designTokens.spacing;
py?: keyof typeof designTokens.spacing;
m?: keyof typeof designTokens.spacing;
mx?: keyof typeof designTokens.spacing;
my?: keyof typeof designTokens.spacing;
bg?: string;
color?: string;
borderRadius?: keyof typeof designTokens.borderRadius;
}
export const Box: React.FC<BoxProps> = ({
children,
className,
style,
p,
px,
py,
m,
mx,
my,
bg,
color,
borderRadius,
...props
}) => {
const { tokens, getColor } = useDesignSystem();
const boxStyle: React.CSSProperties = {
padding: p && tokens.spacing[p],
paddingLeft: px && tokens.spacing[px],
paddingRight: px && tokens.spacing[px],
paddingTop: py && tokens.spacing[py],
paddingBottom: py && tokens.spacing[py],
margin: m && tokens.spacing[m],
marginLeft: mx && tokens.spacing[mx],
marginRight: mx && tokens.spacing[mx],
marginTop: my && tokens.spacing[my],
marginBottom: my && tokens.spacing[my],
backgroundColor: bg && getColor(bg),
color: color && getColor(color),
borderRadius: borderRadius && tokens.borderRadius[borderRadius],
fontFamily: tokens.typography.fontFamily,
...style,
};
return (
<div className={className} style={boxStyle} {...props}>
{children}
</div>
);
};
export interface TextProps {
children: ReactNode;
className?: string;
style?: React.CSSProperties;
variant?: 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
weight?: keyof typeof designTokens.typography.fontWeights;
color?: string;
align?: 'left' | 'center' | 'right' | 'justify';
lineHeight?: keyof typeof designTokens.typography.lineHeights;
letterSpacing?: keyof typeof designTokens.typography.letterSpacing;
}
export const Text: React.FC<TextProps> = ({
children,
className,
style,
variant = 'base',
weight = 'normal',
color = 'text.primary',
align = 'left',
lineHeight = 'normal',
letterSpacing = 'normal',
...props
}) => {
const { tokens, getColor } = useDesignSystem();
const textStyle: React.CSSProperties = {
fontSize: tokens.typography.fontSizes[variant],
fontWeight: tokens.typography.fontWeights[weight],
color: getColor(color),
textAlign: align,
lineHeight: tokens.typography.lineHeights[lineHeight],
letterSpacing: tokens.typography.letterSpacing[letterSpacing],
fontFamily: tokens.typography.fontFamily,
...style,
};
return (
<span className={className} style={textStyle} {...props}>
{children}
</span>
);
};
// Global Styles Component
export const GlobalStyles: React.FC = () => {
const { tokens } = useDesignSystem();
const globalStyles = `
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-family: ${tokens.typography.fontFamily};
font-size: 16px;
line-height: ${tokens.typography.lineHeights.normal};
color: ${tokens.colors.text.primary};
background-color: ${tokens.colors.background};
}
body {
font-family: ${tokens.typography.fontFamily};
font-weight: ${tokens.typography.fontWeights.normal};
line-height: ${tokens.typography.lineHeights.normal};
color: ${tokens.colors.text.primary};
background-color: ${tokens.colors.background};
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3, h4, h5, h6 {
font-family: ${tokens.typography.fontFamily};
font-weight: ${tokens.typography.fontWeights.bold};
line-height: ${tokens.typography.lineHeights.tight};
color: ${tokens.colors.text.primary};
}
h1 {
font-size: ${tokens.typography.fontSizes['4xl']};
letter-spacing: ${tokens.typography.letterSpacing.tight};
}
h2 {
font-size: ${tokens.typography.fontSizes['3xl']};
letter-spacing: ${tokens.typography.letterSpacing.tight};
}
h3 {
font-size: ${tokens.typography.fontSizes['2xl']};
}
h4 {
font-size: ${tokens.typography.fontSizes.xl};
}
h5 {
font-size: ${tokens.typography.fontSizes.lg};
}
h6 {
font-size: ${tokens.typography.fontSizes.base};
}
p {
font-size: ${tokens.typography.fontSizes.base};
line-height: ${tokens.typography.lineHeights.relaxed};
color: ${tokens.colors.text.primary};
}
a {
color: ${tokens.colors.primary};
text-decoration: none;
transition: ${tokens.transitions.default};
}
a:hover {
color: ${tokens.colors.secondary};
}
button {
font-family: ${tokens.typography.fontFamily};
transition: ${tokens.transitions.default};
}
input, textarea, select {
font-family: ${tokens.typography.fontFamily};
}
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: ${tokens.colors.background};
}
::-webkit-scrollbar-thumb {
background: ${tokens.colors.border.medium};
border-radius: ${tokens.borderRadius.full};
}
::-webkit-scrollbar-thumb:hover {
background: ${tokens.colors.border.dark};
}
`;
return <style>{globalStyles}</style>;
};
export default designTokens;
} as const;

View File

@@ -1,4 +0,0 @@
// frontend/src/design/index.ts
export { designTokens } from './DesignSystem';
export { DesignSystemProvider, useDesignSystem, GlobalStyles, Box, Text } from './DesignSystem';
export type { BoxProps, TextProps } from './DesignSystem';

View File

@@ -282,12 +282,7 @@ const Dashboard: React.FC = () => {
percentage
};
};
// Add refresh functionality
const handleRefresh = () => {
loadDashboardData();
};
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '40px' }}>
@@ -370,8 +365,6 @@ const Dashboard: React.FC = () => {
</div>
</div>
<PlanDebugInfo />
{/* Quick Actions - Nur für Admins/Instandhalter */}
{hasRole(['admin', 'instandhalter']) && (
<div style={{ marginBottom: '30px' }}>

View File

@@ -437,6 +437,8 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
await employeeService.updateAvailabilities(employee.id, requestData);
console.log('✅ VERFÜGBARKEITEN ERFOLGREICH GESPEICHERT');
window.dispatchEvent(new CustomEvent('availabilitiesChanged'));
onSave();
} catch (err: any) {

View File

@@ -1,4 +1,4 @@
// frontend/src/pages/Employees/components/EmployeeList.tsx - KORRIGIERT
// frontend/src/pages/Employees/components/EmployeeList.tsx
import React, { useState } from 'react';
import { ROLE_CONFIG, EMPLOYEE_TYPE_CONFIG } from '../../../models/defaults/employeeDefaults';
import { Employee } from '../../../models/Employee';
@@ -11,6 +11,9 @@ interface EmployeeListProps {
onManageAvailability: (employee: Employee) => void;
}
type SortField = 'name' | 'employeeType' | 'canWorkAlone' | 'role' | 'lastLogin';
type SortDirection = 'asc' | 'desc';
const EmployeeList: React.FC<EmployeeListProps> = ({
employees,
onEdit,
@@ -19,8 +22,11 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
}) => {
const [filter, setFilter] = useState<'all' | 'active' | 'inactive'>('active');
const [searchTerm, setSearchTerm] = useState('');
const [sortField, setSortField] = useState<SortField>('name');
const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
const { user: currentUser, hasRole } = useAuth();
// Filter employees based on active/inactive and search term
const filteredEmployees = employees.filter(employee => {
if (filter === 'active' && !employee.isActive) return false;
if (filter === 'inactive' && employee.isActive) return false;
@@ -38,6 +44,60 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
return true;
});
// Sort employees based on selected field and direction
const sortedEmployees = [...filteredEmployees].sort((a, b) => {
let aValue: any;
let bValue: any;
switch (sortField) {
case 'name':
aValue = a.name.toLowerCase();
bValue = b.name.toLowerCase();
break;
case 'employeeType':
aValue = a.employeeType;
bValue = b.employeeType;
break;
case 'canWorkAlone':
aValue = a.canWorkAlone;
bValue = b.canWorkAlone;
break;
case 'role':
aValue = a.role;
bValue = b.role;
break;
case 'lastLogin':
// Handle null values for lastLogin (put them at the end)
aValue = a.lastLogin ? new Date(a.lastLogin).getTime() : 0;
bValue = b.lastLogin ? new Date(b.lastLogin).getTime() : 0;
break;
default:
return 0;
}
if (sortDirection === 'asc') {
return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
} else {
return aValue > bValue ? -1 : aValue < bValue ? 1 : 0;
}
});
const handleSort = (field: SortField) => {
if (sortField === field) {
// Toggle direction if same field
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
// New field, default to ascending
setSortField(field);
setSortDirection('asc');
}
};
const getSortIndicator = (field: SortField) => {
if (sortField !== field) return '↕';
return sortDirection === 'asc' ? '↑' : '↓';
};
// Simplified permission checks
const canDeleteEmployee = (employee: Employee): boolean => {
if (!hasRole(['admin'])) return false;
@@ -78,8 +138,8 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
const getIndependenceBadge = (canWorkAlone: boolean) => {
return canWorkAlone
? { text: 'Eigenständig', color: '#27ae60', bgColor: '#d5f4e6' }
: { text: 'Betreuung', color: '#e74c3c', bgColor: '#fadbd8' };
? { text: 'Eigenständig', color: '#27ae60', bgColor: '#d5f4e6' }
: { text: 'Betreuung', color: '#e74c3c', bgColor: '#fadbd8' };
};
type Role = typeof ROLE_CONFIG[number]['value'];
@@ -161,7 +221,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
</div>
<div style={{ color: '#7f8c8d', fontSize: '14px' }}>
{filteredEmployees.length} von {employees.length} Mitarbeitern
{sortedEmployees.length} von {employees.length} Mitarbeitern
</div>
</div>
@@ -185,16 +245,41 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
color: '#2c3e50',
alignItems: 'center'
}}>
<div>Name & E-Mail</div>
<div style={{ textAlign: 'center' }}>Typ</div>
<div style={{ textAlign: 'center' }}>Eigenständigkeit</div>
<div style={{ textAlign: 'center' }}>Rolle</div>
<div
onClick={() => handleSort('name')}
style={{ cursor: 'pointer', userSelect: 'none', display: 'flex', alignItems: 'center', gap: '5px' }}
>
Name & E-Mail {getSortIndicator('name')}
</div>
<div
onClick={() => handleSort('employeeType')}
style={{ textAlign: 'center', cursor: 'pointer', userSelect: 'none', display: 'flex', alignItems: 'center', gap: '5px', justifyContent: 'center' }}
>
Typ {getSortIndicator('employeeType')}
</div>
<div
onClick={() => handleSort('canWorkAlone')}
style={{ textAlign: 'center', cursor: 'pointer', userSelect: 'none', display: 'flex', alignItems: 'center', gap: '5px', justifyContent: 'center' }}
>
Eigenständigkeit {getSortIndicator('canWorkAlone')}
</div>
<div
onClick={() => handleSort('role')}
style={{ textAlign: 'center', cursor: 'pointer', userSelect: 'none', display: 'flex', alignItems: 'center', gap: '5px', justifyContent: 'center' }}
>
Rolle {getSortIndicator('role')}
</div>
<div style={{ textAlign: 'center' }}>Status</div>
<div style={{ textAlign: 'center' }}>Letzter Login</div>
<div
onClick={() => handleSort('lastLogin')}
style={{ textAlign: 'center', cursor: 'pointer', userSelect: 'none', display: 'flex', alignItems: 'center', gap: '5px', justifyContent: 'center' }}
>
Letzter Login {getSortIndicator('lastLogin')}
</div>
<div style={{ textAlign: 'center' }}>Aktionen</div>
</div>
{filteredEmployees.map(employee => {
{sortedEmployees.map(employee => {
const employeeType = getEmployeeTypeBadge(employee.employeeType);
const independence = getIndependenceBadge(employee.canWorkAlone);
const roleColor = getRoleBadge(employee.role);

View File

@@ -206,102 +206,7 @@ const Help: React.FC = () => {
))}
</div>
</div>
{/* Network Visualization */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: '15px',
marginTop: '30px'
}}>
{/* Employees */}
<div style={{
backgroundColor: '#e8f4fd',
padding: '15px',
borderRadius: '8px',
border: '1px solid #b8d4f0'
}}>
<h4 style={{ margin: '0 0 10px 0', color: '#3498db' }}>👥 Mitarbeiter</h4>
<div style={{ fontSize: '14px', lineHeight: '1.4' }}>
<div> Manager (1)</div>
<div> Erfahrene ({currentStage >= 1 ? '3' : '0'})</div>
<div> Neue ({currentStage >= 1 ? '2' : '0'})</div>
</div>
</div>
{/* Shifts */}
<div style={{
backgroundColor: '#fff3cd',
padding: '15px',
borderRadius: '8px',
border: '1px solid #ffeaa7'
}}>
<h4 style={{ margin: '0 0 10px 0', color: '#f39c12' }}>📅 Schichten</h4>
<div style={{ fontSize: '14px', lineHeight: '1.4' }}>
<div> Vormittag (5)</div>
<div> Nachmittag (4)</div>
<div> Manager-Schichten (3)</div>
</div>
</div>
{/* Current Actions */}
<div style={{
backgroundColor: '#d4edda',
padding: '15px',
borderRadius: '8px',
border: '1px solid #c3e6cb'
}}>
<h4 style={{ margin: '0 0 10px 0', color: '#27ae60' }}> Aktive Aktionen</h4>
<div style={{ fontSize: '14px', lineHeight: '1.4' }}>
{currentStage === 0 && (
<>
<div> Grundzuweisung läuft</div>
<div> Erfahrene priorisieren</div>
</>
)}
{currentStage === 1 && (
<>
<div> Manager wird zugewiesen</div>
<div> Erfahrene suchen</div>
</>
)}
{currentStage === 2 && (
<>
<div> Überbesetzung prüfen</div>
<div> Pool-Verwaltung aktiv</div>
</>
)}
{currentStage === 3 && (
<>
<div> Finale Validierung</div>
<div> Bericht generieren</div>
</>
)}
</div>
</div>
{/* Problems & Solutions */}
<div style={{
backgroundColor: '#f8d7da',
padding: '15px',
borderRadius: '8px',
border: '1px solid #f5c6cb'
}}>
<h4 style={{ margin: '0 0 10px 0', color: '#e74c3c' }}>🔍 Probleme & Lösungen</h4>
<div style={{ fontSize: '14px', lineHeight: '1.4' }}>
{currentStage >= 2 ? (
<>
<div style={{ color: '#27ae60' }}> 2 Probleme behoben</div>
<div style={{ color: '#e74c3c' }}> 0 kritische Probleme</div>
<div style={{ color: '#f39c12' }}> 1 Warnung</div>
</>
) : (
<div>Noch keine Probleme analysiert</div>
)}
</div>
</div>
</div>
</div>
{/* Business Rules */}
<div style={{
@@ -312,7 +217,7 @@ const Help: React.FC = () => {
boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
border: '1px solid #e0e0e0'
}}>
<h2 style={{ color: '#2c3e50', marginBottom: '20px' }}>📋 Geschäftsregeln</h2>
<h2 style={{ color: '#2c3e50', marginBottom: '20px' }}>📋 Validierungs Regeln</h2>
<div style={{ display: 'grid', gap: '10px' }}>
{businessRules.map((rule, index) => (
<div
@@ -383,7 +288,10 @@ const Help: React.FC = () => {
</div>
</div>
<div style={{
</div>
<div style={{
marginTop: '25px',
padding: '20px',
backgroundColor: '#e8f4fd',
@@ -398,7 +306,6 @@ const Help: React.FC = () => {
<li>Planen Sie Manager-Verfügbarkeit im Voraus</li>
</ul>
</div>
</div>
<style>{`
@keyframes pulse {

View File

@@ -5,6 +5,7 @@ import { employeeService } from '../../services/employeeService';
import { useNotification } from '../../contexts/NotificationContext';
import AvailabilityManager from '../Employees/components/AvailabilityManager';
import { Employee } from '../../models/Employee';
import { styles } from './type/SettingsType';
const Settings: React.FC = () => {
const { user: currentUser, updateUser } = useAuth();
@@ -148,7 +149,12 @@ const Settings: React.FC = () => {
};
if (!currentUser) {
return <div>Nicht eingeloggt</div>;
return <div style={{
textAlign: 'center',
padding: '3rem',
color: '#666',
fontSize: '1.1rem'
}}>Nicht eingeloggt</div>;
}
if (showAvailabilityManager) {
@@ -161,395 +167,390 @@ const Settings: React.FC = () => {
);
}
// Style constants for consistency
return (
<div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
<h1> Einstellungen</h1>
{/* Tab Navigation */}
<div style={{
display: 'flex',
borderBottom: '1px solid #e0e0e0',
marginBottom: '30px'
}}>
<button
onClick={() => setActiveTab('profile')}
style={{
padding: '12px 24px',
backgroundColor: activeTab === 'profile' ? '#3498db' : 'transparent',
color: activeTab === 'profile' ? 'white' : '#333',
border: 'none',
borderBottom: activeTab === 'profile' ? '3px solid #3498db' : 'none',
cursor: 'pointer',
fontWeight: 'bold'
}}
>
👤 Profil
</button>
<button
onClick={() => setActiveTab('password')}
style={{
padding: '12px 24px',
backgroundColor: activeTab === 'password' ? '#3498db' : 'transparent',
color: activeTab === 'password' ? 'white' : '#333',
border: 'none',
borderBottom: activeTab === 'password' ? '3px solid #3498db' : 'none',
cursor: 'pointer',
fontWeight: 'bold'
}}
>
🔒 Passwort
</button>
<button
onClick={() => setActiveTab('availability')}
style={{
padding: '12px 24px',
backgroundColor: activeTab === 'availability' ? '#3498db' : 'transparent',
color: activeTab === 'availability' ? 'white' : '#333',
border: 'none',
borderBottom: activeTab === 'availability' ? '3px solid #3498db' : 'none',
cursor: 'pointer',
fontWeight: 'bold'
}}
>
📅 Verfügbarkeit
</button>
<div style={styles.container}>
{/* Left Sidebar with Tabs */}
<div style={styles.sidebar}>
<div style={styles.header}>
<h1 style={styles.title}>Einstellungen</h1>
<div style={styles.subtitle}>Verwalten Sie Ihre Kontoeinstellungen und Präferenzen</div>
</div>
<div style={styles.tabs}>
<button
onClick={() => setActiveTab('profile')}
style={{
...styles.tab,
...(activeTab === 'profile' ? styles.tabActive : {})
}}
onMouseEnter={(e) => {
if (activeTab !== 'profile') {
e.currentTarget.style.background = styles.tabHover.background;
e.currentTarget.style.color = styles.tabHover.color;
e.currentTarget.style.transform = styles.tabHover.transform;
}
}}
onMouseLeave={(e) => {
if (activeTab !== 'profile') {
e.currentTarget.style.background = styles.tab.background;
e.currentTarget.style.color = styles.tab.color;
e.currentTarget.style.transform = 'none';
}
}}
>
<span style={{ color: '#cda8f0', fontSize: '24px' }}>{'\u{1F464}\u{FE0E}'}</span>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
<span style={{ fontSize: '0.95rem', fontWeight: 500 }}>Profil</span>
<span style={{ fontSize: '0.8rem', opacity: 0.7, marginTop: '2px' }}>Persönliche Informationen</span>
</div>
</button>
<button
onClick={() => setActiveTab('password')}
style={{
...styles.tab,
...(activeTab === 'password' ? styles.tabActive : {})
}}
onMouseEnter={(e) => {
if (activeTab !== 'password') {
e.currentTarget.style.background = styles.tabHover.background;
e.currentTarget.style.color = styles.tabHover.color;
e.currentTarget.style.transform = styles.tabHover.transform;
}
}}
onMouseLeave={(e) => {
if (activeTab !== 'password') {
e.currentTarget.style.background = styles.tab.background;
e.currentTarget.style.color = styles.tab.color;
e.currentTarget.style.transform = 'none';
}
}}
>
<span style={{ fontSize: '1.2rem', width: '24px' }}>🔒</span>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
<span style={{ fontSize: '0.95rem', fontWeight: 500 }}>Passwort</span>
<span style={{ fontSize: '0.8rem', opacity: 0.7, marginTop: '2px' }}>Sicherheitseinstellungen</span>
</div>
</button>
<button
onClick={() => setActiveTab('availability')}
style={{
...styles.tab,
...(activeTab === 'availability' ? styles.tabActive : {})
}}
onMouseEnter={(e) => {
if (activeTab !== 'availability') {
e.currentTarget.style.background = styles.tabHover.background;
e.currentTarget.style.color = styles.tabHover.color;
e.currentTarget.style.transform = styles.tabHover.transform;
}
}}
onMouseLeave={(e) => {
if (activeTab !== 'availability') {
e.currentTarget.style.background = styles.tab.background;
e.currentTarget.style.color = styles.tab.color;
e.currentTarget.style.transform = 'none';
}
}}
>
<span style={{ fontSize: '1.2rem', width: '24px' }}>📅</span>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
<span style={{ fontSize: '0.95rem', fontWeight: 500 }}>Verfügbarkeit</span>
<span style={{ fontSize: '0.8rem', opacity: 0.7, marginTop: '2px' }}>Schichtplanung</span>
</div>
</button>
</div>
</div>
{/* Profile Tab */}
{activeTab === 'profile' && (
<div style={{
backgroundColor: 'white',
padding: '30px',
borderRadius: '8px',
border: '1px solid #e0e0e0',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
<h2 style={{ marginTop: 0, color: '#2c3e50' }}>Profilinformationen</h2>
<form onSubmit={handleProfileUpdate}>
<div style={{ display: 'grid', gap: '20px' }}>
{/* Read-only information */}
<div style={{
padding: '15px',
backgroundColor: '#f8f9fa',
borderRadius: '6px',
border: '1px solid #e9ecef'
}}>
<h4 style={{ margin: '0 0 15px 0', color: '#495057' }}>Systeminformationen</h4>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '15px' }}>
<div>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}>
E-Mail
</label>
<input
type="email"
value={currentUser.email}
disabled
style={{
width: '95%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
backgroundColor: '#f8f9fa',
color: '#666'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}>
Rolle
</label>
<input
type="text"
value={currentUser.role}
disabled
style={{
width: '95%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
backgroundColor: '#f8f9fa',
color: '#666'
}}
/>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '15px', marginTop: '15px' }}>
<div>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}>
Mitarbeiter Typ
</label>
<input
type="text"
value={currentUser.employeeType}
disabled
style={{
width: '95%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
backgroundColor: '#f8f9fa',
color: '#666'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}>
Vertragstyp
</label>
<input
type="text"
value={currentUser.contractType}
disabled
style={{
width: '95%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
backgroundColor: '#f8f9fa',
color: '#666'
}}
/>
</div>
</div>
</div>
{/* Right Content Area */}
<div style={styles.content}>
{/* Profile Tab */}
{activeTab === 'profile' && (
<>
<div style={styles.section}>
<h2 style={styles.sectionTitle}>Profilinformationen</h2>
<p style={styles.sectionDescription}>
Verwalten Sie Ihre persönlichen Informationen und Kontaktdaten
</p>
</div>
<div
style={{
marginTop: '10px',
padding: '15px',
backgroundColor: '#f8f9fa',
borderRadius: '6px',
border: '1px solid #e9ecef',
}}
>
<label
style={{
display: 'block',
marginBottom: '8px',
fontWeight: 'bold',
color: '#2c3e50',
}}
>
Vollständiger Name *
</label>
<input
type="text"
name="name"
value={profileForm.name}
onChange={handleProfileChange}
required
style={{
width: '97.5%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px',
}}
placeholder="Ihr vollständiger Name"
/>
</div>
<div style={{
display: 'flex',
gap: '15px',
justifyContent: 'flex-end',
marginTop: '30px',
paddingTop: '20px',
borderTop: '1px solid #f0f0f0'
}}>
<button
type="submit"
disabled={loading || !profileForm.name.trim()}
style={{
padding: '12px 24px',
backgroundColor: loading ? '#bdc3c7' : (!profileForm.name.trim() ? '#95a5a6' : '#27ae60'),
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: (loading || !profileForm.name.trim()) ? 'not-allowed' : 'pointer',
fontWeight: 'bold'
}}
>
{loading ? '⏳ Wird gespeichert...' : 'Profil aktualisieren'}
</button>
</div>
</form>
</div>
)}
{/* Password Tab */}
{activeTab === 'password' && (
<div style={{
backgroundColor: 'white',
padding: '30px',
borderRadius: '8px',
border: '1px solid #e0e0e0',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
<h2 style={{ marginTop: 0, color: '#2c3e50' }}>Passwort ändern</h2>
<form onSubmit={handlePasswordUpdate}>
<div style={{ display: 'grid', gap: '20px', maxWidth: '400px' }}>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
Aktuelles Passwort *
</label>
<input
type="password"
name="currentPassword"
value={passwordForm.currentPassword}
onChange={handlePasswordChange}
required
style={{
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px'
}}
placeholder="Aktuelles Passwort"
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
Neues Passwort *
</label>
<input
type="password"
name="newPassword"
value={passwordForm.newPassword}
onChange={handlePasswordChange}
required
minLength={6}
style={{
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px'
}}
placeholder="Mindestens 6 Zeichen"
/>
<div style={{ fontSize: '12px', color: '#7f8c8d', marginTop: '5px' }}>
Das Passwort muss mindestens 6 Zeichen lang sein.
</div>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
Neues Passwort bestätigen *
</label>
<input
type="password"
name="confirmPassword"
value={passwordForm.confirmPassword}
onChange={handlePasswordChange}
required
style={{
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px'
}}
placeholder="Passwort wiederholen"
/>
</div>
</div>
<div style={{
display: 'flex',
gap: '15px',
justifyContent: 'flex-end',
marginTop: '30px',
paddingTop: '20px',
borderTop: '1px solid #f0f0f0'
}}>
<button
type="submit"
disabled={loading || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword}
style={{
padding: '12px 24px',
backgroundColor: loading ? '#bdc3c7' : (!passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword ? '#95a5a6' : '#3498db'),
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: (loading || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword) ? 'not-allowed' : 'pointer',
fontWeight: 'bold'
}}
>
{loading ? '⏳ Wird geändert...' : 'Passwort ändern'}
</button>
</div>
</form>
</div>
)}
{/* Availability Tab */}
{activeTab === 'availability' && (
<div style={{
backgroundColor: 'white',
padding: '30px',
borderRadius: '8px',
border: '1px solid #e0e0e0',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
<h2 style={{ marginTop: 0, color: '#2c3e50' }}>Meine Verfügbarkeit</h2>
<div style={{
padding: '30px',
textAlign: 'center',
backgroundColor: '#f8f9fa',
borderRadius: '8px',
border: '2px dashed #dee2e6'
}}>
<div style={{ fontSize: '48px', marginBottom: '20px' }}>📅</div>
<h3 style={{ color: '#2c3e50' }}>Verfügbarkeit verwalten</h3>
<p style={{ color: '#6c757d', marginBottom: '25px' }}>
Hier können Sie Ihre persönliche Verfügbarkeit für Schichtpläne festlegen.
Legen Sie für jeden Tag und jede Schicht fest, ob Sie bevorzugt, möglicherweise
oder nicht verfügbar sind.
</p>
<button
onClick={() => setShowAvailabilityManager(true)}
style={{
padding: '12px 24px',
backgroundColor: '#3498db',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontWeight: 'bold',
fontSize: '16px'
}}
>
Verfügbarkeit bearbeiten
</button>
<form onSubmit={handleProfileUpdate} style={{ marginTop: '2rem' }}>
<div style={styles.formGrid}>
{/* Read-only information */}
<div style={styles.infoCard}>
<h4 style={styles.infoCardTitle}>Systeminformationen</h4>
<div style={styles.infoGrid}>
<div style={styles.field}>
<label style={styles.fieldLabel}>
E-Mail
</label>
<input
type="email"
value={currentUser.email}
disabled
style={styles.fieldInputDisabled}
/>
</div>
<div style={styles.field}>
<label style={styles.fieldLabel}>
Rolle
</label>
<input
type="text"
value={currentUser.role}
disabled
style={styles.fieldInputDisabled}
/>
</div>
<div style={styles.field}>
<label style={styles.fieldLabel}>
Mitarbeiter Typ
</label>
<input
type="text"
value={currentUser.employeeType}
disabled
style={styles.fieldInputDisabled}
/>
</div>
<div style={styles.field}>
<label style={styles.fieldLabel}>
Vertragstyp
</label>
<input
type="text"
value={currentUser.contractType}
disabled
style={styles.fieldInputDisabled}
/>
</div>
</div>
</div>
<div style={styles.infoCard}>
{/* Editable name field */}
<div style={{ ...styles.field, marginTop: '1rem' }}>
<label style={styles.fieldLabel}>
Vollständiger Name *
</label>
<input
type="text"
name="name"
value={profileForm.name}
onChange={handleProfileChange}
required
style={{
...styles.fieldInput,
width: '95%'
}}
placeholder="Ihr vollständiger Name"
onFocus={(e) => {
e.target.style.borderColor = '#1a1325';
e.target.style.boxShadow = '0 0 0 3px rgba(26, 19, 37, 0.1)';
}}
onBlur={(e) => {
e.target.style.borderColor = '#e8e8e8';
e.target.style.boxShadow = 'none';
}}
/>
</div>
</div>
</div>
<div style={{
marginTop: '20px',
padding: '15px',
backgroundColor: '#e8f4fd',
border: '1px solid #b6d7e8',
borderRadius: '6px',
fontSize: '14px',
color: '#2c3e50',
textAlign: 'left'
}}>
<strong>💡 Informationen:</strong>
<ul style={{ margin: '8px 0 0 20px', padding: 0 }}>
<li><strong>Bevorzugt:</strong> Sie möchten diese Schicht arbeiten</li>
<li><strong>Möglich:</strong> Sie können diese Schicht arbeiten</li>
<li><strong>Nicht möglich:</strong> Sie können diese Schicht nicht arbeiten</li>
</ul>
<div style={styles.actions}>
<button
type="submit"
disabled={loading || !profileForm.name.trim()}
style={{
...styles.button,
...styles.buttonPrimary,
...((loading || !profileForm.name.trim()) ? styles.buttonDisabled : {})
}}
onMouseEnter={(e) => {
if (!loading && profileForm.name.trim()) {
e.currentTarget.style.background = styles.buttonPrimaryHover.background;
e.currentTarget.style.transform = styles.buttonPrimaryHover.transform;
e.currentTarget.style.boxShadow = styles.buttonPrimaryHover.boxShadow;
}
}}
onMouseLeave={(e) => {
if (!loading && profileForm.name.trim()) {
e.currentTarget.style.background = styles.buttonPrimary.background;
e.currentTarget.style.transform = 'none';
e.currentTarget.style.boxShadow = styles.buttonPrimary.boxShadow;
}
}}
>
{loading ? '⏳ Wird gespeichert...' : 'Profil aktualisieren'}
</button>
</div>
</form>
</>
)}
{/* Password Tab */}
{activeTab === 'password' && (
<>
<div style={styles.section}>
<h2 style={styles.sectionTitle}>Passwort ändern</h2>
<p style={styles.sectionDescription}>
Aktualisieren Sie Ihr Passwort für erhöhte Sicherheit
</p>
</div>
</div>
</div>
)}
<form onSubmit={handlePasswordUpdate} style={{ marginTop: '2rem' }}>
<div style={styles.formGridCompact}>
<div style={styles.field}>
<label style={styles.fieldLabel}>
Aktuelles Passwort *
</label>
<input
type="password"
name="currentPassword"
value={passwordForm.currentPassword}
onChange={handlePasswordChange}
required
style={styles.fieldInput}
placeholder="Aktuelles Passwort"
onFocus={(e) => {
e.target.style.borderColor = '#1a1325';
e.target.style.boxShadow = '0 0 0 3px rgba(26, 19, 37, 0.1)';
}}
onBlur={(e) => {
e.target.style.borderColor = '#e8e8e8';
e.target.style.boxShadow = 'none';
}}
/>
</div>
<div style={styles.field}>
<label style={styles.fieldLabel}>
Neues Passwort *
</label>
<input
type="password"
name="newPassword"
value={passwordForm.newPassword}
onChange={handlePasswordChange}
required
minLength={6}
style={styles.fieldInput}
placeholder="Mindestens 6 Zeichen"
onFocus={(e) => {
e.target.style.borderColor = '#1a1325';
e.target.style.boxShadow = '0 0 0 3px rgba(26, 19, 37, 0.1)';
}}
onBlur={(e) => {
e.target.style.borderColor = '#e8e8e8';
e.target.style.boxShadow = 'none';
}}
/>
<div style={styles.fieldHint}>
Das Passwort muss mindestens 6 Zeichen lang sein.
</div>
</div>
<div style={styles.field}>
<label style={styles.fieldLabel}>
Neues Passwort bestätigen *
</label>
<input
type="password"
name="confirmPassword"
value={passwordForm.confirmPassword}
onChange={handlePasswordChange}
required
style={styles.fieldInput}
placeholder="Passwort wiederholen"
onFocus={(e) => {
e.target.style.borderColor = '#1a1325';
e.target.style.boxShadow = '0 0 0 3px rgba(26, 19, 37, 0.1)';
}}
onBlur={(e) => {
e.target.style.borderColor = '#e8e8e8';
e.target.style.boxShadow = 'none';
}}
/>
</div>
</div>
<div style={styles.actions}>
<button
type="submit"
disabled={loading || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword}
style={{
...styles.button,
...styles.buttonPrimary,
...((loading || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword) ? styles.buttonDisabled : {})
}}
onMouseEnter={(e) => {
if (!loading && passwordForm.currentPassword && passwordForm.newPassword && passwordForm.confirmPassword) {
e.currentTarget.style.background = styles.buttonPrimaryHover.background;
e.currentTarget.style.transform = styles.buttonPrimaryHover.transform;
e.currentTarget.style.boxShadow = styles.buttonPrimaryHover.boxShadow;
}
}}
onMouseLeave={(e) => {
if (!loading && passwordForm.currentPassword && passwordForm.newPassword && passwordForm.confirmPassword) {
e.currentTarget.style.background = styles.buttonPrimary.background;
e.currentTarget.style.transform = 'none';
e.currentTarget.style.boxShadow = styles.buttonPrimary.boxShadow;
}
}}
>
{loading ? '⏳ Wird geändert...' : 'Passwort ändern'}
</button>
</div>
</form>
</>
)}
{/* Availability Tab */}
{activeTab === 'availability' && (
<>
<div style={styles.section}>
<h2 style={styles.sectionTitle}>Meine Verfügbarkeit</h2>
<p style={styles.sectionDescription}>
Legen Sie Ihre persönliche Verfügbarkeit für Schichtpläne fest
</p>
</div>
<div style={styles.availabilityCard}>
<div style={styles.availabilityIcon}>📅</div>
<h3 style={styles.availabilityTitle}>Verfügbarkeit verwalten</h3>
<p style={styles.availabilityDescription}>
Hier können Sie Ihre persönliche Verfügbarkeit für Schichtpläne festlegen.
Legen Sie für jeden Tag und jede Schicht fest, ob Sie bevorzugt, möglicherweise
oder nicht verfügbar sind.
</p>
<button
onClick={() => setShowAvailabilityManager(true)}
style={{
...styles.button,
...styles.buttonPrimary,
marginBottom: '2rem'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = styles.buttonPrimaryHover.background;
e.currentTarget.style.transform = styles.buttonPrimaryHover.transform;
e.currentTarget.style.boxShadow = styles.buttonPrimaryHover.boxShadow;
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = styles.buttonPrimary.background;
e.currentTarget.style.transform = 'none';
e.currentTarget.style.boxShadow = styles.buttonPrimary.boxShadow;
}}
>
Verfügbarkeit bearbeiten
</button>
</div>
</>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,236 @@
export const styles = {
container: {
display: 'flex',
minHeight: 'calc(100vh - 120px)',
background: '#FBFAF6',
padding: '2rem',
maxWidth: '1200px',
margin: '0 auto',
gap: '2rem',
},
sidebar: {
width: '280px',
background: '#FBFAF6',
borderRadius: '16px',
border: '1px solid rgba(255, 255, 255, 0.8)',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.06), 0 1px 4px rgba(0, 0, 0, 0.04)',
padding: '1.5rem',
height: 'fit-content',
position: 'sticky' as const,
top: '2rem',
},
header: {
marginBottom: '2rem',
paddingBottom: '1.5rem',
borderBottom: '1px solid rgba(26, 19, 37, 0.1)',
},
title: {
fontSize: '1.5rem',
fontWeight: 600,
color: '#161718',
margin: '0 0 0.5rem 0',
},
subtitle: {
fontSize: '0.95rem',
color: '#666',
fontWeight: 400,
lineHeight: 1.5,
},
tabs: {
display: 'flex',
flexDirection: 'column' as const,
gap: '0.5rem',
},
tab: {
display: 'flex',
alignItems: 'center',
gap: '1rem',
padding: '1rem 1.25rem',
background: 'transparent',
color: '#666',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
fontWeight: 500,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
textAlign: 'left' as const,
width: '100%',
},
tabActive: {
background: '#51258f',
color: '#FBFAF6',
boxShadow: '0 4px 12px rgba(26, 19, 37, 0.15)',
},
tabHover: {
background: 'rgba(81, 37, 143, 0.1)',
color: '#1a1325',
transform: 'translateX(4px)',
},
content: {
flex: 1,
background: '#FBFAF6',
padding: '2.5rem',
borderRadius: '16px',
border: '1px solid rgba(255, 255, 255, 0.8)',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.06), 0 1px 4px rgba(0, 0, 0, 0.04)',
backdropFilter: 'blur(10px)',
minHeight: '200px',
},
section: {
marginBottom: '2rem',
},
sectionTitle: {
fontSize: '1.75rem',
fontWeight: 600,
color: '#161718',
margin: '0 0 0.5rem 0',
},
sectionDescription: {
color: '#666',
fontSize: '1rem',
margin: 0,
lineHeight: 1.5,
},
formGrid: {
display: 'grid',
gap: '1.5rem',
},
formGridCompact: {
display: 'grid',
gap: '1.5rem',
maxWidth: '500px',
},
infoCard: {
padding: '1.5rem',
background: 'rgba(26, 19, 37, 0.02)',
borderRadius: '12px',
border: '1px solid rgba(26, 19, 37, 0.1)',
},
infoCardTitle: {
fontSize: '1rem',
fontWeight: 600,
color: '#1a1325',
margin: '0 0 1rem 0',
},
infoGrid: {
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '1rem',
},
field: {
display: 'flex',
flexDirection: 'column' as const,
gap: '0.5rem',
},
fieldLabel: {
fontSize: '0.9rem',
fontWeight: 600,
color: '#161718',
},
fieldInput: {
padding: '0.875rem 1rem',
border: '1.5px solid #e8e8e8',
borderRadius: '8px',
fontSize: '0.95rem',
background: '#FBFAF6',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
color: '#161718',
},
fieldInputDisabled: {
padding: '0.875rem 1rem',
border: '1.5px solid rgba(26, 19, 37, 0.1)',
borderRadius: '8px',
fontSize: '0.95rem',
background: 'rgba(26, 19, 37, 0.05)',
color: '#666',
cursor: 'not-allowed',
},
fieldHint: {
fontSize: '0.8rem',
color: '#888',
marginTop: '0.25rem',
},
actions: {
display: 'flex',
justifyContent: 'flex-end',
marginTop: '2.5rem',
paddingTop: '1.5rem',
borderTop: '1px solid rgba(26, 19, 37, 0.1)',
},
button: {
padding: '0.875rem 2rem',
border: 'none',
borderRadius: '8px',
fontSize: '0.95rem',
fontWeight: 600,
cursor: 'pointer',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
position: 'relative' as const,
overflow: 'hidden' as const,
},
buttonPrimary: {
background: '#1a1325',
color: '#FBFAF6',
boxShadow: '0 2px 8px rgba(26, 19, 37, 0.2)',
},
buttonPrimaryHover: {
background: '#24163a',
transform: 'translateY(-1px)',
boxShadow: '0 4px 16px rgba(26, 19, 37, 0.3)',
},
buttonDisabled: {
background: '#ccc',
color: '#666',
cursor: 'not-allowed',
transform: 'none',
boxShadow: 'none',
},
availabilityCard: {
padding: '3rem 2rem',
textAlign: 'center' as const,
background: 'rgba(26, 19, 37, 0.03)',
borderRadius: '16px',
border: '2px dashed rgba(26, 19, 37, 0.1)',
backdropFilter: 'blur(10px)',
},
availabilityIcon: {
fontSize: '3rem',
marginBottom: '1.5rem',
opacity: 0.8,
},
availabilityTitle: {
fontSize: '1.5rem',
fontWeight: 600,
color: '#161718',
margin: '0 0 1rem 0',
},
availabilityDescription: {
color: '#666',
marginBottom: '2rem',
lineHeight: 1.6,
maxWidth: '500px',
marginLeft: 'auto',
marginRight: 'auto',
},
infoHint: {
padding: '1.25rem',
background: 'rgba(26, 19, 37, 0.05)',
border: '1px solid rgba(26, 19, 37, 0.1)',
borderRadius: '12px',
fontSize: '0.9rem',
color: '#161718',
textAlign: 'left' as const,
maxWidth: '400px',
margin: '0 auto',
},
infoList: {
margin: '0.75rem 0 0 1rem',
padding: 0,
listStyle: 'none',
},
infoListItem: {
marginBottom: '0.5rem',
position: 'relative' as const,
paddingLeft: '1rem',
},
};

View File

@@ -5,12 +5,11 @@ import { useAuth } from '../../contexts/AuthContext';
import { shiftPlanService } from '../../services/shiftPlanService';
import { employeeService } from '../../services/employeeService';
import { shiftAssignmentService, ShiftAssignmentService } from '../../services/shiftAssignmentService';
import { AssignmentResult } from '../../services/scheduling';
import { IntelligentShiftScheduler, SchedulingResult, AssignmentResult } from '../../services/scheduling';
import { ShiftPlan, TimeSlot, ScheduledShift } from '../../models/ShiftPlan';
import { Employee, EmployeeAvailability } from '../../models/Employee';
import { useNotification } from '../../contexts/NotificationContext';
import { formatDate, formatTime } from '../../utils/foramatters';
import { isScheduledShift } from '../../models/helpers';
// Local interface extensions (same as AvailabilityManager)
interface ExtendedTimeSlot extends TimeSlot {
@@ -36,125 +35,301 @@ 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);
const [assignmentResult, setAssignmentResult] = useState<AssignmentResult | null>(null); // Add this line
const [loading, setLoading] = useState(true);
const [publishing, setPublishing] = useState(false);
const [scheduledShifts, setScheduledShifts] = useState<ScheduledShift[]>([]);
const [reverting, setReverting] = useState(false);
const [showAssignmentPreview, setShowAssignmentPreview] = useState(false);
const [recreating, setRecreating] = useState(false);
useEffect(() => {
loadShiftPlanData();
debugScheduledShifts();
// Event Listener für Verfügbarkeits-Änderungen
const handleAvailabilityChange = () => {
console.log('📢 Verfügbarkeiten wurden geändert - lade Daten neu...');
reloadAvailabilities();
};
// Globales Event für Verfügbarkeits-Änderungen
window.addEventListener('availabilitiesChanged', handleAvailabilityChange);
return () => {
window.removeEventListener('availabilitiesChanged', handleAvailabilityChange);
};
}, [id]);
const loadShiftPlanData = async () => {
if (!id) return;
useEffect(() => {
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
// Seite ist wieder sichtbar - Daten neu laden
console.log('🔄 Seite ist wieder sichtbar - lade Daten neu...');
reloadAvailabilities();
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, []);
useEffect(() => {
(window as any).debugRenderLogic = debugRenderLogic;
return () => { (window as any).debugRenderLogic = undefined; };
}, [shiftPlan, scheduledShifts]);
const loadShiftPlanData = async () => {
if (!id) return;
try {
setLoading(true);
// Load plan and employees first
const [plan, employeesData] = await Promise.all([
shiftPlanService.getShiftPlan(id),
employeeService.getEmployees(),
]);
setShiftPlan(plan);
setEmployees(employeesData.filter(emp => emp.isActive));
// CRITICAL: Load scheduled shifts and verify they exist
const shiftsData = await shiftAssignmentService.getScheduledShiftsForPlan(id);
console.log('📋 Loaded scheduled shifts:', shiftsData.length);
if (shiftsData.length === 0) {
console.warn('⚠️ No scheduled shifts found for plan:', id);
showNotification({
type: 'warning',
title: 'Keine Schichten gefunden',
message: 'Der Schichtplan hat keine generierten Schichten. Bitte überprüfen Sie die Plan-Konfiguration.'
});
}
setScheduledShifts(shiftsData);
// Load availabilities
const availabilityPromises = employeesData
.filter(emp => emp.isActive)
.map(emp => employeeService.getAvailabilities(emp.id));
const allAvailabilities = await Promise.all(availabilityPromises);
const flattenedAvailabilities = allAvailabilities.flat();
const planAvailabilities = flattenedAvailabilities.filter(
availability => availability.planId === id
);
setAvailabilities(planAvailabilities);
} catch (error) {
console.error('Error loading shift plan data:', error);
showNotification({
type: 'error',
title: 'Fehler',
message: 'Daten konnten nicht geladen werden.'
});
} finally {
setLoading(false);
}
};
const handleRecreateAssignments = async () => {
if (!shiftPlan) return;
try {
setLoading(true);
const [plan, employeesData, shiftsData] = await Promise.all([
shiftPlanService.getShiftPlan(id),
employeeService.getEmployees(),
shiftAssignmentService.getScheduledShiftsForPlan(id) // Load shifts here
]);
setRecreating(true);
if (!window.confirm('Möchten Sie die aktuellen Zuweisungen wirklich zurücksetzen? Alle vorhandenen Zuweisungen werden gelöscht.')) {
return;
}
setShiftPlan(plan);
setEmployees(employeesData.filter(emp => emp.isActive));
setScheduledShifts(shiftsData);
console.log('🔄 STARTING COMPLETE ASSIGNMENT CLEARING PROCESS');
// Load availabilities for all employees
const availabilityPromises = employeesData
.filter(emp => emp.isActive)
.map(emp => employeeService.getAvailabilities(emp.id));
// STEP 1: Get current scheduled shifts
const currentScheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
console.log(`📋 Found ${currentScheduledShifts.length} shifts to clear`);
// STEP 2: Clear ALL assignments by setting empty arrays
const clearPromises = currentScheduledShifts.map(async (scheduledShift) => {
console.log(`🗑️ Clearing assignments for shift: ${scheduledShift.id}`);
await shiftAssignmentService.updateScheduledShift(scheduledShift.id, {
assignedEmployees: [] // EMPTY ARRAY - this clears the assignments
});
});
await Promise.all(clearPromises);
console.log('✅ All assignments cleared from database');
// STEP 3: Update plan status to draft
await shiftPlanService.updateShiftPlan(shiftPlan.id, {
status: 'draft'
});
console.log('📝 Plan status set to draft');
// STEP 4: CRITICAL - Force reload of scheduled shifts to get EMPTY assignments
const refreshedShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
setScheduledShifts(refreshedShifts); // Update state with EMPTY assignments
const allAvailabilities = await Promise.all(availabilityPromises);
const flattenedAvailabilities = allAvailabilities.flat();
// Filter availabilities to only include those for the current shift plan
const planAvailabilities = flattenedAvailabilities.filter(
availability => availability.planId === id
);
setAvailabilities(planAvailabilities);
debugAvailabilities();
// STEP 5: Clear any previous assignment results
setAssignmentResult(null);
setShowAssignmentPreview(false);
// STEP 6: Force complete data refresh
await loadShiftPlanData();
console.log('🎯 ASSIGNMENT CLEARING COMPLETE - Table should now be empty');
showNotification({
type: 'success',
title: 'Zuweisungen gelöscht',
message: 'Alle Zuweisungen wurden erfolgreich gelöscht. Die Tabelle sollte jetzt leer sein.'
});
} catch (error) {
console.error('Error loading shift plan data:', error);
console.error('Error clearing assignments:', error);
showNotification({
type: 'error',
title: 'Fehler',
message: 'Daten konnten nicht geladen werden.'
message: `Löschen der Zuweisungen fehlgeschlagen: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}`
});
} finally {
setLoading(false);
setRecreating(false);
}
};
const debugAvailabilities = () => {
if (!shiftPlan || !employees.length || !availabilities.length) return;
console.log('🔍 AVAILABILITY ANALYSIS:', {
totalAvailabilities: availabilities.length,
employeesWithAvailabilities: new Set(availabilities.map(a => a.employeeId)).size,
totalEmployees: employees.length,
availabilityByEmployee: employees.map(emp => {
const empAvailabilities = availabilities.filter(a => a.employeeId === emp.id);
return {
employee: emp.name,
availabilities: empAvailabilities.length,
preferences: empAvailabilities.map(a => ({
day: a.dayOfWeek,
timeSlot: a.timeSlotId,
preference: a.preferenceLevel
}))
};
})
});
// Prüfe spezifisch für Manager/Admin
const manager = employees.find(emp => emp.role === 'admin');
if (manager) {
const managerAvailabilities = availabilities.filter(a => a.employeeId === manager.id);
console.log('🔍 MANAGER AVAILABILITIES:', {
manager: manager.name,
availabilities: managerAvailabilities.length,
details: managerAvailabilities.map(a => ({
day: a.dayOfWeek,
timeSlot: a.timeSlotId,
preference: a.preferenceLevel
}))
});
}
};
const debugScheduledShifts = async () => {
const debugRenderLogic = () => {
if (!shiftPlan) return;
try {
const shifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
console.log('🔍 SCHEDULED SHIFTS IN DATABASE:', {
total: shifts.length,
shifts: shifts.map(s => ({
id: s.id,
date: s.date,
timeSlotId: s.timeSlotId,
requiredEmployees: s.requiredEmployees
}))
console.log('🔍 RENDER LOGIC DEBUG:');
console.log('=====================');
const { days, allTimeSlots, timeSlotsByDay } = getExtendedTimetableData();
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++;
}
});
// Check if we have any shifts at all
if (shifts.length === 0) {
console.error('❌ NO SCHEDULED SHIFTS IN DATABASE - This is the problem!');
console.log('💡 Solution: Regenerate scheduled shifts for this plan');
});
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`);
});
}
} catch (error) {
console.error('❌ Error loading scheduled shifts:', error);
}
};
const getExtendedTimetableData = () => {
if (!shiftPlan || !shiftPlan.timeSlots) {
return { days: [], timeSlotsByDay: {}, allTimeSlots: [] };
}
// Verwende alle Tage die tatsächlich in scheduledShifts vorkommen
const allDaysInScheduledShifts = [...new Set(scheduledShifts.map(s => getDayOfWeek(s.date)))].sort();
const days = allDaysInScheduledShifts.map(dayId => {
return weekdays.find(day => day.id === dayId) || { id: dayId, name: `Tag ${dayId}` };
});
// Verwende alle TimeSlots die tatsächlich in scheduledShifts vorkommen
const allTimeSlotIdsInScheduledShifts = [...new Set(scheduledShifts.map(s => s.timeSlotId))];
const allTimeSlots = allTimeSlotIdsInScheduledShifts
.map(id => shiftPlan.timeSlots?.find(ts => ts.id === id))
.filter(Boolean)
.map(timeSlot => ({
...timeSlot!,
displayName: `${timeSlot!.name} (${formatTime(timeSlot!.startTime)}-${formatTime(timeSlot!.endTime)})`
}))
.sort((a, b) => a.startTime.localeCompare(b.startTime));
// TimeSlots pro Tag
const timeSlotsByDay: Record<number, ExtendedTimeSlot[]> = {};
days.forEach(day => {
const timeSlotIdsForDay = new Set(
scheduledShifts
.filter(shift => getDayOfWeek(shift.date) === day.id)
.map(shift => shift.timeSlotId)
);
timeSlotsByDay[day.id] = allTimeSlots
.filter(timeSlot => timeSlotIdsForDay.has(timeSlot.id))
.sort((a, b) => a.startTime.localeCompare(b.startTime));
});
/*console.log('🔄 Extended timetable data:', {
days: days.length,
timeSlots: allTimeSlots.length,
totalScheduledShifts: scheduledShifts.length
});*/
return { days, timeSlotsByDay, allTimeSlots };
};
// Extract plan-specific shifts using the same logic as AvailabilityManager
const getTimetableData = () => {
const getTimetableData = () => {
if (!shiftPlan || !shiftPlan.shifts || !shiftPlan.timeSlots) {
return { days: [], timeSlotsByDay: {}, allTimeSlots: [] };
}
@@ -216,174 +391,46 @@ const ShiftPlanView: React.FC = () => {
return date.getDay() === 0 ? 7 : date.getDay();
};
/*const debugManagerAvailability = () => {
if (!shiftPlan || !employees.length || !availabilities.length) return;
const manager = employees.find(emp => emp.role === 'admin');
if (!manager) {
console.log('❌ Kein Manager (admin) gefunden');
return;
}
console.log('🔍 Manager-Analyse:', {
manager: manager.name,
managerId: manager.id,
totalAvailabilities: availabilities.length,
managerAvailabilities: availabilities.filter(a => a.employeeId === manager.id).length
});
// Prüfe speziell die leeren Manager-Schichten
const emptyManagerShifts = [
'a8ef4ce0-adfd-4ec3-8c58-efa0f7347f9f',
'a496a8d6-f7a0-4d77-96de-c165379378c4',
'ea2d73d1-8354-4833-8c87-40f318ce8be0',
'90eb5454-2ae2-4445-86b7-a6e0e2cf0b22'
];
emptyManagerShifts.forEach(shiftId => {
const scheduledShift = shiftPlan.scheduledShifts?.find(s => s.id === shiftId);
if (scheduledShift) {
const dayOfWeek = getDayOfWeek(scheduledShift.date);
const shiftKey = `${dayOfWeek}-${scheduledShift.timeSlotId}`;
const managerAvailability = availabilities.find(a =>
a.employeeId === manager.id &&
a.dayOfWeek === dayOfWeek &&
a.timeSlotId === scheduledShift.timeSlotId
);
console.log(`📊 Schicht ${shiftId}:`, {
date: scheduledShift.date,
dayOfWeek,
timeSlotId: scheduledShift.timeSlotId,
shiftKey,
managerAvailability: managerAvailability ? managerAvailability.preferenceLevel : 'NICHT GEFUNDEN',
status: managerAvailability ?
(managerAvailability.preferenceLevel === 3 ? '❌ NICHT VERFÜGBAR' : '✅ VERFÜGBAR') :
'❌ KEINE VERFÜGBARKEITSDATEN'
});
}
});
};*/
const debugAssignments = async () => {
if (!shiftPlan) return;
try {
const shifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
console.log('🔍 DEBUG - Scheduled Shifts nach Veröffentlichung:', {
totalShifts: shifts.length,
shiftsWithAssignments: shifts.filter(s => s.assignedEmployees && s.assignedEmployees.length > 0).length,
allShifts: shifts.map(s => ({
id: s.id,
date: s.date,
timeSlotId: s.timeSlotId,
assignedEmployees: s.assignedEmployees,
assignedCount: s.assignedEmployees?.length || 0
}))
});
} catch (error) {
console.error('Debug error:', error);
}
};
const handlePreviewAssignment = async () => {
if (!shiftPlan) return;
debugScheduledShifts();
try {
setPublishing(true);
// DEBUG: Überprüfe die Eingabedaten
console.log('🔍 INPUT DATA FOR SCHEDULING:', {
shiftPlan: {
id: shiftPlan.id,
name: shiftPlan.name,
shifts: shiftPlan.shifts?.length,
timeSlots: shiftPlan.timeSlots?.length
},
employees: employees.length,
availabilities: availabilities.length,
employeeDetails: employees.map(emp => ({
id: emp.id,
name: emp.name,
role: emp.role,
employeeType: emp.employeeType,
canWorkAlone: emp.canWorkAlone
}))
});
// 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);
// DEBUG: Verify we have new data
debugSchedulingInput(refreshedEmployees, refreshedAvailabilities);
// ADD THIS: Define constraints object
const constraints = {
enforceNoTraineeAlone: true,
enforceExperiencedWithChef: true,
maxRepairAttempts: 50,
targetEmployeesPerShift: 2
};
// Use the freshly loaded data, not the state
const result = await ShiftAssignmentService.assignShifts(
shiftPlan,
employees,
availabilities,
{
enforceExperiencedWithChef: true,
enforceNoTraineeAlone: true,
maxRepairAttempts: 50
}
refreshedEmployees, // Use fresh array, not state
refreshedAvailabilities, // Use fresh array, not state
constraints // Now this variable is defined
);
// DEBUG: Detaillierte Analyse des Results
console.log('🔍 DETAILED ASSIGNMENT RESULT:', {
totalAssignments: Object.keys(result.assignments).length,
assignments: result.assignments,
violations: result.violations,
hasResolutionReport: !!result.resolutionReport,
assignmentDetails: Object.entries(result.assignments).map(([shiftId, empIds]) => ({
shiftId,
employeeCount: empIds.length,
employees: empIds
}))
});
// DEBUG: Überprüfe die tatsächlichen Violations
console.log('🔍 VIOLATIONS ANALYSIS:', {
allViolations: result.violations,
criticalViolations: result.violations.filter(v =>
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
),
warningViolations: result.violations.filter(v =>
v.includes('WARNING:') || v.includes('⚠️')
),
infoViolations: result.violations.filter(v =>
v.includes('INFO:')
),
criticalCount: result.violations.filter(v =>
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
).length,
canPublish: result.violations.filter(v =>
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
).length === 0
});
setAssignmentResult(result);
setShowAssignmentPreview(true);
// Zeige Reparatur-Bericht in der Konsole
if (result.resolutionReport) {
console.log('🔧 Reparatur-Bericht:');
result.resolutionReport.forEach(line => console.log(line));
}
// Entscheidung basierend auf tatsächlichen kritischen Violations
const criticalCount = result.violations.filter(v =>
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
).length;
if (criticalCount === 0) {
showNotification({
type: 'success',
title: 'Erfolg',
message: 'Alle kritischen Probleme wurden behoben! Der Schichtplan kann veröffentlicht werden.'
});
} else {
showNotification({
type: 'error',
title: 'Kritische Probleme',
message: `${criticalCount} kritische Probleme müssen behoben werden`
});
}
} catch (error) {
console.error('Error during assignment:', error);
showNotification({
@@ -396,6 +443,57 @@ const ShiftPlanView: React.FC = () => {
}
};
const debugSchedulingInput = (employees: Employee[], availabilities: EmployeeAvailability[]) => {
console.log('🔍 DEBUG SCHEDULING INPUT:');
console.log('==========================');
// Check if we have the latest data
console.log('📊 Employee Count:', employees.length);
console.log('📊 Availability Count:', availabilities.length);
// Log each employee's availability
employees.forEach(emp => {
const empAvailabilities = availabilities.filter(avail => avail.employeeId === emp.id);
console.log(`👤 ${emp.name} (${emp.role}, ${emp.employeeType}): ${empAvailabilities.length} availabilities`);
if (empAvailabilities.length > 0) {
empAvailabilities.forEach(avail => {
console.log(` - Day ${avail.dayOfWeek}, TimeSlot ${avail.timeSlotId}: Level ${avail.preferenceLevel}`);
});
} else {
console.log(` ❌ NO AVAILABILITIES SET!`);
}
});
// REMOVED: The problematic code that tries to access shiftPlan.employees
// We don't have old employee data stored in shiftPlan
console.log('🔄 All employees are considered "changed" since we loaded fresh data');
};
const forceRefreshData = async () => {
if (!id) return;
try {
const [plan, employeesData, shiftsData] = await Promise.all([
shiftPlanService.getShiftPlan(id),
employeeService.getEmployees(),
shiftAssignmentService.getScheduledShiftsForPlan(id)
]);
setShiftPlan(plan);
setEmployees(employeesData.filter(emp => emp.isActive));
setScheduledShifts(shiftsData);
// Force refresh availabilities
await refreshAllAvailabilities();
console.log('✅ All data force-refreshed');
} catch (error) {
console.error('Error force-refreshing data:', error);
}
};
const handlePublish = async () => {
if (!shiftPlan || !assignmentResult) return;
@@ -418,15 +516,18 @@ const ShiftPlanView: React.FC = () => {
const updatePromises = updatedShifts.map(async (scheduledShift) => {
const assignedEmployees = assignmentResult.assignments[scheduledShift.id] || [];
console.log(`📝 Updating shift ${scheduledShift.id} with`, assignedEmployees, 'employees');
//console.log(`📝 Updating shift ${scheduledShift.id} with`, assignedEmployees, 'employees');
try {
// Update the shift with assigned employees
const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
await shiftAssignmentService.updateScheduledShift(scheduledShift.id, {
assignedEmployees
});
console.log(`✅ Successfully updated shift ${scheduledShift.id}`);
if (scheduledShifts.some(s => s.id === scheduledShift.id)) {
console.log(`✅ Successfully updated scheduled shift ${scheduledShift.id}`);
}
} catch (error) {
console.error(`❌ Failed to update shift ${scheduledShift.id}:`, error);
throw error;
@@ -450,18 +551,6 @@ const ShiftPlanView: React.FC = () => {
setShiftPlan(reloadedPlan);
setScheduledShifts(reloadedShifts);
// Debug: Überprüfe die aktualisierten Daten
console.log('🔍 After publish - Reloaded data:', {
planStatus: reloadedPlan.status,
scheduledShiftsCount: reloadedShifts.length,
shiftsWithAssignments: reloadedShifts.filter(s => s.assignedEmployees && s.assignedEmployees.length > 0).length,
allAssignments: reloadedShifts.map(s => ({
id: s.id,
date: s.date,
assigned: s.assignedEmployees
}))
});
showNotification({
type: 'success',
title: 'Erfolg',
@@ -488,53 +577,78 @@ const ShiftPlanView: React.FC = () => {
}
};
const handleRevertToDraft = async () => {
if (!shiftPlan || !id) return;
if (!window.confirm('Möchten Sie diesen Schichtplan wirklich zurück in den Entwurfsstatus setzen? Alle Zuweisungen werden entfernt.')) {
return;
}
const refreshAllAvailabilities = async (): Promise<EmployeeAvailability[]> => {
try {
setReverting(true);
console.log('🔄 Force refreshing ALL availabilities with error handling...');
// 1. Zuerst zurücksetzen
const updatedPlan = await shiftPlanService.revertToDraft(id);
// 2. Dann ALLE Daten neu laden
await loadShiftPlanData();
// 3. Assignment-Result zurücksetzen
setAssignmentResult(null);
// 4. Preview schließen falls geöffnet
setShowAssignmentPreview(false);
showNotification({
type: 'success',
title: 'Erfolg',
message: 'Schichtplan wurde erfolgreich zurück in den Entwurfsstatus gesetzt. Alle Daten wurden neu geladen.'
});
//const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
console.log('Scheduled shifts after revert:', {
hasScheduledShifts: !! scheduledShifts,
count: scheduledShifts.length || 0,
firstFew: scheduledShifts?.slice(0, 3)
});
if (!id) {
console.error('❌ No plan ID available');
return [];
}
const availabilityPromises = employees
.filter(emp => emp.isActive)
.map(async (emp) => {
try {
return await employeeService.getAvailabilities(emp.id);
} catch (error) {
console.error(`❌ Failed to load availabilities for ${emp.name}:`, error);
return []; // Return empty array instead of failing entire operation
}
});
const allAvailabilities = await Promise.all(availabilityPromises);
const flattenedAvailabilities = allAvailabilities.flat();
// More robust filtering
const planAvailabilities = flattenedAvailabilities.filter(
availability => availability && availability.planId === id
);
console.log(`✅ Successfully refreshed ${planAvailabilities.length} availabilities for plan ${id}`);
// IMMEDIATELY update state
setAvailabilities(planAvailabilities);
return planAvailabilities;
} catch (error) {
console.error('Error reverting plan to draft:', error);
showNotification({
type: 'error',
title: 'Fehler',
message: 'Schichtplan konnte nicht zurückgesetzt werden.'
});
} finally {
setReverting(false);
console.error('❌ Critical error refreshing availabilities:', error);
// DON'T return old data - throw error or return empty array
throw new Error('Failed to refresh availabilities: ' + error);
}
};
const validateSchedulingData = (): boolean => {
console.log('🔍 Validating scheduling data...');
const totalEmployees = employees.length;
const employeesWithAvailabilities = new Set(
availabilities.map(avail => avail.employeeId)
).size;
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)
);
console.warn('⚠️ Missing availabilities for employees:',
missingEmployees.map(emp => emp.name));
return false;
}
return true;
};
const canPublish = () => {
if (!shiftPlan || shiftPlan.status === 'published') return false;
@@ -560,40 +674,49 @@ const ShiftPlanView: React.FC = () => {
};
};
const debugCurrentState = () => {
console.log('🔍 CURRENT STATE DEBUG:', {
shiftPlan: shiftPlan ? {
id: shiftPlan.id,
name: shiftPlan.name,
status: shiftPlan.status
} : null,
scheduledShifts: {
count: scheduledShifts.length,
withAssignments: scheduledShifts.filter(s => s.assignedEmployees && s.assignedEmployees.length > 0).length,
details: scheduledShifts.map(s => ({
id: s.id,
date: s.date,
timeSlotId: s.timeSlotId,
assignedEmployees: s.assignedEmployees
}))
},
employees: employees.length
const reloadAvailabilities = async () => {
try {
console.log('🔄 Lade Verfügbarkeiten neu...');
// Load availabilities for all employees
const availabilityPromises = employees
.filter(emp => emp.isActive)
.map(emp => employeeService.getAvailabilities(emp.id));
const allAvailabilities = await Promise.all(availabilityPromises);
const flattenedAvailabilities = allAvailabilities.flat();
// Filter availabilities to only include those for the current shift plan
const planAvailabilities = flattenedAvailabilities.filter(
availability => availability.planId === id
);
setAvailabilities(planAvailabilities);
console.log('✅ Verfügbarkeiten neu geladen:', planAvailabilities.length);
} catch (error) {
console.error('❌ Fehler beim Neuladen der Verfügbarkeiten:', error);
}
};
// Füge diese Funktion zu den verfügbaren Aktionen hinzu
const handleReloadData = async () => {
await loadShiftPlanData();
showNotification({
type: 'success',
title: 'Erfolg',
message: 'Daten wurden neu geladen.'
});
};
};
// Render timetable using the same structure as AvailabilityManager
const renderTimetable = () => {
debugAssignments();
debugCurrentState();
const { days, allTimeSlots, timeSlotsByDay } = getTimetableData();
const { days, allTimeSlots, timeSlotsByDay } = getExtendedTimetableData();
if (!shiftPlan?.id) {
console.warn("Shift plan ID is missing");
return null;
}
//const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
if (days.length === 0 || allTimeSlots.length === 0) {
return (
<div style={{
@@ -692,7 +815,6 @@ const ShiftPlanView: React.FC = () => {
);
}
// Get assigned employees for this shift
let assignedEmployees: string[] = [];
let displayText = '';
@@ -701,11 +823,17 @@ const ShiftPlanView: React.FC = () => {
const scheduledShift = scheduledShifts.find(scheduled => {
const scheduledDayOfWeek = getDayOfWeek(scheduled.date);
return scheduledDayOfWeek === weekday.id &&
scheduled.timeSlotId === timeSlot.id;
scheduled.timeSlotId === timeSlot.id;
});
if (scheduledShift) {
assignedEmployees = scheduledShift.assignedEmployees || [];
// DEBUG: Log if we're still seeing old data
if (assignedEmployees.length > 0) {
console.warn(`⚠️ Found non-empty assignments for ${weekday.name} ${timeSlot.name}:`, assignedEmployees);
}
displayText = assignedEmployees.map(empId => {
const employee = employees.find(emp => emp.id === empId);
return employee ? employee.name : 'Unbekannt';
@@ -716,7 +844,7 @@ const ShiftPlanView: React.FC = () => {
const scheduledShift = scheduledShifts.find(scheduled => {
const scheduledDayOfWeek = getDayOfWeek(scheduled.date);
return scheduledDayOfWeek === weekday.id &&
scheduled.timeSlotId === timeSlot.id;
scheduled.timeSlotId === timeSlot.id;
});
if (scheduledShift && assignmentResult.assignments[scheduledShift.id]) {
@@ -728,7 +856,7 @@ const ShiftPlanView: React.FC = () => {
}
}
// If no assignments yet, show required count
// If no assignments yet, show empty or required count
if (!displayText) {
const shiftsForSlot = shiftPlan?.shifts?.filter(shift =>
shift.dayOfWeek === weekday.id &&
@@ -738,7 +866,13 @@ const ShiftPlanView: React.FC = () => {
const totalRequired = shiftsForSlot.reduce((sum, shift) =>
sum + shift.requiredEmployees, 0);
// Show "0/2" instead of just "0" to indicate it's empty
displayText = `0/${totalRequired}`;
// Optional: Show empty state more clearly
if (totalRequired === 0) {
displayText = '-';
}
}
return (
@@ -766,7 +900,7 @@ const ShiftPlanView: React.FC = () => {
if (loading) return <div>Lade Schichtplan...</div>;
if (!shiftPlan) return <div>Schichtplan nicht gefunden</div>;
const { days, allTimeSlots } = getTimetableData();
const { days, allTimeSlots } = getExtendedTimetableData();
const availabilityStatus = getAvailabilityStatus();
@@ -799,25 +933,25 @@ const ShiftPlanView: React.FC = () => {
{shiftPlan.status === 'published' ? 'Veröffentlicht' : 'Entwurf'}
</div>
</div>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
{shiftPlan.status === 'published' && hasRole(['admin', 'instandhalter']) && (
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
{shiftPlan.status === 'published' && hasRole(['admin', 'instandhalter']) && (
<button
onClick={handleRevertToDraft}
disabled={reverting}
onClick={handleRecreateAssignments}
disabled={recreating}
style={{
padding: '10px 20px',
backgroundColor: '#e74c3c',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
cursor: recreating ? 'not-allowed' : 'pointer',
fontWeight: 'bold'
}}
>
{reverting ? 'Zurücksetzen...' : 'Zu Entwurf zurücksetzen'}
{recreating ? 'Lösche Zuweisungen...' : 'Zuweisungen neu berechnen'}
</button>
)}
<button
onClick={() => navigate('/shift-plans')}
style={{

File diff suppressed because it is too large Load Diff

View File

@@ -1,84 +0,0 @@
// frontend/src/services/scheduling/dataAdapter.ts
import { Employee, EmployeeAvailability } from '../../models/Employee';
import { ScheduledShift } from '../../models/ShiftPlan';
import { SchedulingEmployee, SchedulingShift } from './types';
export function transformToSchedulingData(
employees: Employee[],
scheduledShifts: ScheduledShift[],
availabilities: EmployeeAvailability[]
): {
schedulingEmployees: SchedulingEmployee[];
schedulingShifts: SchedulingShift[];
managerShifts: string[];
} {
// Create employee availability map
const availabilityMap = new Map<string, Map<string, number>>();
availabilities.forEach(avail => {
if (!availabilityMap.has(avail.employeeId)) {
availabilityMap.set(avail.employeeId, new Map());
}
// Create a unique key for each shift pattern (dayOfWeek + timeSlotId)
const shiftKey = `${avail.dayOfWeek}-${avail.timeSlotId}`;
availabilityMap.get(avail.employeeId)!.set(shiftKey, avail.preferenceLevel);
});
// Transform employees
const schedulingEmployees: SchedulingEmployee[] = employees.map(emp => {
// Map roles
let role: 'manager' | 'erfahren' | 'neu';
if (emp.role === 'admin') role = 'manager';
else if (emp.employeeType === 'experienced') role = 'erfahren';
else role = 'neu';
// Map contract
const contract = emp.contractType === 'small' ? 1 : 2;
return {
id: emp.id,
name: emp.name,
role,
contract,
availability: availabilityMap.get(emp.id) || new Map(),
assignedCount: 0,
originalData: emp
};
});
// Transform shifts and identify manager shifts
const schedulingShifts: SchedulingShift[] = scheduledShifts.map(scheduledShift => ({
id: scheduledShift.id,
requiredEmployees: scheduledShift.requiredEmployees,
originalData: scheduledShift
}));
// Identify manager shifts (shifts where manager has availability 1 or 2)
const manager = schedulingEmployees.find(emp => emp.role === 'manager');
const managerShifts: string[] = [];
if (manager) {
scheduledShifts.forEach(scheduledShift => {
const dayOfWeek = getDayOfWeek(scheduledShift.date);
const shiftKey = `${dayOfWeek}-${scheduledShift.timeSlotId}`;
const preference = manager.availability.get(shiftKey);
if (preference === 1 || preference === 2) {
managerShifts.push(scheduledShift.id);
}
});
}
return {
schedulingEmployees,
schedulingShifts,
managerShifts
};
}
function getDayOfWeek(dateString: string): number {
const date = new Date(dateString);
return date.getDay() === 0 ? 7 : date.getDay();
}

View File

@@ -1,6 +0,0 @@
// frontend/src/services/scheduling/index.ts
export * from './types';
export * from './utils';
export * from './repairFunctions';
export * from './shiftScheduler';
export * from './dataAdapter';

File diff suppressed because it is too large Load Diff

View File

@@ -1,472 +0,0 @@
// frontend/src/services/scheduling/shiftScheduler.ts
import {
SchedulingEmployee,
SchedulingShift,
Assignment,
SchedulingResult,
SchedulingConstraints,
RepairContext
} from './types';
import {
canAssign,
candidateScore,
onlyNeuAssigned,
assignEmployee,
hasErfahrener,
wouldBeAloneIfAdded,
isManagerShiftWithOnlyNew,
isManagerAlone,
hasExperiencedAloneNotAllowed
} from './utils';
import {
attemptMoveErfahrenerTo,
attemptUnassignOrSwap,
attemptAddErfahrenerToShift,
attemptFillFromPool,
resolveTwoExperiencedInShift,
attemptMoveExperiencedToManagerShift,
attemptSwapForExperienced,
resolveOverstaffedExperienced,
prioritizeWarningsWithPool,
resolveExperiencedAloneNotAllowed,
checkAllProblemsResolved,
createDetailedResolutionReport
} from './repairFunctions';
// Phase A: Regular employee scheduling (without manager)
function phaseAPlan(
shifts: SchedulingShift[],
employees: SchedulingEmployee[],
constraints: SchedulingConstraints
): { assignments: Assignment; warnings: string[] } {
const assignments: Assignment = {};
const warnings: string[] = [];
const employeeMap = new Map(employees.map(emp => [emp.id, emp]));
// Initialize assignments
shifts.forEach(shift => {
assignments[shift.id] = [];
});
// Helper function to find best candidate
function findBestCandidate(candidates: SchedulingEmployee[], shiftId: string): SchedulingEmployee | null {
if (candidates.length === 0) return null;
return candidates.reduce((best, current) => {
const bestScore = candidateScore(best, shiftId);
const currentScore = candidateScore(current, shiftId);
return currentScore < bestScore ? current : best;
});
}
// 1) Basic coverage: at least 1 person per shift, prefer experienced
for (const shift of shifts) {
const candidates = employees.filter(emp => canAssign(emp, shift.id));
if (candidates.length === 0) {
warnings.push(`No available employees for shift ${shift.id}`);
continue;
}
// Prefer erfahrene candidates
const erfahrenCandidates = candidates.filter(emp => emp.role === 'erfahren');
const bestCandidate = findBestCandidate(
erfahrenCandidates.length > 0 ? erfahrenCandidates : candidates,
shift.id
);
if (bestCandidate) {
assignEmployee(bestCandidate, shift.id, assignments);
}
}
// 2) Prevent 'neu alone' if constraint enabled
if (constraints.enforceNoTraineeAlone) {
for (const shift of shifts) {
if (onlyNeuAssigned(assignments[shift.id], employeeMap)) {
const erfahrenCandidates = employees.filter(emp =>
emp.role === 'erfahren' && canAssign(emp, shift.id)
);
if (erfahrenCandidates.length > 0) {
const bestCandidate = findBestCandidate(erfahrenCandidates, shift.id);
if (bestCandidate) {
assignEmployee(bestCandidate, shift.id, assignments);
}
} else {
// Try repair
if (!attemptMoveErfahrenerTo(shift.id, assignments, employeeMap, shifts)) {
warnings.push(`Cannot prevent neu-alone in shift ${shift.id}`);
}
}
}
}
}
// 3) Fill up to target employees per shift
const targetPerShift = constraints.targetEmployeesPerShift || 2;
for (const shift of shifts) {
while (assignments[shift.id].length < targetPerShift) {
const candidates = employees.filter(emp =>
canAssign(emp, shift.id) &&
!assignments[shift.id].includes(emp.id) &&
!wouldBeAloneIfAdded(emp, shift.id, assignments, employeeMap)
);
if (candidates.length === 0) break;
const bestCandidate = findBestCandidate(candidates, shift.id);
if (bestCandidate) {
assignEmployee(bestCandidate, shift.id, assignments);
} else {
break;
}
}
}
return { assignments, warnings };
}
// Phase B: Insert manager and ensure "experienced with manager"
function phaseBInsertManager(
assignments: Assignment,
manager: SchedulingEmployee | undefined,
managerShifts: string[],
employees: SchedulingEmployee[],
nonManagerShifts: SchedulingShift[],
constraints: SchedulingConstraints
): { assignments: Assignment; warnings: string[] } {
const warnings: string[] = [];
const employeeMap = new Map(employees.map(emp => [emp.id, emp]));
if (!manager) return { assignments, warnings };
console.log(`🎯 Phase B: Processing ${managerShifts.length} manager shifts`);
for (const shiftId of managerShifts) {
let _shift = nonManagerShifts.find(s => s.id === shiftId) || { id: shiftId, requiredEmployees: 2 };
// Assign manager to his chosen shifts
if (!assignments[shiftId].includes(manager.id)) {
assignments[shiftId].push(manager.id);
manager.assignedCount++;
console.log(`✅ Assigned manager to shift ${shiftId}`);
}
// Rule: if manager present, MUST have at least one ERFAHRENER
if (constraints.enforceExperiencedWithChef) {
const hasExperienced = hasErfahrener(assignments[shiftId], employeeMap);
if (!hasExperienced) {
console.log(`⚠️ Manager shift ${shiftId} missing experienced employee`);
// Strategy 1: Try to add an experienced employee directly
const erfahrenCandidates = employees.filter(emp =>
emp.role === 'erfahren' &&
canAssign(emp, shiftId) &&
!assignments[shiftId].includes(emp.id)
);
if (erfahrenCandidates.length > 0) {
// Find best candidate using scoring
const bestCandidate = erfahrenCandidates.reduce((best, current) => {
const bestScore = candidateScore(best, shiftId);
const currentScore = candidateScore(current, shiftId);
return currentScore < bestScore ? current : best;
});
assignEmployee(bestCandidate, shiftId, assignments);
console.log(`✅ Added experienced ${bestCandidate.id} to manager shift ${shiftId}`);
continue;
}
// Strategy 2: Try to swap with another shift
if (attemptSwapForExperienced(shiftId, assignments, employeeMap, nonManagerShifts)) {
console.log(`✅ Swapped experienced into manager shift ${shiftId}`);
continue;
}
// Strategy 3: Try to move experienced from overloaded shift
if (attemptMoveExperiencedToManagerShift(shiftId, assignments, employeeMap, nonManagerShifts)) {
console.log(`✅ Moved experienced to manager shift ${shiftId}`);
continue;
}
// Final fallback: Check if we can at least add ANY employee (not just experienced)
const anyCandidates = employees.filter(emp =>
emp.role !== 'manager' &&
canAssign(emp, shiftId) &&
!assignments[shiftId].includes(emp.id) &&
!wouldBeAloneIfAdded(emp, shiftId, assignments, employeeMap)
);
if (anyCandidates.length > 0) {
const bestCandidate = anyCandidates.reduce((best, current) => {
const bestScore = candidateScore(best, shiftId);
const currentScore = candidateScore(current, shiftId);
return currentScore < bestScore ? current : best;
});
assignEmployee(bestCandidate, shiftId, assignments);
warnings.push(`Manager shift ${shiftId} has non-experienced backup: ${bestCandidate.name}`);
console.log(`⚠️ Added non-experienced backup to manager shift ${shiftId}`);
} else {
warnings.push(`Manager alone in shift ${shiftId} - no available employees`);
console.log(`❌ Cannot fix manager alone in shift ${shiftId}`);
}
}
}
}
return { assignments, warnings };
}
// Phase C: Repair and validate
export function enhancedPhaseCRepairValidate(
assignments: Assignment,
employees: SchedulingEmployee[],
shifts: SchedulingShift[],
managerShifts: string[],
constraints: SchedulingConstraints
): { assignments: Assignment; violations: string[]; resolutionReport: string[]; allProblemsResolved: boolean } {
const repairContext: RepairContext = {
lockedShifts: new Set<string>(),
unassignedPool: [],
warnings: [],
violations: []
};
const employeeMap = new Map(employees.map(emp => [emp.id, emp]));
const manager = employees.find(emp => emp.role === 'manager');
console.log('🔄 Starting Enhanced Phase C: Detailed Repair & Validation');
// 1. Manager-Schutzregel
managerShifts.forEach(shiftId => {
repairContext.lockedShifts.add(shiftId);
repairContext.warnings.push(`Schicht ${shiftId} als Manager-Schicht gesperrt`);
});
// 2. Überbesetzte erfahrene Mitarbeiter identifizieren und in Pool verschieben
resolveOverstaffedExperienced(assignments, employeeMap, shifts, repairContext);
// 3. Erfahrene Mitarbeiter, die nicht alleine arbeiten dürfen
resolveExperiencedAloneNotAllowed(assignments, employeeMap, shifts, repairContext);
// 4. Doppel-Erfahrene-Strafe (nur für Nicht-Manager-Schichten)
shifts.forEach(shift => {
if (!managerShifts.includes(shift.id)) {
resolveTwoExperiencedInShift(shift.id, assignments, employeeMap, repairContext);
}
});
// 5. Priorisierte Zuweisung von Pool-Mitarbeitern zu Schichten mit Warnungen
prioritizeWarningsWithPool(assignments, employeeMap, shifts, managerShifts, repairContext);
// 6. Standard-Validation
shifts.forEach(shift => {
const assignment = assignments[shift.id] || [];
// Leere Schichten beheben
if (assignment.length === 0) {
if (!attemptFillFromPool(shift.id, assignments, employeeMap, repairContext, managerShifts)) {
repairContext.violations.push({
type: 'EmptyShift',
shiftId: shift.id,
severity: 'error',
message: `Leere Schicht: ${shift.id}`
});
repairContext.warnings.push(`Konnte leere Schicht ${shift.id} nicht beheben`);
}
}
// Neu-allein Schichten beheben (nur für Nicht-Manager-Schichten)
if (constraints.enforceNoTraineeAlone &&
!managerShifts.includes(shift.id) &&
onlyNeuAssigned(assignment, employeeMap)) {
if (!attemptAddErfahrenerToShift(shift.id, assignments, employeeMap, shifts)) {
repairContext.violations.push({
type: 'NeuAlone',
shiftId: shift.id,
severity: 'error',
message: `Nur neue Mitarbeiter in Schicht: ${shift.id}`
});
}
}
// Erfahrene-allein Prüfung (erneut nach Reparaturen)
const experiencedAloneCheck = hasExperiencedAloneNotAllowed(assignment, employeeMap);
if (experiencedAloneCheck.hasViolation && !repairContext.lockedShifts.has(shift.id)) {
const emp = employeeMap.get(experiencedAloneCheck.employeeId!);
repairContext.violations.push({
type: 'ExperiencedAloneNotAllowed',
shiftId: shift.id,
employeeId: experiencedAloneCheck.employeeId,
severity: 'error',
message: `Erfahrener Mitarbeiter ${emp?.name || experiencedAloneCheck.employeeId} arbeitet allein in Schicht ${shift.id}`
});
}
});
// 7. Vertragsüberschreitungen beheben
employees.forEach(emp => {
if (emp.role !== 'manager' && emp.assignedCount > emp.contract) {
if (!attemptUnassignOrSwap(emp.id, assignments, employeeMap, shifts)) {
repairContext.violations.push({
type: 'ContractExceeded',
employeeId: emp.id,
severity: 'error',
message: `Vertragslimit überschritten für: ${emp.name}`
});
}
}
});
// 8. Nachbesserung: Manager-Schichten prüfen
managerShifts.forEach(shiftId => {
const assignment = assignments[shiftId] || [];
// Manager allein
if (isManagerAlone(assignment, manager?.id)) {
repairContext.violations.push({
type: 'ManagerAlone',
shiftId: shiftId,
severity: 'error',
message: `Manager allein in Schicht ${shiftId}`
});
}
// Manager + nur Neue
if (isManagerShiftWithOnlyNew(assignment, employeeMap, manager?.id)) {
repairContext.violations.push({
type: 'ManagerWithOnlyNew',
shiftId: shiftId,
severity: 'warning',
message: `Manager mit nur Neuen in Schicht ${shiftId}`
});
}
});
// Erstelle finale Violations-Liste
const uniqueViolations = repairContext.violations.filter((v, index, self) =>
index === self.findIndex(t =>
t.type === v.type &&
t.shiftId === v.shiftId &&
t.employeeId === v.employeeId
)
);
const uniqueWarnings = repairContext.warnings.filter((warning, index, array) =>
array.indexOf(warning) === index
);
const finalViolations = [
...uniqueViolations
.filter(v => v.severity === 'error')
.map(v => `ERROR: ${v.message}`),
...uniqueViolations
.filter(v => v.severity === 'warning')
.map(v => `WARNING: ${v.message}`),
...uniqueWarnings.map(w => `INFO: ${w}`)
];
// 9. DETAILLIERTER REPARATUR-BERICHT
const resolutionReport = createDetailedResolutionReport(
assignments,
employeeMap,
shifts,
managerShifts,
repairContext
);
// Bestimme ob alle kritischen Probleme behoben wurden
const criticalProblems = uniqueViolations.filter(v => v.severity === 'error');
const allProblemsResolved = criticalProblems.length === 0;
console.log('📊 Enhanced Phase C completed:', {
totalActions: uniqueWarnings.length,
criticalProblems: criticalProblems.length,
warnings: uniqueViolations.filter(v => v.severity === 'warning').length,
allProblemsResolved
});
return {
assignments,
violations: finalViolations,
resolutionReport,
allProblemsResolved
};
}
export function scheduleWithManager(
shifts: SchedulingShift[],
employees: SchedulingEmployee[],
managerShifts: string[],
constraints: SchedulingConstraints
): SchedulingResult & { resolutionReport?: string[]; allProblemsResolved?: boolean } {
const assignments: Assignment = {};
const allViolations: string[] = [];
// Initialisiere Zuweisungen
shifts.forEach(shift => {
assignments[shift.id] = [];
});
// Finde Manager
const manager = employees.find(emp => emp.role === 'manager');
// Filtere Manager und Nicht-Manager-Schichten für Phase A
const nonManagerEmployees = employees.filter(emp => emp.role !== 'manager');
const nonManagerShifts = shifts.filter(shift => !managerShifts.includes(shift.id));
console.log('🔄 Starting Phase A: Regular employee scheduling');
// Phase A: Reguläre Planung
const phaseAResult = phaseAPlan(nonManagerShifts, nonManagerEmployees, constraints);
Object.assign(assignments, phaseAResult.assignments);
console.log('🔄 Starting Phase B: Enhanced Manager insertion');
// Phase B: Erweiterte Manager-Einfügung
const phaseBResult = phaseBInsertManager(
assignments,
manager,
managerShifts,
employees,
nonManagerShifts,
constraints
);
console.log('🔄 Starting Enhanced Phase C: Smart Repair & Validation');
// Phase C: Erweiterte Reparatur und Validierung mit Pool-Verwaltung
const phaseCResult = enhancedPhaseCRepairValidate(assignments, employees, shifts, managerShifts, constraints);
// Verwende Array.filter für uniqueIssues
const uniqueIssues = phaseCResult.violations.filter((issue, index, array) =>
array.indexOf(issue) === index
);
// Erfolg basiert jetzt auf allProblemsResolved statt nur auf ERRORs
const success = phaseCResult.allProblemsResolved;
console.log('📊 Enhanced scheduling with pool management completed:', {
assignments: Object.keys(assignments).filter(k => assignments[k].length > 0).length,
totalShifts: shifts.length,
totalIssues: uniqueIssues.length,
errors: uniqueIssues.filter(v => v.includes('ERROR:')).length,
warnings: uniqueIssues.filter(v => v.includes('WARNING:')).length,
allProblemsResolved: success
});
return {
assignments,
violations: uniqueIssues,
success: success,
resolutionReport: phaseCResult.resolutionReport,
allProblemsResolved: success
};
}

View File

@@ -1,75 +0,0 @@
// frontend/src/services/scheduling/types.ts
import { ScheduledShift } from "../../models/ShiftPlan";
export interface SchedulingEmployee {
id: string;
name: string;
role: 'manager' | 'erfahren' | 'neu';
contract: number; // max assignments per week
availability: Map<string, number>; // shiftId -> preferenceLevel (1,2,3)
assignedCount: number;
originalData: any; // reference to original employee data
}
export interface SchedulingShift {
id: string;
requiredEmployees: number;
isManagerShift?: boolean;
originalData: any; // reference to original shift data
}
export interface Assignment {
[shiftId: string]: string[]; // employee IDs
}
export interface SchedulingConstraints {
enforceNoTraineeAlone: boolean;
enforceExperiencedWithChef: boolean;
maxRepairAttempts: number;
}
export interface SchedulingResult {
assignments: Assignment;
violations: string[];
success: boolean;
resolutionReport?: string[];
allProblemsResolved?: boolean;
}
export interface AssignmentResult {
assignments: { [shiftId: string]: string[] };
violations: string[];
success: boolean;
pattern: WeeklyPattern;
resolutionReport?: string[];
allProblemsResolved?: boolean;
}
export interface WeeklyPattern {
weekShifts: ScheduledShift[];
assignments: { [shiftId: string]: string[] };
weekNumber: number;
}
export interface SchedulingConstraints {
enforceNoTraineeAlone: boolean;
enforceExperiencedWithChef: boolean;
maxRepairAttempts: number;
targetEmployeesPerShift?: number; // New: flexible target
}
export interface Violation {
type: 'EmptyShift' | 'NeuAlone' | 'ContractExceeded' | 'ManagerWithoutExperienced' |
'TwoExperiencedInShift' | 'ManagerAlone' | 'ManagerWithOnlyNew' | 'ExperiencedAloneNotAllowed';
shiftId?: string;
employeeId?: string;
severity: 'error' | 'warning';
message: string;
}
export interface RepairContext {
lockedShifts: Set<string>;
unassignedPool: string[];
warnings: string[];
violations: Violation[];
}

View File

@@ -1,228 +0,0 @@
// frontend/src/services/scheduling/utils.ts
import { SchedulingEmployee, SchedulingShift, Assignment } from './types';
// Scoring system
export function getAvailabilityScore(preferenceLevel: number): number {
switch (preferenceLevel) {
case 1: return 2; // preferred
case 2: return 1; // available
case 3: return -9999; // unavailable
default: return 0;
}
}
export function canAssign(emp: SchedulingEmployee, shiftId: string): boolean {
if (emp.availability.get(shiftId) === 3) return false;
if (emp.role === 'manager') return false; // Phase A: ignore manager
return emp.assignedCount < emp.contract;
}
export function candidateScore(emp: SchedulingEmployee, shiftId: string): number {
const availability = emp.availability.get(shiftId) || 3;
const baseScore = -getAvailabilityScore(availability); // prefer higher availability scores
const loadPenalty = emp.assignedCount * 0.5; // fairness: penalize already assigned
const rolePenalty = emp.role === 'erfahren' ? 0 : 0.5; // prefer experienced
return baseScore + loadPenalty + rolePenalty;
}
export function onlyNeuAssigned(assignment: string[], employees: Map<string, SchedulingEmployee>): boolean {
if (assignment.length === 0) return false;
return assignment.every(empId => {
const emp = employees.get(empId);
return emp?.role === 'neu';
});
}
export function assignEmployee(emp: SchedulingEmployee, shiftId: string, assignments: Assignment): void {
if (!assignments[shiftId]) {
assignments[shiftId] = [];
}
assignments[shiftId].push(emp.id);
emp.assignedCount++;
}
export function unassignEmployee(emp: SchedulingEmployee, shiftId: string, assignments: Assignment): void {
if (assignments[shiftId]) {
assignments[shiftId] = assignments[shiftId].filter(id => id !== emp.id);
emp.assignedCount--;
}
}
export function hasErfahrener(assignment: string[], employees: Map<string, SchedulingEmployee>): boolean {
return assignment.some(empId => {
const emp = employees.get(empId);
return emp?.role === 'erfahren';
});
}
export function wouldBeAloneIfAdded(
candidate: SchedulingEmployee,
shiftId: string,
assignments: Assignment,
employees: Map<string, SchedulingEmployee>
): boolean {
const currentAssignment = assignments[shiftId] || [];
// If adding to empty shift and candidate is neu, they would be alone
if (currentAssignment.length === 0 && candidate.role === 'neu') {
return true;
}
// If all current assignments are neu and candidate is neu, they would be alone together
if (onlyNeuAssigned(currentAssignment, employees) && candidate.role === 'neu') {
return true;
}
return false;
}
export function findViolations(
assignments: Assignment,
employees: Map<string, SchedulingEmployee>,
shifts: SchedulingShift[],
managerShifts: string[] = []
): { type: string; shiftId?: string; employeeId?: string; severity: string }[] {
const violations: any[] = [];
const employeeMap = employees;
// Check each shift
shifts.forEach(shift => {
const assignment = assignments[shift.id] || [];
// Empty shift violation
if (assignment.length === 0) {
violations.push({
type: 'EmptyShift',
shiftId: shift.id,
severity: 'error'
});
}
// Neu alone violation
if (onlyNeuAssigned(assignment, employeeMap)) {
violations.push({
type: 'NeuAlone',
shiftId: shift.id,
severity: 'error'
});
}
// Manager without experienced (for manager shifts)
if (managerShifts.includes(shift.id)) {
const hasManager = assignment.some(empId => {
const emp = employeeMap.get(empId);
return emp?.role === 'manager';
});
if (hasManager && !hasErfahrener(assignment, employeeMap)) {
violations.push({
type: 'ManagerWithoutExperienced',
shiftId: shift.id,
severity: 'warning' // Could be warning instead of error
});
}
}
});
// Check employee contracts
employeeMap.forEach((emp, empId) => {
if (emp.role !== 'manager' && emp.assignedCount > emp.contract) {
violations.push({
type: 'ContractExceeded',
employeeId: empId,
severity: 'error'
});
}
});
return violations;
}
export function canRemove(
empId: string,
shiftId: string,
lockedShifts: Set<string>,
assignments: Assignment,
employees: Map<string, SchedulingEmployee>
): boolean {
// Wenn Schicht gesperrt ist, kann niemand entfernt werden
if (lockedShifts.has(shiftId)) {
return false;
}
const emp = employees.get(empId);
if (!emp) return false;
// Überprüfe ob Entfernen neue Verletzungen verursachen würde
const currentAssignment = assignments[shiftId] || [];
const wouldBeEmpty = currentAssignment.length <= 1;
const wouldBeNeuAlone = wouldBeEmpty && emp.role === 'erfahren';
const wouldBeExperiencedAlone = wouldBeExperiencedAloneIfRemoved(empId, shiftId, assignments, employees);
return !wouldBeEmpty && !wouldBeNeuAlone && !wouldBeExperiencedAlone;
}
export function countExperiencedCanWorkAlone(
assignment: string[],
employees: Map<string, SchedulingEmployee>
): string[] {
return assignment.filter(empId => {
const emp = employees.get(empId);
return emp?.role === 'erfahren' && emp.originalData?.canWorkAlone;
});
}
export function isManagerShiftWithOnlyNew(
assignment: string[],
employees: Map<string, SchedulingEmployee>,
managerId?: string
): boolean {
if (!managerId || !assignment.includes(managerId)) return false;
const nonManagerEmployees = assignment.filter(id => id !== managerId);
return onlyNeuAssigned(nonManagerEmployees, employees);
}
export function isManagerAlone(
assignment: string[],
managerId?: string
): boolean {
return assignment.length === 1 && assignment[0] === managerId;
}
export function hasExperiencedAloneNotAllowed(
assignment: string[],
employees: Map<string, SchedulingEmployee>
): { hasViolation: boolean; employeeId?: string } {
if (assignment.length !== 1) return { hasViolation: false };
const empId = assignment[0];
const emp = employees.get(empId);
if (emp && emp.role === 'erfahren' && !emp.originalData?.canWorkAlone) {
return { hasViolation: true, employeeId: empId };
}
return { hasViolation: false };
}
export function isExperiencedCanWorkAlone(emp: SchedulingEmployee): boolean {
return emp.role === 'erfahren' && emp.originalData?.canWorkAlone === true;
}
export function wouldBeExperiencedAloneIfRemoved(
empId: string,
shiftId: string,
assignments: Assignment,
employees: Map<string, SchedulingEmployee>
): boolean {
const assignment = assignments[shiftId] || [];
if (assignment.length <= 1) return false;
const remainingAssignment = assignment.filter(id => id !== empId);
if (remainingAssignment.length !== 1) return false;
const remainingEmp = employees.get(remainingAssignment[0]);
return remainingEmp?.role === 'erfahren' && !remainingEmp.originalData?.canWorkAlone;
}

View File

@@ -2,13 +2,13 @@
import { ShiftPlan, ScheduledShift } from '../models/ShiftPlan';
import { Employee, EmployeeAvailability } from '../models/Employee';
import { authService } from './authService';
import { scheduleWithManager } from './scheduling/shiftScheduler';
import { transformToSchedulingData } from './scheduling/dataAdapter';
import { AssignmentResult, WeeklyPattern } from './scheduling/types';
import { IntelligentShiftScheduler, AssignmentResult, WeeklyPattern } from './scheduling';
import { isScheduledShift } from '../models/helpers';
const API_BASE_URL = 'http://localhost:3002/api/scheduled-shifts';
// Helper function to get auth headers
const getAuthHeaders = () => {
const token = localStorage.getItem('token');
@@ -21,7 +21,7 @@ const getAuthHeaders = () => {
export class ShiftAssignmentService {
async updateScheduledShift(id: string, updates: { assignedEmployees: string[] }): Promise<void> {
try {
console.log('🔄 Updating scheduled shift via API:', { id, updates });
//console.log('🔄 Updating scheduled shift via API:', { id, updates });
const response = await fetch(`${API_BASE_URL}/${id}`, {
method: 'PUT',
@@ -141,65 +141,64 @@ export class ShiftAssignmentService {
constraints: any = {}
): Promise<AssignmentResult> {
console.log('🔄 Starting enhanced scheduling algorithm...');
console.log('🧠 Starting intelligent scheduling for FIRST WEEK ONLY...');
// Get defined shifts for the first week
const definedShifts = await this.getDefinedShifts(shiftPlan);
const firstWeekShifts = this.getFirstWeekShifts(definedShifts);
// Load all scheduled shifts
const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
console.log('📊 First week analysis:', {
totalShifts: definedShifts.length,
firstWeekShifts: firstWeekShifts.length,
employees: employees.length
});
if (scheduledShifts.length === 0) {
return {
assignments: {},
violations: ['❌ KRITISCH: Keine Schichten verfügbar für die Zuordnung'],
success: false,
resolutionReport: ['🚨 ABBRUCH: Keine Schichten im Plan verfügbar']
};
}
// Transform data for scheduling algorithm
const { schedulingEmployees, schedulingShifts, managerShifts } = transformToSchedulingData(
// Set cache for scheduler
IntelligentShiftScheduler.scheduledShiftsCache.set(shiftPlan.id, scheduledShifts);
// 🔥 RUN SCHEDULING FOR FIRST WEEK ONLY
const schedulingResult = await IntelligentShiftScheduler.generateOptimalSchedule(
shiftPlan,
employees.filter(emp => emp.isActive),
firstWeekShifts,
availabilities
availabilities,
constraints
);
console.log('🎯 Transformed data for scheduling:', {
employees: schedulingEmployees.length,
shifts: schedulingShifts.length,
managerShifts: managerShifts.length
// Get first week shifts for pattern
const firstWeekShifts = this.getFirstWeekShifts(scheduledShifts);
console.log('🔄 Creating weekly pattern from FIRST WEEK:', {
firstWeekShifts: firstWeekShifts.length,
allShifts: scheduledShifts.length,
patternAssignments: Object.keys(schedulingResult.assignments).length
});
// Run the enhanced scheduling algorithm with better constraints
const schedulingResult = scheduleWithManager(
schedulingShifts,
schedulingEmployees,
managerShifts,
{
enforceNoTraineeAlone: constraints.enforceNoTraineeAlone ?? true,
enforceExperiencedWithChef: constraints.enforceExperiencedWithChef ?? true,
maxRepairAttempts: constraints.maxRepairAttempts ?? 50,
targetEmployeesPerShift: constraints.targetEmployeesPerShift ?? 2 // Flexible target
}
);
console.log('📊 Enhanced scheduling completed:', {
assignments: Object.keys(schedulingResult.assignments).length,
violations: schedulingResult.violations.length,
success: schedulingResult.success
});
// Apply weekly pattern to all shifts
const weeklyPattern: WeeklyPattern = {
weekShifts: firstWeekShifts,
assignments: schedulingResult.assignments,
assignments: schedulingResult.assignments, // 🔥 Diese enthalten nur erste Woche
weekNumber: 1
};
const allAssignments = this.applyWeeklyPattern(definedShifts, weeklyPattern);
// 🔥 APPLY PATTERN TO ALL WEEKS
const allAssignments = this.applyWeeklyPattern(scheduledShifts, weeklyPattern);
console.log('✅ Pattern applied to all weeks:', {
firstWeekAssignments: Object.keys(schedulingResult.assignments).length,
allWeeksAssignments: Object.keys(allAssignments).length
});
// Clean cache
IntelligentShiftScheduler.scheduledShiftsCache.delete(shiftPlan.id);
return {
assignments: allAssignments,
assignments: allAssignments, // 🔥 Diese enthalten alle Wochen
violations: schedulingResult.violations,
success: schedulingResult.violations.length === 0,
success: schedulingResult.success,
pattern: weeklyPattern,
resolutionReport: schedulingResult.resolutionReport // Füge diese Zeile hinzu
resolutionReport: schedulingResult.resolutionReport,
qualityMetrics: schedulingResult.qualityMetrics
};
}
@@ -336,29 +335,48 @@ export class ShiftAssignmentService {
const assignments: { [shiftId: string]: string[] } = {};
// Group all shifts by week
const shiftsByWeek = this.groupShiftsByWeek(allShifts);
// Group all shifts by week AND day-timeSlot combination
const shiftsByPatternKey = new Map<string, ScheduledShift[]>();
console.log('📅 Applying weekly pattern to', Object.keys(shiftsByWeek).length, 'weeks');
// For each week, apply the pattern from week 1
Object.entries(shiftsByWeek).forEach(([weekKey, weekShifts]) => {
const weekNumber = parseInt(weekKey);
allShifts.forEach(shift => {
const dayOfWeek = this.getDayOfWeek(shift.date);
const patternKey = `${dayOfWeek}-${shift.timeSlotId}`;
weekShifts.forEach(shift => {
// Find the corresponding shift in the weekly pattern
const patternShift = this.findMatchingPatternShift(shift, weeklyPattern.weekShifts);
if (patternShift) {
// Use the same assignment as the pattern shift
assignments[shift.id] = [...weeklyPattern.assignments[patternShift.id]];
} else {
// No matching pattern shift, leave empty
assignments[shift.id] = [];
}
});
if (!shiftsByPatternKey.has(patternKey)) {
shiftsByPatternKey.set(patternKey, []);
}
shiftsByPatternKey.get(patternKey)!.push(shift);
});
console.log('📊 Pattern application analysis:');
console.log('- Unique pattern keys:', shiftsByPatternKey.size);
console.log('- Pattern keys:', Array.from(shiftsByPatternKey.keys()));
// For each shift in all weeks, find the matching pattern shift
allShifts.forEach(shift => {
const dayOfWeek = this.getDayOfWeek(shift.date);
//const patternKey = `${dayOfWeek}-${shift.timeSlotId}`;
const patternKey = `${shift.timeSlotId}`;
// Find the pattern shift for this day-timeSlot combination
const patternShift = weeklyPattern.weekShifts.find(patternShift => {
const patternDayOfWeek = this.getDayOfWeek(patternShift.date);
return patternDayOfWeek === dayOfWeek &&
patternShift.timeSlotId === shift.timeSlotId;
});
if (patternShift && weeklyPattern.assignments[patternShift.id]) {
assignments[shift.id] = [...weeklyPattern.assignments[patternShift.id]];
} else {
assignments[shift.id] = [];
console.warn(`❌ No pattern found for shift: ${patternKey}`);
}
});
// DEBUG: Check assignment coverage
const assignedShifts = Object.values(assignments).filter(a => a.length > 0).length;
console.log(`📊 Assignment coverage: ${assignedShifts}/${allShifts.length} shifts assigned`);
return assignments;
}

View File

@@ -128,26 +128,6 @@ export const shiftPlanService = {
}
},
async revertToDraft(id: string): Promise<ShiftPlan> {
const response = await fetch(`${API_BASE}/${id}/revert-to-draft`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...authService.getAuthHeaders()
}
});
if (!response.ok) {
if (response.status === 401) {
authService.logout();
throw new Error('Nicht authorisiert - bitte erneut anmelden');
}
throw new Error('Fehler beim Zurücksetzen des Schichtplans');
}
return response.json();
},
// Get specific template or plan
getTemplate: async (id: string): Promise<ShiftPlan> => {
const response = await fetch(`${API_BASE}/${id}`, {
@@ -219,4 +199,29 @@ export const shiftPlanService = {
description: preset.description
}));
},
async clearAssignments(planId: string): Promise<void> {
try {
console.log('🔄 Clearing assignments for plan:', planId);
const response = await fetch(`${API_BASE}/${planId}/clear-assignments`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...authService.getAuthHeaders()
}
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
throw new Error(errorData.error || `Failed to clear assignments: ${response.status}`);
}
console.log('✅ Assignments cleared successfully');
} catch (error) {
console.error('❌ Error clearing assignments:', error);
throw error;
}
},
};

View File

@@ -19,7 +19,8 @@
"isolatedModules": true,
"noEmit": true,
"ignoreDeprecations": "6.0",
"jsx": "react-jsx"
"jsx": "react-jsx",
"downlevelIteration": true
},
"include": [
"src"