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,8 +132,6 @@ const AppContent: React.FC = () => {
function App() { function App() {
return ( return (
<DesignSystemProvider>
<GlobalStyles />
<NotificationProvider> <NotificationProvider>
<AuthProvider> <AuthProvider>
<Router> <Router>
@@ -143,7 +140,6 @@ function App() {
</Router> </Router>
</AuthProvider> </AuthProvider>
</NotificationProvider> </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,101 +206,6 @@ 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 */}
@@ -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,6 +288,9 @@ const Help: React.FC = () => {
</div> </div>
</div> </div>
</div>
<div style={{ <div style={{
marginTop: '25px', marginTop: '25px',
padding: '20px', padding: '20px',
@@ -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,177 +167,173 @@ 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}>
<h1 style={styles.title}>Einstellungen</h1>
<div style={styles.subtitle}>Verwalten Sie Ihre Kontoeinstellungen und Präferenzen</div>
</div>
<div style={styles.tabs}>
<button <button
onClick={() => setActiveTab('profile')} onClick={() => setActiveTab('profile')}
style={{ style={{
padding: '12px 24px', ...styles.tab,
backgroundColor: activeTab === 'profile' ? '#3498db' : 'transparent', ...(activeTab === 'profile' ? styles.tabActive : {})
color: activeTab === 'profile' ? 'white' : '#333', }}
border: 'none', onMouseEnter={(e) => {
borderBottom: activeTab === 'profile' ? '3px solid #3498db' : 'none', if (activeTab !== 'profile') {
cursor: 'pointer', e.currentTarget.style.background = styles.tabHover.background;
fontWeight: 'bold' e.currentTarget.style.color = styles.tabHover.color;
e.currentTarget.style.transform = styles.tabHover.transform;
}
}}
onMouseLeave={(e) => {
if (activeTab !== 'profile') {
e.currentTarget.style.background = styles.tab.background;
e.currentTarget.style.color = styles.tab.color;
e.currentTarget.style.transform = 'none';
}
}} }}
> >
👤 Profil <span style={{ color: '#cda8f0', fontSize: '24px' }}>{'\u{1F464}\u{FE0E}'}</span>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
<span style={{ fontSize: '0.95rem', fontWeight: 500 }}>Profil</span>
<span style={{ fontSize: '0.8rem', opacity: 0.7, marginTop: '2px' }}>Persönliche Informationen</span>
</div>
</button> </button>
<button <button
onClick={() => setActiveTab('password')} onClick={() => setActiveTab('password')}
style={{ style={{
padding: '12px 24px', ...styles.tab,
backgroundColor: activeTab === 'password' ? '#3498db' : 'transparent', ...(activeTab === 'password' ? styles.tabActive : {})
color: activeTab === 'password' ? 'white' : '#333', }}
border: 'none', onMouseEnter={(e) => {
borderBottom: activeTab === 'password' ? '3px solid #3498db' : 'none', if (activeTab !== 'password') {
cursor: 'pointer', e.currentTarget.style.background = styles.tabHover.background;
fontWeight: 'bold' 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';
}
}} }}
> >
🔒 Passwort <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>
<button <button
onClick={() => setActiveTab('availability')} onClick={() => setActiveTab('availability')}
style={{ style={{
padding: '12px 24px', ...styles.tab,
backgroundColor: activeTab === 'availability' ? '#3498db' : 'transparent', ...(activeTab === 'availability' ? styles.tabActive : {})
color: activeTab === 'availability' ? 'white' : '#333', }}
border: 'none', onMouseEnter={(e) => {
borderBottom: activeTab === 'availability' ? '3px solid #3498db' : 'none', if (activeTab !== 'availability') {
cursor: 'pointer', e.currentTarget.style.background = styles.tabHover.background;
fontWeight: 'bold' 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';
}
}} }}
> >
📅 Verfügbarkeit <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> </button>
</div> </div>
</div>
{/* Right Content Area */}
<div style={styles.content}>
{/* Profile Tab */} {/* Profile Tab */}
{activeTab === 'profile' && ( {activeTab === 'profile' && (
<div style={{ <>
backgroundColor: 'white', <div style={styles.section}>
padding: '30px', <h2 style={styles.sectionTitle}>Profilinformationen</h2>
borderRadius: '8px', <p style={styles.sectionDescription}>
border: '1px solid #e0e0e0', Verwalten Sie Ihre persönlichen Informationen und Kontaktdaten
boxShadow: '0 2px 4px rgba(0,0,0,0.1)' </p>
}}> </div>
<h2 style={{ marginTop: 0, color: '#2c3e50' }}>Profilinformationen</h2>
<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}>
}}>
<h4 style={{ margin: '0 0 15px 0', color: '#495057' }}>Systeminformationen</h4>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '15px' }}>
<div>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}>
E-Mail E-Mail
</label> </label>
<input <input
type="email" type="email"
value={currentUser.email} value={currentUser.email}
disabled disabled
style={{ style={styles.fieldInputDisabled}
width: '95%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
backgroundColor: '#f8f9fa',
color: '#666'
}}
/> />
</div> </div>
<div> <div style={styles.field}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}> <label style={styles.fieldLabel}>
Rolle Rolle
</label> </label>
<input <input
type="text" type="text"
value={currentUser.role} value={currentUser.role}
disabled disabled
style={{ style={styles.fieldInputDisabled}
width: '95%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
backgroundColor: '#f8f9fa',
color: '#666'
}}
/> />
</div> </div>
</div> <div style={styles.field}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '15px', marginTop: '15px' }}> <label style={styles.fieldLabel}>
<div>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}>
Mitarbeiter Typ Mitarbeiter Typ
</label> </label>
<input <input
type="text" type="text"
value={currentUser.employeeType} value={currentUser.employeeType}
disabled disabled
style={{ style={styles.fieldInputDisabled}
width: '95%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
backgroundColor: '#f8f9fa',
color: '#666'
}}
/> />
</div> </div>
<div> <div style={styles.field}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}> <label style={styles.fieldLabel}>
Vertragstyp Vertragstyp
</label> </label>
<input <input
type="text" type="text"
value={currentUser.contractType} value={currentUser.contractType}
disabled disabled
style={{ style={styles.fieldInputDisabled}
width: '95%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
backgroundColor: '#f8f9fa',
color: '#666'
}}
/> />
</div> </div>
</div> </div>
</div> </div>
</div> <div style={styles.infoCard}>
{/* Editable name field */}
<div <div style={{ ...styles.field, marginTop: '1rem' }}>
style={{ <label style={styles.fieldLabel}>
marginTop: '10px',
padding: '15px',
backgroundColor: '#f8f9fa',
borderRadius: '6px',
border: '1px solid #e9ecef',
}}
>
<label
style={{
display: 'block',
marginBottom: '8px',
fontWeight: 'bold',
color: '#2c3e50',
}}
>
Vollständiger Name * Vollständiger Name *
</label> </label>
<input <input
@@ -341,59 +343,68 @@ const Settings: React.FC = () => {
onChange={handleProfileChange} onChange={handleProfileChange}
required required
style={{ style={{
width: '97.5%', ...styles.fieldInput,
padding: '10px', width: '95%'
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px',
}} }}
placeholder="Ihr vollständiger Name" placeholder="Ihr vollständiger Name"
onFocus={(e) => {
e.target.style.borderColor = '#1a1325';
e.target.style.boxShadow = '0 0 0 3px rgba(26, 19, 37, 0.1)';
}}
onBlur={(e) => {
e.target.style.borderColor = '#e8e8e8';
e.target.style.boxShadow = 'none';
}}
/> />
</div> </div>
</div>
</div>
<div style={{ <div style={styles.actions}>
display: 'flex',
gap: '15px',
justifyContent: 'flex-end',
marginTop: '30px',
paddingTop: '20px',
borderTop: '1px solid #f0f0f0'
}}>
<button <button
type="submit" type="submit"
disabled={loading || !profileForm.name.trim()} disabled={loading || !profileForm.name.trim()}
style={{ style={{
padding: '12px 24px', ...styles.button,
backgroundColor: loading ? '#bdc3c7' : (!profileForm.name.trim() ? '#95a5a6' : '#27ae60'), ...styles.buttonPrimary,
color: 'white', ...((loading || !profileForm.name.trim()) ? styles.buttonDisabled : {})
border: 'none', }}
borderRadius: '6px', onMouseEnter={(e) => {
cursor: (loading || !profileForm.name.trim()) ? 'not-allowed' : 'pointer', if (!loading && profileForm.name.trim()) {
fontWeight: 'bold' 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'} {loading ? '⏳ Wird gespeichert...' : 'Profil aktualisieren'}
</button> </button>
</div> </div>
</form> </form>
</div> </>
)} )}
{/* Password Tab */} {/* Password Tab */}
{activeTab === 'password' && ( {activeTab === 'password' && (
<div style={{ <>
backgroundColor: 'white', <div style={styles.section}>
padding: '30px', <h2 style={styles.sectionTitle}>Passwort ändern</h2>
borderRadius: '8px', <p style={styles.sectionDescription}>
border: '1px solid #e0e0e0', Aktualisieren Sie Ihr Passwort für erhöhte Sicherheit
boxShadow: '0 2px 4px rgba(0,0,0,0.1)' </p>
}}> </div>
<h2 style={{ marginTop: 0, color: '#2c3e50' }}>Passwort ändern</h2>
<form onSubmit={handlePasswordUpdate}> <form onSubmit={handlePasswordUpdate} style={{ marginTop: '2rem' }}>
<div style={{ display: 'grid', gap: '20px', maxWidth: '400px' }}> <div style={styles.formGridCompact}>
<div> <div style={styles.field}>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}> <label style={styles.fieldLabel}>
Aktuelles Passwort * Aktuelles Passwort *
</label> </label>
<input <input
@@ -402,19 +413,21 @@ const Settings: React.FC = () => {
value={passwordForm.currentPassword} value={passwordForm.currentPassword}
onChange={handlePasswordChange} onChange={handlePasswordChange}
required required
style={{ style={styles.fieldInput}
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px'
}}
placeholder="Aktuelles Passwort" placeholder="Aktuelles Passwort"
onFocus={(e) => {
e.target.style.borderColor = '#1a1325';
e.target.style.boxShadow = '0 0 0 3px rgba(26, 19, 37, 0.1)';
}}
onBlur={(e) => {
e.target.style.borderColor = '#e8e8e8';
e.target.style.boxShadow = 'none';
}}
/> />
</div> </div>
<div> <div style={styles.field}>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}> <label style={styles.fieldLabel}>
Neues Passwort * Neues Passwort *
</label> </label>
<input <input
@@ -424,22 +437,24 @@ const Settings: React.FC = () => {
onChange={handlePasswordChange} onChange={handlePasswordChange}
required required
minLength={6} minLength={6}
style={{ style={styles.fieldInput}
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px'
}}
placeholder="Mindestens 6 Zeichen" 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={{ fontSize: '12px', color: '#7f8c8d', marginTop: '5px' }}> <div style={styles.fieldHint}>
Das Passwort muss mindestens 6 Zeichen lang sein. Das Passwort muss mindestens 6 Zeichen lang sein.
</div> </div>
</div> </div>
<div> <div style={styles.field}>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}> <label style={styles.fieldLabel}>
Neues Passwort bestätigen * Neues Passwort bestätigen *
</label> </label>
<input <input
@@ -448,67 +463,65 @@ const Settings: React.FC = () => {
value={passwordForm.confirmPassword} value={passwordForm.confirmPassword}
onChange={handlePasswordChange} onChange={handlePasswordChange}
required required
style={{ style={styles.fieldInput}
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px'
}}
placeholder="Passwort wiederholen" 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> </div>
<div style={{ <div style={styles.actions}>
display: 'flex',
gap: '15px',
justifyContent: 'flex-end',
marginTop: '30px',
paddingTop: '20px',
borderTop: '1px solid #f0f0f0'
}}>
<button <button
type="submit" type="submit"
disabled={loading || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword} disabled={loading || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword}
style={{ style={{
padding: '12px 24px', ...styles.button,
backgroundColor: loading ? '#bdc3c7' : (!passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword ? '#95a5a6' : '#3498db'), ...styles.buttonPrimary,
color: 'white', ...((loading || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword) ? styles.buttonDisabled : {})
border: 'none', }}
borderRadius: '6px', onMouseEnter={(e) => {
cursor: (loading || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword) ? 'not-allowed' : 'pointer', if (!loading && passwordForm.currentPassword && passwordForm.newPassword && passwordForm.confirmPassword) {
fontWeight: 'bold' 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'} {loading ? '⏳ Wird geändert...' : 'Passwort ändern'}
</button> </button>
</div> </div>
</form> </form>
</div> </>
)} )}
{/* Availability Tab */} {/* Availability Tab */}
{activeTab === 'availability' && ( {activeTab === 'availability' && (
<div style={{ <>
backgroundColor: 'white', <div style={styles.section}>
padding: '30px', <h2 style={styles.sectionTitle}>Meine Verfügbarkeit</h2>
borderRadius: '8px', <p style={styles.sectionDescription}>
border: '1px solid #e0e0e0', Legen Sie Ihre persönliche Verfügbarkeit für Schichtpläne fest
boxShadow: '0 2px 4px rgba(0,0,0,0.1)' </p>
}}> </div>
<h2 style={{ marginTop: 0, color: '#2c3e50' }}>Meine Verfügbarkeit</h2>
<div style={{ <div style={styles.availabilityCard}>
padding: '30px', <div style={styles.availabilityIcon}>📅</div>
textAlign: 'center', <h3 style={styles.availabilityTitle}>Verfügbarkeit verwalten</h3>
backgroundColor: '#f8f9fa', <p style={styles.availabilityDescription}>
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. 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 Legen Sie für jeden Tag und jede Schicht fest, ob Sie bevorzugt, möglicherweise
oder nicht verfügbar sind. oder nicht verfügbar sind.
@@ -517,40 +530,28 @@ const Settings: React.FC = () => {
<button <button
onClick={() => setShowAvailabilityManager(true)} onClick={() => setShowAvailabilityManager(true)}
style={{ style={{
padding: '12px 24px', ...styles.button,
backgroundColor: '#3498db', ...styles.buttonPrimary,
color: 'white', marginBottom: '2rem'
border: 'none', }}
borderRadius: '6px', onMouseEnter={(e) => {
cursor: 'pointer', e.currentTarget.style.background = styles.buttonPrimaryHover.background;
fontWeight: 'bold', e.currentTarget.style.transform = styles.buttonPrimaryHover.transform;
fontSize: '16px' e.currentTarget.style.boxShadow = styles.buttonPrimaryHover.boxShadow;
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = styles.buttonPrimary.background;
e.currentTarget.style.transform = 'none';
e.currentTarget.style.boxShadow = styles.buttonPrimary.boxShadow;
}} }}
> >
Verfügbarkeit bearbeiten Verfügbarkeit bearbeiten
</button> </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> </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,34 +35,83 @@ 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([
// Load plan and employees first
const [plan, employeesData] = await Promise.all([
shiftPlanService.getShiftPlan(id), shiftPlanService.getShiftPlan(id),
employeeService.getEmployees(), employeeService.getEmployees(),
shiftAssignmentService.getScheduledShiftsForPlan(id) // Load shifts here
]); ]);
setShiftPlan(plan); setShiftPlan(plan);
setEmployees(employeesData.filter(emp => emp.isActive)); setEmployees(employeesData.filter(emp => emp.isActive));
// CRITICAL: Load scheduled shifts and verify they exist
const shiftsData = await shiftAssignmentService.getScheduledShiftsForPlan(id);
console.log('📋 Loaded scheduled shifts:', shiftsData.length);
if (shiftsData.length === 0) {
console.warn('⚠️ No scheduled shifts found for plan:', id);
showNotification({
type: 'warning',
title: 'Keine Schichten gefunden',
message: 'Der Schichtplan hat keine generierten Schichten. Bitte überprüfen Sie die Plan-Konfiguration.'
});
}
setScheduledShifts(shiftsData); setScheduledShifts(shiftsData);
// Load availabilities for all employees // Load availabilities
const availabilityPromises = employeesData const availabilityPromises = employeesData
.filter(emp => emp.isActive) .filter(emp => emp.isActive)
.map(emp => employeeService.getAvailabilities(emp.id)); .map(emp => employeeService.getAvailabilities(emp.id));
@@ -71,13 +119,11 @@ const ShiftPlanView: React.FC = () => {
const allAvailabilities = await Promise.all(availabilityPromises); const allAvailabilities = await Promise.all(availabilityPromises);
const flattenedAvailabilities = allAvailabilities.flat(); const flattenedAvailabilities = allAvailabilities.flat();
// Filter availabilities to only include those for the current shift plan
const planAvailabilities = flattenedAvailabilities.filter( const planAvailabilities = flattenedAvailabilities.filter(
availability => availability.planId === id availability => availability.planId === id
); );
setAvailabilities(planAvailabilities); setAvailabilities(planAvailabilities);
debugAvailabilities();
} catch (error) { } catch (error) {
console.error('Error loading shift plan data:', error); console.error('Error loading shift plan data:', error);
@@ -89,70 +135,199 @@ const ShiftPlanView: React.FC = () => {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const debugAvailabilities = () => { const handleRecreateAssignments = async () => {
if (!shiftPlan || !employees.length || !availabilities.length) return;
console.log('🔍 AVAILABILITY ANALYSIS:', {
totalAvailabilities: availabilities.length,
employeesWithAvailabilities: new Set(availabilities.map(a => a.employeeId)).size,
totalEmployees: employees.length,
availabilityByEmployee: employees.map(emp => {
const empAvailabilities = availabilities.filter(a => a.employeeId === emp.id);
return {
employee: emp.name,
availabilities: empAvailabilities.length,
preferences: empAvailabilities.map(a => ({
day: a.dayOfWeek,
timeSlot: a.timeSlotId,
preference: a.preferenceLevel
}))
};
})
});
// Prüfe spezifisch für Manager/Admin
const manager = employees.find(emp => emp.role === 'admin');
if (manager) {
const managerAvailabilities = availabilities.filter(a => a.employeeId === manager.id);
console.log('🔍 MANAGER AVAILABILITIES:', {
manager: manager.name,
availabilities: managerAvailabilities.length,
details: managerAvailabilities.map(a => ({
day: a.dayOfWeek,
timeSlot: a.timeSlotId,
preference: a.preferenceLevel
}))
});
}
};
const debugScheduledShifts = async () => {
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) {
@@ -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,51 +577,76 @@ 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
await loadShiftPlanData();
// 3. Assignment-Result zurücksetzen
setAssignmentResult(null);
// 4. Preview schließen falls geöffnet
setShowAssignmentPreview(false);
showNotification({
type: 'success',
title: 'Erfolg',
message: 'Schichtplan wurde erfolgreich zurück in den Entwurfsstatus gesetzt. Alle Daten wurden neu geladen.'
});
//const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
console.log('Scheduled shifts after revert:', {
hasScheduledShifts: !! scheduledShifts,
count: scheduledShifts.length || 0,
firstFew: scheduledShifts?.slice(0, 3)
});
} catch (error) {
console.error('Error reverting plan to draft:', error);
showNotification({
type: 'error',
title: 'Fehler',
message: 'Schichtplan konnte nicht zurückgesetzt werden.'
});
} finally {
setReverting(false);
} }
const availabilityPromises = employees
.filter(emp => emp.isActive)
.map(async (emp) => {
try {
return await employeeService.getAvailabilities(emp.id);
} catch (error) {
console.error(`❌ Failed to load availabilities for ${emp.name}:`, error);
return []; // Return empty array instead of failing entire operation
}
});
const allAvailabilities = await Promise.all(availabilityPromises);
const flattenedAvailabilities = allAvailabilities.flat();
// More robust filtering
const planAvailabilities = flattenedAvailabilities.filter(
availability => availability && availability.planId === id
);
console.log(`✅ Successfully refreshed ${planAvailabilities.length} availabilities for plan ${id}`);
// IMMEDIATELY update state
setAvailabilities(planAvailabilities);
return planAvailabilities;
} catch (error) {
console.error('❌ Critical error refreshing availabilities:', error);
// DON'T return old data - throw error or return empty array
throw new Error('Failed to refresh availabilities: ' + error);
}
};
const validateSchedulingData = (): boolean => {
console.log('🔍 Validating scheduling data...');
const totalEmployees = employees.length;
const employeesWithAvailabilities = new Set(
availabilities.map(avail => avail.employeeId)
).size;
const availabilityStatus = {
totalEmployees,
employeesWithAvailabilities,
coverage: Math.round((employeesWithAvailabilities / totalEmployees) * 100)
};
console.log('📊 Availability Coverage:', availabilityStatus);
// Check if we have ALL employee availabilities
if (employeesWithAvailabilities < totalEmployees) {
const missingEmployees = employees.filter(emp =>
!availabilities.some(avail => avail.employeeId === emp.id)
);
console.warn('⚠️ Missing availabilities for employees:',
missingEmployees.map(emp => emp.name));
return false;
}
return true;
}; };
const canPublish = () => { const canPublish = () => {
@@ -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 = '';
@@ -706,6 +828,12 @@ const ShiftPlanView: React.FC = () => {
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';
@@ -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 };
const { schedulingEmployees, schedulingShifts, managerShifts } = transformToSchedulingData(
employees.filter(emp => emp.isActive),
firstWeekShifts,
availabilities
);
console.log('🎯 Transformed data for scheduling:', {
employees: schedulingEmployees.length,
shifts: schedulingShifts.length,
managerShifts: managerShifts.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
} }
// Set cache for scheduler
IntelligentShiftScheduler.scheduledShiftsCache.set(shiftPlan.id, scheduledShifts);
// 🔥 RUN SCHEDULING FOR FIRST WEEK ONLY
const schedulingResult = await IntelligentShiftScheduler.generateOptimalSchedule(
shiftPlan,
employees.filter(emp => emp.isActive),
availabilities,
constraints
); );
console.log('📊 Enhanced scheduling completed:', { // Get first week shifts for pattern
assignments: Object.keys(schedulingResult.assignments).length, const firstWeekShifts = this.getFirstWeekShifts(scheduledShifts);
violations: schedulingResult.violations.length,
success: schedulingResult.success console.log('🔄 Creating weekly pattern from FIRST WEEK:', {
firstWeekShifts: firstWeekShifts.length,
allShifts: scheduledShifts.length,
patternAssignments: Object.keys(schedulingResult.assignments).length
}); });
// 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,28 +335,47 @@ 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 => { console.log('📊 Pattern application analysis:');
// Find the corresponding shift in the weekly pattern console.log('- Unique pattern keys:', shiftsByPatternKey.size);
const patternShift = this.findMatchingPatternShift(shift, weeklyPattern.weekShifts); console.log('- Pattern keys:', Array.from(shiftsByPatternKey.keys()));
if (patternShift) { // For each shift in all weeks, find the matching pattern shift
// Use the same assignment as the 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]]; assignments[shift.id] = [...weeklyPattern.assignments[patternShift.id]];
} else { } else {
// No matching pattern shift, leave empty
assignments[shift.id] = []; 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"