mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-11-30 22:45:46 +01:00
reworked scheduling
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -921,4 +874,63 @@ export const updateScheduledShift = async (req: AuthRequest, res: Response): Pro
|
|||||||
console.error('❌ Error updating scheduled shift:', error);
|
console.error('❌ Error updating scheduled shift:', error);
|
||||||
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' });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -5,7 +5,6 @@ import { AuthProvider, useAuth } from './contexts/AuthContext';
|
|||||||
import { NotificationProvider } from './contexts/NotificationContext';
|
import { NotificationProvider } from './contexts/NotificationContext';
|
||||||
import NotificationContainer from './components/Notification/NotificationContainer';
|
import NotificationContainer from './components/Notification/NotificationContainer';
|
||||||
import Layout from './components/Layout/Layout';
|
import Layout from './components/Layout/Layout';
|
||||||
import { DesignSystemProvider, GlobalStyles } from './design/DesignSystem';
|
|
||||||
import Login from './pages/Auth/Login';
|
import Login from './pages/Auth/Login';
|
||||||
import Dashboard from './pages/Dashboard/Dashboard';
|
import Dashboard from './pages/Dashboard/Dashboard';
|
||||||
import ShiftPlanList from './pages/ShiftPlans/ShiftPlanList';
|
import ShiftPlanList from './pages/ShiftPlans/ShiftPlanList';
|
||||||
@@ -133,17 +132,14 @@ const AppContent: React.FC = () => {
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<DesignSystemProvider>
|
<NotificationProvider>
|
||||||
<GlobalStyles />
|
<AuthProvider>
|
||||||
<NotificationProvider>
|
<Router>
|
||||||
<AuthProvider>
|
<NotificationContainer />
|
||||||
<Router>
|
<AppContent />
|
||||||
<NotificationContainer />
|
</Router>
|
||||||
<AppContent />
|
</AuthProvider>
|
||||||
</Router>
|
</NotificationProvider>
|
||||||
</AuthProvider>
|
|
||||||
</NotificationProvider>
|
|
||||||
</DesignSystemProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}}>
|
||||||
© 2025 Schichtenplaner. Alle Rechte vorbehalten. |
|
© 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>
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -122,275 +120,4 @@ export const designTokens = {
|
|||||||
xl: '1280px',
|
xl: '1280px',
|
||||||
'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;
|
|
||||||
@@ -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';
|
|
||||||
@@ -282,12 +282,7 @@ const Dashboard: React.FC = () => {
|
|||||||
percentage
|
percentage
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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' }}>
|
||||||
|
|||||||
@@ -437,6 +437,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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -206,102 +206,7 @@ const Help: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Network Visualization */}
|
|
||||||
<div style={{
|
|
||||||
display: 'grid',
|
|
||||||
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
|
|
||||||
gap: '15px',
|
|
||||||
marginTop: '30px'
|
|
||||||
}}>
|
|
||||||
{/* Employees */}
|
|
||||||
<div style={{
|
|
||||||
backgroundColor: '#e8f4fd',
|
|
||||||
padding: '15px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid #b8d4f0'
|
|
||||||
}}>
|
|
||||||
<h4 style={{ margin: '0 0 10px 0', color: '#3498db' }}>👥 Mitarbeiter</h4>
|
|
||||||
<div style={{ fontSize: '14px', lineHeight: '1.4' }}>
|
|
||||||
<div>• Manager (1)</div>
|
|
||||||
<div>• Erfahrene ({currentStage >= 1 ? '3' : '0'})</div>
|
|
||||||
<div>• Neue ({currentStage >= 1 ? '2' : '0'})</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Shifts */}
|
|
||||||
<div style={{
|
|
||||||
backgroundColor: '#fff3cd',
|
|
||||||
padding: '15px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid #ffeaa7'
|
|
||||||
}}>
|
|
||||||
<h4 style={{ margin: '0 0 10px 0', color: '#f39c12' }}>📅 Schichten</h4>
|
|
||||||
<div style={{ fontSize: '14px', lineHeight: '1.4' }}>
|
|
||||||
<div>• Vormittag (5)</div>
|
|
||||||
<div>• Nachmittag (4)</div>
|
|
||||||
<div>• Manager-Schichten (3)</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Current Actions */}
|
|
||||||
<div style={{
|
|
||||||
backgroundColor: '#d4edda',
|
|
||||||
padding: '15px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid #c3e6cb'
|
|
||||||
}}>
|
|
||||||
<h4 style={{ margin: '0 0 10px 0', color: '#27ae60' }}>⚡ Aktive Aktionen</h4>
|
|
||||||
<div style={{ fontSize: '14px', lineHeight: '1.4' }}>
|
|
||||||
{currentStage === 0 && (
|
|
||||||
<>
|
|
||||||
<div>• Grundzuweisung läuft</div>
|
|
||||||
<div>• Erfahrene priorisieren</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{currentStage === 1 && (
|
|
||||||
<>
|
|
||||||
<div>• Manager wird zugewiesen</div>
|
|
||||||
<div>• Erfahrene suchen</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{currentStage === 2 && (
|
|
||||||
<>
|
|
||||||
<div>• Überbesetzung prüfen</div>
|
|
||||||
<div>• Pool-Verwaltung aktiv</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{currentStage === 3 && (
|
|
||||||
<>
|
|
||||||
<div>• Finale Validierung</div>
|
|
||||||
<div>• Bericht generieren</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Problems & Solutions */}
|
|
||||||
<div style={{
|
|
||||||
backgroundColor: '#f8d7da',
|
|
||||||
padding: '15px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid #f5c6cb'
|
|
||||||
}}>
|
|
||||||
<h4 style={{ margin: '0 0 10px 0', color: '#e74c3c' }}>🔍 Probleme & Lösungen</h4>
|
|
||||||
<div style={{ fontSize: '14px', lineHeight: '1.4' }}>
|
|
||||||
{currentStage >= 2 ? (
|
|
||||||
<>
|
|
||||||
<div style={{ color: '#27ae60' }}>✅ 2 Probleme behoben</div>
|
|
||||||
<div style={{ color: '#e74c3c' }}>❌ 0 kritische Probleme</div>
|
|
||||||
<div style={{ color: '#f39c12' }}>⚠️ 1 Warnung</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div>Noch keine Probleme analysiert</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Business Rules */}
|
{/* Business Rules */}
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -312,7 +217,7 @@ const Help: React.FC = () => {
|
|||||||
boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
|
boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
|
||||||
border: '1px solid #e0e0e0'
|
border: '1px solid #e0e0e0'
|
||||||
}}>
|
}}>
|
||||||
<h2 style={{ color: '#2c3e50', marginBottom: '20px' }}>📋 Geschäftsregeln</h2>
|
<h2 style={{ color: '#2c3e50', marginBottom: '20px' }}>📋 Validierungs Regeln</h2>
|
||||||
<div style={{ display: 'grid', gap: '10px' }}>
|
<div style={{ display: 'grid', gap: '10px' }}>
|
||||||
{businessRules.map((rule, index) => (
|
{businessRules.map((rule, index) => (
|
||||||
<div
|
<div
|
||||||
@@ -383,7 +288,10 @@ const Help: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
marginTop: '25px',
|
marginTop: '25px',
|
||||||
padding: '20px',
|
padding: '20px',
|
||||||
backgroundColor: '#e8f4fd',
|
backgroundColor: '#e8f4fd',
|
||||||
@@ -398,7 +306,6 @@ const Help: React.FC = () => {
|
|||||||
<li>Planen Sie Manager-Verfügbarkeit im Voraus</li>
|
<li>Planen Sie Manager-Verfügbarkeit im Voraus</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>{`
|
<style>{`
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { employeeService } from '../../services/employeeService';
|
|||||||
import { useNotification } from '../../contexts/NotificationContext';
|
import { useNotification } from '../../contexts/NotificationContext';
|
||||||
import AvailabilityManager from '../Employees/components/AvailabilityManager';
|
import AvailabilityManager from '../Employees/components/AvailabilityManager';
|
||||||
import { Employee } from '../../models/Employee';
|
import { Employee } from '../../models/Employee';
|
||||||
|
import { styles } from './type/SettingsType';
|
||||||
|
|
||||||
const Settings: React.FC = () => {
|
const Settings: React.FC = () => {
|
||||||
const { user: currentUser, updateUser } = useAuth();
|
const { user: currentUser, updateUser } = useAuth();
|
||||||
@@ -148,7 +149,12 @@ const Settings: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
return <div>Nicht eingeloggt</div>;
|
return <div style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '3rem',
|
||||||
|
color: '#666',
|
||||||
|
fontSize: '1.1rem'
|
||||||
|
}}>Nicht eingeloggt</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showAvailabilityManager) {
|
if (showAvailabilityManager) {
|
||||||
@@ -161,395 +167,390 @@ const Settings: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Style constants for consistency
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
|
<div style={styles.container}>
|
||||||
<h1>⚙️ Einstellungen</h1>
|
{/* Left Sidebar with Tabs */}
|
||||||
|
<div style={styles.sidebar}>
|
||||||
{/* Tab Navigation */}
|
<div style={styles.header}>
|
||||||
<div style={{
|
<h1 style={styles.title}>Einstellungen</h1>
|
||||||
display: 'flex',
|
<div style={styles.subtitle}>Verwalten Sie Ihre Kontoeinstellungen und Präferenzen</div>
|
||||||
borderBottom: '1px solid #e0e0e0',
|
</div>
|
||||||
marginBottom: '30px'
|
|
||||||
}}>
|
<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;
|
||||||
>
|
}
|
||||||
👤 Profil
|
}}
|
||||||
</button>
|
onMouseLeave={(e) => {
|
||||||
<button
|
if (activeTab !== 'profile') {
|
||||||
onClick={() => setActiveTab('password')}
|
e.currentTarget.style.background = styles.tab.background;
|
||||||
style={{
|
e.currentTarget.style.color = styles.tab.color;
|
||||||
padding: '12px 24px',
|
e.currentTarget.style.transform = 'none';
|
||||||
backgroundColor: activeTab === 'password' ? '#3498db' : 'transparent',
|
}
|
||||||
color: activeTab === 'password' ? 'white' : '#333',
|
}}
|
||||||
border: 'none',
|
>
|
||||||
borderBottom: activeTab === 'password' ? '3px solid #3498db' : 'none',
|
<span style={{ color: '#cda8f0', fontSize: '24px' }}>{'\u{1F464}\u{FE0E}'}</span>
|
||||||
cursor: 'pointer',
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||||
fontWeight: 'bold'
|
<span style={{ fontSize: '0.95rem', fontWeight: 500 }}>Profil</span>
|
||||||
}}
|
<span style={{ fontSize: '0.8rem', opacity: 0.7, marginTop: '2px' }}>Persönliche Informationen</span>
|
||||||
>
|
</div>
|
||||||
🔒 Passwort
|
</button>
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('availability')}
|
onClick={() => setActiveTab('password')}
|
||||||
style={{
|
style={{
|
||||||
padding: '12px 24px',
|
...styles.tab,
|
||||||
backgroundColor: activeTab === 'availability' ? '#3498db' : 'transparent',
|
...(activeTab === 'password' ? styles.tabActive : {})
|
||||||
color: activeTab === 'availability' ? 'white' : '#333',
|
}}
|
||||||
border: 'none',
|
onMouseEnter={(e) => {
|
||||||
borderBottom: activeTab === 'availability' ? '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;
|
||||||
>
|
}
|
||||||
📅 Verfügbarkeit
|
}}
|
||||||
</button>
|
onMouseLeave={(e) => {
|
||||||
|
if (activeTab !== 'password') {
|
||||||
|
e.currentTarget.style.background = styles.tab.background;
|
||||||
|
e.currentTarget.style.color = styles.tab.color;
|
||||||
|
e.currentTarget.style.transform = 'none';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: '1.2rem', width: '24px' }}>🔒</span>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||||
|
<span style={{ fontSize: '0.95rem', fontWeight: 500 }}>Passwort</span>
|
||||||
|
<span style={{ fontSize: '0.8rem', opacity: 0.7, marginTop: '2px' }}>Sicherheitseinstellungen</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('availability')}
|
||||||
|
style={{
|
||||||
|
...styles.tab,
|
||||||
|
...(activeTab === 'availability' ? styles.tabActive : {})
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (activeTab !== 'availability') {
|
||||||
|
e.currentTarget.style.background = styles.tabHover.background;
|
||||||
|
e.currentTarget.style.color = styles.tabHover.color;
|
||||||
|
e.currentTarget.style.transform = styles.tabHover.transform;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
if (activeTab !== 'availability') {
|
||||||
|
e.currentTarget.style.background = styles.tab.background;
|
||||||
|
e.currentTarget.style.color = styles.tab.color;
|
||||||
|
e.currentTarget.style.transform = 'none';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: '1.2rem', width: '24px' }}>📅</span>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||||
|
<span style={{ fontSize: '0.95rem', fontWeight: 500 }}>Verfügbarkeit</span>
|
||||||
|
<span style={{ fontSize: '0.8rem', opacity: 0.7, marginTop: '2px' }}>Schichtplanung</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Profile Tab */}
|
{/* Right Content Area */}
|
||||||
{activeTab === 'profile' && (
|
<div style={styles.content}>
|
||||||
<div style={{
|
{/* Profile Tab */}
|
||||||
backgroundColor: 'white',
|
{activeTab === 'profile' && (
|
||||||
padding: '30px',
|
<>
|
||||||
borderRadius: '8px',
|
<div style={styles.section}>
|
||||||
border: '1px solid #e0e0e0',
|
<h2 style={styles.sectionTitle}>Profilinformationen</h2>
|
||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
<p style={styles.sectionDescription}>
|
||||||
}}>
|
Verwalten Sie Ihre persönlichen Informationen und Kontaktdaten
|
||||||
<h2 style={{ marginTop: 0, color: '#2c3e50' }}>Profilinformationen</h2>
|
</p>
|
||||||
|
|
||||||
<form onSubmit={handleProfileUpdate}>
|
|
||||||
<div style={{ display: 'grid', gap: '20px' }}>
|
|
||||||
{/* Read-only information */}
|
|
||||||
<div style={{
|
|
||||||
padding: '15px',
|
|
||||||
backgroundColor: '#f8f9fa',
|
|
||||||
borderRadius: '6px',
|
|
||||||
border: '1px solid #e9ecef'
|
|
||||||
}}>
|
|
||||||
<h4 style={{ margin: '0 0 15px 0', color: '#495057' }}>Systeminformationen</h4>
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '15px' }}>
|
|
||||||
<div>
|
|
||||||
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}>
|
|
||||||
E-Mail
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
value={currentUser.email}
|
|
||||||
disabled
|
|
||||||
style={{
|
|
||||||
width: '95%',
|
|
||||||
padding: '10px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
borderRadius: '4px',
|
|
||||||
backgroundColor: '#f8f9fa',
|
|
||||||
color: '#666'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}>
|
|
||||||
Rolle
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={currentUser.role}
|
|
||||||
disabled
|
|
||||||
style={{
|
|
||||||
width: '95%',
|
|
||||||
padding: '10px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
borderRadius: '4px',
|
|
||||||
backgroundColor: '#f8f9fa',
|
|
||||||
color: '#666'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '15px', marginTop: '15px' }}>
|
|
||||||
<div>
|
|
||||||
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}>
|
|
||||||
Mitarbeiter Typ
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={currentUser.employeeType}
|
|
||||||
disabled
|
|
||||||
style={{
|
|
||||||
width: '95%',
|
|
||||||
padding: '10px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
borderRadius: '4px',
|
|
||||||
backgroundColor: '#f8f9fa',
|
|
||||||
color: '#666'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}>
|
|
||||||
Vertragstyp
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={currentUser.contractType}
|
|
||||||
disabled
|
|
||||||
style={{
|
|
||||||
width: '95%',
|
|
||||||
padding: '10px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
borderRadius: '4px',
|
|
||||||
backgroundColor: '#f8f9fa',
|
|
||||||
color: '#666'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
marginTop: '10px',
|
|
||||||
padding: '15px',
|
|
||||||
backgroundColor: '#f8f9fa',
|
|
||||||
borderRadius: '6px',
|
|
||||||
border: '1px solid #e9ecef',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
style={{
|
|
||||||
display: 'block',
|
|
||||||
marginBottom: '8px',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: '#2c3e50',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Vollständiger Name *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="name"
|
|
||||||
value={profileForm.name}
|
|
||||||
onChange={handleProfileChange}
|
|
||||||
required
|
|
||||||
style={{
|
|
||||||
width: '97.5%',
|
|
||||||
padding: '10px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
borderRadius: '4px',
|
|
||||||
fontSize: '16px',
|
|
||||||
}}
|
|
||||||
placeholder="Ihr vollständiger Name"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
|
||||||
gap: '15px',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
marginTop: '30px',
|
|
||||||
paddingTop: '20px',
|
|
||||||
borderTop: '1px solid #f0f0f0'
|
|
||||||
}}>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={loading || !profileForm.name.trim()}
|
|
||||||
style={{
|
|
||||||
padding: '12px 24px',
|
|
||||||
backgroundColor: loading ? '#bdc3c7' : (!profileForm.name.trim() ? '#95a5a6' : '#27ae60'),
|
|
||||||
color: 'white',
|
|
||||||
border: 'none',
|
|
||||||
borderRadius: '6px',
|
|
||||||
cursor: (loading || !profileForm.name.trim()) ? 'not-allowed' : 'pointer',
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{loading ? '⏳ Wird gespeichert...' : 'Profil aktualisieren'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Password Tab */}
|
|
||||||
{activeTab === 'password' && (
|
|
||||||
<div style={{
|
|
||||||
backgroundColor: 'white',
|
|
||||||
padding: '30px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid #e0e0e0',
|
|
||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
|
||||||
}}>
|
|
||||||
<h2 style={{ marginTop: 0, color: '#2c3e50' }}>Passwort ändern</h2>
|
|
||||||
|
|
||||||
<form onSubmit={handlePasswordUpdate}>
|
|
||||||
<div style={{ display: 'grid', gap: '20px', maxWidth: '400px' }}>
|
|
||||||
<div>
|
|
||||||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
|
|
||||||
Aktuelles Passwort *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="currentPassword"
|
|
||||||
value={passwordForm.currentPassword}
|
|
||||||
onChange={handlePasswordChange}
|
|
||||||
required
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
padding: '10px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
borderRadius: '4px',
|
|
||||||
fontSize: '16px'
|
|
||||||
}}
|
|
||||||
placeholder="Aktuelles Passwort"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
|
|
||||||
Neues Passwort *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="newPassword"
|
|
||||||
value={passwordForm.newPassword}
|
|
||||||
onChange={handlePasswordChange}
|
|
||||||
required
|
|
||||||
minLength={6}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
padding: '10px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
borderRadius: '4px',
|
|
||||||
fontSize: '16px'
|
|
||||||
}}
|
|
||||||
placeholder="Mindestens 6 Zeichen"
|
|
||||||
/>
|
|
||||||
<div style={{ fontSize: '12px', color: '#7f8c8d', marginTop: '5px' }}>
|
|
||||||
Das Passwort muss mindestens 6 Zeichen lang sein.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
|
|
||||||
Neues Passwort bestätigen *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="confirmPassword"
|
|
||||||
value={passwordForm.confirmPassword}
|
|
||||||
onChange={handlePasswordChange}
|
|
||||||
required
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
padding: '10px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
borderRadius: '4px',
|
|
||||||
fontSize: '16px'
|
|
||||||
}}
|
|
||||||
placeholder="Passwort wiederholen"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
|
||||||
gap: '15px',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
marginTop: '30px',
|
|
||||||
paddingTop: '20px',
|
|
||||||
borderTop: '1px solid #f0f0f0'
|
|
||||||
}}>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={loading || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword}
|
|
||||||
style={{
|
|
||||||
padding: '12px 24px',
|
|
||||||
backgroundColor: loading ? '#bdc3c7' : (!passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword ? '#95a5a6' : '#3498db'),
|
|
||||||
color: 'white',
|
|
||||||
border: 'none',
|
|
||||||
borderRadius: '6px',
|
|
||||||
cursor: (loading || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword) ? 'not-allowed' : 'pointer',
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{loading ? '⏳ Wird geändert...' : 'Passwort ändern'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Availability Tab */}
|
|
||||||
{activeTab === 'availability' && (
|
|
||||||
<div style={{
|
|
||||||
backgroundColor: 'white',
|
|
||||||
padding: '30px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid #e0e0e0',
|
|
||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
|
||||||
}}>
|
|
||||||
<h2 style={{ marginTop: 0, color: '#2c3e50' }}>Meine Verfügbarkeit</h2>
|
|
||||||
|
|
||||||
<div style={{
|
|
||||||
padding: '30px',
|
|
||||||
textAlign: 'center',
|
|
||||||
backgroundColor: '#f8f9fa',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '2px dashed #dee2e6'
|
|
||||||
}}>
|
|
||||||
<div style={{ fontSize: '48px', marginBottom: '20px' }}>📅</div>
|
|
||||||
<h3 style={{ color: '#2c3e50' }}>Verfügbarkeit verwalten</h3>
|
|
||||||
<p style={{ color: '#6c757d', marginBottom: '25px' }}>
|
|
||||||
Hier können Sie Ihre persönliche Verfügbarkeit für Schichtpläne festlegen.
|
|
||||||
Legen Sie für jeden Tag und jede Schicht fest, ob Sie bevorzugt, möglicherweise
|
|
||||||
oder nicht verfügbar sind.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<button
|
<form onSubmit={handleProfileUpdate} style={{ marginTop: '2rem' }}>
|
||||||
onClick={() => setShowAvailabilityManager(true)}
|
<div style={styles.formGrid}>
|
||||||
style={{
|
{/* Read-only information */}
|
||||||
padding: '12px 24px',
|
<div style={styles.infoCard}>
|
||||||
backgroundColor: '#3498db',
|
<h4 style={styles.infoCardTitle}>Systeminformationen</h4>
|
||||||
color: 'white',
|
<div style={styles.infoGrid}>
|
||||||
border: 'none',
|
<div style={styles.field}>
|
||||||
borderRadius: '6px',
|
<label style={styles.fieldLabel}>
|
||||||
cursor: 'pointer',
|
E-Mail
|
||||||
fontWeight: 'bold',
|
</label>
|
||||||
fontSize: '16px'
|
<input
|
||||||
}}
|
type="email"
|
||||||
>
|
value={currentUser.email}
|
||||||
Verfügbarkeit bearbeiten
|
disabled
|
||||||
</button>
|
style={styles.fieldInputDisabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={styles.field}>
|
||||||
|
<label style={styles.fieldLabel}>
|
||||||
|
Rolle
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={currentUser.role}
|
||||||
|
disabled
|
||||||
|
style={styles.fieldInputDisabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={styles.field}>
|
||||||
|
<label style={styles.fieldLabel}>
|
||||||
|
Mitarbeiter Typ
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={currentUser.employeeType}
|
||||||
|
disabled
|
||||||
|
style={styles.fieldInputDisabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={styles.field}>
|
||||||
|
<label style={styles.fieldLabel}>
|
||||||
|
Vertragstyp
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={currentUser.contractType}
|
||||||
|
disabled
|
||||||
|
style={styles.fieldInputDisabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={styles.infoCard}>
|
||||||
|
{/* Editable name field */}
|
||||||
|
<div style={{ ...styles.field, marginTop: '1rem' }}>
|
||||||
|
<label style={styles.fieldLabel}>
|
||||||
|
Vollständiger Name *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
value={profileForm.name}
|
||||||
|
onChange={handleProfileChange}
|
||||||
|
required
|
||||||
|
style={{
|
||||||
|
...styles.fieldInput,
|
||||||
|
width: '95%'
|
||||||
|
}}
|
||||||
|
placeholder="Ihr vollständiger Name"
|
||||||
|
onFocus={(e) => {
|
||||||
|
e.target.style.borderColor = '#1a1325';
|
||||||
|
e.target.style.boxShadow = '0 0 0 3px rgba(26, 19, 37, 0.1)';
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
e.target.style.borderColor = '#e8e8e8';
|
||||||
|
e.target.style.boxShadow = 'none';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{
|
<div style={styles.actions}>
|
||||||
marginTop: '20px',
|
<button
|
||||||
padding: '15px',
|
type="submit"
|
||||||
backgroundColor: '#e8f4fd',
|
disabled={loading || !profileForm.name.trim()}
|
||||||
border: '1px solid #b6d7e8',
|
style={{
|
||||||
borderRadius: '6px',
|
...styles.button,
|
||||||
fontSize: '14px',
|
...styles.buttonPrimary,
|
||||||
color: '#2c3e50',
|
...((loading || !profileForm.name.trim()) ? styles.buttonDisabled : {})
|
||||||
textAlign: 'left'
|
}}
|
||||||
}}>
|
onMouseEnter={(e) => {
|
||||||
<strong>💡 Informationen:</strong>
|
if (!loading && profileForm.name.trim()) {
|
||||||
<ul style={{ margin: '8px 0 0 20px', padding: 0 }}>
|
e.currentTarget.style.background = styles.buttonPrimaryHover.background;
|
||||||
<li><strong>Bevorzugt:</strong> Sie möchten diese Schicht arbeiten</li>
|
e.currentTarget.style.transform = styles.buttonPrimaryHover.transform;
|
||||||
<li><strong>Möglich:</strong> Sie können diese Schicht arbeiten</li>
|
e.currentTarget.style.boxShadow = styles.buttonPrimaryHover.boxShadow;
|
||||||
<li><strong>Nicht möglich:</strong> Sie können diese Schicht nicht arbeiten</li>
|
}
|
||||||
</ul>
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
if (!loading && profileForm.name.trim()) {
|
||||||
|
e.currentTarget.style.background = styles.buttonPrimary.background;
|
||||||
|
e.currentTarget.style.transform = 'none';
|
||||||
|
e.currentTarget.style.boxShadow = styles.buttonPrimary.boxShadow;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? '⏳ Wird gespeichert...' : 'Profil aktualisieren'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Password Tab */}
|
||||||
|
{activeTab === 'password' && (
|
||||||
|
<>
|
||||||
|
<div style={styles.section}>
|
||||||
|
<h2 style={styles.sectionTitle}>Passwort ändern</h2>
|
||||||
|
<p style={styles.sectionDescription}>
|
||||||
|
Aktualisieren Sie Ihr Passwort für erhöhte Sicherheit
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
<form onSubmit={handlePasswordUpdate} style={{ marginTop: '2rem' }}>
|
||||||
)}
|
<div style={styles.formGridCompact}>
|
||||||
|
<div style={styles.field}>
|
||||||
|
<label style={styles.fieldLabel}>
|
||||||
|
Aktuelles Passwort *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="currentPassword"
|
||||||
|
value={passwordForm.currentPassword}
|
||||||
|
onChange={handlePasswordChange}
|
||||||
|
required
|
||||||
|
style={styles.fieldInput}
|
||||||
|
placeholder="Aktuelles Passwort"
|
||||||
|
onFocus={(e) => {
|
||||||
|
e.target.style.borderColor = '#1a1325';
|
||||||
|
e.target.style.boxShadow = '0 0 0 3px rgba(26, 19, 37, 0.1)';
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
e.target.style.borderColor = '#e8e8e8';
|
||||||
|
e.target.style.boxShadow = 'none';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.field}>
|
||||||
|
<label style={styles.fieldLabel}>
|
||||||
|
Neues Passwort *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="newPassword"
|
||||||
|
value={passwordForm.newPassword}
|
||||||
|
onChange={handlePasswordChange}
|
||||||
|
required
|
||||||
|
minLength={6}
|
||||||
|
style={styles.fieldInput}
|
||||||
|
placeholder="Mindestens 6 Zeichen"
|
||||||
|
onFocus={(e) => {
|
||||||
|
e.target.style.borderColor = '#1a1325';
|
||||||
|
e.target.style.boxShadow = '0 0 0 3px rgba(26, 19, 37, 0.1)';
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
e.target.style.borderColor = '#e8e8e8';
|
||||||
|
e.target.style.boxShadow = 'none';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={styles.fieldHint}>
|
||||||
|
Das Passwort muss mindestens 6 Zeichen lang sein.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.field}>
|
||||||
|
<label style={styles.fieldLabel}>
|
||||||
|
Neues Passwort bestätigen *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="confirmPassword"
|
||||||
|
value={passwordForm.confirmPassword}
|
||||||
|
onChange={handlePasswordChange}
|
||||||
|
required
|
||||||
|
style={styles.fieldInput}
|
||||||
|
placeholder="Passwort wiederholen"
|
||||||
|
onFocus={(e) => {
|
||||||
|
e.target.style.borderColor = '#1a1325';
|
||||||
|
e.target.style.boxShadow = '0 0 0 3px rgba(26, 19, 37, 0.1)';
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
e.target.style.borderColor = '#e8e8e8';
|
||||||
|
e.target.style.boxShadow = 'none';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.actions}>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword}
|
||||||
|
style={{
|
||||||
|
...styles.button,
|
||||||
|
...styles.buttonPrimary,
|
||||||
|
...((loading || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword) ? styles.buttonDisabled : {})
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (!loading && passwordForm.currentPassword && passwordForm.newPassword && passwordForm.confirmPassword) {
|
||||||
|
e.currentTarget.style.background = styles.buttonPrimaryHover.background;
|
||||||
|
e.currentTarget.style.transform = styles.buttonPrimaryHover.transform;
|
||||||
|
e.currentTarget.style.boxShadow = styles.buttonPrimaryHover.boxShadow;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
if (!loading && passwordForm.currentPassword && passwordForm.newPassword && passwordForm.confirmPassword) {
|
||||||
|
e.currentTarget.style.background = styles.buttonPrimary.background;
|
||||||
|
e.currentTarget.style.transform = 'none';
|
||||||
|
e.currentTarget.style.boxShadow = styles.buttonPrimary.boxShadow;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? '⏳ Wird geändert...' : 'Passwort ändern'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Availability Tab */}
|
||||||
|
{activeTab === 'availability' && (
|
||||||
|
<>
|
||||||
|
<div style={styles.section}>
|
||||||
|
<h2 style={styles.sectionTitle}>Meine Verfügbarkeit</h2>
|
||||||
|
<p style={styles.sectionDescription}>
|
||||||
|
Legen Sie Ihre persönliche Verfügbarkeit für Schichtpläne fest
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.availabilityCard}>
|
||||||
|
<div style={styles.availabilityIcon}>📅</div>
|
||||||
|
<h3 style={styles.availabilityTitle}>Verfügbarkeit verwalten</h3>
|
||||||
|
<p style={styles.availabilityDescription}>
|
||||||
|
Hier können Sie Ihre persönliche Verfügbarkeit für Schichtpläne festlegen.
|
||||||
|
Legen Sie für jeden Tag und jede Schicht fest, ob Sie bevorzugt, möglicherweise
|
||||||
|
oder nicht verfügbar sind.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAvailabilityManager(true)}
|
||||||
|
style={{
|
||||||
|
...styles.button,
|
||||||
|
...styles.buttonPrimary,
|
||||||
|
marginBottom: '2rem'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = styles.buttonPrimaryHover.background;
|
||||||
|
e.currentTarget.style.transform = styles.buttonPrimaryHover.transform;
|
||||||
|
e.currentTarget.style.boxShadow = styles.buttonPrimaryHover.boxShadow;
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = styles.buttonPrimary.background;
|
||||||
|
e.currentTarget.style.transform = 'none';
|
||||||
|
e.currentTarget.style.boxShadow = styles.buttonPrimary.boxShadow;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Verfügbarkeit bearbeiten
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
236
frontend/src/pages/Settings/type/SettingsType.tsx
Normal file
236
frontend/src/pages/Settings/type/SettingsType.tsx
Normal 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',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -5,12 +5,11 @@ import { useAuth } from '../../contexts/AuthContext';
|
|||||||
import { shiftPlanService } from '../../services/shiftPlanService';
|
import { shiftPlanService } from '../../services/shiftPlanService';
|
||||||
import { employeeService } from '../../services/employeeService';
|
import { employeeService } from '../../services/employeeService';
|
||||||
import { shiftAssignmentService, ShiftAssignmentService } from '../../services/shiftAssignmentService';
|
import { shiftAssignmentService, ShiftAssignmentService } from '../../services/shiftAssignmentService';
|
||||||
import { AssignmentResult } from '../../services/scheduling';
|
import { IntelligentShiftScheduler, SchedulingResult, AssignmentResult } from '../../services/scheduling';
|
||||||
import { ShiftPlan, TimeSlot, ScheduledShift } from '../../models/ShiftPlan';
|
import { ShiftPlan, TimeSlot, ScheduledShift } from '../../models/ShiftPlan';
|
||||||
import { Employee, EmployeeAvailability } from '../../models/Employee';
|
import { Employee, EmployeeAvailability } from '../../models/Employee';
|
||||||
import { useNotification } from '../../contexts/NotificationContext';
|
import { useNotification } from '../../contexts/NotificationContext';
|
||||||
import { formatDate, formatTime } from '../../utils/foramatters';
|
import { formatDate, formatTime } from '../../utils/foramatters';
|
||||||
import { isScheduledShift } from '../../models/helpers';
|
|
||||||
|
|
||||||
// Local interface extensions (same as AvailabilityManager)
|
// Local interface extensions (same as AvailabilityManager)
|
||||||
interface ExtendedTimeSlot extends TimeSlot {
|
interface ExtendedTimeSlot extends TimeSlot {
|
||||||
@@ -36,125 +35,301 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
const [shiftPlan, setShiftPlan] = useState<ShiftPlan | null>(null);
|
const [shiftPlan, setShiftPlan] = useState<ShiftPlan | null>(null);
|
||||||
const [employees, setEmployees] = useState<Employee[]>([]);
|
const [employees, setEmployees] = useState<Employee[]>([]);
|
||||||
const [availabilities, setAvailabilities] = useState<EmployeeAvailability[]>([]);
|
const [availabilities, setAvailabilities] = useState<EmployeeAvailability[]>([]);
|
||||||
const [assignmentResult, setAssignmentResult] = useState<AssignmentResult | null>(null);
|
const [assignmentResult, setAssignmentResult] = useState<AssignmentResult | null>(null); // Add this line
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [publishing, setPublishing] = useState(false);
|
const [publishing, setPublishing] = useState(false);
|
||||||
const [scheduledShifts, setScheduledShifts] = useState<ScheduledShift[]>([]);
|
const [scheduledShifts, setScheduledShifts] = useState<ScheduledShift[]>([]);
|
||||||
const [reverting, setReverting] = useState(false);
|
|
||||||
const [showAssignmentPreview, setShowAssignmentPreview] = useState(false);
|
const [showAssignmentPreview, setShowAssignmentPreview] = useState(false);
|
||||||
|
const [recreating, setRecreating] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadShiftPlanData();
|
loadShiftPlanData();
|
||||||
debugScheduledShifts();
|
|
||||||
|
// Event Listener für Verfügbarkeits-Änderungen
|
||||||
|
const handleAvailabilityChange = () => {
|
||||||
|
console.log('📢 Verfügbarkeiten wurden geändert - lade Daten neu...');
|
||||||
|
reloadAvailabilities();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Globales Event für Verfügbarkeits-Änderungen
|
||||||
|
window.addEventListener('availabilitiesChanged', handleAvailabilityChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('availabilitiesChanged', handleAvailabilityChange);
|
||||||
|
};
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
const loadShiftPlanData = async () => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
const handleVisibilityChange = () => {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
// Seite ist wieder sichtbar - Daten neu laden
|
||||||
|
console.log('🔄 Seite ist wieder sichtbar - lade Daten neu...');
|
||||||
|
reloadAvailabilities();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(window as any).debugRenderLogic = debugRenderLogic;
|
||||||
|
return () => { (window as any).debugRenderLogic = undefined; };
|
||||||
|
}, [shiftPlan, scheduledShifts]);
|
||||||
|
|
||||||
|
const loadShiftPlanData = async () => {
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Load plan and employees first
|
||||||
|
const [plan, employeesData] = await Promise.all([
|
||||||
|
shiftPlanService.getShiftPlan(id),
|
||||||
|
employeeService.getEmployees(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
setShiftPlan(plan);
|
||||||
|
setEmployees(employeesData.filter(emp => emp.isActive));
|
||||||
|
|
||||||
|
// CRITICAL: Load scheduled shifts and verify they exist
|
||||||
|
const shiftsData = await shiftAssignmentService.getScheduledShiftsForPlan(id);
|
||||||
|
console.log('📋 Loaded scheduled shifts:', shiftsData.length);
|
||||||
|
|
||||||
|
if (shiftsData.length === 0) {
|
||||||
|
console.warn('⚠️ No scheduled shifts found for plan:', id);
|
||||||
|
showNotification({
|
||||||
|
type: 'warning',
|
||||||
|
title: 'Keine Schichten gefunden',
|
||||||
|
message: 'Der Schichtplan hat keine generierten Schichten. Bitte überprüfen Sie die Plan-Konfiguration.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setScheduledShifts(shiftsData);
|
||||||
|
|
||||||
|
// Load availabilities
|
||||||
|
const availabilityPromises = employeesData
|
||||||
|
.filter(emp => emp.isActive)
|
||||||
|
.map(emp => employeeService.getAvailabilities(emp.id));
|
||||||
|
|
||||||
|
const allAvailabilities = await Promise.all(availabilityPromises);
|
||||||
|
const flattenedAvailabilities = allAvailabilities.flat();
|
||||||
|
|
||||||
|
const planAvailabilities = flattenedAvailabilities.filter(
|
||||||
|
availability => availability.planId === id
|
||||||
|
);
|
||||||
|
|
||||||
|
setAvailabilities(planAvailabilities);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading shift plan data:', error);
|
||||||
|
showNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Fehler',
|
||||||
|
message: 'Daten konnten nicht geladen werden.'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRecreateAssignments = async () => {
|
||||||
|
if (!shiftPlan) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setRecreating(true);
|
||||||
const [plan, employeesData, shiftsData] = await Promise.all([
|
|
||||||
shiftPlanService.getShiftPlan(id),
|
if (!window.confirm('Möchten Sie die aktuellen Zuweisungen wirklich zurücksetzen? Alle vorhandenen Zuweisungen werden gelöscht.')) {
|
||||||
employeeService.getEmployees(),
|
return;
|
||||||
shiftAssignmentService.getScheduledShiftsForPlan(id) // Load shifts here
|
}
|
||||||
]);
|
|
||||||
|
|
||||||
setShiftPlan(plan);
|
console.log('🔄 STARTING COMPLETE ASSIGNMENT CLEARING PROCESS');
|
||||||
setEmployees(employeesData.filter(emp => emp.isActive));
|
|
||||||
setScheduledShifts(shiftsData);
|
|
||||||
|
|
||||||
// Load availabilities for all employees
|
// STEP 1: Get current scheduled shifts
|
||||||
const availabilityPromises = employeesData
|
const currentScheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
||||||
.filter(emp => emp.isActive)
|
console.log(`📋 Found ${currentScheduledShifts.length} shifts to clear`);
|
||||||
.map(emp => employeeService.getAvailabilities(emp.id));
|
|
||||||
|
// STEP 2: Clear ALL assignments by setting empty arrays
|
||||||
|
const clearPromises = currentScheduledShifts.map(async (scheduledShift) => {
|
||||||
|
console.log(`🗑️ Clearing assignments for shift: ${scheduledShift.id}`);
|
||||||
|
await shiftAssignmentService.updateScheduledShift(scheduledShift.id, {
|
||||||
|
assignedEmployees: [] // EMPTY ARRAY - this clears the assignments
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(clearPromises);
|
||||||
|
console.log('✅ All assignments cleared from database');
|
||||||
|
|
||||||
|
// STEP 3: Update plan status to draft
|
||||||
|
await shiftPlanService.updateShiftPlan(shiftPlan.id, {
|
||||||
|
status: 'draft'
|
||||||
|
});
|
||||||
|
console.log('📝 Plan status set to draft');
|
||||||
|
|
||||||
|
// STEP 4: CRITICAL - Force reload of scheduled shifts to get EMPTY assignments
|
||||||
|
const refreshedShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
||||||
|
setScheduledShifts(refreshedShifts); // Update state with EMPTY assignments
|
||||||
|
|
||||||
const allAvailabilities = await Promise.all(availabilityPromises);
|
// STEP 5: Clear any previous assignment results
|
||||||
const flattenedAvailabilities = allAvailabilities.flat();
|
setAssignmentResult(null);
|
||||||
|
setShowAssignmentPreview(false);
|
||||||
// Filter availabilities to only include those for the current shift plan
|
|
||||||
const planAvailabilities = flattenedAvailabilities.filter(
|
// STEP 6: Force complete data refresh
|
||||||
availability => availability.planId === id
|
await loadShiftPlanData();
|
||||||
);
|
|
||||||
|
console.log('🎯 ASSIGNMENT CLEARING COMPLETE - Table should now be empty');
|
||||||
setAvailabilities(planAvailabilities);
|
|
||||||
debugAvailabilities();
|
showNotification({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Zuweisungen gelöscht',
|
||||||
|
message: 'Alle Zuweisungen wurden erfolgreich gelöscht. Die Tabelle sollte jetzt leer sein.'
|
||||||
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading shift plan data:', error);
|
console.error('❌ Error clearing assignments:', error);
|
||||||
showNotification({
|
showNotification({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'Fehler',
|
title: 'Fehler',
|
||||||
message: 'Daten konnten nicht geladen werden.'
|
message: `Löschen der Zuweisungen fehlgeschlagen: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}`
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setRecreating(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const debugAvailabilities = () => {
|
const debugRenderLogic = () => {
|
||||||
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 {
|
console.log('🔍 RENDER LOGIC DEBUG:');
|
||||||
const shifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
console.log('=====================');
|
||||||
console.log('🔍 SCHEDULED SHIFTS IN DATABASE:', {
|
|
||||||
total: shifts.length,
|
const { days, allTimeSlots, timeSlotsByDay } = getExtendedTimetableData();
|
||||||
shifts: shifts.map(s => ({
|
|
||||||
id: s.id,
|
console.log('📊 TABLE STRUCTURE:');
|
||||||
date: s.date,
|
console.log('- Days in table:', days.length);
|
||||||
timeSlotId: s.timeSlotId,
|
console.log('- TimeSlots in table:', allTimeSlots.length);
|
||||||
requiredEmployees: s.requiredEmployees
|
console.log('- Days with data:', Object.keys(timeSlotsByDay).length);
|
||||||
}))
|
|
||||||
|
// Zeige die tatsächliche Struktur der Tabelle
|
||||||
|
console.log('\n📅 ACTUAL TABLE DAYS:');
|
||||||
|
days.forEach(day => {
|
||||||
|
const slotsForDay = timeSlotsByDay[day.id] || [];
|
||||||
|
console.log(`- ${day.name}: ${slotsForDay.length} time slots`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('\n⏰ ACTUAL TIME SLOTS:');
|
||||||
|
allTimeSlots.forEach(slot => {
|
||||||
|
console.log(`- ${slot.name} (${slot.startTime}-${slot.endTime})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prüfe wie viele Scheduled Shifts tatsächlich gerendert werden
|
||||||
|
console.log('\n🔍 SCHEDULED SHIFTS RENDER ANALYSIS:');
|
||||||
|
|
||||||
|
let totalRenderedShifts = 0;
|
||||||
|
let shiftsWithAssignments = 0;
|
||||||
|
|
||||||
|
days.forEach(day => {
|
||||||
|
const slotsForDay = timeSlotsByDay[day.id] || [];
|
||||||
|
slotsForDay.forEach(timeSlot => {
|
||||||
|
totalRenderedShifts++;
|
||||||
|
|
||||||
|
// Finde den entsprechenden Scheduled Shift
|
||||||
|
const scheduledShift = scheduledShifts.find(scheduled => {
|
||||||
|
const scheduledDayOfWeek = getDayOfWeek(scheduled.date);
|
||||||
|
return scheduledDayOfWeek === day.id &&
|
||||||
|
scheduled.timeSlotId === timeSlot.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (scheduledShift && scheduledShift.assignedEmployees && scheduledShift.assignedEmployees.length > 0) {
|
||||||
|
shiftsWithAssignments++;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
// Check if we have any shifts at all
|
|
||||||
if (shifts.length === 0) {
|
console.log(`- Total shifts in table: ${totalRenderedShifts}`);
|
||||||
console.error('❌ NO SCHEDULED SHIFTS IN DATABASE - This is the problem!');
|
console.log(`- Shifts with assignments: ${shiftsWithAssignments}`);
|
||||||
console.log('💡 Solution: Regenerate scheduled shifts for this plan');
|
console.log(`- Total scheduled shifts: ${scheduledShifts.length}`);
|
||||||
|
console.log(`- Coverage: ${Math.round((totalRenderedShifts / scheduledShifts.length) * 100)}%`);
|
||||||
|
|
||||||
|
// Problem-Analyse
|
||||||
|
if (totalRenderedShifts < scheduledShifts.length) {
|
||||||
|
console.log('\n🚨 PROBLEM: Table is not showing all scheduled shifts!');
|
||||||
|
console.log('💡 The table structure (days × timeSlots) is smaller than actual scheduled shifts');
|
||||||
|
|
||||||
|
// Zeige die fehlenden Shifts
|
||||||
|
const missingShifts = scheduledShifts.filter(scheduled => {
|
||||||
|
const dayOfWeek = getDayOfWeek(scheduled.date);
|
||||||
|
const timeSlotExists = allTimeSlots.some(ts => ts.id === scheduled.timeSlotId);
|
||||||
|
const dayExists = days.some(day => day.id === dayOfWeek);
|
||||||
|
|
||||||
|
return !(timeSlotExists && dayExists);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (missingShifts.length > 0) {
|
||||||
|
console.log(`❌ ${missingShifts.length} shifts cannot be rendered in table:`);
|
||||||
|
missingShifts.slice(0, 5).forEach(shift => {
|
||||||
|
const dayOfWeek = getDayOfWeek(shift.date);
|
||||||
|
const timeSlot = shiftPlan.timeSlots?.find(ts => ts.id === shift.timeSlotId);
|
||||||
|
console.log(` - ${shift.date} (Day ${dayOfWeek}): ${timeSlot?.name || 'Unknown'} - ${shift.assignedEmployees?.length || 0} assignments`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Error loading scheduled shifts:', error);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getExtendedTimetableData = () => {
|
||||||
|
if (!shiftPlan || !shiftPlan.timeSlots) {
|
||||||
|
return { days: [], timeSlotsByDay: {}, allTimeSlots: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verwende alle Tage die tatsächlich in scheduledShifts vorkommen
|
||||||
|
const allDaysInScheduledShifts = [...new Set(scheduledShifts.map(s => getDayOfWeek(s.date)))].sort();
|
||||||
|
|
||||||
|
const days = allDaysInScheduledShifts.map(dayId => {
|
||||||
|
return weekdays.find(day => day.id === dayId) || { id: dayId, name: `Tag ${dayId}` };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verwende alle TimeSlots die tatsächlich in scheduledShifts vorkommen
|
||||||
|
const allTimeSlotIdsInScheduledShifts = [...new Set(scheduledShifts.map(s => s.timeSlotId))];
|
||||||
|
|
||||||
|
const allTimeSlots = allTimeSlotIdsInScheduledShifts
|
||||||
|
.map(id => shiftPlan.timeSlots?.find(ts => ts.id === id))
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(timeSlot => ({
|
||||||
|
...timeSlot!,
|
||||||
|
displayName: `${timeSlot!.name} (${formatTime(timeSlot!.startTime)}-${formatTime(timeSlot!.endTime)})`
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.startTime.localeCompare(b.startTime));
|
||||||
|
|
||||||
|
// TimeSlots pro Tag
|
||||||
|
const timeSlotsByDay: Record<number, ExtendedTimeSlot[]> = {};
|
||||||
|
|
||||||
|
days.forEach(day => {
|
||||||
|
const timeSlotIdsForDay = new Set(
|
||||||
|
scheduledShifts
|
||||||
|
.filter(shift => getDayOfWeek(shift.date) === day.id)
|
||||||
|
.map(shift => shift.timeSlotId)
|
||||||
|
);
|
||||||
|
|
||||||
|
timeSlotsByDay[day.id] = allTimeSlots
|
||||||
|
.filter(timeSlot => timeSlotIdsForDay.has(timeSlot.id))
|
||||||
|
.sort((a, b) => a.startTime.localeCompare(b.startTime));
|
||||||
|
});
|
||||||
|
|
||||||
|
/*console.log('🔄 Extended timetable data:', {
|
||||||
|
days: days.length,
|
||||||
|
timeSlots: allTimeSlots.length,
|
||||||
|
totalScheduledShifts: scheduledShifts.length
|
||||||
|
});*/
|
||||||
|
|
||||||
|
return { days, timeSlotsByDay, allTimeSlots };
|
||||||
|
};
|
||||||
|
|
||||||
// Extract plan-specific shifts using the same logic as AvailabilityManager
|
// Extract plan-specific shifts using the same logic as AvailabilityManager
|
||||||
const getTimetableData = () => {
|
const getTimetableData = () => {
|
||||||
if (!shiftPlan || !shiftPlan.shifts || !shiftPlan.timeSlots) {
|
if (!shiftPlan || !shiftPlan.shifts || !shiftPlan.timeSlots) {
|
||||||
return { days: [], timeSlotsByDay: {}, allTimeSlots: [] };
|
return { days: [], timeSlotsByDay: {}, allTimeSlots: [] };
|
||||||
}
|
}
|
||||||
@@ -216,174 +391,46 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
return date.getDay() === 0 ? 7 : date.getDay();
|
return date.getDay() === 0 ? 7 : date.getDay();
|
||||||
};
|
};
|
||||||
|
|
||||||
/*const debugManagerAvailability = () => {
|
|
||||||
if (!shiftPlan || !employees.length || !availabilities.length) return;
|
|
||||||
|
|
||||||
const manager = employees.find(emp => emp.role === 'admin');
|
|
||||||
if (!manager) {
|
|
||||||
console.log('❌ Kein Manager (admin) gefunden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('🔍 Manager-Analyse:', {
|
|
||||||
manager: manager.name,
|
|
||||||
managerId: manager.id,
|
|
||||||
totalAvailabilities: availabilities.length,
|
|
||||||
managerAvailabilities: availabilities.filter(a => a.employeeId === manager.id).length
|
|
||||||
});
|
|
||||||
|
|
||||||
// Prüfe speziell die leeren Manager-Schichten
|
|
||||||
const emptyManagerShifts = [
|
|
||||||
'a8ef4ce0-adfd-4ec3-8c58-efa0f7347f9f',
|
|
||||||
'a496a8d6-f7a0-4d77-96de-c165379378c4',
|
|
||||||
'ea2d73d1-8354-4833-8c87-40f318ce8be0',
|
|
||||||
'90eb5454-2ae2-4445-86b7-a6e0e2cf0b22'
|
|
||||||
];
|
|
||||||
|
|
||||||
emptyManagerShifts.forEach(shiftId => {
|
|
||||||
const scheduledShift = shiftPlan.scheduledShifts?.find(s => s.id === shiftId);
|
|
||||||
if (scheduledShift) {
|
|
||||||
const dayOfWeek = getDayOfWeek(scheduledShift.date);
|
|
||||||
const shiftKey = `${dayOfWeek}-${scheduledShift.timeSlotId}`;
|
|
||||||
|
|
||||||
const managerAvailability = availabilities.find(a =>
|
|
||||||
a.employeeId === manager.id &&
|
|
||||||
a.dayOfWeek === dayOfWeek &&
|
|
||||||
a.timeSlotId === scheduledShift.timeSlotId
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`📊 Schicht ${shiftId}:`, {
|
|
||||||
date: scheduledShift.date,
|
|
||||||
dayOfWeek,
|
|
||||||
timeSlotId: scheduledShift.timeSlotId,
|
|
||||||
shiftKey,
|
|
||||||
managerAvailability: managerAvailability ? managerAvailability.preferenceLevel : 'NICHT GEFUNDEN',
|
|
||||||
status: managerAvailability ?
|
|
||||||
(managerAvailability.preferenceLevel === 3 ? '❌ NICHT VERFÜGBAR' : '✅ VERFÜGBAR') :
|
|
||||||
'❌ KEINE VERFÜGBARKEITSDATEN'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};*/
|
|
||||||
|
|
||||||
const debugAssignments = async () => {
|
|
||||||
if (!shiftPlan) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const shifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
|
||||||
console.log('🔍 DEBUG - Scheduled Shifts nach Veröffentlichung:', {
|
|
||||||
totalShifts: shifts.length,
|
|
||||||
shiftsWithAssignments: shifts.filter(s => s.assignedEmployees && s.assignedEmployees.length > 0).length,
|
|
||||||
allShifts: shifts.map(s => ({
|
|
||||||
id: s.id,
|
|
||||||
date: s.date,
|
|
||||||
timeSlotId: s.timeSlotId,
|
|
||||||
assignedEmployees: s.assignedEmployees,
|
|
||||||
assignedCount: s.assignedEmployees?.length || 0
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Debug error:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePreviewAssignment = async () => {
|
const handlePreviewAssignment = async () => {
|
||||||
if (!shiftPlan) return;
|
if (!shiftPlan) return;
|
||||||
debugScheduledShifts();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setPublishing(true);
|
setPublishing(true);
|
||||||
|
|
||||||
// DEBUG: Überprüfe die Eingabedaten
|
// FORCE COMPLETE REFRESH - don't rely on cached state
|
||||||
console.log('🔍 INPUT DATA FOR SCHEDULING:', {
|
const [refreshedEmployees, refreshedAvailabilities] = await Promise.all([
|
||||||
shiftPlan: {
|
// Reload employees fresh
|
||||||
id: shiftPlan.id,
|
employeeService.getEmployees().then(emps => emps.filter(emp => emp.isActive)),
|
||||||
name: shiftPlan.name,
|
// Reload availabilities fresh
|
||||||
shifts: shiftPlan.shifts?.length,
|
refreshAllAvailabilities()
|
||||||
timeSlots: shiftPlan.timeSlots?.length
|
]);
|
||||||
},
|
|
||||||
employees: employees.length,
|
|
||||||
availabilities: availabilities.length,
|
|
||||||
employeeDetails: employees.map(emp => ({
|
|
||||||
id: emp.id,
|
|
||||||
name: emp.name,
|
|
||||||
role: emp.role,
|
|
||||||
employeeType: emp.employeeType,
|
|
||||||
canWorkAlone: emp.canWorkAlone
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
|
|
||||||
|
console.log('🔄 USING FRESH DATA:');
|
||||||
|
console.log('- Employees:', refreshedEmployees.length);
|
||||||
|
console.log('- Availabilities:', refreshedAvailabilities.length);
|
||||||
|
|
||||||
|
// DEBUG: Verify we have new data
|
||||||
|
debugSchedulingInput(refreshedEmployees, refreshedAvailabilities);
|
||||||
|
|
||||||
|
// ADD THIS: Define constraints object
|
||||||
|
const constraints = {
|
||||||
|
enforceNoTraineeAlone: true,
|
||||||
|
enforceExperiencedWithChef: true,
|
||||||
|
maxRepairAttempts: 50,
|
||||||
|
targetEmployeesPerShift: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use the freshly loaded data, not the state
|
||||||
const result = await ShiftAssignmentService.assignShifts(
|
const result = await ShiftAssignmentService.assignShifts(
|
||||||
shiftPlan,
|
shiftPlan,
|
||||||
employees,
|
refreshedEmployees, // Use fresh array, not state
|
||||||
availabilities,
|
refreshedAvailabilities, // Use fresh array, not state
|
||||||
{
|
constraints // Now this variable is defined
|
||||||
enforceExperiencedWithChef: true,
|
|
||||||
enforceNoTraineeAlone: true,
|
|
||||||
maxRepairAttempts: 50
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// DEBUG: Detaillierte Analyse des Results
|
|
||||||
console.log('🔍 DETAILED ASSIGNMENT RESULT:', {
|
|
||||||
totalAssignments: Object.keys(result.assignments).length,
|
|
||||||
assignments: result.assignments,
|
|
||||||
violations: result.violations,
|
|
||||||
hasResolutionReport: !!result.resolutionReport,
|
|
||||||
assignmentDetails: Object.entries(result.assignments).map(([shiftId, empIds]) => ({
|
|
||||||
shiftId,
|
|
||||||
employeeCount: empIds.length,
|
|
||||||
employees: empIds
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
// DEBUG: Überprüfe die tatsächlichen Violations
|
|
||||||
console.log('🔍 VIOLATIONS ANALYSIS:', {
|
|
||||||
allViolations: result.violations,
|
|
||||||
criticalViolations: result.violations.filter(v =>
|
|
||||||
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
|
|
||||||
),
|
|
||||||
warningViolations: result.violations.filter(v =>
|
|
||||||
v.includes('WARNING:') || v.includes('⚠️')
|
|
||||||
),
|
|
||||||
infoViolations: result.violations.filter(v =>
|
|
||||||
v.includes('INFO:')
|
|
||||||
),
|
|
||||||
criticalCount: result.violations.filter(v =>
|
|
||||||
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
|
|
||||||
).length,
|
|
||||||
canPublish: result.violations.filter(v =>
|
|
||||||
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
|
|
||||||
).length === 0
|
|
||||||
});
|
|
||||||
|
|
||||||
setAssignmentResult(result);
|
setAssignmentResult(result);
|
||||||
setShowAssignmentPreview(true);
|
setShowAssignmentPreview(true);
|
||||||
|
|
||||||
// Zeige Reparatur-Bericht in der Konsole
|
|
||||||
if (result.resolutionReport) {
|
|
||||||
console.log('🔧 Reparatur-Bericht:');
|
|
||||||
result.resolutionReport.forEach(line => console.log(line));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Entscheidung basierend auf tatsächlichen kritischen Violations
|
|
||||||
const criticalCount = result.violations.filter(v =>
|
|
||||||
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
|
|
||||||
).length;
|
|
||||||
|
|
||||||
if (criticalCount === 0) {
|
|
||||||
showNotification({
|
|
||||||
type: 'success',
|
|
||||||
title: 'Erfolg',
|
|
||||||
message: 'Alle kritischen Probleme wurden behoben! Der Schichtplan kann veröffentlicht werden.'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
showNotification({
|
|
||||||
type: 'error',
|
|
||||||
title: 'Kritische Probleme',
|
|
||||||
message: `${criticalCount} kritische Probleme müssen behoben werden`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during assignment:', error);
|
console.error('Error during assignment:', error);
|
||||||
showNotification({
|
showNotification({
|
||||||
@@ -396,6 +443,57 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const debugSchedulingInput = (employees: Employee[], availabilities: EmployeeAvailability[]) => {
|
||||||
|
console.log('🔍 DEBUG SCHEDULING INPUT:');
|
||||||
|
console.log('==========================');
|
||||||
|
|
||||||
|
// Check if we have the latest data
|
||||||
|
console.log('📊 Employee Count:', employees.length);
|
||||||
|
console.log('📊 Availability Count:', availabilities.length);
|
||||||
|
|
||||||
|
// Log each employee's availability
|
||||||
|
employees.forEach(emp => {
|
||||||
|
const empAvailabilities = availabilities.filter(avail => avail.employeeId === emp.id);
|
||||||
|
console.log(`👤 ${emp.name} (${emp.role}, ${emp.employeeType}): ${empAvailabilities.length} availabilities`);
|
||||||
|
|
||||||
|
if (empAvailabilities.length > 0) {
|
||||||
|
empAvailabilities.forEach(avail => {
|
||||||
|
console.log(` - Day ${avail.dayOfWeek}, TimeSlot ${avail.timeSlotId}: Level ${avail.preferenceLevel}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(` ❌ NO AVAILABILITIES SET!`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// REMOVED: The problematic code that tries to access shiftPlan.employees
|
||||||
|
// We don't have old employee data stored in shiftPlan
|
||||||
|
|
||||||
|
console.log('🔄 All employees are considered "changed" since we loaded fresh data');
|
||||||
|
};
|
||||||
|
|
||||||
|
const forceRefreshData = async () => {
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [plan, employeesData, shiftsData] = await Promise.all([
|
||||||
|
shiftPlanService.getShiftPlan(id),
|
||||||
|
employeeService.getEmployees(),
|
||||||
|
shiftAssignmentService.getScheduledShiftsForPlan(id)
|
||||||
|
]);
|
||||||
|
|
||||||
|
setShiftPlan(plan);
|
||||||
|
setEmployees(employeesData.filter(emp => emp.isActive));
|
||||||
|
setScheduledShifts(shiftsData);
|
||||||
|
|
||||||
|
// Force refresh availabilities
|
||||||
|
await refreshAllAvailabilities();
|
||||||
|
|
||||||
|
console.log('✅ All data force-refreshed');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error force-refreshing data:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handlePublish = async () => {
|
const handlePublish = async () => {
|
||||||
if (!shiftPlan || !assignmentResult) return;
|
if (!shiftPlan || !assignmentResult) return;
|
||||||
|
|
||||||
@@ -418,15 +516,18 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
const updatePromises = updatedShifts.map(async (scheduledShift) => {
|
const updatePromises = updatedShifts.map(async (scheduledShift) => {
|
||||||
const assignedEmployees = assignmentResult.assignments[scheduledShift.id] || [];
|
const assignedEmployees = assignmentResult.assignments[scheduledShift.id] || [];
|
||||||
|
|
||||||
console.log(`📝 Updating shift ${scheduledShift.id} with`, assignedEmployees, 'employees');
|
//console.log(`📝 Updating shift ${scheduledShift.id} with`, assignedEmployees, 'employees');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Update the shift with assigned employees
|
// Update the shift with assigned employees
|
||||||
|
const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
||||||
await shiftAssignmentService.updateScheduledShift(scheduledShift.id, {
|
await shiftAssignmentService.updateScheduledShift(scheduledShift.id, {
|
||||||
assignedEmployees
|
assignedEmployees
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`✅ Successfully updated shift ${scheduledShift.id}`);
|
if (scheduledShifts.some(s => s.id === scheduledShift.id)) {
|
||||||
|
console.log(`✅ Successfully updated scheduled shift ${scheduledShift.id}`);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ Failed to update shift ${scheduledShift.id}:`, error);
|
console.error(`❌ Failed to update shift ${scheduledShift.id}:`, error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -450,18 +551,6 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
setShiftPlan(reloadedPlan);
|
setShiftPlan(reloadedPlan);
|
||||||
setScheduledShifts(reloadedShifts);
|
setScheduledShifts(reloadedShifts);
|
||||||
|
|
||||||
// Debug: Überprüfe die aktualisierten Daten
|
|
||||||
console.log('🔍 After publish - Reloaded data:', {
|
|
||||||
planStatus: reloadedPlan.status,
|
|
||||||
scheduledShiftsCount: reloadedShifts.length,
|
|
||||||
shiftsWithAssignments: reloadedShifts.filter(s => s.assignedEmployees && s.assignedEmployees.length > 0).length,
|
|
||||||
allAssignments: reloadedShifts.map(s => ({
|
|
||||||
id: s.id,
|
|
||||||
date: s.date,
|
|
||||||
assigned: s.assignedEmployees
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
|
|
||||||
showNotification({
|
showNotification({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: 'Erfolg',
|
title: 'Erfolg',
|
||||||
@@ -488,53 +577,78 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRevertToDraft = async () => {
|
const refreshAllAvailabilities = async (): Promise<EmployeeAvailability[]> => {
|
||||||
if (!shiftPlan || !id) return;
|
|
||||||
|
|
||||||
if (!window.confirm('Möchten Sie diesen Schichtplan wirklich zurück in den Entwurfsstatus setzen? Alle Zuweisungen werden entfernt.')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setReverting(true);
|
console.log('🔄 Force refreshing ALL availabilities with error handling...');
|
||||||
|
|
||||||
// 1. Zuerst zurücksetzen
|
if (!id) {
|
||||||
const updatedPlan = await shiftPlanService.revertToDraft(id);
|
console.error('❌ No plan ID available');
|
||||||
|
return [];
|
||||||
// 2. Dann ALLE Daten neu laden
|
}
|
||||||
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)
|
|
||||||
});
|
|
||||||
|
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error('Error reverting plan to draft:', error);
|
console.error('❌ Critical error refreshing availabilities:', error);
|
||||||
showNotification({
|
// DON'T return old data - throw error or return empty array
|
||||||
type: 'error',
|
throw new Error('Failed to refresh availabilities: ' + error);
|
||||||
title: 'Fehler',
|
|
||||||
message: 'Schichtplan konnte nicht zurückgesetzt werden.'
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setReverting(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validateSchedulingData = (): boolean => {
|
||||||
|
console.log('🔍 Validating scheduling data...');
|
||||||
|
|
||||||
|
const totalEmployees = employees.length;
|
||||||
|
const employeesWithAvailabilities = new Set(
|
||||||
|
availabilities.map(avail => avail.employeeId)
|
||||||
|
).size;
|
||||||
|
|
||||||
|
const availabilityStatus = {
|
||||||
|
totalEmployees,
|
||||||
|
employeesWithAvailabilities,
|
||||||
|
coverage: Math.round((employeesWithAvailabilities / totalEmployees) * 100)
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('📊 Availability Coverage:', availabilityStatus);
|
||||||
|
|
||||||
|
// Check if we have ALL employee availabilities
|
||||||
|
if (employeesWithAvailabilities < totalEmployees) {
|
||||||
|
const missingEmployees = employees.filter(emp =>
|
||||||
|
!availabilities.some(avail => avail.employeeId === emp.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
console.warn('⚠️ Missing availabilities for employees:',
|
||||||
|
missingEmployees.map(emp => emp.name));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const canPublish = () => {
|
const canPublish = () => {
|
||||||
if (!shiftPlan || shiftPlan.status === 'published') return false;
|
if (!shiftPlan || shiftPlan.status === 'published') return false;
|
||||||
|
|
||||||
@@ -560,40 +674,49 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const debugCurrentState = () => {
|
const reloadAvailabilities = async () => {
|
||||||
console.log('🔍 CURRENT STATE DEBUG:', {
|
try {
|
||||||
shiftPlan: shiftPlan ? {
|
console.log('🔄 Lade Verfügbarkeiten neu...');
|
||||||
id: shiftPlan.id,
|
|
||||||
name: shiftPlan.name,
|
// Load availabilities for all employees
|
||||||
status: shiftPlan.status
|
const availabilityPromises = employees
|
||||||
} : null,
|
.filter(emp => emp.isActive)
|
||||||
scheduledShifts: {
|
.map(emp => employeeService.getAvailabilities(emp.id));
|
||||||
count: scheduledShifts.length,
|
|
||||||
withAssignments: scheduledShifts.filter(s => s.assignedEmployees && s.assignedEmployees.length > 0).length,
|
const allAvailabilities = await Promise.all(availabilityPromises);
|
||||||
details: scheduledShifts.map(s => ({
|
const flattenedAvailabilities = allAvailabilities.flat();
|
||||||
id: s.id,
|
|
||||||
date: s.date,
|
// Filter availabilities to only include those for the current shift plan
|
||||||
timeSlotId: s.timeSlotId,
|
const planAvailabilities = flattenedAvailabilities.filter(
|
||||||
assignedEmployees: s.assignedEmployees
|
availability => availability.planId === id
|
||||||
}))
|
);
|
||||||
},
|
|
||||||
employees: employees.length
|
setAvailabilities(planAvailabilities);
|
||||||
|
console.log('✅ Verfügbarkeiten neu geladen:', planAvailabilities.length);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Fehler beim Neuladen der Verfügbarkeiten:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Füge diese Funktion zu den verfügbaren Aktionen hinzu
|
||||||
|
const handleReloadData = async () => {
|
||||||
|
await loadShiftPlanData();
|
||||||
|
showNotification({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Erfolg',
|
||||||
|
message: 'Daten wurden neu geladen.'
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Render timetable using the same structure as AvailabilityManager
|
// Render timetable using the same structure as AvailabilityManager
|
||||||
const renderTimetable = () => {
|
const renderTimetable = () => {
|
||||||
debugAssignments();
|
const { days, allTimeSlots, timeSlotsByDay } = getExtendedTimetableData();
|
||||||
debugCurrentState();
|
|
||||||
const { days, allTimeSlots, timeSlotsByDay } = getTimetableData();
|
|
||||||
if (!shiftPlan?.id) {
|
if (!shiftPlan?.id) {
|
||||||
console.warn("Shift plan ID is missing");
|
console.warn("Shift plan ID is missing");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
//const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
|
||||||
|
|
||||||
|
|
||||||
if (days.length === 0 || allTimeSlots.length === 0) {
|
if (days.length === 0 || allTimeSlots.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -692,7 +815,6 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get assigned employees for this shift
|
|
||||||
let assignedEmployees: string[] = [];
|
let assignedEmployees: string[] = [];
|
||||||
let displayText = '';
|
let displayText = '';
|
||||||
|
|
||||||
@@ -701,11 +823,17 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
const scheduledShift = scheduledShifts.find(scheduled => {
|
const scheduledShift = scheduledShifts.find(scheduled => {
|
||||||
const scheduledDayOfWeek = getDayOfWeek(scheduled.date);
|
const scheduledDayOfWeek = getDayOfWeek(scheduled.date);
|
||||||
return scheduledDayOfWeek === weekday.id &&
|
return scheduledDayOfWeek === weekday.id &&
|
||||||
scheduled.timeSlotId === timeSlot.id;
|
scheduled.timeSlotId === timeSlot.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (scheduledShift) {
|
if (scheduledShift) {
|
||||||
assignedEmployees = scheduledShift.assignedEmployees || [];
|
assignedEmployees = scheduledShift.assignedEmployees || [];
|
||||||
|
|
||||||
|
// DEBUG: Log if we're still seeing old data
|
||||||
|
if (assignedEmployees.length > 0) {
|
||||||
|
console.warn(`⚠️ Found non-empty assignments for ${weekday.name} ${timeSlot.name}:`, assignedEmployees);
|
||||||
|
}
|
||||||
|
|
||||||
displayText = assignedEmployees.map(empId => {
|
displayText = assignedEmployees.map(empId => {
|
||||||
const employee = employees.find(emp => emp.id === empId);
|
const employee = employees.find(emp => emp.id === empId);
|
||||||
return employee ? employee.name : 'Unbekannt';
|
return employee ? employee.name : 'Unbekannt';
|
||||||
@@ -716,7 +844,7 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
const scheduledShift = scheduledShifts.find(scheduled => {
|
const scheduledShift = scheduledShifts.find(scheduled => {
|
||||||
const scheduledDayOfWeek = getDayOfWeek(scheduled.date);
|
const scheduledDayOfWeek = getDayOfWeek(scheduled.date);
|
||||||
return scheduledDayOfWeek === weekday.id &&
|
return scheduledDayOfWeek === weekday.id &&
|
||||||
scheduled.timeSlotId === timeSlot.id;
|
scheduled.timeSlotId === timeSlot.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (scheduledShift && assignmentResult.assignments[scheduledShift.id]) {
|
if (scheduledShift && assignmentResult.assignments[scheduledShift.id]) {
|
||||||
@@ -728,7 +856,7 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no assignments yet, show required count
|
// If no assignments yet, show empty or required count
|
||||||
if (!displayText) {
|
if (!displayText) {
|
||||||
const shiftsForSlot = shiftPlan?.shifts?.filter(shift =>
|
const shiftsForSlot = shiftPlan?.shifts?.filter(shift =>
|
||||||
shift.dayOfWeek === weekday.id &&
|
shift.dayOfWeek === weekday.id &&
|
||||||
@@ -738,7 +866,13 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
const totalRequired = shiftsForSlot.reduce((sum, shift) =>
|
const totalRequired = shiftsForSlot.reduce((sum, shift) =>
|
||||||
sum + shift.requiredEmployees, 0);
|
sum + shift.requiredEmployees, 0);
|
||||||
|
|
||||||
|
// Show "0/2" instead of just "0" to indicate it's empty
|
||||||
displayText = `0/${totalRequired}`;
|
displayText = `0/${totalRequired}`;
|
||||||
|
|
||||||
|
// Optional: Show empty state more clearly
|
||||||
|
if (totalRequired === 0) {
|
||||||
|
displayText = '-';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -766,7 +900,7 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
if (loading) return <div>Lade Schichtplan...</div>;
|
if (loading) return <div>Lade Schichtplan...</div>;
|
||||||
if (!shiftPlan) return <div>Schichtplan nicht gefunden</div>;
|
if (!shiftPlan) return <div>Schichtplan nicht gefunden</div>;
|
||||||
|
|
||||||
const { days, allTimeSlots } = getTimetableData();
|
const { days, allTimeSlots } = getExtendedTimetableData();
|
||||||
const availabilityStatus = getAvailabilityStatus();
|
const availabilityStatus = getAvailabilityStatus();
|
||||||
|
|
||||||
|
|
||||||
@@ -799,25 +933,25 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
{shiftPlan.status === 'published' ? 'Veröffentlicht' : 'Entwurf'}
|
{shiftPlan.status === 'published' ? 'Veröffentlicht' : 'Entwurf'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
|
||||||
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
|
{shiftPlan.status === 'published' && hasRole(['admin', 'instandhalter']) && (
|
||||||
{shiftPlan.status === 'published' && hasRole(['admin', 'instandhalter']) && (
|
|
||||||
<button
|
<button
|
||||||
onClick={handleRevertToDraft}
|
onClick={handleRecreateAssignments}
|
||||||
disabled={reverting}
|
disabled={recreating}
|
||||||
style={{
|
style={{
|
||||||
padding: '10px 20px',
|
padding: '10px 20px',
|
||||||
backgroundColor: '#e74c3c',
|
backgroundColor: '#e74c3c',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
cursor: 'pointer',
|
cursor: recreating ? 'not-allowed' : 'pointer',
|
||||||
fontWeight: 'bold'
|
fontWeight: 'bold'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{reverting ? 'Zurücksetzen...' : 'Zu Entwurf zurücksetzen'}
|
{recreating ? 'Lösche Zuweisungen...' : 'Zuweisungen neu berechnen'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/shift-plans')}
|
onClick={() => navigate('/shift-plans')}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
1829
frontend/src/services/scheduling.ts
Normal file
1829
frontend/src/services/scheduling.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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[];
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -2,13 +2,13 @@
|
|||||||
import { ShiftPlan, ScheduledShift } from '../models/ShiftPlan';
|
import { ShiftPlan, ScheduledShift } from '../models/ShiftPlan';
|
||||||
import { Employee, EmployeeAvailability } from '../models/Employee';
|
import { Employee, EmployeeAvailability } from '../models/Employee';
|
||||||
import { authService } from './authService';
|
import { authService } from './authService';
|
||||||
import { scheduleWithManager } from './scheduling/shiftScheduler';
|
import { IntelligentShiftScheduler, AssignmentResult, WeeklyPattern } from './scheduling';
|
||||||
import { transformToSchedulingData } from './scheduling/dataAdapter';
|
|
||||||
import { AssignmentResult, WeeklyPattern } from './scheduling/types';
|
|
||||||
import { isScheduledShift } from '../models/helpers';
|
import { isScheduledShift } from '../models/helpers';
|
||||||
|
|
||||||
const API_BASE_URL = 'http://localhost:3002/api/scheduled-shifts';
|
const API_BASE_URL = 'http://localhost:3002/api/scheduled-shifts';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Helper function to get auth headers
|
// Helper function to get auth headers
|
||||||
const getAuthHeaders = () => {
|
const getAuthHeaders = () => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
@@ -21,7 +21,7 @@ const getAuthHeaders = () => {
|
|||||||
export class ShiftAssignmentService {
|
export class ShiftAssignmentService {
|
||||||
async updateScheduledShift(id: string, updates: { assignedEmployees: string[] }): Promise<void> {
|
async updateScheduledShift(id: string, updates: { assignedEmployees: string[] }): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('🔄 Updating scheduled shift via API:', { id, updates });
|
//console.log('🔄 Updating scheduled shift via API:', { id, updates });
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE_URL}/${id}`, {
|
const response = await fetch(`${API_BASE_URL}/${id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
@@ -141,65 +141,64 @@ export class ShiftAssignmentService {
|
|||||||
constraints: any = {}
|
constraints: any = {}
|
||||||
): Promise<AssignmentResult> {
|
): Promise<AssignmentResult> {
|
||||||
|
|
||||||
console.log('🔄 Starting enhanced scheduling algorithm...');
|
console.log('🧠 Starting intelligent scheduling for FIRST WEEK ONLY...');
|
||||||
|
|
||||||
// Get defined shifts for the first week
|
// Load all scheduled shifts
|
||||||
const definedShifts = await this.getDefinedShifts(shiftPlan);
|
const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
||||||
const firstWeekShifts = this.getFirstWeekShifts(definedShifts);
|
|
||||||
|
|
||||||
console.log('📊 First week analysis:', {
|
if (scheduledShifts.length === 0) {
|
||||||
totalShifts: definedShifts.length,
|
return {
|
||||||
firstWeekShifts: firstWeekShifts.length,
|
assignments: {},
|
||||||
employees: employees.length
|
violations: ['❌ KRITISCH: Keine Schichten verfügbar für die Zuordnung'],
|
||||||
});
|
success: false,
|
||||||
|
resolutionReport: ['🚨 ABBRUCH: Keine Schichten im Plan verfügbar']
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Transform data for scheduling algorithm
|
// Set cache for scheduler
|
||||||
const { schedulingEmployees, schedulingShifts, managerShifts } = transformToSchedulingData(
|
IntelligentShiftScheduler.scheduledShiftsCache.set(shiftPlan.id, scheduledShifts);
|
||||||
|
|
||||||
|
// 🔥 RUN SCHEDULING FOR FIRST WEEK ONLY
|
||||||
|
const schedulingResult = await IntelligentShiftScheduler.generateOptimalSchedule(
|
||||||
|
shiftPlan,
|
||||||
employees.filter(emp => emp.isActive),
|
employees.filter(emp => emp.isActive),
|
||||||
firstWeekShifts,
|
availabilities,
|
||||||
availabilities
|
constraints
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('🎯 Transformed data for scheduling:', {
|
// Get first week shifts for pattern
|
||||||
employees: schedulingEmployees.length,
|
const firstWeekShifts = this.getFirstWeekShifts(scheduledShifts);
|
||||||
shifts: schedulingShifts.length,
|
|
||||||
managerShifts: managerShifts.length
|
console.log('🔄 Creating weekly pattern from FIRST WEEK:', {
|
||||||
|
firstWeekShifts: firstWeekShifts.length,
|
||||||
|
allShifts: scheduledShifts.length,
|
||||||
|
patternAssignments: Object.keys(schedulingResult.assignments).length
|
||||||
});
|
});
|
||||||
|
|
||||||
// Run the enhanced scheduling algorithm with better constraints
|
|
||||||
const schedulingResult = scheduleWithManager(
|
|
||||||
schedulingShifts,
|
|
||||||
schedulingEmployees,
|
|
||||||
managerShifts,
|
|
||||||
{
|
|
||||||
enforceNoTraineeAlone: constraints.enforceNoTraineeAlone ?? true,
|
|
||||||
enforceExperiencedWithChef: constraints.enforceExperiencedWithChef ?? true,
|
|
||||||
maxRepairAttempts: constraints.maxRepairAttempts ?? 50,
|
|
||||||
targetEmployeesPerShift: constraints.targetEmployeesPerShift ?? 2 // Flexible target
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('📊 Enhanced scheduling completed:', {
|
|
||||||
assignments: Object.keys(schedulingResult.assignments).length,
|
|
||||||
violations: schedulingResult.violations.length,
|
|
||||||
success: schedulingResult.success
|
|
||||||
});
|
|
||||||
|
|
||||||
// Apply weekly pattern to all shifts
|
|
||||||
const weeklyPattern: WeeklyPattern = {
|
const weeklyPattern: WeeklyPattern = {
|
||||||
weekShifts: firstWeekShifts,
|
weekShifts: firstWeekShifts,
|
||||||
assignments: schedulingResult.assignments,
|
assignments: schedulingResult.assignments, // 🔥 Diese enthalten nur erste Woche
|
||||||
weekNumber: 1
|
weekNumber: 1
|
||||||
};
|
};
|
||||||
|
|
||||||
const allAssignments = this.applyWeeklyPattern(definedShifts, weeklyPattern);
|
// 🔥 APPLY PATTERN TO ALL WEEKS
|
||||||
|
const allAssignments = this.applyWeeklyPattern(scheduledShifts, weeklyPattern);
|
||||||
|
|
||||||
|
console.log('✅ Pattern applied to all weeks:', {
|
||||||
|
firstWeekAssignments: Object.keys(schedulingResult.assignments).length,
|
||||||
|
allWeeksAssignments: Object.keys(allAssignments).length
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean cache
|
||||||
|
IntelligentShiftScheduler.scheduledShiftsCache.delete(shiftPlan.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
assignments: allAssignments,
|
assignments: allAssignments, // 🔥 Diese enthalten alle Wochen
|
||||||
violations: schedulingResult.violations,
|
violations: schedulingResult.violations,
|
||||||
success: schedulingResult.violations.length === 0,
|
success: schedulingResult.success,
|
||||||
pattern: weeklyPattern,
|
pattern: weeklyPattern,
|
||||||
resolutionReport: schedulingResult.resolutionReport // Füge diese Zeile hinzu
|
resolutionReport: schedulingResult.resolutionReport,
|
||||||
|
qualityMetrics: schedulingResult.qualityMetrics
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,29 +335,48 @@ export class ShiftAssignmentService {
|
|||||||
|
|
||||||
const assignments: { [shiftId: string]: string[] } = {};
|
const assignments: { [shiftId: string]: string[] } = {};
|
||||||
|
|
||||||
// Group all shifts by week
|
// Group all shifts by week AND day-timeSlot combination
|
||||||
const shiftsByWeek = this.groupShiftsByWeek(allShifts);
|
const shiftsByPatternKey = new Map<string, ScheduledShift[]>();
|
||||||
|
|
||||||
console.log('📅 Applying weekly pattern to', Object.keys(shiftsByWeek).length, 'weeks');
|
allShifts.forEach(shift => {
|
||||||
|
const dayOfWeek = this.getDayOfWeek(shift.date);
|
||||||
// For each week, apply the pattern from week 1
|
const patternKey = `${dayOfWeek}-${shift.timeSlotId}`;
|
||||||
Object.entries(shiftsByWeek).forEach(([weekKey, weekShifts]) => {
|
|
||||||
const weekNumber = parseInt(weekKey);
|
|
||||||
|
|
||||||
weekShifts.forEach(shift => {
|
if (!shiftsByPatternKey.has(patternKey)) {
|
||||||
// Find the corresponding shift in the weekly pattern
|
shiftsByPatternKey.set(patternKey, []);
|
||||||
const patternShift = this.findMatchingPatternShift(shift, weeklyPattern.weekShifts);
|
}
|
||||||
|
shiftsByPatternKey.get(patternKey)!.push(shift);
|
||||||
if (patternShift) {
|
|
||||||
// Use the same assignment as the pattern shift
|
|
||||||
assignments[shift.id] = [...weeklyPattern.assignments[patternShift.id]];
|
|
||||||
} else {
|
|
||||||
// No matching pattern shift, leave empty
|
|
||||||
assignments[shift.id] = [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('📊 Pattern application analysis:');
|
||||||
|
console.log('- Unique pattern keys:', shiftsByPatternKey.size);
|
||||||
|
console.log('- Pattern keys:', Array.from(shiftsByPatternKey.keys()));
|
||||||
|
|
||||||
|
// For each shift in all weeks, find the matching pattern shift
|
||||||
|
allShifts.forEach(shift => {
|
||||||
|
const dayOfWeek = this.getDayOfWeek(shift.date);
|
||||||
|
//const patternKey = `${dayOfWeek}-${shift.timeSlotId}`;
|
||||||
|
const patternKey = `${shift.timeSlotId}`;
|
||||||
|
|
||||||
|
// Find the pattern shift for this day-timeSlot combination
|
||||||
|
const patternShift = weeklyPattern.weekShifts.find(patternShift => {
|
||||||
|
const patternDayOfWeek = this.getDayOfWeek(patternShift.date);
|
||||||
|
return patternDayOfWeek === dayOfWeek &&
|
||||||
|
patternShift.timeSlotId === shift.timeSlotId;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (patternShift && weeklyPattern.assignments[patternShift.id]) {
|
||||||
|
assignments[shift.id] = [...weeklyPattern.assignments[patternShift.id]];
|
||||||
|
} else {
|
||||||
|
assignments[shift.id] = [];
|
||||||
|
console.warn(`❌ No pattern found for shift: ${patternKey}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// DEBUG: Check assignment coverage
|
||||||
|
const assignedShifts = Object.values(assignments).filter(a => a.length > 0).length;
|
||||||
|
console.log(`📊 Assignment coverage: ${assignedShifts}/${allShifts.length} shifts assigned`);
|
||||||
|
|
||||||
return assignments;
|
return assignments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user