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 ORDER BY day_of_week, time_slot_id
`, [employeeId]); `, [employeeId]);
//console.log('✅ Successfully got availabilities from employee:', availabilities);
res.json(availabilities.map(avail => ({ res.json(availabilities.map(avail => ({
id: avail.id, id: avail.id,
employeeId: avail.employee_id, employeeId: avail.employee_id,
@@ -348,6 +350,9 @@ export const updateAvailabilities = async (req: AuthRequest, res: Response): Pro
await db.run('COMMIT'); await db.run('COMMIT');
console.log('✅ Successfully updated availablities employee:', );
// Return updated availabilities // Return updated availabilities
const updatedAvailabilities = await db.all<any>(` const updatedAvailabilities = await db.all<any>(`
SELECT * FROM employee_availability SELECT * FROM employee_availability
@@ -365,6 +370,8 @@ export const updateAvailabilities = async (req: AuthRequest, res: Response): Pro
notes: avail.notes notes: avail.notes
}))); })));
console.log('✅ Successfully updated employee:', updateAvailabilities);
} catch (error) { } catch (error) {
await db.run('ROLLBACK'); await db.run('ROLLBACK');
throw error; 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> => { export const regenerateScheduledShifts = async (req: Request, res: Response): Promise<void> => {
try { try {
@@ -922,3 +875,62 @@ export const updateScheduledShift = async (req: AuthRequest, res: Response): Pro
res.status(500).json({ error: 'Internal server error: ' + error.message }); 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); router.use(authMiddleware);
// Employee CRUD Routes // Employee CRUD Routes
router.get('/', requireRole(['admin', 'instandhalter']), getEmployees); router.get('/', authMiddleware, getEmployees);
router.get('/:id', requireRole(['admin', 'instandhalter']), getEmployee); router.get('/:id', requireRole(['admin', 'instandhalter']), getEmployee);
router.post('/', requireRole(['admin']), createEmployee); router.post('/', requireRole(['admin']), createEmployee);
router.put('/:id', requireRole(['admin']), updateEmployee); 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); router.post('/:id/regenerate-shifts', requireRole(['admin', 'instandhalter']), regenerateScheduledShifts);
// GET all scheduled shifts for a plan // GET all scheduled shifts for a plan
router.get('/plan/:planId', requireRole(['admin']), getScheduledShiftsFromPlan); router.get('/plan/:planId', authMiddleware, getScheduledShiftsFromPlan);
// GET specific scheduled shift // GET specific scheduled shift
router.get('/:id', requireRole(['admin']), getScheduledShift); router.get('/:id', authMiddleware, getScheduledShift);
// UPDATE scheduled shift // UPDATE scheduled shift
router.put('/:id', requireRole(['admin']), updateScheduledShift); router.put('/:id', authMiddleware, updateScheduledShift);
export default router; export default router;

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,9 @@
// frontend/src/design/DesignSystem.tsx // frontend/src/design/DesignSystem.tsx
import React, { createContext, useContext, ReactNode } from 'react';
// Design Tokens
export const designTokens = { export const designTokens = {
colors: { colors: {
// Primary Colors // Primary Colors
white: '#FBFAF6', default_white: '#ddd', //boxes
white: '#FBFAF6', //background, fonts
black: '#161718', black: '#161718',
// Purple Gradients // Purple Gradients
@@ -123,274 +121,3 @@ export const designTokens = {
'2xl': '1536px', '2xl': '1536px',
}, },
} as const; } 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;

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

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

View File

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

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 React, { useState } from 'react';
import { ROLE_CONFIG, EMPLOYEE_TYPE_CONFIG } from '../../../models/defaults/employeeDefaults'; import { ROLE_CONFIG, EMPLOYEE_TYPE_CONFIG } from '../../../models/defaults/employeeDefaults';
import { Employee } from '../../../models/Employee'; import { Employee } from '../../../models/Employee';
@@ -11,6 +11,9 @@ interface EmployeeListProps {
onManageAvailability: (employee: Employee) => void; onManageAvailability: (employee: Employee) => void;
} }
type SortField = 'name' | 'employeeType' | 'canWorkAlone' | 'role' | 'lastLogin';
type SortDirection = 'asc' | 'desc';
const EmployeeList: React.FC<EmployeeListProps> = ({ const EmployeeList: React.FC<EmployeeListProps> = ({
employees, employees,
onEdit, onEdit,
@@ -19,8 +22,11 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
}) => { }) => {
const [filter, setFilter] = useState<'all' | 'active' | 'inactive'>('active'); const [filter, setFilter] = useState<'all' | 'active' | 'inactive'>('active');
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [sortField, setSortField] = useState<SortField>('name');
const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
const { user: currentUser, hasRole } = useAuth(); const { user: currentUser, hasRole } = useAuth();
// Filter employees based on active/inactive and search term
const filteredEmployees = employees.filter(employee => { const filteredEmployees = employees.filter(employee => {
if (filter === 'active' && !employee.isActive) return false; if (filter === 'active' && !employee.isActive) return false;
if (filter === 'inactive' && employee.isActive) return false; if (filter === 'inactive' && employee.isActive) return false;
@@ -38,6 +44,60 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
return true; 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 // Simplified permission checks
const canDeleteEmployee = (employee: Employee): boolean => { const canDeleteEmployee = (employee: Employee): boolean => {
if (!hasRole(['admin'])) return false; if (!hasRole(['admin'])) return false;
@@ -78,8 +138,8 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
const getIndependenceBadge = (canWorkAlone: boolean) => { const getIndependenceBadge = (canWorkAlone: boolean) => {
return canWorkAlone return canWorkAlone
? { text: 'Eigenständig', color: '#27ae60', bgColor: '#d5f4e6' } ? { text: 'Eigenständig', color: '#27ae60', bgColor: '#d5f4e6' }
: { text: 'Betreuung', color: '#e74c3c', bgColor: '#fadbd8' }; : { text: 'Betreuung', color: '#e74c3c', bgColor: '#fadbd8' };
}; };
type Role = typeof ROLE_CONFIG[number]['value']; type Role = typeof ROLE_CONFIG[number]['value'];
@@ -161,7 +221,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
</div> </div>
<div style={{ color: '#7f8c8d', fontSize: '14px' }}> <div style={{ color: '#7f8c8d', fontSize: '14px' }}>
{filteredEmployees.length} von {employees.length} Mitarbeitern {sortedEmployees.length} von {employees.length} Mitarbeitern
</div> </div>
</div> </div>
@@ -185,16 +245,41 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
color: '#2c3e50', color: '#2c3e50',
alignItems: 'center' alignItems: 'center'
}}> }}>
<div>Name & E-Mail</div> <div
<div style={{ textAlign: 'center' }}>Typ</div> onClick={() => handleSort('name')}
<div style={{ textAlign: 'center' }}>Eigenständigkeit</div> style={{ cursor: 'pointer', userSelect: 'none', display: 'flex', alignItems: 'center', gap: '5px' }}
<div style={{ textAlign: 'center' }}>Rolle</div> >
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' }}>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 style={{ textAlign: 'center' }}>Aktionen</div>
</div> </div>
{filteredEmployees.map(employee => { {sortedEmployees.map(employee => {
const employeeType = getEmployeeTypeBadge(employee.employeeType); const employeeType = getEmployeeTypeBadge(employee.employeeType);
const independence = getIndependenceBadge(employee.canWorkAlone); const independence = getIndependenceBadge(employee.canWorkAlone);
const roleColor = getRoleBadge(employee.role); const roleColor = getRoleBadge(employee.role);

View File

@@ -206,102 +206,7 @@ const Help: React.FC = () => {
))} ))}
</div> </div>
</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>
</div>
{/* Business Rules */} {/* Business Rules */}
<div style={{ <div style={{
@@ -312,7 +217,7 @@ const Help: React.FC = () => {
boxShadow: '0 4px 6px rgba(0,0,0,0.1)', boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
border: '1px solid #e0e0e0' 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' }}> <div style={{ display: 'grid', gap: '10px' }}>
{businessRules.map((rule, index) => ( {businessRules.map((rule, index) => (
<div <div
@@ -383,7 +288,10 @@ const Help: React.FC = () => {
</div> </div>
</div> </div>
<div style={{
</div>
<div style={{
marginTop: '25px', marginTop: '25px',
padding: '20px', padding: '20px',
backgroundColor: '#e8f4fd', backgroundColor: '#e8f4fd',
@@ -398,7 +306,6 @@ const Help: React.FC = () => {
<li>Planen Sie Manager-Verfügbarkeit im Voraus</li> <li>Planen Sie Manager-Verfügbarkeit im Voraus</li>
</ul> </ul>
</div> </div>
</div>
<style>{` <style>{`
@keyframes pulse { @keyframes pulse {

View File

@@ -5,6 +5,7 @@ import { employeeService } from '../../services/employeeService';
import { useNotification } from '../../contexts/NotificationContext'; import { useNotification } from '../../contexts/NotificationContext';
import AvailabilityManager from '../Employees/components/AvailabilityManager'; import AvailabilityManager from '../Employees/components/AvailabilityManager';
import { Employee } from '../../models/Employee'; import { Employee } from '../../models/Employee';
import { styles } from './type/SettingsType';
const Settings: React.FC = () => { const Settings: React.FC = () => {
const { user: currentUser, updateUser } = useAuth(); const { user: currentUser, updateUser } = useAuth();
@@ -148,7 +149,12 @@ const Settings: React.FC = () => {
}; };
if (!currentUser) { if (!currentUser) {
return <div>Nicht eingeloggt</div>; return <div style={{
textAlign: 'center',
padding: '3rem',
color: '#666',
fontSize: '1.1rem'
}}>Nicht eingeloggt</div>;
} }
if (showAvailabilityManager) { if (showAvailabilityManager) {
@@ -161,395 +167,390 @@ const Settings: React.FC = () => {
); );
} }
return ( // Style constants for consistency
<div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
<h1> Einstellungen</h1>
{/* Tab Navigation */}
<div style={{ return (
display: 'flex', <div style={styles.container}>
borderBottom: '1px solid #e0e0e0', {/* Left Sidebar with Tabs */}
marginBottom: '30px' <div style={styles.sidebar}>
}}> <div style={styles.header}>
<button <h1 style={styles.title}>Einstellungen</h1>
onClick={() => setActiveTab('profile')} <div style={styles.subtitle}>Verwalten Sie Ihre Kontoeinstellungen und Präferenzen</div>
style={{ </div>
padding: '12px 24px',
backgroundColor: activeTab === 'profile' ? '#3498db' : 'transparent', <div style={styles.tabs}>
color: activeTab === 'profile' ? 'white' : '#333', <button
border: 'none', onClick={() => setActiveTab('profile')}
borderBottom: activeTab === 'profile' ? '3px solid #3498db' : 'none', style={{
cursor: 'pointer', ...styles.tab,
fontWeight: 'bold' ...(activeTab === 'profile' ? styles.tabActive : {})
}} }}
> onMouseEnter={(e) => {
👤 Profil if (activeTab !== 'profile') {
</button> e.currentTarget.style.background = styles.tabHover.background;
<button e.currentTarget.style.color = styles.tabHover.color;
onClick={() => setActiveTab('password')} e.currentTarget.style.transform = styles.tabHover.transform;
style={{ }
padding: '12px 24px', }}
backgroundColor: activeTab === 'password' ? '#3498db' : 'transparent', onMouseLeave={(e) => {
color: activeTab === 'password' ? 'white' : '#333', if (activeTab !== 'profile') {
border: 'none', e.currentTarget.style.background = styles.tab.background;
borderBottom: activeTab === 'password' ? '3px solid #3498db' : 'none', e.currentTarget.style.color = styles.tab.color;
cursor: 'pointer', e.currentTarget.style.transform = 'none';
fontWeight: 'bold' }
}} }}
> >
🔒 Passwort <span style={{ color: '#cda8f0', fontSize: '24px' }}>{'\u{1F464}\u{FE0E}'}</span>
</button> <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
<button <span style={{ fontSize: '0.95rem', fontWeight: 500 }}>Profil</span>
onClick={() => setActiveTab('availability')} <span style={{ fontSize: '0.8rem', opacity: 0.7, marginTop: '2px' }}>Persönliche Informationen</span>
style={{ </div>
padding: '12px 24px', </button>
backgroundColor: activeTab === 'availability' ? '#3498db' : 'transparent',
color: activeTab === 'availability' ? 'white' : '#333', <button
border: 'none', onClick={() => setActiveTab('password')}
borderBottom: activeTab === 'availability' ? '3px solid #3498db' : 'none', style={{
cursor: 'pointer', ...styles.tab,
fontWeight: 'bold' ...(activeTab === 'password' ? styles.tabActive : {})
}} }}
> onMouseEnter={(e) => {
📅 Verfügbarkeit if (activeTab !== 'password') {
</button> 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> </div>
{/* Profile Tab */} {/* Right Content Area */}
{activeTab === 'profile' && ( <div style={styles.content}>
<div style={{ {/* Profile Tab */}
backgroundColor: 'white', {activeTab === 'profile' && (
padding: '30px', <>
borderRadius: '8px', <div style={styles.section}>
border: '1px solid #e0e0e0', <h2 style={styles.sectionTitle}>Profilinformationen</h2>
boxShadow: '0 2px 4px rgba(0,0,0,0.1)' <p style={styles.sectionDescription}>
}}> Verwalten Sie Ihre persönlichen Informationen und Kontaktdaten
<h2 style={{ marginTop: 0, color: '#2c3e50' }}>Profilinformationen</h2> </p>
</div>
<form onSubmit={handleProfileUpdate}> <form onSubmit={handleProfileUpdate} style={{ marginTop: '2rem' }}>
<div style={{ display: 'grid', gap: '20px' }}> <div style={styles.formGrid}>
{/* Read-only information */} {/* Read-only information */}
<div style={{ <div style={styles.infoCard}>
padding: '15px', <h4 style={styles.infoCardTitle}>Systeminformationen</h4>
backgroundColor: '#f8f9fa', <div style={styles.infoGrid}>
borderRadius: '6px', <div style={styles.field}>
border: '1px solid #e9ecef' <label style={styles.fieldLabel}>
}}> E-Mail
<h4 style={{ margin: '0 0 15px 0', color: '#495057' }}>Systeminformationen</h4> </label>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '15px' }}> <input
<div> type="email"
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}> value={currentUser.email}
E-Mail disabled
</label> style={styles.fieldInputDisabled}
<input />
type="email" </div>
value={currentUser.email} <div style={styles.field}>
disabled <label style={styles.fieldLabel}>
style={{ Rolle
width: '95%', </label>
padding: '10px', <input
border: '1px solid #ddd', type="text"
borderRadius: '4px', value={currentUser.role}
backgroundColor: '#f8f9fa', disabled
color: '#666' style={styles.fieldInputDisabled}
}} />
/> </div>
</div> <div style={styles.field}>
<div> <label style={styles.fieldLabel}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}> Mitarbeiter Typ
Rolle </label>
</label> <input
<input type="text"
type="text" value={currentUser.employeeType}
value={currentUser.role} disabled
disabled style={styles.fieldInputDisabled}
style={{ />
width: '95%', </div>
padding: '10px', <div style={styles.field}>
border: '1px solid #ddd', <label style={styles.fieldLabel}>
borderRadius: '4px', Vertragstyp
backgroundColor: '#f8f9fa', </label>
color: '#666' <input
}} type="text"
/> value={currentUser.contractType}
disabled
style={styles.fieldInputDisabled}
/>
</div>
</div> </div>
</div> </div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '15px', marginTop: '15px' }}> <div style={styles.infoCard}>
<div> {/* Editable name field */}
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}> <div style={{ ...styles.field, marginTop: '1rem' }}>
Mitarbeiter Typ <label style={styles.fieldLabel}>
Vollständiger Name *
</label> </label>
<input <input
type="text" type="text"
value={currentUser.employeeType} name="name"
disabled value={profileForm.name}
onChange={handleProfileChange}
required
style={{ style={{
width: '95%', ...styles.fieldInput,
padding: '10px', width: '95%'
border: '1px solid #ddd',
borderRadius: '4px',
backgroundColor: '#f8f9fa',
color: '#666'
}} }}
/> placeholder="Ihr vollständiger Name"
</div> onFocus={(e) => {
<div> e.target.style.borderColor = '#1a1325';
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}> e.target.style.boxShadow = '0 0 0 3px rgba(26, 19, 37, 0.1)';
Vertragstyp }}
</label> onBlur={(e) => {
<input e.target.style.borderColor = '#e8e8e8';
type="text" e.target.style.boxShadow = 'none';
value={currentUser.contractType}
disabled
style={{
width: '95%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
backgroundColor: '#f8f9fa',
color: '#666'
}} }}
/> />
</div> </div>
</div> </div>
</div> </div>
<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' }}>
style={{ <div style={styles.formGridCompact}>
marginTop: '10px', <div style={styles.field}>
padding: '15px', <label style={styles.fieldLabel}>
backgroundColor: '#f8f9fa', Aktuelles Passwort *
borderRadius: '6px', </label>
border: '1px solid #e9ecef', <input
}} type="password"
> name="currentPassword"
<label value={passwordForm.currentPassword}
style={{ onChange={handlePasswordChange}
display: 'block', required
marginBottom: '8px', style={styles.fieldInput}
fontWeight: 'bold', placeholder="Aktuelles Passwort"
color: '#2c3e50', onFocus={(e) => {
}} e.target.style.borderColor = '#1a1325';
> e.target.style.boxShadow = '0 0 0 3px rgba(26, 19, 37, 0.1)';
Vollständiger Name * }}
</label> onBlur={(e) => {
<input e.target.style.borderColor = '#e8e8e8';
type="text" e.target.style.boxShadow = 'none';
name="name" }}
value={profileForm.name} />
onChange={handleProfileChange} </div>
required
style={{ <div style={styles.field}>
width: '97.5%', <label style={styles.fieldLabel}>
padding: '10px', Neues Passwort *
border: '1px solid #ddd', </label>
borderRadius: '4px', <input
fontSize: '16px', type="password"
}} name="newPassword"
placeholder="Ihr vollständiger Name" 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>
<div style={{ <div style={styles.availabilityCard}>
display: 'flex', <div style={styles.availabilityIcon}>📅</div>
gap: '15px', <h3 style={styles.availabilityTitle}>Verfügbarkeit verwalten</h3>
justifyContent: 'flex-end', <p style={styles.availabilityDescription}>
marginTop: '30px', Hier können Sie Ihre persönliche Verfügbarkeit für Schichtpläne festlegen.
paddingTop: '20px', Legen Sie für jeden Tag und jede Schicht fest, ob Sie bevorzugt, möglicherweise
borderTop: '1px solid #f0f0f0' oder nicht verfügbar sind.
}}> </p>
<button <button
type="submit" onClick={() => setShowAvailabilityManager(true)}
disabled={loading || !profileForm.name.trim()}
style={{ style={{
padding: '12px 24px', ...styles.button,
backgroundColor: loading ? '#bdc3c7' : (!profileForm.name.trim() ? '#95a5a6' : '#27ae60'), ...styles.buttonPrimary,
color: 'white', marginBottom: '2rem'
border: 'none', }}
borderRadius: '6px', onMouseEnter={(e) => {
cursor: (loading || !profileForm.name.trim()) ? 'not-allowed' : 'pointer', e.currentTarget.style.background = styles.buttonPrimaryHover.background;
fontWeight: 'bold' 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;
}} }}
> >
{loading ? '⏳ Wird gespeichert...' : 'Profil aktualisieren'} Verfügbarkeit bearbeiten
</button> </button>
</div> </div>
</form> </>
</div> )}
)} </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>
<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>
</div>
</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 { shiftPlanService } from '../../services/shiftPlanService';
import { employeeService } from '../../services/employeeService'; import { employeeService } from '../../services/employeeService';
import { shiftAssignmentService, ShiftAssignmentService } from '../../services/shiftAssignmentService'; 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 { ShiftPlan, TimeSlot, ScheduledShift } from '../../models/ShiftPlan';
import { Employee, EmployeeAvailability } from '../../models/Employee'; import { Employee, EmployeeAvailability } from '../../models/Employee';
import { useNotification } from '../../contexts/NotificationContext'; import { useNotification } from '../../contexts/NotificationContext';
import { formatDate, formatTime } from '../../utils/foramatters'; import { formatDate, formatTime } from '../../utils/foramatters';
import { isScheduledShift } from '../../models/helpers';
// Local interface extensions (same as AvailabilityManager) // Local interface extensions (same as AvailabilityManager)
interface ExtendedTimeSlot extends TimeSlot { interface ExtendedTimeSlot extends TimeSlot {
@@ -36,125 +35,301 @@ const ShiftPlanView: React.FC = () => {
const [shiftPlan, setShiftPlan] = useState<ShiftPlan | null>(null); const [shiftPlan, setShiftPlan] = useState<ShiftPlan | null>(null);
const [employees, setEmployees] = useState<Employee[]>([]); const [employees, setEmployees] = useState<Employee[]>([]);
const [availabilities, setAvailabilities] = useState<EmployeeAvailability[]>([]); 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 [loading, setLoading] = useState(true);
const [publishing, setPublishing] = useState(false); const [publishing, setPublishing] = useState(false);
const [scheduledShifts, setScheduledShifts] = useState<ScheduledShift[]>([]); const [scheduledShifts, setScheduledShifts] = useState<ScheduledShift[]>([]);
const [reverting, setReverting] = useState(false);
const [showAssignmentPreview, setShowAssignmentPreview] = useState(false); const [showAssignmentPreview, setShowAssignmentPreview] = useState(false);
const [recreating, setRecreating] = useState(false);
useEffect(() => { useEffect(() => {
loadShiftPlanData(); 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]); }, [id]);
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 () => { const loadShiftPlanData = async () => {
if (!id) return; if (!id) return;
try { try {
setLoading(true); setLoading(true);
const [plan, employeesData, shiftsData] = await Promise.all([
shiftPlanService.getShiftPlan(id),
employeeService.getEmployees(),
shiftAssignmentService.getScheduledShiftsForPlan(id) // Load shifts here
]);
setShiftPlan(plan); // Load plan and employees first
setEmployees(employeesData.filter(emp => emp.isActive)); const [plan, employeesData] = await Promise.all([
setScheduledShifts(shiftsData); shiftPlanService.getShiftPlan(id),
employeeService.getEmployees(),
]);
// Load availabilities for all employees setShiftPlan(plan);
const availabilityPromises = employeesData setEmployees(employeesData.filter(emp => emp.isActive));
.filter(emp => emp.isActive)
.map(emp => employeeService.getAvailabilities(emp.id));
const allAvailabilities = await Promise.all(availabilityPromises); // CRITICAL: Load scheduled shifts and verify they exist
const flattenedAvailabilities = allAvailabilities.flat(); const shiftsData = await shiftAssignmentService.getScheduledShiftsForPlan(id);
console.log('📋 Loaded scheduled shifts:', shiftsData.length);
// Filter availabilities to only include those for the current shift plan if (shiftsData.length === 0) {
const planAvailabilities = flattenedAvailabilities.filter( console.warn('⚠️ No scheduled shifts found for plan:', id);
availability => availability.planId === id
);
setAvailabilities(planAvailabilities);
debugAvailabilities();
} catch (error) {
console.error('Error loading shift plan data:', error);
showNotification({ showNotification({
type: 'error', type: 'warning',
title: 'Fehler', title: 'Keine Schichten gefunden',
message: 'Daten konnten nicht geladen werden.' message: 'Der Schichtplan hat keine generierten Schichten. Bitte überprüfen Sie die Plan-Konfiguration.'
}); });
} finally {
setLoading(false);
} }
};
const debugAvailabilities = () => { setScheduledShifts(shiftsData);
if (!shiftPlan || !employees.length || !availabilities.length) return;
console.log('🔍 AVAILABILITY ANALYSIS:', { // Load availabilities
totalAvailabilities: availabilities.length, const availabilityPromises = employeesData
employeesWithAvailabilities: new Set(availabilities.map(a => a.employeeId)).size, .filter(emp => emp.isActive)
totalEmployees: employees.length, .map(emp => employeeService.getAvailabilities(emp.id));
availabilityByEmployee: employees.map(emp => {
const empAvailabilities = availabilities.filter(a => a.employeeId === emp.id); const allAvailabilities = await Promise.all(availabilityPromises);
return { const flattenedAvailabilities = allAvailabilities.flat();
employee: emp.name,
availabilities: empAvailabilities.length, const planAvailabilities = flattenedAvailabilities.filter(
preferences: empAvailabilities.map(a => ({ availability => availability.planId === id
day: a.dayOfWeek, );
timeSlot: a.timeSlotId,
preference: a.preferenceLevel 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);
}
};
// Prüfe spezifisch für Manager/Admin const handleRecreateAssignments = async () => {
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 () => {
if (!shiftPlan) return; if (!shiftPlan) return;
try { try {
const shifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id); setRecreating(true);
console.log('🔍 SCHEDULED SHIFTS IN DATABASE:', {
total: shifts.length, if (!window.confirm('Möchten Sie die aktuellen Zuweisungen wirklich zurücksetzen? Alle vorhandenen Zuweisungen werden gelöscht.')) {
shifts: shifts.map(s => ({ return;
id: s.id, }
date: s.date,
timeSlotId: s.timeSlotId, console.log('🔄 STARTING COMPLETE ASSIGNMENT CLEARING PROCESS');
requiredEmployees: s.requiredEmployees
})) // 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
// 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.'
}); });
// 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');
}
} catch (error) { } catch (error) {
console.error('❌ Error loading scheduled shifts:', error); console.error('❌ Error clearing assignments:', error);
showNotification({
type: 'error',
title: 'Fehler',
message: `Löschen der Zuweisungen fehlgeschlagen: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}`
});
} finally {
setRecreating(false);
} }
}; };
const debugRenderLogic = () => {
if (!shiftPlan) return;
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++;
}
});
});
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`);
});
}
}
};
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 // Extract plan-specific shifts using the same logic as AvailabilityManager
const getTimetableData = () => { const getTimetableData = () => {
if (!shiftPlan || !shiftPlan.shifts || !shiftPlan.timeSlots) { if (!shiftPlan || !shiftPlan.shifts || !shiftPlan.timeSlots) {
return { days: [], timeSlotsByDay: {}, allTimeSlots: [] }; return { days: [], timeSlotsByDay: {}, allTimeSlots: [] };
} }
@@ -216,174 +391,46 @@ const ShiftPlanView: React.FC = () => {
return date.getDay() === 0 ? 7 : date.getDay(); 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 () => { const handlePreviewAssignment = async () => {
if (!shiftPlan) return; if (!shiftPlan) return;
debugScheduledShifts();
try { try {
setPublishing(true); setPublishing(true);
// DEBUG: Überprüfe die Eingabedaten // FORCE COMPLETE REFRESH - don't rely on cached state
console.log('🔍 INPUT DATA FOR SCHEDULING:', { const [refreshedEmployees, refreshedAvailabilities] = await Promise.all([
shiftPlan: { // Reload employees fresh
id: shiftPlan.id, employeeService.getEmployees().then(emps => emps.filter(emp => emp.isActive)),
name: shiftPlan.name, // Reload availabilities fresh
shifts: shiftPlan.shifts?.length, refreshAllAvailabilities()
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
}))
});
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( const result = await ShiftAssignmentService.assignShifts(
shiftPlan, shiftPlan,
employees, refreshedEmployees, // Use fresh array, not state
availabilities, refreshedAvailabilities, // Use fresh array, not state
{ constraints // Now this variable is defined
enforceExperiencedWithChef: true,
enforceNoTraineeAlone: true,
maxRepairAttempts: 50
}
); );
// 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); setAssignmentResult(result);
setShowAssignmentPreview(true); 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) { } catch (error) {
console.error('Error during assignment:', error); console.error('Error during assignment:', error);
showNotification({ 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 () => { const handlePublish = async () => {
if (!shiftPlan || !assignmentResult) return; if (!shiftPlan || !assignmentResult) return;
@@ -418,15 +516,18 @@ const ShiftPlanView: React.FC = () => {
const updatePromises = updatedShifts.map(async (scheduledShift) => { const updatePromises = updatedShifts.map(async (scheduledShift) => {
const assignedEmployees = assignmentResult.assignments[scheduledShift.id] || []; 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 { try {
// Update the shift with assigned employees // Update the shift with assigned employees
const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
await shiftAssignmentService.updateScheduledShift(scheduledShift.id, { await shiftAssignmentService.updateScheduledShift(scheduledShift.id, {
assignedEmployees 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) { } catch (error) {
console.error(`❌ Failed to update shift ${scheduledShift.id}:`, error); console.error(`❌ Failed to update shift ${scheduledShift.id}:`, error);
throw error; throw error;
@@ -450,18 +551,6 @@ const ShiftPlanView: React.FC = () => {
setShiftPlan(reloadedPlan); setShiftPlan(reloadedPlan);
setScheduledShifts(reloadedShifts); 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({ showNotification({
type: 'success', type: 'success',
title: 'Erfolg', title: 'Erfolg',
@@ -488,53 +577,78 @@ const ShiftPlanView: React.FC = () => {
} }
}; };
const handleRevertToDraft = async () => { const refreshAllAvailabilities = async (): Promise<EmployeeAvailability[]> => {
if (!shiftPlan || !id) return;
if (!window.confirm('Möchten Sie diesen Schichtplan wirklich zurück in den Entwurfsstatus setzen? Alle Zuweisungen werden entfernt.')) {
return;
}
try { try {
setReverting(true); console.log('🔄 Force refreshing ALL availabilities with error handling...');
// 1. Zuerst zurücksetzen if (!id) {
const updatedPlan = await shiftPlanService.revertToDraft(id); console.error('❌ No plan ID available');
return [];
}
// 2. Dann ALLE Daten neu laden const availabilityPromises = employees
await loadShiftPlanData(); .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
}
});
// 3. Assignment-Result zurücksetzen const allAvailabilities = await Promise.all(availabilityPromises);
setAssignmentResult(null); const flattenedAvailabilities = allAvailabilities.flat();
// 4. Preview schließen falls geöffnet // More robust filtering
setShowAssignmentPreview(false); const planAvailabilities = flattenedAvailabilities.filter(
availability => availability && availability.planId === id
);
showNotification({ console.log(`✅ Successfully refreshed ${planAvailabilities.length} availabilities for plan ${id}`);
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); // IMMEDIATELY update state
console.log('Scheduled shifts after revert:', { setAvailabilities(planAvailabilities);
hasScheduledShifts: !! scheduledShifts,
count: scheduledShifts.length || 0,
firstFew: scheduledShifts?.slice(0, 3)
});
return planAvailabilities;
} catch (error) { } catch (error) {
console.error('Error reverting plan to draft:', error); console.error('❌ Critical error refreshing availabilities:', error);
showNotification({ // DON'T return old data - throw error or return empty array
type: 'error', throw new Error('Failed to refresh availabilities: ' + error);
title: 'Fehler',
message: 'Schichtplan konnte nicht zurückgesetzt werden.'
});
} finally {
setReverting(false);
} }
}; };
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 = () => { const canPublish = () => {
if (!shiftPlan || shiftPlan.status === 'published') return false; if (!shiftPlan || shiftPlan.status === 'published') return false;
@@ -560,40 +674,49 @@ const ShiftPlanView: React.FC = () => {
}; };
}; };
const debugCurrentState = () => { const reloadAvailabilities = async () => {
console.log('🔍 CURRENT STATE DEBUG:', { try {
shiftPlan: shiftPlan ? { console.log('🔄 Lade Verfügbarkeiten neu...');
id: shiftPlan.id,
name: shiftPlan.name, // Load availabilities for all employees
status: shiftPlan.status const availabilityPromises = employees
} : null, .filter(emp => emp.isActive)
scheduledShifts: { .map(emp => employeeService.getAvailabilities(emp.id));
count: scheduledShifts.length,
withAssignments: scheduledShifts.filter(s => s.assignedEmployees && s.assignedEmployees.length > 0).length, const allAvailabilities = await Promise.all(availabilityPromises);
details: scheduledShifts.map(s => ({ const flattenedAvailabilities = allAvailabilities.flat();
id: s.id,
date: s.date, // Filter availabilities to only include those for the current shift plan
timeSlotId: s.timeSlotId, const planAvailabilities = flattenedAvailabilities.filter(
assignedEmployees: s.assignedEmployees availability => availability.planId === id
})) );
},
employees: employees.length 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 // Render timetable using the same structure as AvailabilityManager
const renderTimetable = () => { const renderTimetable = () => {
debugAssignments(); const { days, allTimeSlots, timeSlotsByDay } = getExtendedTimetableData();
debugCurrentState();
const { days, allTimeSlots, timeSlotsByDay } = getTimetableData();
if (!shiftPlan?.id) { if (!shiftPlan?.id) {
console.warn("Shift plan ID is missing"); console.warn("Shift plan ID is missing");
return null; return null;
} }
//const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
if (days.length === 0 || allTimeSlots.length === 0) { if (days.length === 0 || allTimeSlots.length === 0) {
return ( return (
<div style={{ <div style={{
@@ -692,7 +815,6 @@ const ShiftPlanView: React.FC = () => {
); );
} }
// Get assigned employees for this shift
let assignedEmployees: string[] = []; let assignedEmployees: string[] = [];
let displayText = ''; let displayText = '';
@@ -701,11 +823,17 @@ const ShiftPlanView: React.FC = () => {
const scheduledShift = scheduledShifts.find(scheduled => { const scheduledShift = scheduledShifts.find(scheduled => {
const scheduledDayOfWeek = getDayOfWeek(scheduled.date); const scheduledDayOfWeek = getDayOfWeek(scheduled.date);
return scheduledDayOfWeek === weekday.id && return scheduledDayOfWeek === weekday.id &&
scheduled.timeSlotId === timeSlot.id; scheduled.timeSlotId === timeSlot.id;
}); });
if (scheduledShift) { if (scheduledShift) {
assignedEmployees = scheduledShift.assignedEmployees || []; 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 => { displayText = assignedEmployees.map(empId => {
const employee = employees.find(emp => emp.id === empId); const employee = employees.find(emp => emp.id === empId);
return employee ? employee.name : 'Unbekannt'; return employee ? employee.name : 'Unbekannt';
@@ -716,7 +844,7 @@ const ShiftPlanView: React.FC = () => {
const scheduledShift = scheduledShifts.find(scheduled => { const scheduledShift = scheduledShifts.find(scheduled => {
const scheduledDayOfWeek = getDayOfWeek(scheduled.date); const scheduledDayOfWeek = getDayOfWeek(scheduled.date);
return scheduledDayOfWeek === weekday.id && return scheduledDayOfWeek === weekday.id &&
scheduled.timeSlotId === timeSlot.id; scheduled.timeSlotId === timeSlot.id;
}); });
if (scheduledShift && assignmentResult.assignments[scheduledShift.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) { if (!displayText) {
const shiftsForSlot = shiftPlan?.shifts?.filter(shift => const shiftsForSlot = shiftPlan?.shifts?.filter(shift =>
shift.dayOfWeek === weekday.id && shift.dayOfWeek === weekday.id &&
@@ -738,7 +866,13 @@ const ShiftPlanView: React.FC = () => {
const totalRequired = shiftsForSlot.reduce((sum, shift) => const totalRequired = shiftsForSlot.reduce((sum, shift) =>
sum + shift.requiredEmployees, 0); sum + shift.requiredEmployees, 0);
// Show "0/2" instead of just "0" to indicate it's empty
displayText = `0/${totalRequired}`; displayText = `0/${totalRequired}`;
// Optional: Show empty state more clearly
if (totalRequired === 0) {
displayText = '-';
}
} }
return ( return (
@@ -766,7 +900,7 @@ const ShiftPlanView: React.FC = () => {
if (loading) return <div>Lade Schichtplan...</div>; if (loading) return <div>Lade Schichtplan...</div>;
if (!shiftPlan) return <div>Schichtplan nicht gefunden</div>; if (!shiftPlan) return <div>Schichtplan nicht gefunden</div>;
const { days, allTimeSlots } = getTimetableData(); const { days, allTimeSlots } = getExtendedTimetableData();
const availabilityStatus = getAvailabilityStatus(); const availabilityStatus = getAvailabilityStatus();
@@ -799,25 +933,25 @@ const ShiftPlanView: React.FC = () => {
{shiftPlan.status === 'published' ? 'Veröffentlicht' : 'Entwurf'} {shiftPlan.status === 'published' ? 'Veröffentlicht' : 'Entwurf'}
</div> </div>
</div> </div>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}> {shiftPlan.status === 'published' && hasRole(['admin', 'instandhalter']) && (
{shiftPlan.status === 'published' && hasRole(['admin', 'instandhalter']) && (
<button <button
onClick={handleRevertToDraft} onClick={handleRecreateAssignments}
disabled={reverting} disabled={recreating}
style={{ style={{
padding: '10px 20px', padding: '10px 20px',
backgroundColor: '#e74c3c', backgroundColor: '#e74c3c',
color: 'white', color: 'white',
border: 'none', border: 'none',
borderRadius: '4px', borderRadius: '4px',
cursor: 'pointer', cursor: recreating ? 'not-allowed' : 'pointer',
fontWeight: 'bold' fontWeight: 'bold'
}} }}
> >
{reverting ? 'Zurücksetzen...' : 'Zu Entwurf zurücksetzen'} {recreating ? 'Lösche Zuweisungen...' : 'Zuweisungen neu berechnen'}
</button> </button>
)} )}
<button <button
onClick={() => navigate('/shift-plans')} onClick={() => navigate('/shift-plans')}
style={{ 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 { ShiftPlan, ScheduledShift } from '../models/ShiftPlan';
import { Employee, EmployeeAvailability } from '../models/Employee'; import { Employee, EmployeeAvailability } from '../models/Employee';
import { authService } from './authService'; import { authService } from './authService';
import { scheduleWithManager } from './scheduling/shiftScheduler'; import { IntelligentShiftScheduler, AssignmentResult, WeeklyPattern } from './scheduling';
import { transformToSchedulingData } from './scheduling/dataAdapter';
import { AssignmentResult, WeeklyPattern } from './scheduling/types';
import { isScheduledShift } from '../models/helpers'; import { isScheduledShift } from '../models/helpers';
const API_BASE_URL = 'http://localhost:3002/api/scheduled-shifts'; const API_BASE_URL = 'http://localhost:3002/api/scheduled-shifts';
// Helper function to get auth headers // Helper function to get auth headers
const getAuthHeaders = () => { const getAuthHeaders = () => {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
@@ -21,7 +21,7 @@ const getAuthHeaders = () => {
export class ShiftAssignmentService { export class ShiftAssignmentService {
async updateScheduledShift(id: string, updates: { assignedEmployees: string[] }): Promise<void> { async updateScheduledShift(id: string, updates: { assignedEmployees: string[] }): Promise<void> {
try { 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}`, { const response = await fetch(`${API_BASE_URL}/${id}`, {
method: 'PUT', method: 'PUT',
@@ -141,65 +141,64 @@ export class ShiftAssignmentService {
constraints: any = {} constraints: any = {}
): Promise<AssignmentResult> { ): Promise<AssignmentResult> {
console.log('🔄 Starting enhanced scheduling algorithm...'); console.log('🧠 Starting intelligent scheduling for FIRST WEEK ONLY...');
// Get defined shifts for the first week // Load all scheduled shifts
const definedShifts = await this.getDefinedShifts(shiftPlan); const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
const firstWeekShifts = this.getFirstWeekShifts(definedShifts);
console.log('📊 First week analysis:', { if (scheduledShifts.length === 0) {
totalShifts: definedShifts.length, return {
firstWeekShifts: firstWeekShifts.length, assignments: {},
employees: employees.length 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 // Set cache for scheduler
const { schedulingEmployees, schedulingShifts, managerShifts } = transformToSchedulingData( IntelligentShiftScheduler.scheduledShiftsCache.set(shiftPlan.id, scheduledShifts);
// 🔥 RUN SCHEDULING FOR FIRST WEEK ONLY
const schedulingResult = await IntelligentShiftScheduler.generateOptimalSchedule(
shiftPlan,
employees.filter(emp => emp.isActive), employees.filter(emp => emp.isActive),
firstWeekShifts, availabilities,
availabilities constraints
); );
console.log('🎯 Transformed data for scheduling:', { // Get first week shifts for pattern
employees: schedulingEmployees.length, const firstWeekShifts = this.getFirstWeekShifts(scheduledShifts);
shifts: schedulingShifts.length,
managerShifts: managerShifts.length 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 = { const weeklyPattern: WeeklyPattern = {
weekShifts: firstWeekShifts, weekShifts: firstWeekShifts,
assignments: schedulingResult.assignments, assignments: schedulingResult.assignments, // 🔥 Diese enthalten nur erste Woche
weekNumber: 1 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 { return {
assignments: allAssignments, assignments: allAssignments, // 🔥 Diese enthalten alle Wochen
violations: schedulingResult.violations, violations: schedulingResult.violations,
success: schedulingResult.violations.length === 0, success: schedulingResult.success,
pattern: weeklyPattern, 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[] } = {}; const assignments: { [shiftId: string]: string[] } = {};
// Group all shifts by week // Group all shifts by week AND day-timeSlot combination
const shiftsByWeek = this.groupShiftsByWeek(allShifts); const shiftsByPatternKey = new Map<string, ScheduledShift[]>();
console.log('📅 Applying weekly pattern to', Object.keys(shiftsByWeek).length, 'weeks'); allShifts.forEach(shift => {
const dayOfWeek = this.getDayOfWeek(shift.date);
const patternKey = `${dayOfWeek}-${shift.timeSlotId}`;
// For each week, apply the pattern from week 1 if (!shiftsByPatternKey.has(patternKey)) {
Object.entries(shiftsByWeek).forEach(([weekKey, weekShifts]) => { shiftsByPatternKey.set(patternKey, []);
const weekNumber = parseInt(weekKey); }
shiftsByPatternKey.get(patternKey)!.push(shift);
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] = [];
}
});
}); });
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; 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 // Get specific template or plan
getTemplate: async (id: string): Promise<ShiftPlan> => { getTemplate: async (id: string): Promise<ShiftPlan> => {
const response = await fetch(`${API_BASE}/${id}`, { const response = await fetch(`${API_BASE}/${id}`, {
@@ -219,4 +199,29 @@ export const shiftPlanService = {
description: preset.description 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, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"ignoreDeprecations": "6.0", "ignoreDeprecations": "6.0",
"jsx": "react-jsx" "jsx": "react-jsx",
"downlevelIteration": true
}, },
"include": [ "include": [
"src" "src"