diff --git a/backend/src/controllers/shiftPlanController.ts b/backend/src/controllers/shiftPlanController.ts index 57d5edb..1c6d2e1 100644 --- a/backend/src/controllers/shiftPlanController.ts +++ b/backend/src/controllers/shiftPlanController.ts @@ -592,6 +592,14 @@ async function getShiftPlanById(planId: string): Promise { `, [planId]); } + // NEW: Load employees for export functionality + const employees = await db.all(` + SELECT id, firstname, lastname, email, role, isActive + FROM employees + WHERE isActive = 1 + ORDER BY firstname, lastname + `, []); + return { ...plan, isTemplate: plan.is_template === 1, @@ -629,6 +637,14 @@ async function getShiftPlanById(planId: string): Promise { requiredEmployees: shift.required_employees, assignedEmployees: JSON.parse(shift.assigned_employees || '[]'), timeSlotName: shift.time_slot_name + })), + employees: employees.map(emp => ({ + id: emp.id, + firstname: emp.firstname, + lastname: emp.lastname, + email: emp.email, + role: emp.role, + isActive: emp.isActive === 1 })) }; } @@ -932,4 +948,247 @@ export const clearAssignments = async (req: Request, res: Response): Promise => { + try { + const { id } = req.params; + + console.log('📊 Starting Excel export for plan:', id); + + // Check if plan exists + const plan = await getShiftPlanById(id); + if (!plan) { + res.status(404).json({ error: 'Shift plan not found' }); + return; + } + + if (plan.status !== 'published') { + res.status(400).json({ error: 'Can only export published shift plans' }); + return; + } + + // For now, return a simple CSV as placeholder + // In a real implementation, you would use a library like exceljs or xlsx + + const csvData = generateCSVFromPlan(plan); + + res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + res.setHeader('Content-Disposition', `attachment; filename="Schichtplan_${plan.name}_${new Date().toISOString().split('T')[0]}.xlsx"`); + + // For now, return CSV as placeholder - replace with actual Excel generation + res.setHeader('Content-Type', 'text/csv'); + res.setHeader('Content-Disposition', `attachment; filename="Schichtplan_${plan.name}_${new Date().toISOString().split('T')[0]}.csv"`); + res.send(csvData); + + console.log('✅ Excel export completed for plan:', id); + + } catch (error) { + console.error('❌ Error exporting to Excel:', error); + res.status(500).json({ error: 'Internal server error during Excel export' }); + } +}; + +export const exportShiftPlanToPDF = async (req: Request, res: Response): Promise => { + try { + const { id } = req.params; + + console.log('📄 Starting PDF export for plan:', id); + + // Check if plan exists + const plan = await getShiftPlanById(id); + if (!plan) { + res.status(404).json({ error: 'Shift plan not found' }); + return; + } + + if (plan.status !== 'published') { + res.status(400).json({ error: 'Can only export published shift plans' }); + return; + } + + // For now, return a simple HTML as placeholder + // In a real implementation, you would use a library like pdfkit, puppeteer, or html-pdf + + const pdfData = generateHTMLFromPlan(plan); + + res.setHeader('Content-Type', 'application/pdf'); + res.setHeader('Content-Disposition', `attachment; filename="Schichtplan_${plan.name}_${new Date().toISOString().split('T')[0]}.pdf"`); + + // For now, return HTML as placeholder - replace with actual PDF generation + res.setHeader('Content-Type', 'text/html'); + res.setHeader('Content-Disposition', `attachment; filename="Schichtplan_${plan.name}_${new Date().toISOString().split('T')[0]}.html"`); + res.send(pdfData); + + console.log('✅ PDF export completed for plan:', id); + + } catch (error) { + console.error('❌ Error exporting to PDF:', error); + res.status(500).json({ error: 'Internal server error during PDF export' }); + } +}; + +// Helper function to generate CSV data +function generateCSVFromPlan(plan: any): string { + const headers = ['Datum', 'Tag', 'Schicht', 'Zeit', 'Zugewiesene Mitarbeiter', 'Benötigte Mitarbeiter']; + const rows: string[] = [headers.join(';')]; + + // Group scheduled shifts by date for better organization + const shiftsByDate = new Map(); + + plan.scheduledShifts?.forEach((scheduledShift: any) => { + const date = scheduledShift.date; + if (!shiftsByDate.has(date)) { + shiftsByDate.set(date, []); + } + shiftsByDate.get(date).push(scheduledShift); + }); + + // Sort dates chronologically + const sortedDates = Array.from(shiftsByDate.keys()).sort(); + + sortedDates.forEach(date => { + const dateShifts = shiftsByDate.get(date); + const dateObj = new Date(date); + const dayName = getGermanDayName(dateObj.getDay()); + + dateShifts.forEach((scheduledShift: any) => { + const timeSlot = plan.timeSlots?.find((ts: any) => ts.id === scheduledShift.timeSlotId); + const employeeNames = scheduledShift.assignedEmployees.map((empId: string) => { + const employee = plan.employees?.find((emp: any) => emp.id === empId); + return employee ? `${employee.firstname} ${employee.lastname}` : 'Unbekannt'; + }).join(', '); + + const row = [ + date, + dayName, + timeSlot?.name || 'Unbekannt', + timeSlot ? `${timeSlot.startTime} - ${timeSlot.endTime}` : '', + employeeNames || 'Keine Zuweisungen', + scheduledShift.requiredEmployees || 2 + ].map(field => `"${field}"`).join(';'); + + rows.push(row); + }); + }); + + // Add plan summary + rows.push(''); + rows.push('Plan Zusammenfassung'); + rows.push(`"Plan Name";"${plan.name}"`); + rows.push(`"Zeitraum";"${plan.startDate} bis ${plan.endDate}"`); + rows.push(`"Status";"${plan.status}"`); + rows.push(`"Erstellt von";"${plan.created_by_name || 'Unbekannt'}"`); + rows.push(`"Erstellt am";"${plan.createdAt}"`); + rows.push(`"Anzahl Schichten";"${plan.scheduledShifts?.length || 0}"`); + + return rows.join('\n'); +} + +// Helper function to generate HTML data +function generateHTMLFromPlan(plan: any): string { + const shiftsByDate = new Map(); + + plan.scheduledShifts?.forEach((scheduledShift: any) => { + const date = scheduledShift.date; + if (!shiftsByDate.has(date)) { + shiftsByDate.set(date, []); + } + shiftsByDate.get(date).push(scheduledShift); + }); + + const sortedDates = Array.from(shiftsByDate.keys()).sort(); + + let html = ` + + + + + Schichtplan: ${plan.name} + + + +

Schichtplan: ${plan.name}

+ +
+

Plan Informationen

+

Zeitraum: ${plan.startDate} bis ${plan.endDate}

+

Status: ${plan.status}

+

Erstellt von: ${plan.created_by_name || 'Unbekannt'}

+

Erstellt am: ${plan.createdAt}

+

Anzahl Schichten: ${plan.scheduledShifts?.length || 0}

+
+ +

Schichtzuweisungen

+ `; + + sortedDates.forEach(date => { + const dateShifts = shiftsByDate.get(date); + const dateObj = new Date(date); + const dayName = getGermanDayName(dateObj.getDay()); + + html += ` +
+

${date} (${dayName})

+ + + + + + + + + + + `; + + dateShifts.forEach((scheduledShift: any) => { + const timeSlot = plan.timeSlots?.find((ts: any) => ts.id === scheduledShift.timeSlotId); + const employeeNames = scheduledShift.assignedEmployees.map((empId: string) => { + const employee = plan.employees?.find((emp: any) => emp.id === empId); + return employee ? `${employee.firstname} ${employee.lastname}` : 'Unbekannt'; + }).join(', ') || 'Keine Zuweisungen'; + + html += ` + + + + + + + `; + }); + + html += ` + +
SchichtZeitZugewiesene MitarbeiterBenötigte Mitarbeiter
${timeSlot?.name || 'Unbekannt'}${timeSlot ? `${timeSlot.startTime} - ${timeSlot.endTime}` : ''}${employeeNames}${scheduledShift.requiredEmployees || 2}
+
+ `; + }); + + html += ` +
+ Erstellt am: ${new Date().toLocaleString('de-DE')} +
+ + + `; + + return html; +} + +// Helper function to get German day names +function getGermanDayName(dayIndex: number): string { + const days = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag']; + return days[dayIndex]; +} \ No newline at end of file diff --git a/backend/src/routes/shiftPlans.ts b/backend/src/routes/shiftPlans.ts index de6dfe0..3cb4651 100644 --- a/backend/src/routes/shiftPlans.ts +++ b/backend/src/routes/shiftPlans.ts @@ -7,7 +7,9 @@ import { updateShiftPlan, deleteShiftPlan, createFromPreset, - clearAssignments + clearAssignments, + exportShiftPlanToExcel, + exportShiftPlanToPDF } from '../controllers/shiftPlanController.js'; import { validateShiftPlan, @@ -30,4 +32,7 @@ router.put('/:id', validateId, validateShiftPlanUpdate, handleValidationErrors, router.delete('/:id', validateId, handleValidationErrors, requireRole(['admin', 'maintenance']), deleteShiftPlan); router.post('/:id/clear-assignments', validateId, handleValidationErrors, requireRole(['admin', 'maintenance']), clearAssignments); +router.get('/:id/export/excel', validateId, handleValidationErrors, requireRole(['admin', 'maintenance']), exportShiftPlanToExcel); +router.get('/:id/export/pdf', validateId, handleValidationErrors, requireRole(['admin', 'maintenance']), exportShiftPlanToPDF); + export default router; \ No newline at end of file