diff --git a/backend/src/controllers/shiftPlanController.ts b/backend/src/controllers/shiftPlanController.ts index 84c0d55..c41bf35 100644 --- a/backend/src/controllers/shiftPlanController.ts +++ b/backend/src/controllers/shiftPlanController.ts @@ -681,33 +681,53 @@ async function generateScheduledShifts(planId: string, startDate: string, endDat } } -/*export const getTemplates = async (req: Request, res: Response): Promise => { +export const revertToDraft = async (req: Request, res: Response): Promise => { try { - console.log('🔍 Lade Vorlagen...'); + const { id } = req.params; + const userId = (req as AuthRequest).user?.userId; - const templates = await db.all(` - SELECT sp.*, e.name as created_by_name - FROM shift_plans sp - LEFT JOIN employees e ON sp.created_by = e.id - WHERE sp.is_template = 1 - ORDER BY sp.created_at DESC - `); + if (!userId) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } - console.log(`✅ ${templates.length} Vorlagen gefunden:`, templates.map(t => t.name)); + // 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; + } - const templatesWithDetails = await Promise.all( - templates.map(async (template) => { - const details = await getPlanWithDetails(template.id); - return details ? { ...details.plan, timeSlots: details.timeSlots, shifts: details.shifts } : null; - }) + // 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] ); - res.json(templatesWithDetails.filter(Boolean)); + // 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 fetching templates:', error); + console.error('Error reverting plan to draft:', error); res.status(500).json({ error: 'Internal server error' }); } -};*/ +}; // Neue Funktion: Create from Template /*export const createFromTemplate = async (req: Request, res: Response): Promise => { diff --git a/backend/src/routes/shiftPlans.ts b/backend/src/routes/shiftPlans.ts index 720fc90..b94bf38 100644 --- a/backend/src/routes/shiftPlans.ts +++ b/backend/src/routes/shiftPlans.ts @@ -9,7 +9,8 @@ import { deleteShiftPlan, //getTemplates, //createFromTemplate, - createFromPreset + createFromPreset, + revertToDraft } from '../controllers/shiftPlanController.js'; const router = express.Router(); @@ -42,4 +43,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); + export default router; \ No newline at end of file diff --git a/frontend/src/pages/ShiftPlans/ShiftPlanView.tsx b/frontend/src/pages/ShiftPlans/ShiftPlanView.tsx index b7d7054..6575aec 100644 --- a/frontend/src/pages/ShiftPlans/ShiftPlanView.tsx +++ b/frontend/src/pages/ShiftPlans/ShiftPlanView.tsx @@ -32,6 +32,7 @@ const ShiftPlanView: React.FC = () => { const [assignmentResult, setAssignmentResult] = useState(null); const [loading, setLoading] = useState(true); const [publishing, setPublishing] = useState(false); + const [reverting, setReverting] = useState(false); const [showAssignmentPreview, setShowAssignmentPreview] = useState(false); useEffect(() => { @@ -166,8 +167,13 @@ const ShiftPlanView: React.FC = () => { message: 'Schichtplan wurde erfolgreich veröffentlicht!' }); - // Reload the plan to reflect changes - loadShiftPlanData(); + // Lade den Plan neu, um die aktuellen Daten zu erhalten + const updatedPlan = await shiftPlanService.getShiftPlan(shiftPlan.id); + setShiftPlan(updatedPlan); + + // Behalte die assignmentResult für die Anzeige bei + // Die Tabelle wird nun automatisch die tatsächlichen Mitarbeiternamen anzeigen + setShowAssignmentPreview(false); } catch (error) { @@ -202,6 +208,62 @@ const ShiftPlanView: React.FC = () => { return employeesWithoutAvailabilities.length === 0; }; + 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; + } + + try { + setReverting(true); + + const updatedPlan = await shiftPlanService.revertToDraft(id); + setShiftPlan(updatedPlan); + setAssignmentResult(null); + + showNotification({ + type: 'success', + title: 'Erfolg', + message: 'Schichtplan wurde erfolgreich zurück in den Entwurfsstatus gesetzt.' + }); + + // Verfügbarkeiten neu laden + loadAvailabilities(); + + } catch (error) { + console.error('Error reverting plan to draft:', error); + showNotification({ + type: 'error', + title: 'Fehler', + message: 'Schichtplan konnte nicht zurückgesetzt werden.' + }); + } finally { + setReverting(false); + } + }; + + const loadAvailabilities = async () => { + if (!employees.length) return; + + try { + const availabilityPromises = employees + .filter(emp => emp.isActive) + .map(emp => employeeService.getAvailabilities(emp.id)); + + const allAvailabilities = await Promise.all(availabilityPromises); + const flattenedAvailabilities = allAvailabilities.flat(); + + const planAvailabilities = flattenedAvailabilities.filter( + availability => availability.planId === id + ); + + setAvailabilities(planAvailabilities); + } catch (error) { + console.error('Error loading availabilities:', error); + } + }; + const getAvailabilityStatus = () => { const totalEmployees = employees.length; const employeesWithAvailabilities = new Set( @@ -219,9 +281,10 @@ const ShiftPlanView: React.FC = () => { const getTimetableData = () => { if (!shiftPlan) return { shifts: [], weekdays: [] }; - // Use timeSlots directly since shifts reference them + const hasAssignments = shiftPlan.status === 'published' || (assignmentResult && Object.keys(assignmentResult.assignments).length > 0); + const timetableShifts = shiftPlan.timeSlots.map(timeSlot => { - const weekdayData: Record = {}; + const weekdayData: Record = {}; weekdays.forEach(weekday => { const shiftsOnDay = shiftPlan.shifts.filter(shift => @@ -230,12 +293,47 @@ const ShiftPlanView: React.FC = () => { ); if (shiftsOnDay.length === 0) { - weekdayData[weekday.id] = ''; + weekdayData[weekday.id] = { employees: [], display: '' }; } else { const totalRequired = shiftsOnDay.reduce((sum, shift) => sum + shift.requiredEmployees, 0); - // For now, show required count since we don't have assigned employees in Shift - weekdayData[weekday.id] = `0/${totalRequired}`; + + let assignedEmployees: string[] = []; + + if (hasAssignments && shiftPlan.scheduledShifts) { + const scheduledShift = shiftPlan.scheduledShifts.find(scheduled => { + const scheduledDayOfWeek = getDayOfWeek(scheduled.date); + return scheduledDayOfWeek === weekday.id && + scheduled.timeSlotId === timeSlot.id; + }); + + if (scheduledShift) { + if (shiftPlan.status === 'published') { + // Verwende tatsächliche Zuweisungen aus der Datenbank + assignedEmployees = scheduledShift.assignedEmployees + .map(empId => { + const employee = employees.find(emp => emp.id === empId); + return employee ? employee.name : 'Unbekannt'; + }); + } else if (assignmentResult && assignmentResult.assignments[scheduledShift.id]) { + // Verwende Preview-Zuweisungen + assignedEmployees = assignmentResult.assignments[scheduledShift.id] + .map(empId => { + const employee = employees.find(emp => emp.id === empId); + return employee ? employee.name : 'Unbekannt'; + }); + } + } + } + + const displayText = hasAssignments + ? assignedEmployees.join(', ') || `0/${totalRequired}` + : `0/${totalRequired}`; + + weekdayData[weekday.id] = { + employees: assignedEmployees, + display: displayText + }; } }); @@ -249,17 +347,83 @@ const ShiftPlanView: React.FC = () => { return { shifts: timetableShifts, weekdays }; }; + const getDayOfWeek = (dateString: string): number => { + const date = new Date(dateString); + return date.getDay() === 0 ? 7 : date.getDay(); + }; + if (loading) return
Lade Schichtplan...
; if (!shiftPlan) return
Schichtplan nicht gefunden
; const timetableData = getTimetableData(); + const availabilityStatus = getAvailabilityStatus(); return (
- {/* Existing header code... */} + {/* Header mit Plan-Informationen und Aktionen */} +
+
+

{shiftPlan.name}

+

+ {shiftPlan.startDate && shiftPlan.endDate && + `Zeitraum: ${formatDate(shiftPlan.startDate)} - ${formatDate(shiftPlan.endDate)}` + } +

+
+ {shiftPlan.status === 'published' ? 'Veröffentlicht' : 'Entwurf'} +
+
+ +
+ {shiftPlan.status === 'published' && hasRole(['admin', 'instandhalter']) && ( + + )} + +
+
- {/* Availability Status */} - {shiftPlan?.status === 'draft' && ( + {/* Availability Status - nur für Entwürfe anzeigen */} + {shiftPlan.status === 'draft' && (
{ Verfügbarkeitseinträge:
- {getAvailabilityStatus().completed} / {getAvailabilityStatus().total} Mitarbeiter + {availabilityStatus.completed} / {availabilityStatus.total} Mitarbeiter
{ }}>
@@ -315,9 +479,9 @@ const ShiftPlanView: React.FC = () => { {!canPublish() && (
- {getAvailabilityStatus().percentage === 100 + {availabilityStatus.percentage === 100 ? 'Bereit zur Veröffentlichung' - : `${getAvailabilityStatus().total - getAvailabilityStatus().completed} Mitarbeiter müssen noch Verfügbarkeit eintragen`} + : `${availabilityStatus.total - availabilityStatus.completed} Mitarbeiter müssen noch Verfügbarkeit eintragen`}
)}
@@ -441,9 +605,12 @@ const ShiftPlanView: React.FC = () => { padding: '20px', boxShadow: '0 2px 4px rgba(0,0,0,0.1)' }}> - {/* Timetable */}
-

Schichtplan

+

+ Schichtplan + {shiftPlan.status === 'published' && ' (Aktuelle Zuweisungen)'} + {assignmentResult && shiftPlan.status === 'draft' && ' (Exemplarische Woche)'} +

{timetableData.shifts.length === 0 ? (
{ textAlign: 'center', border: '1px solid #dee2e6', fontWeight: 'bold', - minWidth: '80px' + minWidth: '120px' }}> {weekday.name} @@ -505,9 +672,10 @@ const ShiftPlanView: React.FC = () => { padding: '12px 16px', border: '1px solid #dee2e6', textAlign: 'center', - color: shift.weekdayData[weekday.id] ? '#2c3e50' : '#bdc3c7' + color: shift.weekdayData[weekday.id].display ? '#2c3e50' : '#bdc3c7', + fontSize: shift.weekdayData[weekday.id].employees.length > 0 ? '14px' : 'inherit' }}> - {shift.weekdayData[weekday.id] || '–'} + {shift.weekdayData[weekday.id].display || '–'} ))} @@ -523,12 +691,18 @@ const ShiftPlanView: React.FC = () => {
- Legende: Angezeigt wird "zugewiesene/benötigte Mitarbeiter" pro Schicht und Wochentag + Legende: { + shiftPlan.status === 'published' + ? 'Angezeigt werden die aktuell zugewiesenen Mitarbeiter' + : assignmentResult + ? 'Angezeigt werden die vorgeschlagenen Mitarbeiter für eine exemplarische Woche' + : 'Angezeigt wird "zugewiesene/benötigte Mitarbeiter" pro Schicht und Wochentag' + }
)}
diff --git a/frontend/src/services/shiftPlanService.ts b/frontend/src/services/shiftPlanService.ts index 39fe99d..e69ce99 100644 --- a/frontend/src/services/shiftPlanService.ts +++ b/frontend/src/services/shiftPlanService.ts @@ -122,12 +122,25 @@ export const shiftPlanService = { } }, - /*getTemplates: async (): Promise => { - const response = await fetch(`${API_BASE}/templates`, { - headers: getAuthHeaders() + async revertToDraft(id: string): Promise { + const response = await fetch(`${API_BASE}/${id}/revert-to-draft`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + ...authService.getAuthHeaders() + } }); - return handleResponse(response); - },*/ + + 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 => {