mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-12-01 06:55:45 +01:00
removed unnecessary scheduledshift files in backend and put in shiftPlan
This commit is contained in:
@@ -1,111 +0,0 @@
|
|||||||
// backend/src/controllers/scheduledShiftController.ts
|
|
||||||
import { Request, Response } from 'express';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import bcrypt from 'bcryptjs';
|
|
||||||
import { db } from '../services/databaseService.js';
|
|
||||||
import { AuthRequest } from '../middleware/auth.js';
|
|
||||||
import { CreateEmployeeRequest } from '../models/Employee.js';
|
|
||||||
|
|
||||||
export const getScheduledShiftsFromPlan = async (req: AuthRequest, res: Response): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const { planId } = req.params;
|
|
||||||
|
|
||||||
const shifts = await db.all(
|
|
||||||
`SELECT * FROM scheduled_shifts WHERE plan_id = ? ORDER BY date, time_slot_id`,
|
|
||||||
[planId]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Parse JSON arrays safely
|
|
||||||
const parsedShifts = shifts.map((shift: any) => {
|
|
||||||
try {
|
|
||||||
return {
|
|
||||||
...shift,
|
|
||||||
assigned_employees: JSON.parse(shift.assigned_employees || '[]')
|
|
||||||
};
|
|
||||||
} catch (parseError) {
|
|
||||||
console.error('Error parsing assigned_employees:', parseError);
|
|
||||||
return {
|
|
||||||
...shift,
|
|
||||||
assigned_employees: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json(parsedShifts);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching scheduled shifts:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getScheduledShift = async (req: AuthRequest, res: Response): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
|
|
||||||
const shift = await db.get(
|
|
||||||
'SELECT * FROM scheduled_shifts WHERE id = ?',
|
|
||||||
[id]
|
|
||||||
) as any;
|
|
||||||
|
|
||||||
if (!shift) {
|
|
||||||
res.status(404).json({ error: 'Scheduled shift not found' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse JSON array
|
|
||||||
const parsedShift = {
|
|
||||||
...shift,
|
|
||||||
assigned_employees: JSON.parse(shift.assigned_employees || '[]')
|
|
||||||
};
|
|
||||||
|
|
||||||
res.json(parsedShift);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Error fetching scheduled shift:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error: ' + error.message });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateScheduledShift = async (req: AuthRequest, res: Response): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const { assignedEmployees } = req.body;
|
|
||||||
|
|
||||||
console.log('🔄 Updating scheduled shift:', {
|
|
||||||
id,
|
|
||||||
assignedEmployees,
|
|
||||||
body: req.body
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!Array.isArray(assignedEmployees)) {
|
|
||||||
res.status(400).json({ error: 'assignedEmployees must be an array' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if shift exists
|
|
||||||
const existingShift = await db.get(
|
|
||||||
'SELECT id FROM scheduled_shifts WHERE id = ?',
|
|
||||||
[id]
|
|
||||||
) as any;
|
|
||||||
|
|
||||||
if (!existingShift) {
|
|
||||||
console.error('❌ Scheduled shift not found:', id);
|
|
||||||
res.status(404).json({ error: `Scheduled shift ${id} not found` });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the shift
|
|
||||||
const result = await db.run(
|
|
||||||
'UPDATE scheduled_shifts SET assigned_employees = ? WHERE id = ?',
|
|
||||||
[JSON.stringify(assignedEmployees), id]
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('✅ Scheduled shift updated successfully');
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
message: 'Scheduled shift updated successfully',
|
|
||||||
id: id,
|
|
||||||
assignedEmployees: assignedEmployees
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('❌ Error updating scheduled shift:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error: ' + error.message });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -635,9 +635,9 @@ async function getShiftPlanById(planId: string): Promise<any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to generate scheduled shifts from template
|
// Helper function to generate scheduled shifts from template
|
||||||
async function generateScheduledShifts(planId: string, startDate: string, endDate: string): Promise<void> {
|
export const generateScheduledShifts = async(planId: string, startDate: string, endDate: string): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
console.log(`🔄 Generiere geplante Schichten für Plan ${planId} von ${startDate} bis ${endDate}`);
|
console.log(`🔄 Generating scheduled shifts for Plan ${planId} from ${startDate} to ${endDate}`);
|
||||||
|
|
||||||
// Get plan with shifts and time slots
|
// Get plan with shifts and time slots
|
||||||
const plan = await getShiftPlanById(planId);
|
const plan = await getShiftPlanById(planId);
|
||||||
@@ -645,6 +645,9 @@ async function generateScheduledShifts(planId: string, startDate: string, endDat
|
|||||||
throw new Error('Plan not found');
|
throw new Error('Plan not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('📋 Plan shifts:', plan.shifts?.length);
|
||||||
|
console.log('⏰ Plan time slots:', plan.timeSlots?.length);
|
||||||
|
|
||||||
const start = new Date(startDate);
|
const start = new Date(startDate);
|
||||||
const end = new Date(endDate);
|
const end = new Date(endDate);
|
||||||
|
|
||||||
@@ -655,6 +658,8 @@ async function generateScheduledShifts(planId: string, startDate: string, endDat
|
|||||||
// Find shifts for this day of week
|
// Find shifts for this day of week
|
||||||
const shiftsForDay = plan.shifts.filter((shift: any) => shift.dayOfWeek === dayOfWeek);
|
const shiftsForDay = plan.shifts.filter((shift: any) => shift.dayOfWeek === dayOfWeek);
|
||||||
|
|
||||||
|
console.log(`📅 Date: ${date.toISOString().split('T')[0]}, Day: ${dayOfWeek}, Shifts: ${shiftsForDay.length}`);
|
||||||
|
|
||||||
for (const shift of shiftsForDay) {
|
for (const shift of shiftsForDay) {
|
||||||
const scheduledShiftId = uuidv4();
|
const scheduledShiftId = uuidv4();
|
||||||
|
|
||||||
@@ -667,20 +672,74 @@ async function generateScheduledShifts(planId: string, startDate: string, endDat
|
|||||||
date.toISOString().split('T')[0], // YYYY-MM-DD format
|
date.toISOString().split('T')[0], // YYYY-MM-DD format
|
||||||
shift.timeSlotId,
|
shift.timeSlotId,
|
||||||
shift.requiredEmployees,
|
shift.requiredEmployees,
|
||||||
JSON.stringify([])
|
JSON.stringify([]) // Start with empty assignments
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log(`✅ Created scheduled shift: ${scheduledShiftId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`✅ Geplante Schichten generiert für Plan ${planId}`);
|
console.log(`✅ Scheduled shifts generated for Plan ${planId}`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Fehler beim Generieren der geplanten Schichten:', error);
|
console.error('❌ Error generating scheduled shifts:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const generateScheduledShiftsForPlan = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
// Check if plan exists
|
||||||
|
const existingPlan = await getShiftPlanById(id);
|
||||||
|
if (!existingPlan) {
|
||||||
|
res.status(404).json({ error: 'Shift plan not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔄 Manually generating scheduled shifts for plan:', {
|
||||||
|
id,
|
||||||
|
name: existingPlan.name,
|
||||||
|
isTemplate: existingPlan.isTemplate,
|
||||||
|
startDate: existingPlan.startDate,
|
||||||
|
endDate: existingPlan.endDate,
|
||||||
|
hasShifts: existingPlan.shifts?.length || 0
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingPlan.isTemplate) {
|
||||||
|
res.status(400).json({ error: 'Cannot generate scheduled shifts for templates' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existingPlan.startDate || !existingPlan.endDate) {
|
||||||
|
res.status(400).json({ error: 'Plan must have start and end dates' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete existing scheduled shifts
|
||||||
|
await db.run('DELETE FROM scheduled_shifts WHERE plan_id = ?', [id]);
|
||||||
|
console.log('🗑️ Deleted existing scheduled shifts');
|
||||||
|
|
||||||
|
// Generate new scheduled shifts
|
||||||
|
await generateScheduledShifts(id, existingPlan.startDate, existingPlan.endDate);
|
||||||
|
|
||||||
|
// Return updated plan
|
||||||
|
const updatedPlan = await getShiftPlanById(id);
|
||||||
|
|
||||||
|
console.log('✅ Successfully generated scheduled shifts:', {
|
||||||
|
scheduledShifts: updatedPlan.scheduledShifts?.length || 0
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(updatedPlan);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error generating scheduled shifts:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const revertToDraft = async (req: Request, res: Response): Promise<void> => {
|
export const revertToDraft = async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
@@ -729,95 +788,137 @@ export const revertToDraft = async (req: Request, res: Response): Promise<void>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Neue Funktion: Create from Template
|
export const regenerateScheduledShifts = async (req: Request, res: Response): Promise<void> => {
|
||||||
/*export const createFromTemplate = async (req: Request, res: Response): Promise<void> => {
|
|
||||||
try {
|
try {
|
||||||
const { templatePlanId, name, startDate, endDate, description } = req.body;
|
const { id } = req.params;
|
||||||
const userId = (req as AuthRequest).user?.userId;
|
|
||||||
|
|
||||||
if (!userId) {
|
// Check if plan exists
|
||||||
res.status(401).json({ error: 'Unauthorized' });
|
const existingPlan = await getShiftPlanById(id);
|
||||||
|
if (!existingPlan) {
|
||||||
|
res.status(404).json({ error: 'Shift plan not found' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the template plan
|
// Delete existing scheduled shifts
|
||||||
const templatePlan = await getShiftPlanById(templatePlanId);
|
await db.run('DELETE FROM scheduled_shifts WHERE plan_id = ?', [id]);
|
||||||
if (!templatePlan) {
|
|
||||||
res.status(404).json({ error: 'Template plan not found' });
|
// Generate new scheduled shifts
|
||||||
return;
|
if (existingPlan.startDate && existingPlan.endDate) {
|
||||||
|
await generateScheduledShifts(id, existingPlan.startDate, existingPlan.endDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!templatePlan.isTemplate) {
|
console.log(`✅ Regenerated scheduled shifts for plan ${id}`);
|
||||||
res.status(400).json({ error: 'Specified plan is not a template' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const planId = uuidv4();
|
// Return updated plan
|
||||||
|
const updatedPlan = await getShiftPlanById(id);
|
||||||
await db.run('BEGIN TRANSACTION');
|
res.json(updatedPlan);
|
||||||
|
|
||||||
try {
|
|
||||||
// Create new plan from template
|
|
||||||
await db.run(
|
|
||||||
`INSERT INTO shift_plans (id, name, description, start_date, end_date, is_template, status, created_by)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
||||||
[planId, name, description || templatePlan.description, startDate, endDate, 0, 'draft', userId]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Copy time slots
|
|
||||||
for (const timeSlot of templatePlan.timeSlots) {
|
|
||||||
const newTimeSlotId = uuidv4();
|
|
||||||
await db.run(
|
|
||||||
`INSERT INTO time_slots (id, plan_id, name, start_time, end_time, description)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
||||||
[newTimeSlotId, planId, timeSlot.name, timeSlot.startTime, timeSlot.endTime, timeSlot.description || '']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the newly created time slots
|
|
||||||
const newTimeSlots = await db.all<any>(`
|
|
||||||
SELECT * FROM time_slots WHERE plan_id = ? ORDER BY start_time
|
|
||||||
`, [planId]);
|
|
||||||
|
|
||||||
// Copy shifts
|
|
||||||
for (const shift of templatePlan.shifts) {
|
|
||||||
const shiftId = uuidv4();
|
|
||||||
|
|
||||||
// Find matching time slot in new plan
|
|
||||||
const originalTimeSlot = templatePlan.timeSlots.find((ts: any) => ts.id === shift.timeSlotId);
|
|
||||||
const newTimeSlot = newTimeSlots.find((ts: any) =>
|
|
||||||
ts.name === originalTimeSlot?.name &&
|
|
||||||
ts.start_time === originalTimeSlot?.startTime &&
|
|
||||||
ts.end_time === originalTimeSlot?.endTime
|
|
||||||
);
|
|
||||||
|
|
||||||
if (newTimeSlot) {
|
|
||||||
await db.run(
|
|
||||||
`INSERT INTO shifts (id, plan_id, day_of_week, time_slot_id, required_employees, color)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
||||||
[shiftId, planId, shift.dayOfWeek, newTimeSlot.id, shift.requiredEmployees, shift.color || '#3498db']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate scheduled shifts for the date range
|
|
||||||
if (startDate && endDate) {
|
|
||||||
await generateScheduledShifts(planId, startDate, endDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.run('COMMIT');
|
|
||||||
|
|
||||||
// Return created plan
|
|
||||||
const createdPlan = await getShiftPlanById(planId);
|
|
||||||
res.status(201).json(createdPlan);
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await db.run('ROLLBACK');
|
console.error('Error regenerating scheduled shifts:', error);
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error creating plan from template:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
};*/
|
};
|
||||||
|
|
||||||
|
export const getScheduledShiftsFromPlan = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { planId } = req.params;
|
||||||
|
|
||||||
|
const shifts = await db.all(
|
||||||
|
`SELECT * FROM scheduled_shifts WHERE plan_id = ? ORDER BY date, time_slot_id`,
|
||||||
|
[planId]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Parse JSON arrays safely
|
||||||
|
const parsedShifts = shifts.map((shift: any) => {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
...shift,
|
||||||
|
assigned_employees: JSON.parse(shift.assigned_employees || '[]')
|
||||||
|
};
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error('Error parsing assigned_employees:', parseError);
|
||||||
|
return {
|
||||||
|
...shift,
|
||||||
|
assigned_employees: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(parsedShifts);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching scheduled shifts:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getScheduledShift = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const shift = await db.get(
|
||||||
|
'SELECT * FROM scheduled_shifts WHERE id = ?',
|
||||||
|
[id]
|
||||||
|
) as any;
|
||||||
|
|
||||||
|
if (!shift) {
|
||||||
|
res.status(404).json({ error: 'Scheduled shift not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSON array
|
||||||
|
const parsedShift = {
|
||||||
|
...shift,
|
||||||
|
assigned_employees: JSON.parse(shift.assigned_employees || '[]')
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json(parsedShift);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching scheduled shift:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error: ' + error.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateScheduledShift = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { assignedEmployees } = req.body;
|
||||||
|
|
||||||
|
console.log('🔄 Updating scheduled shift:', {
|
||||||
|
id,
|
||||||
|
assignedEmployees,
|
||||||
|
body: req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!Array.isArray(assignedEmployees)) {
|
||||||
|
res.status(400).json({ error: 'assignedEmployees must be an array' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if shift exists
|
||||||
|
const existingShift = await db.get(
|
||||||
|
'SELECT id FROM scheduled_shifts WHERE id = ?',
|
||||||
|
[id]
|
||||||
|
) as any;
|
||||||
|
|
||||||
|
if (!existingShift) {
|
||||||
|
console.error('❌ Scheduled shift not found:', id);
|
||||||
|
res.status(404).json({ error: `Scheduled shift ${id} not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the shift
|
||||||
|
const result = await db.run(
|
||||||
|
'UPDATE scheduled_shifts SET assigned_employees = ? WHERE id = ?',
|
||||||
|
[JSON.stringify(assignedEmployees), id]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('✅ Scheduled shift updated successfully');
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'Scheduled shift updated successfully',
|
||||||
|
id: id,
|
||||||
|
assignedEmployees: assignedEmployees
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ Error updating scheduled shift:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error: ' + error.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
// backend/src/routes/scheduledShifts.ts - COMPLETE REWRITE
|
|
||||||
import express from 'express';
|
|
||||||
import { authMiddleware, requireRole } from '../middleware/auth.js';
|
|
||||||
import { getScheduledShiftsFromPlan, getScheduledShift, updateScheduledShift } from '../controllers/scheduledShiftController.js';
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.use(authMiddleware);
|
|
||||||
|
|
||||||
// Add a simple test route first
|
|
||||||
/*router.get('/test', (req, res) => {
|
|
||||||
console.log('✅ /api/scheduled-shifts/test route hit');
|
|
||||||
res.json({ message: 'Scheduled shifts router is working!' });
|
|
||||||
});*/
|
|
||||||
|
|
||||||
// GET all scheduled shifts for a plan (for debugging)
|
|
||||||
router.get('/plan/:planId', requireRole(['admin']), getScheduledShiftsFromPlan);
|
|
||||||
|
|
||||||
// GET specific scheduled shift
|
|
||||||
router.get('/:id', requireRole(['admin']), getScheduledShift);
|
|
||||||
|
|
||||||
// UPDATE scheduled shift
|
|
||||||
router.put('/:id', requireRole(['admin']), updateScheduledShift);
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -10,7 +10,12 @@ import {
|
|||||||
//getTemplates,
|
//getTemplates,
|
||||||
//createFromTemplate,
|
//createFromTemplate,
|
||||||
createFromPreset,
|
createFromPreset,
|
||||||
revertToDraft
|
revertToDraft,
|
||||||
|
regenerateScheduledShifts,
|
||||||
|
generateScheduledShiftsForPlan,
|
||||||
|
getScheduledShift,
|
||||||
|
getScheduledShiftsFromPlan,
|
||||||
|
updateScheduledShift
|
||||||
} from '../controllers/shiftPlanController.js';
|
} from '../controllers/shiftPlanController.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -46,4 +51,21 @@ router.delete('/:id', requireRole(['admin', 'instandhalter']), deleteShiftPlan);
|
|||||||
// PUT revert published plan to draft
|
// PUT revert published plan to draft
|
||||||
router.put('/:id/revert-to-draft', requireRole(['admin', 'instandhalter']), revertToDraft);
|
router.put('/:id/revert-to-draft', requireRole(['admin', 'instandhalter']), revertToDraft);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// SCHEDULED SHIFTS
|
||||||
|
|
||||||
|
router.post('/:id/generate-shifts', requireRole(['admin', 'instandhalter']), generateScheduledShiftsForPlan);
|
||||||
|
|
||||||
|
router.post('/:id/regenerate-shifts', requireRole(['admin', 'instandhalter']), regenerateScheduledShifts);
|
||||||
|
|
||||||
|
// GET all scheduled shifts for a plan (for debugging)
|
||||||
|
router.get('/plan/:planId', requireRole(['admin']), getScheduledShiftsFromPlan);
|
||||||
|
|
||||||
|
// GET specific scheduled shift
|
||||||
|
router.get('/:id', requireRole(['admin']), getScheduledShift);
|
||||||
|
|
||||||
|
// UPDATE scheduled shift
|
||||||
|
router.put('/:id', requireRole(['admin']), updateScheduledShift);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -8,7 +8,6 @@ import authRoutes from './routes/auth.js';
|
|||||||
import employeeRoutes from './routes/employees.js';
|
import employeeRoutes from './routes/employees.js';
|
||||||
import shiftPlanRoutes from './routes/shiftPlans.js';
|
import shiftPlanRoutes from './routes/shiftPlans.js';
|
||||||
import setupRoutes from './routes/setup.js';
|
import setupRoutes from './routes/setup.js';
|
||||||
import scheduledShiftsRoutes from './routes/scheduledShifts.js';
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = 3002;
|
const PORT = 3002;
|
||||||
@@ -22,7 +21,6 @@ app.use('/api/setup', setupRoutes);
|
|||||||
app.use('/api/auth', authRoutes);
|
app.use('/api/auth', authRoutes);
|
||||||
app.use('/api/employees', employeeRoutes);
|
app.use('/api/employees', employeeRoutes);
|
||||||
app.use('/api/shift-plans', shiftPlanRoutes);
|
app.use('/api/shift-plans', shiftPlanRoutes);
|
||||||
app.use('/api/scheduled-shifts', scheduledShiftsRoutes);
|
|
||||||
|
|
||||||
// Error handling middleware should come after routes
|
// Error handling middleware should come after routes
|
||||||
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
|||||||
@@ -1,45 +1,274 @@
|
|||||||
// frontend/src/pages/Dashboard/Dashboard.tsx
|
// frontend/src/pages/Dashboard/Dashboard.tsx - Updated calculations
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
|
import { shiftPlanService } from '../../services/shiftPlanService';
|
||||||
|
import { employeeService } from '../../services/employeeService';
|
||||||
|
import { ShiftPlan } from '../../models/ShiftPlan';
|
||||||
|
import { Employee } from '../../models/Employee';
|
||||||
|
|
||||||
// Mock Data für die Demo
|
interface DashboardData {
|
||||||
const mockData = {
|
currentShiftPlan: ShiftPlan | null;
|
||||||
currentShiftPlan: {
|
upcomingShifts: Array<{
|
||||||
id: '1',
|
id: string;
|
||||||
name: 'November Schichtplan 2024',
|
date: string;
|
||||||
period: '01.11.2024 - 30.11.2024',
|
time: string;
|
||||||
status: 'Aktiv',
|
type: string;
|
||||||
shiftsCovered: 85,
|
assigned: boolean;
|
||||||
totalShifts: 120
|
planName: string;
|
||||||
},
|
}>;
|
||||||
upcomingShifts: [
|
|
||||||
{ id: '1', date: 'Heute', time: '08:00 - 16:00', type: 'Frühschicht', assigned: true },
|
|
||||||
{ id: '2', date: 'Morgen', time: '14:00 - 22:00', type: 'Spätschicht', assigned: true },
|
|
||||||
{ id: '3', date: '15.11.2024', time: '08:00 - 16:00', type: 'Frühschicht', assigned: false }
|
|
||||||
],
|
|
||||||
teamStats: {
|
teamStats: {
|
||||||
totalEmployees: 24,
|
totalEmployees: number;
|
||||||
availableToday: 18,
|
availableToday: number;
|
||||||
onVacation: 3,
|
onVacation: number;
|
||||||
sickLeave: 2
|
sickLeave: number;
|
||||||
},
|
|
||||||
recentActivities: [
|
|
||||||
{ id: '1', action: 'Schichtplan veröffentlicht', user: 'Max Mustermann', time: 'vor 2 Stunden' },
|
|
||||||
{ id: '2', action: 'Mitarbeiter hinzugefügt', user: 'Sarah Admin', time: 'vor 4 Stunden' },
|
|
||||||
{ id: '3', action: 'Verfügbarkeit geändert', user: 'Tom Bauer', time: 'vor 1 Tag' }
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
recentPlans: ShiftPlan[];
|
||||||
|
}
|
||||||
|
|
||||||
const Dashboard: React.FC = () => {
|
const Dashboard: React.FC = () => {
|
||||||
const { user, hasRole } = useAuth();
|
const { user, hasRole } = useAuth();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [data, setData] = useState<DashboardData>({
|
||||||
|
currentShiftPlan: null,
|
||||||
|
upcomingShifts: [],
|
||||||
|
teamStats: {
|
||||||
|
totalEmployees: 0,
|
||||||
|
availableToday: 0,
|
||||||
|
onVacation: 0,
|
||||||
|
sickLeave: 0
|
||||||
|
},
|
||||||
|
recentPlans: []
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Simuliere Daten laden
|
loadDashboardData();
|
||||||
setTimeout(() => setLoading(false), 1000);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const loadDashboardData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
console.log('🔄 Loading dashboard data...');
|
||||||
|
|
||||||
|
const [shiftPlans, employees] = await Promise.all([
|
||||||
|
shiftPlanService.getShiftPlans(),
|
||||||
|
employeeService.getEmployees()
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log('📊 Loaded data:', {
|
||||||
|
plans: shiftPlans.length,
|
||||||
|
employees: employees.length,
|
||||||
|
plansWithShifts: shiftPlans.filter(p => p.scheduledShifts && p.scheduledShifts.length > 0).length
|
||||||
|
});
|
||||||
|
|
||||||
|
// Debug: Log plan details
|
||||||
|
shiftPlans.forEach(plan => {
|
||||||
|
console.log(`Plan: ${plan.name}`, {
|
||||||
|
status: plan.status,
|
||||||
|
startDate: plan.startDate,
|
||||||
|
endDate: plan.endDate,
|
||||||
|
scheduledShifts: plan.scheduledShifts?.length || 0,
|
||||||
|
isTemplate: plan.isTemplate
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find current shift plan (published and current date within range)
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
const currentPlan = findCurrentShiftPlan(shiftPlans, today);
|
||||||
|
|
||||||
|
// Get user's upcoming shifts
|
||||||
|
const userShifts = await loadUserUpcomingShifts(shiftPlans, today);
|
||||||
|
|
||||||
|
// Calculate team stats
|
||||||
|
const activeEmployees = employees.filter(emp => emp.isActive);
|
||||||
|
const teamStats = calculateTeamStats(activeEmployees);
|
||||||
|
|
||||||
|
// Get recent plans (non-templates, sorted by creation date)
|
||||||
|
const recentPlans = getRecentPlans(shiftPlans);
|
||||||
|
|
||||||
|
setData({
|
||||||
|
currentShiftPlan: currentPlan,
|
||||||
|
upcomingShifts: userShifts,
|
||||||
|
teamStats,
|
||||||
|
recentPlans
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Dashboard data loaded:', {
|
||||||
|
currentPlan: currentPlan?.name,
|
||||||
|
userShifts: userShifts.length,
|
||||||
|
teamStats,
|
||||||
|
recentPlans: recentPlans.length
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error loading dashboard data:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findCurrentShiftPlan = (plans: ShiftPlan[], today: string): ShiftPlan | null => {
|
||||||
|
// First, try to find a published plan where today is within the date range
|
||||||
|
const activePlan = plans.find(plan =>
|
||||||
|
plan.status === 'published' &&
|
||||||
|
!plan.isTemplate &&
|
||||||
|
plan.startDate &&
|
||||||
|
plan.endDate &&
|
||||||
|
plan.startDate <= today &&
|
||||||
|
plan.endDate >= today
|
||||||
|
);
|
||||||
|
|
||||||
|
if (activePlan) {
|
||||||
|
console.log('✅ Found active plan:', activePlan.name);
|
||||||
|
return activePlan;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no active plan found, try to find the most recent published plan
|
||||||
|
const publishedPlans = plans
|
||||||
|
.filter(plan => plan.status === 'published' && !plan.isTemplate)
|
||||||
|
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||||
|
|
||||||
|
console.log('📅 Published plans available:', publishedPlans.map(p => p.name));
|
||||||
|
|
||||||
|
return publishedPlans[0] || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadUserUpcomingShifts = async (shiftPlans: ShiftPlan[], today: string): Promise<DashboardData['upcomingShifts']> => {
|
||||||
|
if (!user) return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userShifts: DashboardData['upcomingShifts'] = [];
|
||||||
|
|
||||||
|
// Check each plan for user assignments
|
||||||
|
for (const plan of shiftPlans) {
|
||||||
|
if (plan.status !== 'published' || !plan.scheduledShifts || plan.scheduledShifts.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🔍 Checking plan ${plan.name} for user shifts:`, plan.scheduledShifts.length);
|
||||||
|
|
||||||
|
for (const scheduledShift of plan.scheduledShifts) {
|
||||||
|
// Ensure assignedEmployees is an array
|
||||||
|
const assignedEmployees = Array.isArray(scheduledShift.assignedEmployees)
|
||||||
|
? scheduledShift.assignedEmployees
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (scheduledShift.date >= today && assignedEmployees.includes(user.id)) {
|
||||||
|
const timeSlot = plan.timeSlots.find(ts => ts.id === scheduledShift.timeSlotId);
|
||||||
|
|
||||||
|
userShifts.push({
|
||||||
|
id: scheduledShift.id,
|
||||||
|
date: formatShiftDate(scheduledShift.date),
|
||||||
|
time: timeSlot ? `${timeSlot.startTime} - ${timeSlot.endTime}` : 'Unbekannt',
|
||||||
|
type: timeSlot?.name || 'Unbekannt',
|
||||||
|
assigned: true,
|
||||||
|
planName: plan.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by date and limit to 5 upcoming shifts
|
||||||
|
return userShifts
|
||||||
|
.sort((a, b) => {
|
||||||
|
// Convert formatted dates back to Date objects for sorting
|
||||||
|
const dateA = a.date === 'Heute' ? today : a.date === 'Morgen' ?
|
||||||
|
new Date(new Date().setDate(new Date().getDate() + 1)).toISOString().split('T')[0] : a.date;
|
||||||
|
const dateB = b.date === 'Heute' ? today : b.date === 'Morgen' ?
|
||||||
|
new Date(new Date().setDate(new Date().getDate() + 1)).toISOString().split('T')[0] : b.date;
|
||||||
|
|
||||||
|
return new Date(dateA).getTime() - new Date(dateB).getTime();
|
||||||
|
})
|
||||||
|
.slice(0, 5);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading user shifts:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateTeamStats = (employees: Employee[]) => {
|
||||||
|
const totalEmployees = employees.length;
|
||||||
|
|
||||||
|
// For now, we'll use simpler calculations
|
||||||
|
// In a real app, you'd check actual availability from the database
|
||||||
|
const availableToday = Math.max(1, Math.floor(totalEmployees * 0.7)); // 70% available as estimate
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalEmployees,
|
||||||
|
availableToday,
|
||||||
|
onVacation: 0, // Would need vacation tracking
|
||||||
|
sickLeave: 0 // Would need sick leave tracking
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRecentPlans = (plans: ShiftPlan[]): ShiftPlan[] => {
|
||||||
|
return plans
|
||||||
|
.filter(plan => !plan.isTemplate)
|
||||||
|
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||||
|
.slice(0, 3);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatShiftDate = (dateString: string): string => {
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
const tomorrow = new Date();
|
||||||
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||||
|
const tomorrowString = tomorrow.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
if (dateString === today) {
|
||||||
|
return 'Heute';
|
||||||
|
} else if (dateString === tomorrowString) {
|
||||||
|
return 'Morgen';
|
||||||
|
} else {
|
||||||
|
return new Date(dateString).toLocaleDateString('de-DE', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPlanPeriod = (plan: ShiftPlan): string => {
|
||||||
|
if (!plan.startDate || !plan.endDate) return 'Kein Zeitraum definiert';
|
||||||
|
|
||||||
|
const start = new Date(plan.startDate).toLocaleDateString('de-DE');
|
||||||
|
const end = new Date(plan.endDate).toLocaleDateString('de-DE');
|
||||||
|
return `${start} - ${end}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculatePlanProgress = (plan: ShiftPlan): { covered: number; total: number; percentage: number } => {
|
||||||
|
if (!plan.scheduledShifts || plan.scheduledShifts.length === 0) {
|
||||||
|
console.log(`📊 Plan ${plan.name} has no scheduled shifts`);
|
||||||
|
return { covered: 0, total: 0, percentage: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalShifts = plan.scheduledShifts.length;
|
||||||
|
const coveredShifts = plan.scheduledShifts.filter(shift => {
|
||||||
|
const assigned = Array.isArray(shift.assignedEmployees) ? shift.assignedEmployees : [];
|
||||||
|
return assigned.length > 0;
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
const percentage = totalShifts > 0 ? Math.round((coveredShifts / totalShifts) * 100) : 0;
|
||||||
|
|
||||||
|
console.log(`📊 Plan ${plan.name} progress:`, {
|
||||||
|
totalShifts,
|
||||||
|
coveredShifts,
|
||||||
|
percentage
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
covered: coveredShifts,
|
||||||
|
total: totalShifts,
|
||||||
|
percentage
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add refresh functionality
|
||||||
|
const handleRefresh = () => {
|
||||||
|
loadDashboardData();
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div style={{ textAlign: 'center', padding: '40px' }}>
|
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||||
@@ -48,6 +277,50 @@ const Dashboard: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const regenerateScheduledShifts = async (planId: string) => {
|
||||||
|
await shiftPlanService.regenerateScheduledShifts(planId);
|
||||||
|
loadDashboardData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const PlanDebugInfo = () => {
|
||||||
|
if (!data.currentShiftPlan) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: '#fff3cd',
|
||||||
|
padding: '15px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
marginBottom: '20px',
|
||||||
|
border: '1px solid #ffeaa7',
|
||||||
|
fontSize: '14px'
|
||||||
|
}}>
|
||||||
|
<h4>🔍 Plan Debug Information:</h4>
|
||||||
|
<div><strong>Plan ID:</strong> {data.currentShiftPlan.id}</div>
|
||||||
|
<div><strong>Status:</strong> {data.currentShiftPlan.status}</div>
|
||||||
|
<div><strong>Is Template:</strong> {data.currentShiftPlan.isTemplate ? 'Yes' : 'No'}</div>
|
||||||
|
<div><strong>Start Date:</strong> {data.currentShiftPlan.startDate}</div>
|
||||||
|
<div><strong>End Date:</strong> {data.currentShiftPlan.endDate}</div>
|
||||||
|
<div><strong>Shifts Defined:</strong> {data.currentShiftPlan.shifts?.length || 0}</div>
|
||||||
|
<div><strong>Time Slots:</strong> {data.currentShiftPlan.timeSlots?.length || 0}</div>
|
||||||
|
<div><strong>Scheduled Shifts:</strong> {data.currentShiftPlan.scheduledShifts?.length || 0}</div>
|
||||||
|
|
||||||
|
{data.currentShiftPlan.shifts && data.currentShiftPlan.shifts.length > 0 && (
|
||||||
|
<div style={{ marginTop: '10px' }}>
|
||||||
|
<strong>Defined Shifts:</strong>
|
||||||
|
{data.currentShiftPlan.shifts.slice(0, 3).map(shift => (
|
||||||
|
<div key={shift.id} style={{ marginLeft: '10px', fontSize: '12px' }}>
|
||||||
|
Day {shift.dayOfWeek} - TimeSlot: {shift.timeSlotId} - Required: {shift.requiredEmployees}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{data.currentShiftPlan.shifts.length > 3 && <div>... and {data.currentShiftPlan.shifts.length - 3} more</div>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const progress = data.currentShiftPlan ? calculatePlanProgress(data.currentShiftPlan) : { covered: 0, total: 0, percentage: 0 };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* Willkommens-Bereich */}
|
{/* Willkommens-Bereich */}
|
||||||
@@ -56,8 +329,12 @@ const Dashboard: React.FC = () => {
|
|||||||
padding: '25px',
|
padding: '25px',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
marginBottom: '30px',
|
marginBottom: '30px',
|
||||||
border: '1px solid #b6d7e8'
|
border: '1px solid #b6d7e8',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center'
|
||||||
}}>
|
}}>
|
||||||
|
<div>
|
||||||
<h1 style={{ margin: '0 0 10px 0', color: '#2c3e50' }}>
|
<h1 style={{ margin: '0 0 10px 0', color: '#2c3e50' }}>
|
||||||
Willkommen zurück, {user?.name}! 👋
|
Willkommen zurück, {user?.name}! 👋
|
||||||
</h1>
|
</h1>
|
||||||
@@ -70,6 +347,22 @@ const Dashboard: React.FC = () => {
|
|||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleRefresh}
|
||||||
|
style={{
|
||||||
|
padding: '8px 16px',
|
||||||
|
backgroundColor: '#3498db',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🔄 Aktualisieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PlanDebugInfo />
|
||||||
|
|
||||||
{/* Quick Actions - Nur für Admins/Instandhalter */}
|
{/* Quick Actions - Nur für Admins/Instandhalter */}
|
||||||
{hasRole(['admin', 'instandhalter']) && (
|
{hasRole(['admin', 'instandhalter']) && (
|
||||||
@@ -159,19 +452,23 @@ const Dashboard: React.FC = () => {
|
|||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
||||||
}}>
|
}}>
|
||||||
<h3 style={{ margin: '0 0 15px 0', color: '#2c3e50' }}>📊 Aktueller Schichtplan</h3>
|
<h3 style={{ margin: '0 0 15px 0', color: '#2c3e50' }}>📊 Aktueller Schichtplan</h3>
|
||||||
|
{data.currentShiftPlan ? (
|
||||||
|
<>
|
||||||
<div style={{ marginBottom: '15px' }}>
|
<div style={{ marginBottom: '15px' }}>
|
||||||
<div style={{ fontWeight: 'bold', fontSize: '18px' }}>
|
<div style={{ fontWeight: 'bold', fontSize: '18px' }}>
|
||||||
{mockData.currentShiftPlan.name}
|
{data.currentShiftPlan.name}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ color: '#666', fontSize: '14px' }}>
|
<div style={{ color: '#666', fontSize: '14px' }}>
|
||||||
{mockData.currentShiftPlan.period}
|
{formatPlanPeriod(data.currentShiftPlan)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: '15px' }}>
|
<div style={{ marginBottom: '15px' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '5px' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '5px' }}>
|
||||||
<span>Fortschritt:</span>
|
<span>Fortschritt:</span>
|
||||||
<span>{mockData.currentShiftPlan.shiftsCovered}/{mockData.currentShiftPlan.totalShifts} Schichten</span>
|
<span>
|
||||||
|
{progress.covered}/{progress.total} Schichten ({progress.percentage}%)
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -180,25 +477,53 @@ const Dashboard: React.FC = () => {
|
|||||||
overflow: 'hidden'
|
overflow: 'hidden'
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: `${(mockData.currentShiftPlan.shiftsCovered / mockData.currentShiftPlan.totalShifts) * 100}%`,
|
width: `${progress.percentage}%`,
|
||||||
backgroundColor: '#3498db',
|
backgroundColor: progress.percentage > 0 ? '#3498db' : '#95a5a6',
|
||||||
height: '8px',
|
height: '8px',
|
||||||
borderRadius: '10px'
|
borderRadius: '10px',
|
||||||
|
transition: 'width 0.3s ease'
|
||||||
}} />
|
}} />
|
||||||
</div>
|
</div>
|
||||||
|
{progress.total === 0 && (
|
||||||
|
<div style={{ fontSize: '12px', color: '#e74c3c', marginTop: '5px' }}>
|
||||||
|
Keine Schichten im Plan definiert
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
backgroundColor: mockData.currentShiftPlan.status === 'Aktiv' ? '#2ecc71' : '#f39c12',
|
backgroundColor: data.currentShiftPlan.status === 'published' ? '#2ecc71' : '#f39c12',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
padding: '4px 12px',
|
padding: '4px 12px',
|
||||||
borderRadius: '20px',
|
borderRadius: '20px',
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
fontWeight: 'bold'
|
fontWeight: 'bold'
|
||||||
}}>
|
}}>
|
||||||
{mockData.currentShiftPlan.status}
|
{data.currentShiftPlan.status === 'published' ? 'Aktiv' : 'Entwurf'}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div style={{ textAlign: 'center', padding: '20px', color: '#666' }}>
|
||||||
|
<div style={{ fontSize: '48px', marginBottom: '10px' }}>📅</div>
|
||||||
|
<div>Kein aktiver Schichtplan</div>
|
||||||
|
{hasRole(['admin', 'instandhalter']) && (
|
||||||
|
<Link to="/shift-plans/new">
|
||||||
|
<button style={{
|
||||||
|
marginTop: '10px',
|
||||||
|
padding: '8px 16px',
|
||||||
|
backgroundColor: '#3498db',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}>
|
||||||
|
Ersten Plan erstellen
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Team-Statistiken */}
|
{/* Team-Statistiken */}
|
||||||
@@ -214,25 +539,25 @@ const Dashboard: React.FC = () => {
|
|||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<span>Gesamt Mitarbeiter:</span>
|
<span>Gesamt Mitarbeiter:</span>
|
||||||
<span style={{ fontWeight: 'bold', fontSize: '18px' }}>
|
<span style={{ fontWeight: 'bold', fontSize: '18px' }}>
|
||||||
{mockData.teamStats.totalEmployees}
|
{data.teamStats.totalEmployees}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<span>Verfügbar heute:</span>
|
<span>Verfügbar heute:</span>
|
||||||
<span style={{ fontWeight: 'bold', color: '#2ecc71' }}>
|
<span style={{ fontWeight: 'bold', color: '#2ecc71' }}>
|
||||||
{mockData.teamStats.availableToday}
|
{data.teamStats.availableToday}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<span>Im Urlaub:</span>
|
<span>Im Urlaub:</span>
|
||||||
<span style={{ fontWeight: 'bold', color: '#f39c12' }}>
|
<span style={{ fontWeight: 'bold', color: '#f39c12' }}>
|
||||||
{mockData.teamStats.onVacation}
|
{data.teamStats.onVacation}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<span>Krankgeschrieben:</span>
|
<span>Krankgeschrieben:</span>
|
||||||
<span style={{ fontWeight: 'bold', color: '#e74c3c' }}>
|
<span style={{ fontWeight: 'bold', color: '#e74c3c' }}>
|
||||||
{mockData.teamStats.sickLeave}
|
{data.teamStats.sickLeave}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -255,8 +580,9 @@ const Dashboard: React.FC = () => {
|
|||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
||||||
}}>
|
}}>
|
||||||
<h3 style={{ margin: '0 0 15px 0', color: '#2c3e50' }}>⏰ Meine nächsten Schichten</h3>
|
<h3 style={{ margin: '0 0 15px 0', color: '#2c3e50' }}>⏰ Meine nächsten Schichten</h3>
|
||||||
|
{data.upcomingShifts.length > 0 ? (
|
||||||
<div style={{ display: 'grid', gap: '10px' }}>
|
<div style={{ display: 'grid', gap: '10px' }}>
|
||||||
{mockData.upcomingShifts.map(shift => (
|
{data.upcomingShifts.map(shift => (
|
||||||
<div key={shift.id} style={{
|
<div key={shift.id} style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
@@ -264,30 +590,37 @@ const Dashboard: React.FC = () => {
|
|||||||
padding: '12px',
|
padding: '12px',
|
||||||
backgroundColor: '#f8f9fa',
|
backgroundColor: '#f8f9fa',
|
||||||
borderRadius: '6px',
|
borderRadius: '6px',
|
||||||
border: shift.assigned ? '1px solid #d4edda' : '1px solid #fff3cd'
|
border: '1px solid #d4edda'
|
||||||
}}>
|
}}>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontWeight: 'bold' }}>{shift.date}</div>
|
<div style={{ fontWeight: 'bold' }}>{shift.date}</div>
|
||||||
<div style={{ fontSize: '14px', color: '#666' }}>{shift.time}</div>
|
<div style={{ fontSize: '14px', color: '#666' }}>{shift.time}</div>
|
||||||
<div style={{ fontSize: '12px', color: '#999' }}>{shift.type}</div>
|
<div style={{ fontSize: '12px', color: '#999' }}>{shift.type}</div>
|
||||||
|
<div style={{ fontSize: '11px', color: '#666' }}>{shift.planName}</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '4px 8px',
|
padding: '4px 8px',
|
||||||
backgroundColor: shift.assigned ? '#d4edda' : '#fff3cd',
|
backgroundColor: '#d4edda',
|
||||||
color: shift.assigned ? '#155724' : '#856404',
|
color: '#155724',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
fontWeight: 'bold'
|
fontWeight: 'bold'
|
||||||
}}>
|
}}>
|
||||||
{shift.assigned ? 'Zugewiesen' : 'Noch offen'}
|
Zugewiesen
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ textAlign: 'center', padding: '20px', color: '#666' }}>
|
||||||
|
<div style={{ fontSize: '48px', marginBottom: '10px' }}>⏰</div>
|
||||||
|
<div>Keine anstehenden Schichten</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Letzte Aktivitäten (für Admins/Instandhalter) */}
|
{/* Letzte Schichtpläne (für Admins/Instandhalter) */}
|
||||||
{hasRole(['admin', 'instandhalter']) && (
|
{hasRole(['admin', 'instandhalter']) && (
|
||||||
<div style={{
|
<div style={{
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
@@ -296,27 +629,63 @@ const Dashboard: React.FC = () => {
|
|||||||
border: '1px solid #e0e0e0',
|
border: '1px solid #e0e0e0',
|
||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
||||||
}}>
|
}}>
|
||||||
<h3 style={{ margin: '0 0 15px 0', color: '#2c3e50' }}>📝 Letzte Aktivitäten</h3>
|
<h3 style={{ margin: '0 0 15px 0', color: '#2c3e50' }}>📝 Letzte Schichtpläne</h3>
|
||||||
|
{data.recentPlans.length > 0 ? (
|
||||||
<div style={{ display: 'grid', gap: '12px' }}>
|
<div style={{ display: 'grid', gap: '12px' }}>
|
||||||
{mockData.recentActivities.map(activity => (
|
{data.recentPlans.map(plan => (
|
||||||
<div key={activity.id} style={{
|
<div key={plan.id} style={{
|
||||||
padding: '12px',
|
padding: '12px',
|
||||||
backgroundColor: '#f8f9fa',
|
backgroundColor: '#f8f9fa',
|
||||||
borderRadius: '6px',
|
borderRadius: '6px',
|
||||||
borderLeft: '4px solid #3498db'
|
borderLeft: `4px solid ${
|
||||||
|
plan.status === 'published' ? '#2ecc71' :
|
||||||
|
plan.status === 'draft' ? '#f39c12' : '#95a5a6'
|
||||||
|
}`
|
||||||
}}>
|
}}>
|
||||||
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>
|
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>
|
||||||
{activity.action}
|
{plan.name}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '14px', color: '#666' }}>
|
<div style={{ fontSize: '14px', color: '#666' }}>
|
||||||
von {activity.user}
|
{formatPlanPeriod(plan)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '12px', color: '#999' }}>
|
<div style={{
|
||||||
{activity.time}
|
fontSize: '12px',
|
||||||
|
color: '#999',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: '4px'
|
||||||
|
}}>
|
||||||
|
<span>
|
||||||
|
Status: {plan.status === 'published' ? 'Veröffentlicht' :
|
||||||
|
plan.status === 'draft' ? 'Entwurf' : 'Archiviert'}
|
||||||
|
</span>
|
||||||
|
<Link to={`/shift-plans/${plan.id}`} style={{ color: '#3498db', textDecoration: 'none' }}>
|
||||||
|
Anzeigen →
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ textAlign: 'center', padding: '20px', color: '#666' }}>
|
||||||
|
<div style={{ fontSize: '48px', marginBottom: '10px' }}>📋</div>
|
||||||
|
<div>Noch keine Schichtpläne erstellt</div>
|
||||||
|
<Link to="/shift-plans/new">
|
||||||
|
<button style={{
|
||||||
|
marginTop: '10px',
|
||||||
|
padding: '8px 16px',
|
||||||
|
backgroundColor: '#3498db',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}>
|
||||||
|
Ersten Plan erstellen
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,13 @@ export const shiftPlanService = {
|
|||||||
throw new Error('Fehler beim Laden der Schichtpläne');
|
throw new Error('Fehler beim Laden der Schichtpläne');
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.json();
|
const plans = await response.json();
|
||||||
|
|
||||||
|
// Ensure scheduledShifts is always an array
|
||||||
|
return plans.map((plan: any) => ({
|
||||||
|
...plan,
|
||||||
|
scheduledShifts: plan.scheduledShifts || []
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
async getShiftPlan(id: string): Promise<ShiftPlan> {
|
async getShiftPlan(id: string): Promise<ShiftPlan> {
|
||||||
@@ -150,15 +156,29 @@ export const shiftPlanService = {
|
|||||||
return handleResponse(response);
|
return handleResponse(response);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Create plan from template
|
|
||||||
/*createFromTemplate: async (data: CreateShiftFromTemplateRequest): Promise<ShiftPlan> => {
|
async regenerateScheduledShifts(planId: string):Promise<void> {
|
||||||
const response = await fetch(`${API_BASE}/from-template`, {
|
try {
|
||||||
|
console.log('🔄 Attempting to regenerate scheduled shifts...');
|
||||||
|
|
||||||
|
// You'll need to add this API endpoint to your backend
|
||||||
|
const response = await fetch(`${API_BASE}/${planId}/regenerate-shifts`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: getAuthHeaders(),
|
headers: {
|
||||||
body: JSON.stringify(data),
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return handleResponse(response);
|
|
||||||
},*/
|
if (response.ok) {
|
||||||
|
console.log('✅ Scheduled shifts regenerated');
|
||||||
|
} else {
|
||||||
|
console.error('❌ Failed to regenerate shifts');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error regenerating shifts:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Create new plan
|
// Create new plan
|
||||||
createPlan: async (data: CreateShiftPlanRequest): Promise<ShiftPlan> => {
|
createPlan: async (data: CreateShiftPlanRequest): Promise<ShiftPlan> => {
|
||||||
|
|||||||
Reference in New Issue
Block a user