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
|
||||
`, [employeeId]);
|
||||
|
||||
//console.log('✅ Successfully got availabilities from employee:', availabilities);
|
||||
|
||||
res.json(availabilities.map(avail => ({
|
||||
id: avail.id,
|
||||
employeeId: avail.employee_id,
|
||||
@@ -348,6 +350,9 @@ export const updateAvailabilities = async (req: AuthRequest, res: Response): Pro
|
||||
|
||||
await db.run('COMMIT');
|
||||
|
||||
console.log('✅ Successfully updated availablities employee:', );
|
||||
|
||||
|
||||
// Return updated availabilities
|
||||
const updatedAvailabilities = await db.all<any>(`
|
||||
SELECT * FROM employee_availability
|
||||
@@ -365,6 +370,8 @@ export const updateAvailabilities = async (req: AuthRequest, res: Response): Pro
|
||||
notes: avail.notes
|
||||
})));
|
||||
|
||||
console.log('✅ Successfully updated employee:', updateAvailabilities);
|
||||
|
||||
} catch (error) {
|
||||
await db.run('ROLLBACK');
|
||||
throw error;
|
||||
|
||||
@@ -740,53 +740,6 @@ export const generateScheduledShiftsForPlan = async (req: Request, res: Response
|
||||
}
|
||||
};
|
||||
|
||||
export const revertToDraft = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const userId = (req as AuthRequest).user?.userId;
|
||||
|
||||
if (!userId) {
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if plan exists
|
||||
const existingPlan = await getShiftPlanById(id);
|
||||
//const existingPlan: ShiftPlan = await db.get('SELECT * FROM shift_plans WHERE id = ?', [id]);
|
||||
if (!existingPlan) {
|
||||
res.status(404).json({ error: 'Shift plan not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Only allow reverting from published to draft
|
||||
if (existingPlan.status !== 'published') {
|
||||
res.status(400).json({ error: 'Can only revert published plans to draft' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Update plan status to draft
|
||||
await db.run(
|
||||
'UPDATE shift_plans SET status = ? WHERE id = ?',
|
||||
['draft', id]
|
||||
);
|
||||
|
||||
// Clear all assigned employees from scheduled shifts
|
||||
await db.run(
|
||||
'UPDATE scheduled_shifts SET assigned_employees = ? WHERE plan_id = ?',
|
||||
[JSON.stringify([]), id]
|
||||
);
|
||||
|
||||
console.log(`✅ Plan ${id} reverted to draft status`);
|
||||
|
||||
// Return updated plan
|
||||
const updatedPlan = await getShiftPlanById(id);
|
||||
res.json(updatedPlan);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error reverting plan to draft:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const regenerateScheduledShifts = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
@@ -922,3 +875,62 @@ export const updateScheduledShift = async (req: AuthRequest, res: Response): Pro
|
||||
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);
|
||||
|
||||
// Employee CRUD Routes
|
||||
router.get('/', requireRole(['admin', 'instandhalter']), getEmployees);
|
||||
router.get('/', authMiddleware, getEmployees);
|
||||
router.get('/:id', requireRole(['admin', 'instandhalter']), getEmployee);
|
||||
router.post('/', requireRole(['admin']), createEmployee);
|
||||
router.put('/:id', requireRole(['admin']), updateEmployee);
|
||||
|
||||
@@ -19,12 +19,12 @@ router.post('/:id/generate-shifts', requireRole(['admin', 'instandhalter']), gen
|
||||
router.post('/:id/regenerate-shifts', requireRole(['admin', 'instandhalter']), regenerateScheduledShifts);
|
||||
|
||||
// GET all scheduled shifts for a plan
|
||||
router.get('/plan/:planId', requireRole(['admin']), getScheduledShiftsFromPlan);
|
||||
router.get('/plan/:planId', authMiddleware, getScheduledShiftsFromPlan);
|
||||
|
||||
// GET specific scheduled shift
|
||||
router.get('/:id', requireRole(['admin']), getScheduledShift);
|
||||
router.get('/:id', authMiddleware, getScheduledShift);
|
||||
|
||||
// UPDATE scheduled shift
|
||||
router.put('/:id', requireRole(['admin']), updateScheduledShift);
|
||||
router.put('/:id', authMiddleware, updateScheduledShift);
|
||||
|
||||
export default router;
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
updateShiftPlan,
|
||||
deleteShiftPlan,
|
||||
createFromPreset,
|
||||
revertToDraft,
|
||||
clearAssignments
|
||||
} from '../controllers/shiftPlanController.js';
|
||||
|
||||
const router = express.Router();
|
||||
@@ -18,13 +18,13 @@ router.use(authMiddleware);
|
||||
// Combined routes for both shift plans and templates
|
||||
|
||||
// GET all shift plans (including templates)
|
||||
router.get('/', getShiftPlans);
|
||||
router.get('/' , authMiddleware, getShiftPlans);
|
||||
|
||||
// GET templates only
|
||||
//router.get('/templates', getTemplates);
|
||||
|
||||
// GET specific shift plan or template
|
||||
router.get('/:id', getShiftPlan);
|
||||
router.get('/:id', authMiddleware, getShiftPlan);
|
||||
|
||||
// POST create new shift plan
|
||||
router.post('/', requireRole(['admin', 'instandhalter']), createShiftPlan);
|
||||
@@ -41,7 +41,7 @@ router.put('/:id', requireRole(['admin', 'instandhalter']), updateShiftPlan);
|
||||
// DELETE shift plan or template
|
||||
router.delete('/:id', requireRole(['admin', 'instandhalter']), deleteShiftPlan);
|
||||
|
||||
// PUT revert published plan to draft
|
||||
router.put('/:id/revert-to-draft', requireRole(['admin', 'instandhalter']), revertToDraft);
|
||||
// POST clear assignments and reset to draft
|
||||
router.post('/:id/clear-assignments', requireRole(['admin', 'instandhalter']), clearAssignments);
|
||||
|
||||
export default router;
|
||||
@@ -5,7 +5,6 @@ import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||
import { NotificationProvider } from './contexts/NotificationContext';
|
||||
import NotificationContainer from './components/Notification/NotificationContainer';
|
||||
import Layout from './components/Layout/Layout';
|
||||
import { DesignSystemProvider, GlobalStyles } from './design/DesignSystem';
|
||||
import Login from './pages/Auth/Login';
|
||||
import Dashboard from './pages/Dashboard/Dashboard';
|
||||
import ShiftPlanList from './pages/ShiftPlans/ShiftPlanList';
|
||||
@@ -133,8 +132,6 @@ const AppContent: React.FC = () => {
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<DesignSystemProvider>
|
||||
<GlobalStyles />
|
||||
<NotificationProvider>
|
||||
<AuthProvider>
|
||||
<Router>
|
||||
@@ -143,7 +140,6 @@ function App() {
|
||||
</Router>
|
||||
</AuthProvider>
|
||||
</NotificationProvider>
|
||||
</DesignSystemProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import React from 'react';
|
||||
const Footer: React.FC = () => {
|
||||
const styles = {
|
||||
footer: {
|
||||
background: 'linear-gradient(135deg, #1a1325 0%, #24163a 100%)',
|
||||
background: 'linear-gradient(0deg, #161718 0%, #24163a 100%)',
|
||||
color: 'white',
|
||||
marginTop: 'auto',
|
||||
borderTop: '1px solid rgba(251, 250, 246, 0.1)',
|
||||
@@ -44,7 +44,7 @@ const Footer: React.FC = () => {
|
||||
borderTop: '1px solid rgba(251, 250, 246, 0.1)',
|
||||
padding: '1.5rem 2rem',
|
||||
textAlign: 'center' as const,
|
||||
color: 'rgba(251, 250, 246, 0.6)',
|
||||
color: '#FBFAF6',
|
||||
fontSize: '0.9rem',
|
||||
},
|
||||
};
|
||||
@@ -201,8 +201,8 @@ const Footer: React.FC = () => {
|
||||
|
||||
<div style={styles.footerBottom}>
|
||||
<p style={{margin: 0}}>
|
||||
© 2025 Schichtenplaner. Alle Rechte vorbehalten. |
|
||||
Made with ❤️ for efficient team management
|
||||
© 2025 Schichtenplaner |
|
||||
Made with <span style={{ color: '#854eca' }}>♥</span> for efficient team management
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
// frontend/src/design/DesignSystem.tsx
|
||||
import React, { createContext, useContext, ReactNode } from 'react';
|
||||
|
||||
// Design Tokens
|
||||
export const designTokens = {
|
||||
colors: {
|
||||
// Primary Colors
|
||||
white: '#FBFAF6',
|
||||
default_white: '#ddd', //boxes
|
||||
white: '#FBFAF6', //background, fonts
|
||||
black: '#161718',
|
||||
|
||||
// Purple Gradients
|
||||
@@ -123,274 +121,3 @@ export const designTokens = {
|
||||
'2xl': '1536px',
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Context for Design System
|
||||
interface DesignSystemContextType {
|
||||
tokens: typeof designTokens;
|
||||
getColor: (path: string) => string;
|
||||
getSpacing: (size: keyof typeof designTokens.spacing) => string;
|
||||
}
|
||||
|
||||
const DesignSystemContext = createContext<DesignSystemContextType | undefined>(undefined);
|
||||
|
||||
// Design System Provider
|
||||
interface DesignSystemProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const DesignSystemProvider: React.FC<DesignSystemProviderProps> = ({ children }) => {
|
||||
const getColor = (path: string): string => {
|
||||
const parts = path.split('.');
|
||||
let current: any = designTokens.colors;
|
||||
|
||||
for (const part of parts) {
|
||||
if (current[part] === undefined) {
|
||||
console.warn(`Color path "${path}" not found in design tokens`);
|
||||
return designTokens.colors.primary;
|
||||
}
|
||||
current = current[part];
|
||||
}
|
||||
|
||||
return current;
|
||||
};
|
||||
|
||||
const getSpacing = (size: keyof typeof designTokens.spacing): string => {
|
||||
return designTokens.spacing[size];
|
||||
};
|
||||
|
||||
const value: DesignSystemContextType = {
|
||||
tokens: designTokens,
|
||||
getColor,
|
||||
getSpacing,
|
||||
};
|
||||
|
||||
return (
|
||||
<DesignSystemContext.Provider value={value}>
|
||||
{children}
|
||||
</DesignSystemContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// Hook to use Design System
|
||||
export const useDesignSystem = (): DesignSystemContextType => {
|
||||
const context = useContext(DesignSystemContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useDesignSystem must be used within a DesignSystemProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
// Utility Components
|
||||
export interface BoxProps {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
p?: keyof typeof designTokens.spacing;
|
||||
px?: keyof typeof designTokens.spacing;
|
||||
py?: keyof typeof designTokens.spacing;
|
||||
m?: keyof typeof designTokens.spacing;
|
||||
mx?: keyof typeof designTokens.spacing;
|
||||
my?: keyof typeof designTokens.spacing;
|
||||
bg?: string;
|
||||
color?: string;
|
||||
borderRadius?: keyof typeof designTokens.borderRadius;
|
||||
}
|
||||
|
||||
export const Box: React.FC<BoxProps> = ({
|
||||
children,
|
||||
className,
|
||||
style,
|
||||
p,
|
||||
px,
|
||||
py,
|
||||
m,
|
||||
mx,
|
||||
my,
|
||||
bg,
|
||||
color,
|
||||
borderRadius,
|
||||
...props
|
||||
}) => {
|
||||
const { tokens, getColor } = useDesignSystem();
|
||||
|
||||
const boxStyle: React.CSSProperties = {
|
||||
padding: p && tokens.spacing[p],
|
||||
paddingLeft: px && tokens.spacing[px],
|
||||
paddingRight: px && tokens.spacing[px],
|
||||
paddingTop: py && tokens.spacing[py],
|
||||
paddingBottom: py && tokens.spacing[py],
|
||||
margin: m && tokens.spacing[m],
|
||||
marginLeft: mx && tokens.spacing[mx],
|
||||
marginRight: mx && tokens.spacing[mx],
|
||||
marginTop: my && tokens.spacing[my],
|
||||
marginBottom: my && tokens.spacing[my],
|
||||
backgroundColor: bg && getColor(bg),
|
||||
color: color && getColor(color),
|
||||
borderRadius: borderRadius && tokens.borderRadius[borderRadius],
|
||||
fontFamily: tokens.typography.fontFamily,
|
||||
...style,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className} style={boxStyle} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface TextProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
variant?: 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
|
||||
weight?: keyof typeof designTokens.typography.fontWeights;
|
||||
color?: string;
|
||||
align?: 'left' | 'center' | 'right' | 'justify';
|
||||
lineHeight?: keyof typeof designTokens.typography.lineHeights;
|
||||
letterSpacing?: keyof typeof designTokens.typography.letterSpacing;
|
||||
}
|
||||
|
||||
export const Text: React.FC<TextProps> = ({
|
||||
children,
|
||||
className,
|
||||
style,
|
||||
variant = 'base',
|
||||
weight = 'normal',
|
||||
color = 'text.primary',
|
||||
align = 'left',
|
||||
lineHeight = 'normal',
|
||||
letterSpacing = 'normal',
|
||||
...props
|
||||
}) => {
|
||||
const { tokens, getColor } = useDesignSystem();
|
||||
|
||||
const textStyle: React.CSSProperties = {
|
||||
fontSize: tokens.typography.fontSizes[variant],
|
||||
fontWeight: tokens.typography.fontWeights[weight],
|
||||
color: getColor(color),
|
||||
textAlign: align,
|
||||
lineHeight: tokens.typography.lineHeights[lineHeight],
|
||||
letterSpacing: tokens.typography.letterSpacing[letterSpacing],
|
||||
fontFamily: tokens.typography.fontFamily,
|
||||
...style,
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={className} style={textStyle} {...props}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// Global Styles Component
|
||||
export const GlobalStyles: React.FC = () => {
|
||||
const { tokens } = useDesignSystem();
|
||||
|
||||
const globalStyles = `
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: ${tokens.typography.fontFamily};
|
||||
font-size: 16px;
|
||||
line-height: ${tokens.typography.lineHeights.normal};
|
||||
color: ${tokens.colors.text.primary};
|
||||
background-color: ${tokens.colors.background};
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: ${tokens.typography.fontFamily};
|
||||
font-weight: ${tokens.typography.fontWeights.normal};
|
||||
line-height: ${tokens.typography.lineHeights.normal};
|
||||
color: ${tokens.colors.text.primary};
|
||||
background-color: ${tokens.colors.background};
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: ${tokens.typography.fontFamily};
|
||||
font-weight: ${tokens.typography.fontWeights.bold};
|
||||
line-height: ${tokens.typography.lineHeights.tight};
|
||||
color: ${tokens.colors.text.primary};
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: ${tokens.typography.fontSizes['4xl']};
|
||||
letter-spacing: ${tokens.typography.letterSpacing.tight};
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: ${tokens.typography.fontSizes['3xl']};
|
||||
letter-spacing: ${tokens.typography.letterSpacing.tight};
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: ${tokens.typography.fontSizes['2xl']};
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: ${tokens.typography.fontSizes.xl};
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: ${tokens.typography.fontSizes.lg};
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: ${tokens.typography.fontSizes.base};
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: ${tokens.typography.fontSizes.base};
|
||||
line-height: ${tokens.typography.lineHeights.relaxed};
|
||||
color: ${tokens.colors.text.primary};
|
||||
}
|
||||
|
||||
a {
|
||||
color: ${tokens.colors.primary};
|
||||
text-decoration: none;
|
||||
transition: ${tokens.transitions.default};
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: ${tokens.colors.secondary};
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: ${tokens.typography.fontFamily};
|
||||
transition: ${tokens.transitions.default};
|
||||
}
|
||||
|
||||
input, textarea, select {
|
||||
font-family: ${tokens.typography.fontFamily};
|
||||
}
|
||||
|
||||
/* Scrollbar Styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: ${tokens.colors.background};
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: ${tokens.colors.border.medium};
|
||||
border-radius: ${tokens.borderRadius.full};
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: ${tokens.colors.border.dark};
|
||||
}
|
||||
`;
|
||||
|
||||
return <style>{globalStyles}</style>;
|
||||
};
|
||||
|
||||
export default designTokens;
|
||||
@@ -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';
|
||||
@@ -283,11 +283,6 @@ const Dashboard: React.FC = () => {
|
||||
};
|
||||
};
|
||||
|
||||
// Add refresh functionality
|
||||
const handleRefresh = () => {
|
||||
loadDashboardData();
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||
@@ -370,8 +365,6 @@ const Dashboard: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PlanDebugInfo />
|
||||
|
||||
{/* Quick Actions - Nur für Admins/Instandhalter */}
|
||||
{hasRole(['admin', 'instandhalter']) && (
|
||||
<div style={{ marginBottom: '30px' }}>
|
||||
|
||||
@@ -438,6 +438,8 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
await employeeService.updateAvailabilities(employee.id, requestData);
|
||||
console.log('✅ VERFÜGBARKEITEN ERFOLGREICH GESPEICHERT');
|
||||
|
||||
window.dispatchEvent(new CustomEvent('availabilitiesChanged'));
|
||||
|
||||
onSave();
|
||||
} catch (err: any) {
|
||||
console.error('❌ FEHLER BEIM SPEICHERN:', err);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// frontend/src/pages/Employees/components/EmployeeList.tsx - KORRIGIERT
|
||||
// frontend/src/pages/Employees/components/EmployeeList.tsx
|
||||
import React, { useState } from 'react';
|
||||
import { ROLE_CONFIG, EMPLOYEE_TYPE_CONFIG } from '../../../models/defaults/employeeDefaults';
|
||||
import { Employee } from '../../../models/Employee';
|
||||
@@ -11,6 +11,9 @@ interface EmployeeListProps {
|
||||
onManageAvailability: (employee: Employee) => void;
|
||||
}
|
||||
|
||||
type SortField = 'name' | 'employeeType' | 'canWorkAlone' | 'role' | 'lastLogin';
|
||||
type SortDirection = 'asc' | 'desc';
|
||||
|
||||
const EmployeeList: React.FC<EmployeeListProps> = ({
|
||||
employees,
|
||||
onEdit,
|
||||
@@ -19,8 +22,11 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
||||
}) => {
|
||||
const [filter, setFilter] = useState<'all' | 'active' | 'inactive'>('active');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [sortField, setSortField] = useState<SortField>('name');
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
|
||||
const { user: currentUser, hasRole } = useAuth();
|
||||
|
||||
// Filter employees based on active/inactive and search term
|
||||
const filteredEmployees = employees.filter(employee => {
|
||||
if (filter === 'active' && !employee.isActive) return false;
|
||||
if (filter === 'inactive' && employee.isActive) return false;
|
||||
@@ -38,6 +44,60 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
||||
return true;
|
||||
});
|
||||
|
||||
// Sort employees based on selected field and direction
|
||||
const sortedEmployees = [...filteredEmployees].sort((a, b) => {
|
||||
let aValue: any;
|
||||
let bValue: any;
|
||||
|
||||
switch (sortField) {
|
||||
case 'name':
|
||||
aValue = a.name.toLowerCase();
|
||||
bValue = b.name.toLowerCase();
|
||||
break;
|
||||
case 'employeeType':
|
||||
aValue = a.employeeType;
|
||||
bValue = b.employeeType;
|
||||
break;
|
||||
case 'canWorkAlone':
|
||||
aValue = a.canWorkAlone;
|
||||
bValue = b.canWorkAlone;
|
||||
break;
|
||||
case 'role':
|
||||
aValue = a.role;
|
||||
bValue = b.role;
|
||||
break;
|
||||
case 'lastLogin':
|
||||
// Handle null values for lastLogin (put them at the end)
|
||||
aValue = a.lastLogin ? new Date(a.lastLogin).getTime() : 0;
|
||||
bValue = b.lastLogin ? new Date(b.lastLogin).getTime() : 0;
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (sortDirection === 'asc') {
|
||||
return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
|
||||
} else {
|
||||
return aValue > bValue ? -1 : aValue < bValue ? 1 : 0;
|
||||
}
|
||||
});
|
||||
|
||||
const handleSort = (field: SortField) => {
|
||||
if (sortField === field) {
|
||||
// Toggle direction if same field
|
||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
// New field, default to ascending
|
||||
setSortField(field);
|
||||
setSortDirection('asc');
|
||||
}
|
||||
};
|
||||
|
||||
const getSortIndicator = (field: SortField) => {
|
||||
if (sortField !== field) return '↕';
|
||||
return sortDirection === 'asc' ? '↑' : '↓';
|
||||
};
|
||||
|
||||
// Simplified permission checks
|
||||
const canDeleteEmployee = (employee: Employee): boolean => {
|
||||
if (!hasRole(['admin'])) return false;
|
||||
@@ -78,8 +138,8 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
||||
|
||||
const getIndependenceBadge = (canWorkAlone: boolean) => {
|
||||
return canWorkAlone
|
||||
? { text: '✅ Eigenständig', color: '#27ae60', bgColor: '#d5f4e6' }
|
||||
: { text: '❌ Betreuung', color: '#e74c3c', bgColor: '#fadbd8' };
|
||||
? { text: 'Eigenständig', color: '#27ae60', bgColor: '#d5f4e6' }
|
||||
: { text: 'Betreuung', color: '#e74c3c', bgColor: '#fadbd8' };
|
||||
};
|
||||
|
||||
type Role = typeof ROLE_CONFIG[number]['value'];
|
||||
@@ -161,7 +221,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
||||
</div>
|
||||
|
||||
<div style={{ color: '#7f8c8d', fontSize: '14px' }}>
|
||||
{filteredEmployees.length} von {employees.length} Mitarbeitern
|
||||
{sortedEmployees.length} von {employees.length} Mitarbeitern
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -185,16 +245,41 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
||||
color: '#2c3e50',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<div>Name & E-Mail</div>
|
||||
<div style={{ textAlign: 'center' }}>Typ</div>
|
||||
<div style={{ textAlign: 'center' }}>Eigenständigkeit</div>
|
||||
<div style={{ textAlign: 'center' }}>Rolle</div>
|
||||
<div
|
||||
onClick={() => handleSort('name')}
|
||||
style={{ cursor: 'pointer', userSelect: 'none', display: 'flex', alignItems: 'center', gap: '5px' }}
|
||||
>
|
||||
Name & E-Mail {getSortIndicator('name')}
|
||||
</div>
|
||||
<div
|
||||
onClick={() => handleSort('employeeType')}
|
||||
style={{ textAlign: 'center', cursor: 'pointer', userSelect: 'none', display: 'flex', alignItems: 'center', gap: '5px', justifyContent: 'center' }}
|
||||
>
|
||||
Typ {getSortIndicator('employeeType')}
|
||||
</div>
|
||||
<div
|
||||
onClick={() => handleSort('canWorkAlone')}
|
||||
style={{ textAlign: 'center', cursor: 'pointer', userSelect: 'none', display: 'flex', alignItems: 'center', gap: '5px', justifyContent: 'center' }}
|
||||
>
|
||||
Eigenständigkeit {getSortIndicator('canWorkAlone')}
|
||||
</div>
|
||||
<div
|
||||
onClick={() => handleSort('role')}
|
||||
style={{ textAlign: 'center', cursor: 'pointer', userSelect: 'none', display: 'flex', alignItems: 'center', gap: '5px', justifyContent: 'center' }}
|
||||
>
|
||||
Rolle {getSortIndicator('role')}
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>Status</div>
|
||||
<div style={{ textAlign: 'center' }}>Letzter Login</div>
|
||||
<div
|
||||
onClick={() => handleSort('lastLogin')}
|
||||
style={{ textAlign: 'center', cursor: 'pointer', userSelect: 'none', display: 'flex', alignItems: 'center', gap: '5px', justifyContent: 'center' }}
|
||||
>
|
||||
Letzter Login {getSortIndicator('lastLogin')}
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>Aktionen</div>
|
||||
</div>
|
||||
|
||||
{filteredEmployees.map(employee => {
|
||||
{sortedEmployees.map(employee => {
|
||||
const employeeType = getEmployeeTypeBadge(employee.employeeType);
|
||||
const independence = getIndependenceBadge(employee.canWorkAlone);
|
||||
const roleColor = getRoleBadge(employee.role);
|
||||
|
||||
@@ -206,101 +206,6 @@ const Help: React.FC = () => {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Network Visualization */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
|
||||
gap: '15px',
|
||||
marginTop: '30px'
|
||||
}}>
|
||||
{/* Employees */}
|
||||
<div style={{
|
||||
backgroundColor: '#e8f4fd',
|
||||
padding: '15px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #b8d4f0'
|
||||
}}>
|
||||
<h4 style={{ margin: '0 0 10px 0', color: '#3498db' }}>👥 Mitarbeiter</h4>
|
||||
<div style={{ fontSize: '14px', lineHeight: '1.4' }}>
|
||||
<div>• Manager (1)</div>
|
||||
<div>• Erfahrene ({currentStage >= 1 ? '3' : '0'})</div>
|
||||
<div>• Neue ({currentStage >= 1 ? '2' : '0'})</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Shifts */}
|
||||
<div style={{
|
||||
backgroundColor: '#fff3cd',
|
||||
padding: '15px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ffeaa7'
|
||||
}}>
|
||||
<h4 style={{ margin: '0 0 10px 0', color: '#f39c12' }}>📅 Schichten</h4>
|
||||
<div style={{ fontSize: '14px', lineHeight: '1.4' }}>
|
||||
<div>• Vormittag (5)</div>
|
||||
<div>• Nachmittag (4)</div>
|
||||
<div>• Manager-Schichten (3)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current Actions */}
|
||||
<div style={{
|
||||
backgroundColor: '#d4edda',
|
||||
padding: '15px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #c3e6cb'
|
||||
}}>
|
||||
<h4 style={{ margin: '0 0 10px 0', color: '#27ae60' }}>⚡ Aktive Aktionen</h4>
|
||||
<div style={{ fontSize: '14px', lineHeight: '1.4' }}>
|
||||
{currentStage === 0 && (
|
||||
<>
|
||||
<div>• Grundzuweisung läuft</div>
|
||||
<div>• Erfahrene priorisieren</div>
|
||||
</>
|
||||
)}
|
||||
{currentStage === 1 && (
|
||||
<>
|
||||
<div>• Manager wird zugewiesen</div>
|
||||
<div>• Erfahrene suchen</div>
|
||||
</>
|
||||
)}
|
||||
{currentStage === 2 && (
|
||||
<>
|
||||
<div>• Überbesetzung prüfen</div>
|
||||
<div>• Pool-Verwaltung aktiv</div>
|
||||
</>
|
||||
)}
|
||||
{currentStage === 3 && (
|
||||
<>
|
||||
<div>• Finale Validierung</div>
|
||||
<div>• Bericht generieren</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Problems & Solutions */}
|
||||
<div style={{
|
||||
backgroundColor: '#f8d7da',
|
||||
padding: '15px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #f5c6cb'
|
||||
}}>
|
||||
<h4 style={{ margin: '0 0 10px 0', color: '#e74c3c' }}>🔍 Probleme & Lösungen</h4>
|
||||
<div style={{ fontSize: '14px', lineHeight: '1.4' }}>
|
||||
{currentStage >= 2 ? (
|
||||
<>
|
||||
<div style={{ color: '#27ae60' }}>✅ 2 Probleme behoben</div>
|
||||
<div style={{ color: '#e74c3c' }}>❌ 0 kritische Probleme</div>
|
||||
<div style={{ color: '#f39c12' }}>⚠️ 1 Warnung</div>
|
||||
</>
|
||||
) : (
|
||||
<div>Noch keine Probleme analysiert</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Business Rules */}
|
||||
@@ -312,7 +217,7 @@ const Help: React.FC = () => {
|
||||
boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
|
||||
border: '1px solid #e0e0e0'
|
||||
}}>
|
||||
<h2 style={{ color: '#2c3e50', marginBottom: '20px' }}>📋 Geschäftsregeln</h2>
|
||||
<h2 style={{ color: '#2c3e50', marginBottom: '20px' }}>📋 Validierungs Regeln</h2>
|
||||
<div style={{ display: 'grid', gap: '10px' }}>
|
||||
{businessRules.map((rule, index) => (
|
||||
<div
|
||||
@@ -383,6 +288,9 @@ const Help: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
marginTop: '25px',
|
||||
padding: '20px',
|
||||
@@ -398,7 +306,6 @@ const Help: React.FC = () => {
|
||||
<li>Planen Sie Manager-Verfügbarkeit im Voraus</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@keyframes pulse {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { employeeService } from '../../services/employeeService';
|
||||
import { useNotification } from '../../contexts/NotificationContext';
|
||||
import AvailabilityManager from '../Employees/components/AvailabilityManager';
|
||||
import { Employee } from '../../models/Employee';
|
||||
import { styles } from './type/SettingsType';
|
||||
|
||||
const Settings: React.FC = () => {
|
||||
const { user: currentUser, updateUser } = useAuth();
|
||||
@@ -148,7 +149,12 @@ const Settings: React.FC = () => {
|
||||
};
|
||||
|
||||
if (!currentUser) {
|
||||
return <div>Nicht eingeloggt</div>;
|
||||
return <div style={{
|
||||
textAlign: 'center',
|
||||
padding: '3rem',
|
||||
color: '#666',
|
||||
fontSize: '1.1rem'
|
||||
}}>Nicht eingeloggt</div>;
|
||||
}
|
||||
|
||||
if (showAvailabilityManager) {
|
||||
@@ -161,177 +167,173 @@ const Settings: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
|
||||
<h1>⚙️ Einstellungen</h1>
|
||||
// Style constants for consistency
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
borderBottom: '1px solid #e0e0e0',
|
||||
marginBottom: '30px'
|
||||
}}>
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{/* Left Sidebar with Tabs */}
|
||||
<div style={styles.sidebar}>
|
||||
<div style={styles.header}>
|
||||
<h1 style={styles.title}>Einstellungen</h1>
|
||||
<div style={styles.subtitle}>Verwalten Sie Ihre Kontoeinstellungen und Präferenzen</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.tabs}>
|
||||
<button
|
||||
onClick={() => setActiveTab('profile')}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
backgroundColor: activeTab === 'profile' ? '#3498db' : 'transparent',
|
||||
color: activeTab === 'profile' ? 'white' : '#333',
|
||||
border: 'none',
|
||||
borderBottom: activeTab === 'profile' ? '3px solid #3498db' : 'none',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'bold'
|
||||
...styles.tab,
|
||||
...(activeTab === 'profile' ? styles.tabActive : {})
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (activeTab !== 'profile') {
|
||||
e.currentTarget.style.background = styles.tabHover.background;
|
||||
e.currentTarget.style.color = styles.tabHover.color;
|
||||
e.currentTarget.style.transform = styles.tabHover.transform;
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (activeTab !== 'profile') {
|
||||
e.currentTarget.style.background = styles.tab.background;
|
||||
e.currentTarget.style.color = styles.tab.color;
|
||||
e.currentTarget.style.transform = 'none';
|
||||
}
|
||||
}}
|
||||
>
|
||||
👤 Profil
|
||||
<span style={{ color: '#cda8f0', fontSize: '24px' }}>{'\u{1F464}\u{FE0E}'}</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<span style={{ fontSize: '0.95rem', fontWeight: 500 }}>Profil</span>
|
||||
<span style={{ fontSize: '0.8rem', opacity: 0.7, marginTop: '2px' }}>Persönliche Informationen</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('password')}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
backgroundColor: activeTab === 'password' ? '#3498db' : 'transparent',
|
||||
color: activeTab === 'password' ? 'white' : '#333',
|
||||
border: 'none',
|
||||
borderBottom: activeTab === 'password' ? '3px solid #3498db' : 'none',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'bold'
|
||||
...styles.tab,
|
||||
...(activeTab === 'password' ? styles.tabActive : {})
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (activeTab !== 'password') {
|
||||
e.currentTarget.style.background = styles.tabHover.background;
|
||||
e.currentTarget.style.color = styles.tabHover.color;
|
||||
e.currentTarget.style.transform = styles.tabHover.transform;
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (activeTab !== 'password') {
|
||||
e.currentTarget.style.background = styles.tab.background;
|
||||
e.currentTarget.style.color = styles.tab.color;
|
||||
e.currentTarget.style.transform = 'none';
|
||||
}
|
||||
}}
|
||||
>
|
||||
🔒 Passwort
|
||||
<span style={{ fontSize: '1.2rem', width: '24px' }}>🔒</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<span style={{ fontSize: '0.95rem', fontWeight: 500 }}>Passwort</span>
|
||||
<span style={{ fontSize: '0.8rem', opacity: 0.7, marginTop: '2px' }}>Sicherheitseinstellungen</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('availability')}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
backgroundColor: activeTab === 'availability' ? '#3498db' : 'transparent',
|
||||
color: activeTab === 'availability' ? 'white' : '#333',
|
||||
border: 'none',
|
||||
borderBottom: activeTab === 'availability' ? '3px solid #3498db' : 'none',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'bold'
|
||||
...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';
|
||||
}
|
||||
}}
|
||||
>
|
||||
📅 Verfügbarkeit
|
||||
<span style={{ fontSize: '1.2rem', width: '24px' }}>📅</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<span style={{ fontSize: '0.95rem', fontWeight: 500 }}>Verfügbarkeit</span>
|
||||
<span style={{ fontSize: '0.8rem', opacity: 0.7, marginTop: '2px' }}>Schichtplanung</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Content Area */}
|
||||
<div style={styles.content}>
|
||||
{/* Profile Tab */}
|
||||
{activeTab === 'profile' && (
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
padding: '30px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #e0e0e0',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
||||
}}>
|
||||
<h2 style={{ marginTop: 0, color: '#2c3e50' }}>Profilinformationen</h2>
|
||||
<>
|
||||
<div style={styles.section}>
|
||||
<h2 style={styles.sectionTitle}>Profilinformationen</h2>
|
||||
<p style={styles.sectionDescription}>
|
||||
Verwalten Sie Ihre persönlichen Informationen und Kontaktdaten
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleProfileUpdate}>
|
||||
<div style={{ display: 'grid', gap: '20px' }}>
|
||||
<form onSubmit={handleProfileUpdate} style={{ marginTop: '2rem' }}>
|
||||
<div style={styles.formGrid}>
|
||||
{/* 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' }}>
|
||||
<div style={styles.infoCard}>
|
||||
<h4 style={styles.infoCardTitle}>Systeminformationen</h4>
|
||||
<div style={styles.infoGrid}>
|
||||
<div style={styles.field}>
|
||||
<label style={styles.fieldLabel}>
|
||||
E-Mail
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={currentUser.email}
|
||||
disabled
|
||||
style={{
|
||||
width: '95%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
color: '#666'
|
||||
}}
|
||||
style={styles.fieldInputDisabled}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}>
|
||||
<div style={styles.field}>
|
||||
<label style={styles.fieldLabel}>
|
||||
Rolle
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={currentUser.role}
|
||||
disabled
|
||||
style={{
|
||||
width: '95%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
color: '#666'
|
||||
}}
|
||||
style={styles.fieldInputDisabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '15px', marginTop: '15px' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}>
|
||||
<div style={styles.field}>
|
||||
<label style={styles.fieldLabel}>
|
||||
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'
|
||||
}}
|
||||
style={styles.fieldInputDisabled}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}>
|
||||
<div style={styles.field}>
|
||||
<label style={styles.fieldLabel}>
|
||||
Vertragstyp
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={currentUser.contractType}
|
||||
disabled
|
||||
style={{
|
||||
width: '95%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
color: '#666'
|
||||
}}
|
||||
style={styles.fieldInputDisabled}
|
||||
/>
|
||||
</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',
|
||||
}}
|
||||
>
|
||||
<div style={styles.infoCard}>
|
||||
{/* Editable name field */}
|
||||
<div style={{ ...styles.field, marginTop: '1rem' }}>
|
||||
<label style={styles.fieldLabel}>
|
||||
Vollständiger Name *
|
||||
</label>
|
||||
<input
|
||||
@@ -341,59 +343,68 @@ const Settings: React.FC = () => {
|
||||
onChange={handleProfileChange}
|
||||
required
|
||||
style={{
|
||||
width: '97.5%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
fontSize: '16px',
|
||||
...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={{
|
||||
display: 'flex',
|
||||
gap: '15px',
|
||||
justifyContent: 'flex-end',
|
||||
marginTop: '30px',
|
||||
paddingTop: '20px',
|
||||
borderTop: '1px solid #f0f0f0'
|
||||
}}>
|
||||
<div style={styles.actions}>
|
||||
<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'
|
||||
...styles.button,
|
||||
...styles.buttonPrimary,
|
||||
...((loading || !profileForm.name.trim()) ? styles.buttonDisabled : {})
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!loading && profileForm.name.trim()) {
|
||||
e.currentTarget.style.background = styles.buttonPrimaryHover.background;
|
||||
e.currentTarget.style.transform = styles.buttonPrimaryHover.transform;
|
||||
e.currentTarget.style.boxShadow = styles.buttonPrimaryHover.boxShadow;
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!loading && profileForm.name.trim()) {
|
||||
e.currentTarget.style.background = styles.buttonPrimary.background;
|
||||
e.currentTarget.style.transform = 'none';
|
||||
e.currentTarget.style.boxShadow = styles.buttonPrimary.boxShadow;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? '⏳ Wird gespeichert...' : 'Profil aktualisieren'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</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>
|
||||
<>
|
||||
<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>
|
||||
|
||||
<form onSubmit={handlePasswordUpdate}>
|
||||
<div style={{ display: 'grid', gap: '20px', maxWidth: '400px' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
|
||||
<form onSubmit={handlePasswordUpdate} style={{ marginTop: '2rem' }}>
|
||||
<div style={styles.formGridCompact}>
|
||||
<div style={styles.field}>
|
||||
<label style={styles.fieldLabel}>
|
||||
Aktuelles Passwort *
|
||||
</label>
|
||||
<input
|
||||
@@ -402,19 +413,21 @@ const Settings: React.FC = () => {
|
||||
value={passwordForm.currentPassword}
|
||||
onChange={handlePasswordChange}
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
fontSize: '16px'
|
||||
}}
|
||||
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>
|
||||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
|
||||
<div style={styles.field}>
|
||||
<label style={styles.fieldLabel}>
|
||||
Neues Passwort *
|
||||
</label>
|
||||
<input
|
||||
@@ -424,22 +437,24 @@ const Settings: React.FC = () => {
|
||||
onChange={handlePasswordChange}
|
||||
required
|
||||
minLength={6}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
fontSize: '16px'
|
||||
}}
|
||||
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={{ fontSize: '12px', color: '#7f8c8d', marginTop: '5px' }}>
|
||||
<div style={styles.fieldHint}>
|
||||
Das Passwort muss mindestens 6 Zeichen lang sein.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
|
||||
<div style={styles.field}>
|
||||
<label style={styles.fieldLabel}>
|
||||
Neues Passwort bestätigen *
|
||||
</label>
|
||||
<input
|
||||
@@ -448,67 +463,65 @@ const Settings: React.FC = () => {
|
||||
value={passwordForm.confirmPassword}
|
||||
onChange={handlePasswordChange}
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
fontSize: '16px'
|
||||
}}
|
||||
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={{
|
||||
display: 'flex',
|
||||
gap: '15px',
|
||||
justifyContent: 'flex-end',
|
||||
marginTop: '30px',
|
||||
paddingTop: '20px',
|
||||
borderTop: '1px solid #f0f0f0'
|
||||
}}>
|
||||
<div style={styles.actions}>
|
||||
<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'
|
||||
...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>
|
||||
</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={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={{
|
||||
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' }}>
|
||||
<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.
|
||||
@@ -517,40 +530,28 @@ const Settings: React.FC = () => {
|
||||
<button
|
||||
onClick={() => setShowAvailabilityManager(true)}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
backgroundColor: '#3498db',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '16px'
|
||||
...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 style={{
|
||||
marginTop: '20px',
|
||||
padding: '15px',
|
||||
backgroundColor: '#e8f4fd',
|
||||
border: '1px solid #b6d7e8',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
color: '#2c3e50',
|
||||
textAlign: 'left'
|
||||
}}>
|
||||
<strong>💡 Informationen:</strong>
|
||||
<ul style={{ margin: '8px 0 0 20px', padding: 0 }}>
|
||||
<li><strong>Bevorzugt:</strong> Sie möchten diese Schicht arbeiten</li>
|
||||
<li><strong>Möglich:</strong> Sie können diese Schicht arbeiten</li>
|
||||
<li><strong>Nicht möglich:</strong> Sie können diese Schicht nicht arbeiten</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
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 { employeeService } from '../../services/employeeService';
|
||||
import { shiftAssignmentService, ShiftAssignmentService } from '../../services/shiftAssignmentService';
|
||||
import { AssignmentResult } from '../../services/scheduling';
|
||||
import { IntelligentShiftScheduler, SchedulingResult, AssignmentResult } from '../../services/scheduling';
|
||||
import { ShiftPlan, TimeSlot, ScheduledShift } from '../../models/ShiftPlan';
|
||||
import { Employee, EmployeeAvailability } from '../../models/Employee';
|
||||
import { useNotification } from '../../contexts/NotificationContext';
|
||||
import { formatDate, formatTime } from '../../utils/foramatters';
|
||||
import { isScheduledShift } from '../../models/helpers';
|
||||
|
||||
// Local interface extensions (same as AvailabilityManager)
|
||||
interface ExtendedTimeSlot extends TimeSlot {
|
||||
@@ -36,34 +35,83 @@ const ShiftPlanView: React.FC = () => {
|
||||
const [shiftPlan, setShiftPlan] = useState<ShiftPlan | null>(null);
|
||||
const [employees, setEmployees] = useState<Employee[]>([]);
|
||||
const [availabilities, setAvailabilities] = useState<EmployeeAvailability[]>([]);
|
||||
const [assignmentResult, setAssignmentResult] = useState<AssignmentResult | null>(null);
|
||||
const [assignmentResult, setAssignmentResult] = useState<AssignmentResult | null>(null); // Add this line
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [publishing, setPublishing] = useState(false);
|
||||
const [scheduledShifts, setScheduledShifts] = useState<ScheduledShift[]>([]);
|
||||
const [reverting, setReverting] = useState(false);
|
||||
const [showAssignmentPreview, setShowAssignmentPreview] = useState(false);
|
||||
const [recreating, setRecreating] = useState(false);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
loadShiftPlanData();
|
||||
debugScheduledShifts();
|
||||
|
||||
// Event Listener für Verfügbarkeits-Änderungen
|
||||
const handleAvailabilityChange = () => {
|
||||
console.log('📢 Verfügbarkeiten wurden geändert - lade Daten neu...');
|
||||
reloadAvailabilities();
|
||||
};
|
||||
|
||||
// Globales Event für Verfügbarkeits-Änderungen
|
||||
window.addEventListener('availabilitiesChanged', handleAvailabilityChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('availabilitiesChanged', handleAvailabilityChange);
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
// Seite ist wieder sichtbar - Daten neu laden
|
||||
console.log('🔄 Seite ist wieder sichtbar - lade Daten neu...');
|
||||
reloadAvailabilities();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
(window as any).debugRenderLogic = debugRenderLogic;
|
||||
return () => { (window as any).debugRenderLogic = undefined; };
|
||||
}, [shiftPlan, scheduledShifts]);
|
||||
|
||||
const loadShiftPlanData = async () => {
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const [plan, employeesData, shiftsData] = await Promise.all([
|
||||
|
||||
// Load plan and employees first
|
||||
const [plan, employeesData] = await Promise.all([
|
||||
shiftPlanService.getShiftPlan(id),
|
||||
employeeService.getEmployees(),
|
||||
shiftAssignmentService.getScheduledShiftsForPlan(id) // Load shifts here
|
||||
]);
|
||||
|
||||
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 for all employees
|
||||
// Load availabilities
|
||||
const availabilityPromises = employeesData
|
||||
.filter(emp => emp.isActive)
|
||||
.map(emp => employeeService.getAvailabilities(emp.id));
|
||||
@@ -71,13 +119,11 @@ const ShiftPlanView: React.FC = () => {
|
||||
const allAvailabilities = await Promise.all(availabilityPromises);
|
||||
const flattenedAvailabilities = allAvailabilities.flat();
|
||||
|
||||
// Filter availabilities to only include those for the current shift plan
|
||||
const planAvailabilities = flattenedAvailabilities.filter(
|
||||
availability => availability.planId === id
|
||||
);
|
||||
|
||||
setAvailabilities(planAvailabilities);
|
||||
debugAvailabilities();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading shift plan data:', error);
|
||||
@@ -91,68 +137,197 @@ const ShiftPlanView: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const debugAvailabilities = () => {
|
||||
if (!shiftPlan || !employees.length || !availabilities.length) return;
|
||||
|
||||
console.log('🔍 AVAILABILITY ANALYSIS:', {
|
||||
totalAvailabilities: availabilities.length,
|
||||
employeesWithAvailabilities: new Set(availabilities.map(a => a.employeeId)).size,
|
||||
totalEmployees: employees.length,
|
||||
availabilityByEmployee: employees.map(emp => {
|
||||
const empAvailabilities = availabilities.filter(a => a.employeeId === emp.id);
|
||||
return {
|
||||
employee: emp.name,
|
||||
availabilities: empAvailabilities.length,
|
||||
preferences: empAvailabilities.map(a => ({
|
||||
day: a.dayOfWeek,
|
||||
timeSlot: a.timeSlotId,
|
||||
preference: a.preferenceLevel
|
||||
}))
|
||||
};
|
||||
})
|
||||
});
|
||||
|
||||
// Prüfe spezifisch für Manager/Admin
|
||||
const manager = employees.find(emp => emp.role === 'admin');
|
||||
if (manager) {
|
||||
const managerAvailabilities = availabilities.filter(a => a.employeeId === manager.id);
|
||||
console.log('🔍 MANAGER AVAILABILITIES:', {
|
||||
manager: manager.name,
|
||||
availabilities: managerAvailabilities.length,
|
||||
details: managerAvailabilities.map(a => ({
|
||||
day: a.dayOfWeek,
|
||||
timeSlot: a.timeSlotId,
|
||||
preference: a.preferenceLevel
|
||||
}))
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const debugScheduledShifts = async () => {
|
||||
const handleRecreateAssignments = async () => {
|
||||
if (!shiftPlan) return;
|
||||
|
||||
try {
|
||||
const shifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
||||
console.log('🔍 SCHEDULED SHIFTS IN DATABASE:', {
|
||||
total: shifts.length,
|
||||
shifts: shifts.map(s => ({
|
||||
id: s.id,
|
||||
date: s.date,
|
||||
timeSlotId: s.timeSlotId,
|
||||
requiredEmployees: s.requiredEmployees
|
||||
}))
|
||||
setRecreating(true);
|
||||
|
||||
if (!window.confirm('Möchten Sie die aktuellen Zuweisungen wirklich zurücksetzen? Alle vorhandenen Zuweisungen werden gelöscht.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔄 STARTING COMPLETE ASSIGNMENT CLEARING PROCESS');
|
||||
|
||||
// STEP 1: Get current scheduled shifts
|
||||
const currentScheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
||||
console.log(`📋 Found ${currentScheduledShifts.length} shifts to clear`);
|
||||
|
||||
// STEP 2: Clear ALL assignments by setting empty arrays
|
||||
const clearPromises = currentScheduledShifts.map(async (scheduledShift) => {
|
||||
console.log(`🗑️ Clearing assignments for shift: ${scheduledShift.id}`);
|
||||
await shiftAssignmentService.updateScheduledShift(scheduledShift.id, {
|
||||
assignedEmployees: [] // EMPTY ARRAY - this clears the assignments
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.all(clearPromises);
|
||||
console.log('✅ All assignments cleared from database');
|
||||
|
||||
// STEP 3: Update plan status to draft
|
||||
await shiftPlanService.updateShiftPlan(shiftPlan.id, {
|
||||
status: 'draft'
|
||||
});
|
||||
console.log('📝 Plan status set to draft');
|
||||
|
||||
// STEP 4: CRITICAL - Force reload of scheduled shifts to get EMPTY assignments
|
||||
const refreshedShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
||||
setScheduledShifts(refreshedShifts); // Update state with EMPTY assignments
|
||||
|
||||
// STEP 5: Clear any previous assignment results
|
||||
setAssignmentResult(null);
|
||||
setShowAssignmentPreview(false);
|
||||
|
||||
// STEP 6: Force complete data refresh
|
||||
await loadShiftPlanData();
|
||||
|
||||
console.log('🎯 ASSIGNMENT CLEARING COMPLETE - Table should now be empty');
|
||||
|
||||
showNotification({
|
||||
type: 'success',
|
||||
title: 'Zuweisungen gelöscht',
|
||||
message: 'Alle Zuweisungen wurden erfolgreich gelöscht. Die Tabelle sollte jetzt leer sein.'
|
||||
});
|
||||
|
||||
// Check if we have any shifts at all
|
||||
if (shifts.length === 0) {
|
||||
console.error('❌ NO SCHEDULED SHIFTS IN DATABASE - This is the problem!');
|
||||
console.log('💡 Solution: Regenerate scheduled shifts for this plan');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error loading scheduled shifts:', error);
|
||||
console.error('❌ Error clearing assignments:', error);
|
||||
showNotification({
|
||||
type: 'error',
|
||||
title: 'Fehler',
|
||||
message: `Löschen der Zuweisungen fehlgeschlagen: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}`
|
||||
});
|
||||
} finally {
|
||||
setRecreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const debugRenderLogic = () => {
|
||||
if (!shiftPlan) return;
|
||||
|
||||
console.log('🔍 RENDER LOGIC DEBUG:');
|
||||
console.log('=====================');
|
||||
|
||||
const { days, allTimeSlots, timeSlotsByDay } = getExtendedTimetableData();
|
||||
|
||||
console.log('📊 TABLE STRUCTURE:');
|
||||
console.log('- Days in table:', days.length);
|
||||
console.log('- TimeSlots in table:', allTimeSlots.length);
|
||||
console.log('- Days with data:', Object.keys(timeSlotsByDay).length);
|
||||
|
||||
// Zeige die tatsächliche Struktur der Tabelle
|
||||
console.log('\n📅 ACTUAL TABLE DAYS:');
|
||||
days.forEach(day => {
|
||||
const slotsForDay = timeSlotsByDay[day.id] || [];
|
||||
console.log(`- ${day.name}: ${slotsForDay.length} time slots`);
|
||||
});
|
||||
|
||||
console.log('\n⏰ ACTUAL TIME SLOTS:');
|
||||
allTimeSlots.forEach(slot => {
|
||||
console.log(`- ${slot.name} (${slot.startTime}-${slot.endTime})`);
|
||||
});
|
||||
|
||||
// Prüfe wie viele Scheduled Shifts tatsächlich gerendert werden
|
||||
console.log('\n🔍 SCHEDULED SHIFTS RENDER ANALYSIS:');
|
||||
|
||||
let totalRenderedShifts = 0;
|
||||
let shiftsWithAssignments = 0;
|
||||
|
||||
days.forEach(day => {
|
||||
const slotsForDay = timeSlotsByDay[day.id] || [];
|
||||
slotsForDay.forEach(timeSlot => {
|
||||
totalRenderedShifts++;
|
||||
|
||||
// Finde den entsprechenden Scheduled Shift
|
||||
const scheduledShift = scheduledShifts.find(scheduled => {
|
||||
const scheduledDayOfWeek = getDayOfWeek(scheduled.date);
|
||||
return scheduledDayOfWeek === day.id &&
|
||||
scheduled.timeSlotId === timeSlot.id;
|
||||
});
|
||||
|
||||
if (scheduledShift && scheduledShift.assignedEmployees && scheduledShift.assignedEmployees.length > 0) {
|
||||
shiftsWithAssignments++;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`- Total shifts in table: ${totalRenderedShifts}`);
|
||||
console.log(`- Shifts with assignments: ${shiftsWithAssignments}`);
|
||||
console.log(`- Total scheduled shifts: ${scheduledShifts.length}`);
|
||||
console.log(`- Coverage: ${Math.round((totalRenderedShifts / scheduledShifts.length) * 100)}%`);
|
||||
|
||||
// Problem-Analyse
|
||||
if (totalRenderedShifts < scheduledShifts.length) {
|
||||
console.log('\n🚨 PROBLEM: Table is not showing all scheduled shifts!');
|
||||
console.log('💡 The table structure (days × timeSlots) is smaller than actual scheduled shifts');
|
||||
|
||||
// Zeige die fehlenden Shifts
|
||||
const missingShifts = scheduledShifts.filter(scheduled => {
|
||||
const dayOfWeek = getDayOfWeek(scheduled.date);
|
||||
const timeSlotExists = allTimeSlots.some(ts => ts.id === scheduled.timeSlotId);
|
||||
const dayExists = days.some(day => day.id === dayOfWeek);
|
||||
|
||||
return !(timeSlotExists && dayExists);
|
||||
});
|
||||
|
||||
if (missingShifts.length > 0) {
|
||||
console.log(`❌ ${missingShifts.length} shifts cannot be rendered in table:`);
|
||||
missingShifts.slice(0, 5).forEach(shift => {
|
||||
const dayOfWeek = getDayOfWeek(shift.date);
|
||||
const timeSlot = shiftPlan.timeSlots?.find(ts => ts.id === shift.timeSlotId);
|
||||
console.log(` - ${shift.date} (Day ${dayOfWeek}): ${timeSlot?.name || 'Unknown'} - ${shift.assignedEmployees?.length || 0} assignments`);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getExtendedTimetableData = () => {
|
||||
if (!shiftPlan || !shiftPlan.timeSlots) {
|
||||
return { days: [], timeSlotsByDay: {}, allTimeSlots: [] };
|
||||
}
|
||||
|
||||
// Verwende alle Tage die tatsächlich in scheduledShifts vorkommen
|
||||
const allDaysInScheduledShifts = [...new Set(scheduledShifts.map(s => getDayOfWeek(s.date)))].sort();
|
||||
|
||||
const days = allDaysInScheduledShifts.map(dayId => {
|
||||
return weekdays.find(day => day.id === dayId) || { id: dayId, name: `Tag ${dayId}` };
|
||||
});
|
||||
|
||||
// Verwende alle TimeSlots die tatsächlich in scheduledShifts vorkommen
|
||||
const allTimeSlotIdsInScheduledShifts = [...new Set(scheduledShifts.map(s => s.timeSlotId))];
|
||||
|
||||
const allTimeSlots = allTimeSlotIdsInScheduledShifts
|
||||
.map(id => shiftPlan.timeSlots?.find(ts => ts.id === id))
|
||||
.filter(Boolean)
|
||||
.map(timeSlot => ({
|
||||
...timeSlot!,
|
||||
displayName: `${timeSlot!.name} (${formatTime(timeSlot!.startTime)}-${formatTime(timeSlot!.endTime)})`
|
||||
}))
|
||||
.sort((a, b) => a.startTime.localeCompare(b.startTime));
|
||||
|
||||
// TimeSlots pro Tag
|
||||
const timeSlotsByDay: Record<number, ExtendedTimeSlot[]> = {};
|
||||
|
||||
days.forEach(day => {
|
||||
const timeSlotIdsForDay = new Set(
|
||||
scheduledShifts
|
||||
.filter(shift => getDayOfWeek(shift.date) === day.id)
|
||||
.map(shift => shift.timeSlotId)
|
||||
);
|
||||
|
||||
timeSlotsByDay[day.id] = allTimeSlots
|
||||
.filter(timeSlot => timeSlotIdsForDay.has(timeSlot.id))
|
||||
.sort((a, b) => a.startTime.localeCompare(b.startTime));
|
||||
});
|
||||
|
||||
/*console.log('🔄 Extended timetable data:', {
|
||||
days: days.length,
|
||||
timeSlots: allTimeSlots.length,
|
||||
totalScheduledShifts: scheduledShifts.length
|
||||
});*/
|
||||
|
||||
return { days, timeSlotsByDay, allTimeSlots };
|
||||
};
|
||||
|
||||
// Extract plan-specific shifts using the same logic as AvailabilityManager
|
||||
const getTimetableData = () => {
|
||||
if (!shiftPlan || !shiftPlan.shifts || !shiftPlan.timeSlots) {
|
||||
@@ -216,174 +391,46 @@ const ShiftPlanView: React.FC = () => {
|
||||
return date.getDay() === 0 ? 7 : date.getDay();
|
||||
};
|
||||
|
||||
/*const debugManagerAvailability = () => {
|
||||
if (!shiftPlan || !employees.length || !availabilities.length) return;
|
||||
|
||||
const manager = employees.find(emp => emp.role === 'admin');
|
||||
if (!manager) {
|
||||
console.log('❌ Kein Manager (admin) gefunden');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔍 Manager-Analyse:', {
|
||||
manager: manager.name,
|
||||
managerId: manager.id,
|
||||
totalAvailabilities: availabilities.length,
|
||||
managerAvailabilities: availabilities.filter(a => a.employeeId === manager.id).length
|
||||
});
|
||||
|
||||
// Prüfe speziell die leeren Manager-Schichten
|
||||
const emptyManagerShifts = [
|
||||
'a8ef4ce0-adfd-4ec3-8c58-efa0f7347f9f',
|
||||
'a496a8d6-f7a0-4d77-96de-c165379378c4',
|
||||
'ea2d73d1-8354-4833-8c87-40f318ce8be0',
|
||||
'90eb5454-2ae2-4445-86b7-a6e0e2cf0b22'
|
||||
];
|
||||
|
||||
emptyManagerShifts.forEach(shiftId => {
|
||||
const scheduledShift = shiftPlan.scheduledShifts?.find(s => s.id === shiftId);
|
||||
if (scheduledShift) {
|
||||
const dayOfWeek = getDayOfWeek(scheduledShift.date);
|
||||
const shiftKey = `${dayOfWeek}-${scheduledShift.timeSlotId}`;
|
||||
|
||||
const managerAvailability = availabilities.find(a =>
|
||||
a.employeeId === manager.id &&
|
||||
a.dayOfWeek === dayOfWeek &&
|
||||
a.timeSlotId === scheduledShift.timeSlotId
|
||||
);
|
||||
|
||||
console.log(`📊 Schicht ${shiftId}:`, {
|
||||
date: scheduledShift.date,
|
||||
dayOfWeek,
|
||||
timeSlotId: scheduledShift.timeSlotId,
|
||||
shiftKey,
|
||||
managerAvailability: managerAvailability ? managerAvailability.preferenceLevel : 'NICHT GEFUNDEN',
|
||||
status: managerAvailability ?
|
||||
(managerAvailability.preferenceLevel === 3 ? '❌ NICHT VERFÜGBAR' : '✅ VERFÜGBAR') :
|
||||
'❌ KEINE VERFÜGBARKEITSDATEN'
|
||||
});
|
||||
}
|
||||
});
|
||||
};*/
|
||||
|
||||
const debugAssignments = async () => {
|
||||
if (!shiftPlan) return;
|
||||
|
||||
try {
|
||||
const shifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
||||
console.log('🔍 DEBUG - Scheduled Shifts nach Veröffentlichung:', {
|
||||
totalShifts: shifts.length,
|
||||
shiftsWithAssignments: shifts.filter(s => s.assignedEmployees && s.assignedEmployees.length > 0).length,
|
||||
allShifts: shifts.map(s => ({
|
||||
id: s.id,
|
||||
date: s.date,
|
||||
timeSlotId: s.timeSlotId,
|
||||
assignedEmployees: s.assignedEmployees,
|
||||
assignedCount: s.assignedEmployees?.length || 0
|
||||
}))
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Debug error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePreviewAssignment = async () => {
|
||||
if (!shiftPlan) return;
|
||||
debugScheduledShifts();
|
||||
|
||||
try {
|
||||
setPublishing(true);
|
||||
|
||||
// DEBUG: Überprüfe die Eingabedaten
|
||||
console.log('🔍 INPUT DATA FOR SCHEDULING:', {
|
||||
shiftPlan: {
|
||||
id: shiftPlan.id,
|
||||
name: shiftPlan.name,
|
||||
shifts: shiftPlan.shifts?.length,
|
||||
timeSlots: shiftPlan.timeSlots?.length
|
||||
},
|
||||
employees: employees.length,
|
||||
availabilities: availabilities.length,
|
||||
employeeDetails: employees.map(emp => ({
|
||||
id: emp.id,
|
||||
name: emp.name,
|
||||
role: emp.role,
|
||||
employeeType: emp.employeeType,
|
||||
canWorkAlone: emp.canWorkAlone
|
||||
}))
|
||||
});
|
||||
// FORCE COMPLETE REFRESH - don't rely on cached state
|
||||
const [refreshedEmployees, refreshedAvailabilities] = await Promise.all([
|
||||
// Reload employees fresh
|
||||
employeeService.getEmployees().then(emps => emps.filter(emp => emp.isActive)),
|
||||
// Reload availabilities fresh
|
||||
refreshAllAvailabilities()
|
||||
]);
|
||||
|
||||
console.log('🔄 USING FRESH DATA:');
|
||||
console.log('- Employees:', refreshedEmployees.length);
|
||||
console.log('- Availabilities:', refreshedAvailabilities.length);
|
||||
|
||||
// DEBUG: Verify we have new data
|
||||
debugSchedulingInput(refreshedEmployees, refreshedAvailabilities);
|
||||
|
||||
// ADD THIS: Define constraints object
|
||||
const constraints = {
|
||||
enforceNoTraineeAlone: true,
|
||||
enforceExperiencedWithChef: true,
|
||||
maxRepairAttempts: 50,
|
||||
targetEmployeesPerShift: 2
|
||||
};
|
||||
|
||||
// Use the freshly loaded data, not the state
|
||||
const result = await ShiftAssignmentService.assignShifts(
|
||||
shiftPlan,
|
||||
employees,
|
||||
availabilities,
|
||||
{
|
||||
enforceExperiencedWithChef: true,
|
||||
enforceNoTraineeAlone: true,
|
||||
maxRepairAttempts: 50
|
||||
}
|
||||
refreshedEmployees, // Use fresh array, not state
|
||||
refreshedAvailabilities, // Use fresh array, not state
|
||||
constraints // Now this variable is defined
|
||||
);
|
||||
|
||||
// DEBUG: Detaillierte Analyse des Results
|
||||
console.log('🔍 DETAILED ASSIGNMENT RESULT:', {
|
||||
totalAssignments: Object.keys(result.assignments).length,
|
||||
assignments: result.assignments,
|
||||
violations: result.violations,
|
||||
hasResolutionReport: !!result.resolutionReport,
|
||||
assignmentDetails: Object.entries(result.assignments).map(([shiftId, empIds]) => ({
|
||||
shiftId,
|
||||
employeeCount: empIds.length,
|
||||
employees: empIds
|
||||
}))
|
||||
});
|
||||
// DEBUG: Überprüfe die tatsächlichen Violations
|
||||
console.log('🔍 VIOLATIONS ANALYSIS:', {
|
||||
allViolations: result.violations,
|
||||
criticalViolations: result.violations.filter(v =>
|
||||
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
|
||||
),
|
||||
warningViolations: result.violations.filter(v =>
|
||||
v.includes('WARNING:') || v.includes('⚠️')
|
||||
),
|
||||
infoViolations: result.violations.filter(v =>
|
||||
v.includes('INFO:')
|
||||
),
|
||||
criticalCount: result.violations.filter(v =>
|
||||
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
|
||||
).length,
|
||||
canPublish: result.violations.filter(v =>
|
||||
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
|
||||
).length === 0
|
||||
});
|
||||
|
||||
setAssignmentResult(result);
|
||||
setShowAssignmentPreview(true);
|
||||
|
||||
// Zeige Reparatur-Bericht in der Konsole
|
||||
if (result.resolutionReport) {
|
||||
console.log('🔧 Reparatur-Bericht:');
|
||||
result.resolutionReport.forEach(line => console.log(line));
|
||||
}
|
||||
|
||||
// Entscheidung basierend auf tatsächlichen kritischen Violations
|
||||
const criticalCount = result.violations.filter(v =>
|
||||
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
|
||||
).length;
|
||||
|
||||
if (criticalCount === 0) {
|
||||
showNotification({
|
||||
type: 'success',
|
||||
title: 'Erfolg',
|
||||
message: 'Alle kritischen Probleme wurden behoben! Der Schichtplan kann veröffentlicht werden.'
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
type: 'error',
|
||||
title: 'Kritische Probleme',
|
||||
message: `${criticalCount} kritische Probleme müssen behoben werden`
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during assignment:', error);
|
||||
showNotification({
|
||||
@@ -396,6 +443,57 @@ const ShiftPlanView: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const debugSchedulingInput = (employees: Employee[], availabilities: EmployeeAvailability[]) => {
|
||||
console.log('🔍 DEBUG SCHEDULING INPUT:');
|
||||
console.log('==========================');
|
||||
|
||||
// Check if we have the latest data
|
||||
console.log('📊 Employee Count:', employees.length);
|
||||
console.log('📊 Availability Count:', availabilities.length);
|
||||
|
||||
// Log each employee's availability
|
||||
employees.forEach(emp => {
|
||||
const empAvailabilities = availabilities.filter(avail => avail.employeeId === emp.id);
|
||||
console.log(`👤 ${emp.name} (${emp.role}, ${emp.employeeType}): ${empAvailabilities.length} availabilities`);
|
||||
|
||||
if (empAvailabilities.length > 0) {
|
||||
empAvailabilities.forEach(avail => {
|
||||
console.log(` - Day ${avail.dayOfWeek}, TimeSlot ${avail.timeSlotId}: Level ${avail.preferenceLevel}`);
|
||||
});
|
||||
} else {
|
||||
console.log(` ❌ NO AVAILABILITIES SET!`);
|
||||
}
|
||||
});
|
||||
|
||||
// REMOVED: The problematic code that tries to access shiftPlan.employees
|
||||
// We don't have old employee data stored in shiftPlan
|
||||
|
||||
console.log('🔄 All employees are considered "changed" since we loaded fresh data');
|
||||
};
|
||||
|
||||
const forceRefreshData = async () => {
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
const [plan, employeesData, shiftsData] = await Promise.all([
|
||||
shiftPlanService.getShiftPlan(id),
|
||||
employeeService.getEmployees(),
|
||||
shiftAssignmentService.getScheduledShiftsForPlan(id)
|
||||
]);
|
||||
|
||||
setShiftPlan(plan);
|
||||
setEmployees(employeesData.filter(emp => emp.isActive));
|
||||
setScheduledShifts(shiftsData);
|
||||
|
||||
// Force refresh availabilities
|
||||
await refreshAllAvailabilities();
|
||||
|
||||
console.log('✅ All data force-refreshed');
|
||||
} catch (error) {
|
||||
console.error('Error force-refreshing data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePublish = async () => {
|
||||
if (!shiftPlan || !assignmentResult) return;
|
||||
|
||||
@@ -418,15 +516,18 @@ const ShiftPlanView: React.FC = () => {
|
||||
const updatePromises = updatedShifts.map(async (scheduledShift) => {
|
||||
const assignedEmployees = assignmentResult.assignments[scheduledShift.id] || [];
|
||||
|
||||
console.log(`📝 Updating shift ${scheduledShift.id} with`, assignedEmployees, 'employees');
|
||||
//console.log(`📝 Updating shift ${scheduledShift.id} with`, assignedEmployees, 'employees');
|
||||
|
||||
try {
|
||||
// Update the shift with assigned employees
|
||||
const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
||||
await shiftAssignmentService.updateScheduledShift(scheduledShift.id, {
|
||||
assignedEmployees
|
||||
});
|
||||
|
||||
console.log(`✅ Successfully updated shift ${scheduledShift.id}`);
|
||||
if (scheduledShifts.some(s => s.id === scheduledShift.id)) {
|
||||
console.log(`✅ Successfully updated scheduled shift ${scheduledShift.id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to update shift ${scheduledShift.id}:`, error);
|
||||
throw error;
|
||||
@@ -450,18 +551,6 @@ const ShiftPlanView: React.FC = () => {
|
||||
setShiftPlan(reloadedPlan);
|
||||
setScheduledShifts(reloadedShifts);
|
||||
|
||||
// Debug: Überprüfe die aktualisierten Daten
|
||||
console.log('🔍 After publish - Reloaded data:', {
|
||||
planStatus: reloadedPlan.status,
|
||||
scheduledShiftsCount: reloadedShifts.length,
|
||||
shiftsWithAssignments: reloadedShifts.filter(s => s.assignedEmployees && s.assignedEmployees.length > 0).length,
|
||||
allAssignments: reloadedShifts.map(s => ({
|
||||
id: s.id,
|
||||
date: s.date,
|
||||
assigned: s.assignedEmployees
|
||||
}))
|
||||
});
|
||||
|
||||
showNotification({
|
||||
type: 'success',
|
||||
title: 'Erfolg',
|
||||
@@ -488,51 +577,76 @@ const ShiftPlanView: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRevertToDraft = async () => {
|
||||
if (!shiftPlan || !id) return;
|
||||
|
||||
if (!window.confirm('Möchten Sie diesen Schichtplan wirklich zurück in den Entwurfsstatus setzen? Alle Zuweisungen werden entfernt.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const refreshAllAvailabilities = async (): Promise<EmployeeAvailability[]> => {
|
||||
try {
|
||||
setReverting(true);
|
||||
console.log('🔄 Force refreshing ALL availabilities with error handling...');
|
||||
|
||||
// 1. Zuerst zurücksetzen
|
||||
const updatedPlan = await shiftPlanService.revertToDraft(id);
|
||||
|
||||
// 2. Dann ALLE Daten neu laden
|
||||
await loadShiftPlanData();
|
||||
|
||||
// 3. Assignment-Result zurücksetzen
|
||||
setAssignmentResult(null);
|
||||
|
||||
// 4. Preview schließen falls geöffnet
|
||||
setShowAssignmentPreview(false);
|
||||
|
||||
showNotification({
|
||||
type: 'success',
|
||||
title: 'Erfolg',
|
||||
message: 'Schichtplan wurde erfolgreich zurück in den Entwurfsstatus gesetzt. Alle Daten wurden neu geladen.'
|
||||
});
|
||||
|
||||
//const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
||||
console.log('Scheduled shifts after revert:', {
|
||||
hasScheduledShifts: !! scheduledShifts,
|
||||
count: scheduledShifts.length || 0,
|
||||
firstFew: scheduledShifts?.slice(0, 3)
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error reverting plan to draft:', error);
|
||||
showNotification({
|
||||
type: 'error',
|
||||
title: 'Fehler',
|
||||
message: 'Schichtplan konnte nicht zurückgesetzt werden.'
|
||||
});
|
||||
} finally {
|
||||
setReverting(false);
|
||||
if (!id) {
|
||||
console.error('❌ No plan ID available');
|
||||
return [];
|
||||
}
|
||||
|
||||
const availabilityPromises = employees
|
||||
.filter(emp => emp.isActive)
|
||||
.map(async (emp) => {
|
||||
try {
|
||||
return await employeeService.getAvailabilities(emp.id);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to load availabilities for ${emp.name}:`, error);
|
||||
return []; // Return empty array instead of failing entire operation
|
||||
}
|
||||
});
|
||||
|
||||
const allAvailabilities = await Promise.all(availabilityPromises);
|
||||
const flattenedAvailabilities = allAvailabilities.flat();
|
||||
|
||||
// More robust filtering
|
||||
const planAvailabilities = flattenedAvailabilities.filter(
|
||||
availability => availability && availability.planId === id
|
||||
);
|
||||
|
||||
console.log(`✅ Successfully refreshed ${planAvailabilities.length} availabilities for plan ${id}`);
|
||||
|
||||
// IMMEDIATELY update state
|
||||
setAvailabilities(planAvailabilities);
|
||||
|
||||
return planAvailabilities;
|
||||
} catch (error) {
|
||||
console.error('❌ Critical error refreshing availabilities:', error);
|
||||
// DON'T return old data - throw error or return empty array
|
||||
throw new Error('Failed to refresh availabilities: ' + error);
|
||||
}
|
||||
};
|
||||
|
||||
const validateSchedulingData = (): boolean => {
|
||||
console.log('🔍 Validating scheduling data...');
|
||||
|
||||
const totalEmployees = employees.length;
|
||||
const employeesWithAvailabilities = new Set(
|
||||
availabilities.map(avail => avail.employeeId)
|
||||
).size;
|
||||
|
||||
const availabilityStatus = {
|
||||
totalEmployees,
|
||||
employeesWithAvailabilities,
|
||||
coverage: Math.round((employeesWithAvailabilities / totalEmployees) * 100)
|
||||
};
|
||||
|
||||
console.log('📊 Availability Coverage:', availabilityStatus);
|
||||
|
||||
// Check if we have ALL employee availabilities
|
||||
if (employeesWithAvailabilities < totalEmployees) {
|
||||
const missingEmployees = employees.filter(emp =>
|
||||
!availabilities.some(avail => avail.employeeId === emp.id)
|
||||
);
|
||||
|
||||
console.warn('⚠️ Missing availabilities for employees:',
|
||||
missingEmployees.map(emp => emp.name));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const canPublish = () => {
|
||||
@@ -560,40 +674,49 @@ const ShiftPlanView: React.FC = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const debugCurrentState = () => {
|
||||
console.log('🔍 CURRENT STATE DEBUG:', {
|
||||
shiftPlan: shiftPlan ? {
|
||||
id: shiftPlan.id,
|
||||
name: shiftPlan.name,
|
||||
status: shiftPlan.status
|
||||
} : null,
|
||||
scheduledShifts: {
|
||||
count: scheduledShifts.length,
|
||||
withAssignments: scheduledShifts.filter(s => s.assignedEmployees && s.assignedEmployees.length > 0).length,
|
||||
details: scheduledShifts.map(s => ({
|
||||
id: s.id,
|
||||
date: s.date,
|
||||
timeSlotId: s.timeSlotId,
|
||||
assignedEmployees: s.assignedEmployees
|
||||
}))
|
||||
},
|
||||
employees: employees.length
|
||||
const reloadAvailabilities = async () => {
|
||||
try {
|
||||
console.log('🔄 Lade Verfügbarkeiten neu...');
|
||||
|
||||
// Load availabilities for all employees
|
||||
const availabilityPromises = employees
|
||||
.filter(emp => emp.isActive)
|
||||
.map(emp => employeeService.getAvailabilities(emp.id));
|
||||
|
||||
const allAvailabilities = await Promise.all(availabilityPromises);
|
||||
const flattenedAvailabilities = allAvailabilities.flat();
|
||||
|
||||
// Filter availabilities to only include those for the current shift plan
|
||||
const planAvailabilities = flattenedAvailabilities.filter(
|
||||
availability => availability.planId === id
|
||||
);
|
||||
|
||||
setAvailabilities(planAvailabilities);
|
||||
console.log('✅ Verfügbarkeiten neu geladen:', planAvailabilities.length);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Neuladen der Verfügbarkeiten:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Füge diese Funktion zu den verfügbaren Aktionen hinzu
|
||||
const handleReloadData = async () => {
|
||||
await loadShiftPlanData();
|
||||
showNotification({
|
||||
type: 'success',
|
||||
title: 'Erfolg',
|
||||
message: 'Daten wurden neu geladen.'
|
||||
});
|
||||
};
|
||||
|
||||
// Render timetable using the same structure as AvailabilityManager
|
||||
const renderTimetable = () => {
|
||||
debugAssignments();
|
||||
debugCurrentState();
|
||||
const { days, allTimeSlots, timeSlotsByDay } = getTimetableData();
|
||||
const { days, allTimeSlots, timeSlotsByDay } = getExtendedTimetableData();
|
||||
if (!shiftPlan?.id) {
|
||||
console.warn("Shift plan ID is missing");
|
||||
return null;
|
||||
}
|
||||
|
||||
//const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
||||
|
||||
|
||||
if (days.length === 0 || allTimeSlots.length === 0) {
|
||||
return (
|
||||
<div style={{
|
||||
@@ -692,7 +815,6 @@ const ShiftPlanView: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Get assigned employees for this shift
|
||||
let assignedEmployees: string[] = [];
|
||||
let displayText = '';
|
||||
|
||||
@@ -706,6 +828,12 @@ const ShiftPlanView: React.FC = () => {
|
||||
|
||||
if (scheduledShift) {
|
||||
assignedEmployees = scheduledShift.assignedEmployees || [];
|
||||
|
||||
// DEBUG: Log if we're still seeing old data
|
||||
if (assignedEmployees.length > 0) {
|
||||
console.warn(`⚠️ Found non-empty assignments for ${weekday.name} ${timeSlot.name}:`, assignedEmployees);
|
||||
}
|
||||
|
||||
displayText = assignedEmployees.map(empId => {
|
||||
const employee = employees.find(emp => emp.id === empId);
|
||||
return employee ? employee.name : 'Unbekannt';
|
||||
@@ -728,7 +856,7 @@ const ShiftPlanView: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// If no assignments yet, show required count
|
||||
// If no assignments yet, show empty or required count
|
||||
if (!displayText) {
|
||||
const shiftsForSlot = shiftPlan?.shifts?.filter(shift =>
|
||||
shift.dayOfWeek === weekday.id &&
|
||||
@@ -738,7 +866,13 @@ const ShiftPlanView: React.FC = () => {
|
||||
const totalRequired = shiftsForSlot.reduce((sum, shift) =>
|
||||
sum + shift.requiredEmployees, 0);
|
||||
|
||||
// Show "0/2" instead of just "0" to indicate it's empty
|
||||
displayText = `0/${totalRequired}`;
|
||||
|
||||
// Optional: Show empty state more clearly
|
||||
if (totalRequired === 0) {
|
||||
displayText = '-';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -766,7 +900,7 @@ const ShiftPlanView: React.FC = () => {
|
||||
if (loading) return <div>Lade Schichtplan...</div>;
|
||||
if (!shiftPlan) return <div>Schichtplan nicht gefunden</div>;
|
||||
|
||||
const { days, allTimeSlots } = getTimetableData();
|
||||
const { days, allTimeSlots } = getExtendedTimetableData();
|
||||
const availabilityStatus = getAvailabilityStatus();
|
||||
|
||||
|
||||
@@ -799,25 +933,25 @@ const ShiftPlanView: React.FC = () => {
|
||||
{shiftPlan.status === 'published' ? 'Veröffentlicht' : 'Entwurf'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
|
||||
{shiftPlan.status === 'published' && hasRole(['admin', 'instandhalter']) && (
|
||||
<button
|
||||
onClick={handleRevertToDraft}
|
||||
disabled={reverting}
|
||||
onClick={handleRecreateAssignments}
|
||||
disabled={recreating}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
backgroundColor: '#e74c3c',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
cursor: recreating ? 'not-allowed' : 'pointer',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
{reverting ? 'Zurücksetzen...' : 'Zu Entwurf zurücksetzen'}
|
||||
{recreating ? 'Lösche Zuweisungen...' : 'Zuweisungen neu berechnen'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => navigate('/shift-plans')}
|
||||
style={{
|
||||
|
||||
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 { Employee, EmployeeAvailability } from '../models/Employee';
|
||||
import { authService } from './authService';
|
||||
import { scheduleWithManager } from './scheduling/shiftScheduler';
|
||||
import { transformToSchedulingData } from './scheduling/dataAdapter';
|
||||
import { AssignmentResult, WeeklyPattern } from './scheduling/types';
|
||||
import { IntelligentShiftScheduler, AssignmentResult, WeeklyPattern } from './scheduling';
|
||||
import { isScheduledShift } from '../models/helpers';
|
||||
|
||||
const API_BASE_URL = 'http://localhost:3002/api/scheduled-shifts';
|
||||
|
||||
|
||||
|
||||
// Helper function to get auth headers
|
||||
const getAuthHeaders = () => {
|
||||
const token = localStorage.getItem('token');
|
||||
@@ -21,7 +21,7 @@ const getAuthHeaders = () => {
|
||||
export class ShiftAssignmentService {
|
||||
async updateScheduledShift(id: string, updates: { assignedEmployees: string[] }): Promise<void> {
|
||||
try {
|
||||
console.log('🔄 Updating scheduled shift via API:', { id, updates });
|
||||
//console.log('🔄 Updating scheduled shift via API:', { id, updates });
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/${id}`, {
|
||||
method: 'PUT',
|
||||
@@ -141,65 +141,64 @@ export class ShiftAssignmentService {
|
||||
constraints: any = {}
|
||||
): Promise<AssignmentResult> {
|
||||
|
||||
console.log('🔄 Starting enhanced scheduling algorithm...');
|
||||
console.log('🧠 Starting intelligent scheduling for FIRST WEEK ONLY...');
|
||||
|
||||
// Get defined shifts for the first week
|
||||
const definedShifts = await this.getDefinedShifts(shiftPlan);
|
||||
const firstWeekShifts = this.getFirstWeekShifts(definedShifts);
|
||||
// Load all scheduled shifts
|
||||
const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
||||
|
||||
console.log('📊 First week analysis:', {
|
||||
totalShifts: definedShifts.length,
|
||||
firstWeekShifts: firstWeekShifts.length,
|
||||
employees: employees.length
|
||||
});
|
||||
|
||||
// Transform data for scheduling algorithm
|
||||
const { schedulingEmployees, schedulingShifts, managerShifts } = transformToSchedulingData(
|
||||
employees.filter(emp => emp.isActive),
|
||||
firstWeekShifts,
|
||||
availabilities
|
||||
);
|
||||
|
||||
console.log('🎯 Transformed data for scheduling:', {
|
||||
employees: schedulingEmployees.length,
|
||||
shifts: schedulingShifts.length,
|
||||
managerShifts: managerShifts.length
|
||||
});
|
||||
|
||||
// Run the enhanced scheduling algorithm with better constraints
|
||||
const schedulingResult = scheduleWithManager(
|
||||
schedulingShifts,
|
||||
schedulingEmployees,
|
||||
managerShifts,
|
||||
{
|
||||
enforceNoTraineeAlone: constraints.enforceNoTraineeAlone ?? true,
|
||||
enforceExperiencedWithChef: constraints.enforceExperiencedWithChef ?? true,
|
||||
maxRepairAttempts: constraints.maxRepairAttempts ?? 50,
|
||||
targetEmployeesPerShift: constraints.targetEmployeesPerShift ?? 2 // Flexible target
|
||||
if (scheduledShifts.length === 0) {
|
||||
return {
|
||||
assignments: {},
|
||||
violations: ['❌ KRITISCH: Keine Schichten verfügbar für die Zuordnung'],
|
||||
success: false,
|
||||
resolutionReport: ['🚨 ABBRUCH: Keine Schichten im Plan verfügbar']
|
||||
};
|
||||
}
|
||||
|
||||
// Set cache for scheduler
|
||||
IntelligentShiftScheduler.scheduledShiftsCache.set(shiftPlan.id, scheduledShifts);
|
||||
|
||||
// 🔥 RUN SCHEDULING FOR FIRST WEEK ONLY
|
||||
const schedulingResult = await IntelligentShiftScheduler.generateOptimalSchedule(
|
||||
shiftPlan,
|
||||
employees.filter(emp => emp.isActive),
|
||||
availabilities,
|
||||
constraints
|
||||
);
|
||||
|
||||
console.log('📊 Enhanced scheduling completed:', {
|
||||
assignments: Object.keys(schedulingResult.assignments).length,
|
||||
violations: schedulingResult.violations.length,
|
||||
success: schedulingResult.success
|
||||
// Get first week shifts for pattern
|
||||
const firstWeekShifts = this.getFirstWeekShifts(scheduledShifts);
|
||||
|
||||
console.log('🔄 Creating weekly pattern from FIRST WEEK:', {
|
||||
firstWeekShifts: firstWeekShifts.length,
|
||||
allShifts: scheduledShifts.length,
|
||||
patternAssignments: Object.keys(schedulingResult.assignments).length
|
||||
});
|
||||
|
||||
// Apply weekly pattern to all shifts
|
||||
const weeklyPattern: WeeklyPattern = {
|
||||
weekShifts: firstWeekShifts,
|
||||
assignments: schedulingResult.assignments,
|
||||
assignments: schedulingResult.assignments, // 🔥 Diese enthalten nur erste Woche
|
||||
weekNumber: 1
|
||||
};
|
||||
|
||||
const allAssignments = this.applyWeeklyPattern(definedShifts, weeklyPattern);
|
||||
// 🔥 APPLY PATTERN TO ALL WEEKS
|
||||
const allAssignments = this.applyWeeklyPattern(scheduledShifts, weeklyPattern);
|
||||
|
||||
console.log('✅ Pattern applied to all weeks:', {
|
||||
firstWeekAssignments: Object.keys(schedulingResult.assignments).length,
|
||||
allWeeksAssignments: Object.keys(allAssignments).length
|
||||
});
|
||||
|
||||
// Clean cache
|
||||
IntelligentShiftScheduler.scheduledShiftsCache.delete(shiftPlan.id);
|
||||
|
||||
return {
|
||||
assignments: allAssignments,
|
||||
assignments: allAssignments, // 🔥 Diese enthalten alle Wochen
|
||||
violations: schedulingResult.violations,
|
||||
success: schedulingResult.violations.length === 0,
|
||||
success: schedulingResult.success,
|
||||
pattern: weeklyPattern,
|
||||
resolutionReport: schedulingResult.resolutionReport // Füge diese Zeile hinzu
|
||||
resolutionReport: schedulingResult.resolutionReport,
|
||||
qualityMetrics: schedulingResult.qualityMetrics
|
||||
};
|
||||
}
|
||||
|
||||
@@ -336,28 +335,47 @@ export class ShiftAssignmentService {
|
||||
|
||||
const assignments: { [shiftId: string]: string[] } = {};
|
||||
|
||||
// Group all shifts by week
|
||||
const shiftsByWeek = this.groupShiftsByWeek(allShifts);
|
||||
// Group all shifts by week AND day-timeSlot combination
|
||||
const shiftsByPatternKey = new Map<string, ScheduledShift[]>();
|
||||
|
||||
console.log('📅 Applying weekly pattern to', Object.keys(shiftsByWeek).length, 'weeks');
|
||||
allShifts.forEach(shift => {
|
||||
const dayOfWeek = this.getDayOfWeek(shift.date);
|
||||
const patternKey = `${dayOfWeek}-${shift.timeSlotId}`;
|
||||
|
||||
// For each week, apply the pattern from week 1
|
||||
Object.entries(shiftsByWeek).forEach(([weekKey, weekShifts]) => {
|
||||
const weekNumber = parseInt(weekKey);
|
||||
if (!shiftsByPatternKey.has(patternKey)) {
|
||||
shiftsByPatternKey.set(patternKey, []);
|
||||
}
|
||||
shiftsByPatternKey.get(patternKey)!.push(shift);
|
||||
});
|
||||
|
||||
weekShifts.forEach(shift => {
|
||||
// Find the corresponding shift in the weekly pattern
|
||||
const patternShift = this.findMatchingPatternShift(shift, weeklyPattern.weekShifts);
|
||||
console.log('📊 Pattern application analysis:');
|
||||
console.log('- Unique pattern keys:', shiftsByPatternKey.size);
|
||||
console.log('- Pattern keys:', Array.from(shiftsByPatternKey.keys()));
|
||||
|
||||
if (patternShift) {
|
||||
// Use the same assignment as the pattern shift
|
||||
// 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 {
|
||||
// No matching pattern shift, leave empty
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -128,26 +128,6 @@ export const shiftPlanService = {
|
||||
}
|
||||
},
|
||||
|
||||
async revertToDraft(id: string): Promise<ShiftPlan> {
|
||||
const response = await fetch(`${API_BASE}/${id}/revert-to-draft`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authService.getAuthHeaders()
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
authService.logout();
|
||||
throw new Error('Nicht authorisiert - bitte erneut anmelden');
|
||||
}
|
||||
throw new Error('Fehler beim Zurücksetzen des Schichtplans');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
|
||||
// Get specific template or plan
|
||||
getTemplate: async (id: string): Promise<ShiftPlan> => {
|
||||
const response = await fetch(`${API_BASE}/${id}`, {
|
||||
@@ -219,4 +199,29 @@ export const shiftPlanService = {
|
||||
description: preset.description
|
||||
}));
|
||||
},
|
||||
|
||||
async clearAssignments(planId: string): Promise<void> {
|
||||
try {
|
||||
console.log('🔄 Clearing assignments for plan:', planId);
|
||||
|
||||
const response = await fetch(`${API_BASE}/${planId}/clear-assignments`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authService.getAuthHeaders()
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||
throw new Error(errorData.error || `Failed to clear assignments: ${response.status}`);
|
||||
}
|
||||
|
||||
console.log('✅ Assignments cleared successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error clearing assignments:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -19,7 +19,8 @@
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"ignoreDeprecations": "6.0",
|
||||
"jsx": "react-jsx"
|
||||
"jsx": "react-jsx",
|
||||
"downlevelIteration": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
|
||||
Reference in New Issue
Block a user