From 738b7f645aeb6568277a6f0026a1136288e5a67c Mon Sep 17 00:00:00 2001 From: donpat1to Date: Sat, 11 Oct 2025 15:50:29 +0200 Subject: [PATCH] fixed most of dependencies errors --- .../src/controllers/shiftPlanController.ts | 706 +++++++++++++----- .../controllers/shiftTemplateController.ts | 651 +++++++++++----- backend/src/models/ShiftPlan.ts | 123 +-- backend/src/routes/shiftTemplates.ts | 19 +- backend/src/scripts/checkTemplates.ts | 10 +- backend/src/scripts/setupDefaultTemplate.ts | 2 +- 6 files changed, 989 insertions(+), 522 deletions(-) diff --git a/backend/src/controllers/shiftPlanController.ts b/backend/src/controllers/shiftPlanController.ts index 1f1d85c..5a13753 100644 --- a/backend/src/controllers/shiftPlanController.ts +++ b/backend/src/controllers/shiftPlanController.ts @@ -2,148 +2,331 @@ import { Request, Response } from 'express'; import { v4 as uuidv4 } from 'uuid'; import { db } from '../services/databaseService.js'; +import { + CreateShiftPlanRequest, + UpdateShiftPlanRequest, + ShiftPlan +} from '../models/ShiftPlan.js'; import { AuthRequest } from '../middleware/auth.js'; -import { ShiftPlan, CreateShiftPlanRequest } from '../models/ShiftPlan.js'; +import { createPlanFromPreset, TEMPLATE_PRESETS } from '../models/defaults/shiftPlanDefaults.js'; -export const getShiftPlans = async (req: AuthRequest, res: Response): Promise => { +export const getShiftPlans = async (req: Request, res: Response): Promise => { try { - const userId = req.user?.userId; - const userRole = req.user?.role; + console.log('🔍 Lade Schichtpläne...'); - let query = ` - SELECT sp.*, u.name as created_by_name + const shiftPlans = await db.all(` + SELECT sp.*, e.name as created_by_name FROM shift_plans sp - LEFT JOIN users u ON sp.created_by = u.id - `; + LEFT JOIN employees e ON sp.created_by = e.id + ORDER BY sp.created_at DESC + `); - // Regular users can only see published plans - if (userRole === 'user') { - query += ` WHERE sp.status = 'published'`; - } + console.log(`✅ ${shiftPlans.length} Schichtpläne gefunden:`, shiftPlans.map(p => p.name)); - query += ` ORDER BY sp.created_at DESC`; + // Für jeden Plan die Schichten und Zeit-Slots laden + const plansWithDetails = await Promise.all( + shiftPlans.map(async (plan) => { + // Lade Zeit-Slots + const timeSlots = await db.all(` + SELECT * FROM time_slots + WHERE plan_id = ? + ORDER BY start_time + `, [plan.id]); - const shiftPlans = await db.all(query); + // Lade Schichten + const shifts = await db.all(` + SELECT s.*, ts.name as time_slot_name, ts.start_time, ts.end_time + FROM shifts s + LEFT JOIN time_slots ts ON s.time_slot_id = ts.id + WHERE s.plan_id = ? + ORDER BY s.day_of_week, ts.start_time + `, [plan.id]); - res.json(shiftPlans); + // Lade geplante Schichten (nur für nicht-Template Pläne) + let scheduledShifts = []; + if (!plan.is_template) { + scheduledShifts = await db.all(` + SELECT ss.*, ts.name as time_slot_name + FROM scheduled_shifts ss + LEFT JOIN time_slots ts ON ss.time_slot_id = ts.id + WHERE ss.plan_id = ? + ORDER BY ss.date, ts.start_time + `, [plan.id]); + } + + return { + ...plan, + isTemplate: plan.is_template === 1, + startDate: plan.start_date, + endDate: plan.end_date, + createdBy: plan.created_by, + createdAt: plan.created_at, + timeSlots: timeSlots.map(slot => ({ + id: slot.id, + planId: slot.plan_id, + name: slot.name, + startTime: slot.start_time, + endTime: slot.end_time, + description: slot.description + })), + shifts: shifts.map(shift => ({ + id: shift.id, + planId: shift.plan_id, + timeSlotId: shift.time_slot_id, + dayOfWeek: shift.day_of_week, + requiredEmployees: shift.required_employees, + color: shift.color, + timeSlot: { + id: shift.time_slot_id, + name: shift.time_slot_name, + startTime: shift.start_time, + endTime: shift.end_time + } + })), + scheduledShifts: scheduledShifts.map(shift => ({ + id: shift.id, + planId: shift.plan_id, + date: shift.date, + timeSlotId: shift.time_slot_id, + requiredEmployees: shift.required_employees, + assignedEmployees: JSON.parse(shift.assigned_employees || '[]'), + timeSlotName: shift.time_slot_name + })) + }; + }) + ); + + res.json(plansWithDetails); } catch (error) { console.error('Error fetching shift plans:', error); res.status(500).json({ error: 'Internal server error' }); } }; -export const getShiftPlan = async (req: AuthRequest, res: Response): Promise => { +export const getShiftPlan = async (req: Request, res: Response): Promise => { try { const { id } = req.params; - const userId = req.user?.userId; - const userRole = req.user?.role; - let query = ` - SELECT sp.*, u.name as created_by_name + const plan = await db.get(` + SELECT sp.*, e.name as created_by_name FROM shift_plans sp - LEFT JOIN users u ON sp.created_by = u.id + LEFT JOIN employees e ON sp.created_by = e.id WHERE sp.id = ? - `; + `, [id]); - // Regular users can only see published plans - if (userRole === 'user') { - query += ` AND sp.status = 'published'`; - } - - const shiftPlan = await db.get(query, [id]); - - if (!shiftPlan) { + if (!plan) { res.status(404).json({ error: 'Shift plan not found' }); return; } - // Load assigned shifts - const assignedShifts = await db.all(` - SELECT * FROM assigned_shifts - WHERE shift_plan_id = ? - ORDER BY date, start_time + // Lade Zeit-Slots + const timeSlots = await db.all(` + SELECT * FROM time_slots + WHERE plan_id = ? + ORDER BY start_time `, [id]); - const shiftPlanWithShifts = { - ...shiftPlan, - shifts: assignedShifts.map(shift => ({ + // Lade Schichten + const shifts = await db.all(` + SELECT s.*, ts.name as time_slot_name, ts.start_time, ts.end_time + FROM shifts s + LEFT JOIN time_slots ts ON s.time_slot_id = ts.id + WHERE s.plan_id = ? + ORDER BY s.day_of_week, ts.start_time + `, [id]); + + // Lade geplante Schichten (nur für nicht-Template Pläne) + let scheduledShifts = []; + if (!plan.is_template) { + scheduledShifts = await db.all(` + SELECT ss.*, ts.name as time_slot_name + FROM scheduled_shifts ss + LEFT JOIN time_slots ts ON ss.time_slot_id = ts.id + WHERE ss.plan_id = ? + ORDER BY ss.date, ts.start_time + `, [id]); + } + + const planWithData = { + ...plan, + isTemplate: plan.is_template === 1, + startDate: plan.start_date, + endDate: plan.end_date, + createdBy: plan.created_by, + createdAt: plan.created_at, + timeSlots: timeSlots.map(slot => ({ + id: slot.id, + planId: slot.plan_id, + name: slot.name, + startTime: slot.start_time, + endTime: slot.end_time, + description: slot.description + })), + shifts: shifts.map(shift => ({ id: shift.id, - date: shift.date, - name: shift.name, - startTime: shift.start_time, - endTime: shift.end_time, + planId: shift.plan_id, + timeSlotId: shift.time_slot_id, + dayOfWeek: shift.day_of_week, requiredEmployees: shift.required_employees, - assignedEmployees: JSON.parse(shift.assigned_employees || '[]') + color: shift.color, + timeSlot: { + id: shift.time_slot_id, + name: shift.time_slot_name, + startTime: shift.start_time, + endTime: shift.end_time + } + })), + scheduledShifts: scheduledShifts.map(shift => ({ + id: shift.id, + planId: shift.plan_id, + date: shift.date, + timeSlotId: shift.time_slot_id, + requiredEmployees: shift.required_employees, + assignedEmployees: JSON.parse(shift.assigned_employees || '[]'), + timeSlotName: shift.time_slot_name })) }; - res.json(shiftPlanWithShifts); + res.json(planWithData); } catch (error) { console.error('Error fetching shift plan:', error); res.status(500).json({ error: 'Internal server error' }); } }; -export const createShiftPlan = async (req: AuthRequest, res: Response): Promise => { +export const createDefaultTemplate = async (userId: string): Promise => { try { - const { name, startDate, endDate, templateId }: CreateShiftPlanRequest = req.body; - const userId = req.user?.userId; + const planId = uuidv4(); + + await db.run('BEGIN TRANSACTION'); + + try { + // Erstelle den Standard-Plan (als Template) + await db.run( + `INSERT INTO shift_plans (id, name, description, is_template, status, created_by) + VALUES (?, ?, ?, ?, ?, ?)`, + [planId, 'Standardwoche', 'Standard Vorlage mit konfigurierten Zeit-Slots', true, 'template', userId] + ); + + // Füge Zeit-Slots hinzu + const timeSlots = [ + { id: uuidv4(), name: 'Vormittag', startTime: '08:00', endTime: '12:00', description: 'Vormittagsschicht' }, + { id: uuidv4(), name: 'Nachmittag', startTime: '11:30', endTime: '15:30', description: 'Nachmittagsschicht' } + ]; + + for (const slot of timeSlots) { + await db.run( + `INSERT INTO time_slots (id, plan_id, name, start_time, end_time, description) + VALUES (?, ?, ?, ?, ?, ?)`, + [slot.id, planId, slot.name, slot.startTime, slot.endTime, slot.description] + ); + } + + // Erstelle Schichten für Mo-Do mit Zeit-Slot Referenzen + for (let day = 1; day <= 4; day++) { + // Vormittagsschicht + await db.run( + `INSERT INTO shifts (id, plan_id, day_of_week, time_slot_id, required_employees, color) + VALUES (?, ?, ?, ?, ?, ?)`, + [uuidv4(), planId, day, timeSlots[0].id, 2, '#3498db'] + ); + + // Nachmittagsschicht + await db.run( + `INSERT INTO shifts (id, plan_id, day_of_week, time_slot_id, required_employees, color) + VALUES (?, ?, ?, ?, ?, ?)`, + [uuidv4(), planId, day, timeSlots[1].id, 2, '#e74c3c'] + ); + } + + // Freitag nur Vormittagsschicht + await db.run( + `INSERT INTO shifts (id, plan_id, day_of_week, time_slot_id, required_employees, color) + VALUES (?, ?, ?, ?, ?, ?)`, + [uuidv4(), planId, 5, timeSlots[0].id, 2, '#3498db'] + ); + + await db.run('COMMIT'); + return planId; + } catch (error) { + await db.run('ROLLBACK'); + throw error; + } + } catch (error) { + console.error('Error creating default template:', error); + throw error; + } +}; + +export const createShiftPlan = async (req: Request, res: Response): Promise => { + try { + const { name, description, startDate, endDate, isTemplate, timeSlots, shifts }: CreateShiftPlanRequest = req.body; + const userId = (req as AuthRequest).user?.userId; if (!userId) { res.status(401).json({ error: 'Unauthorized' }); return; } - if (!name || !startDate || !endDate) { - res.status(400).json({ error: 'Name, start date and end date are required' }); - return; - } - - const shiftPlanId = uuidv4(); + const planId = uuidv4(); + const status = isTemplate ? 'template' : 'draft'; + // Start transaction await db.run('BEGIN TRANSACTION'); try { - // Create shift plan + // Insert plan await db.run( - `INSERT INTO shift_plans (id, name, start_date, end_date, template_id, status, created_by) - VALUES (?, ?, ?, ?, ?, ?, ?)`, - [shiftPlanId, name, startDate, endDate, templateId, 'draft', userId] + `INSERT INTO shift_plans (id, name, description, start_date, end_date, is_template, status, created_by) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + [planId, name, description, startDate, endDate, isTemplate ? 1 : 0, status, userId] ); - // If template is provided, generate shifts from template - if (templateId) { - await generateShiftsFromTemplate(shiftPlanId, templateId, startDate, endDate); + // Create mapping for time slot IDs + const timeSlotIdMap = new Map(); + + // Insert time slots - always generate new IDs + for (const timeSlot of timeSlots) { + const timeSlotId = uuidv4(); + await db.run( + `INSERT INTO time_slots (id, plan_id, name, start_time, end_time, description) + VALUES (?, ?, ?, ?, ?, ?)`, + [timeSlotId, planId, timeSlot.name, timeSlot.startTime, timeSlot.endTime, timeSlot.description || ''] + ); + + // Store the mapping if the timeSlot had a temporary ID + if ((timeSlot as any).id) { + timeSlotIdMap.set((timeSlot as any).id, timeSlotId); + } + } + + // Insert shifts - update timeSlotId using the mapping if needed + for (const shift of shifts) { + const shiftId = uuidv4(); + let finalTimeSlotId = shift.timeSlotId; + + // If timeSlotId exists in mapping, use the new ID + if (timeSlotIdMap.has(shift.timeSlotId)) { + finalTimeSlotId = timeSlotIdMap.get(shift.timeSlotId)!; + } + + await db.run( + `INSERT INTO shifts (id, plan_id, day_of_week, time_slot_id, required_employees, color) + VALUES (?, ?, ?, ?, ?, ?)`, + [shiftId, planId, shift.dayOfWeek, finalTimeSlotId, shift.requiredEmployees, shift.color || '#3498db'] + ); + } + + // If this is not a template, generate scheduled shifts + if (!isTemplate && startDate && endDate) { + await generateScheduledShifts(planId, startDate, endDate); } await db.run('COMMIT'); - // Return created shift plan - const createdPlan = await db.get(` - SELECT sp.*, u.name as created_by_name - FROM shift_plans sp - LEFT JOIN users u ON sp.created_by = u.id - WHERE sp.id = ? - `, [shiftPlanId]); - - const assignedShifts = await db.all(` - SELECT * FROM assigned_shifts - WHERE shift_plan_id = ? - ORDER BY date, start_time - `, [shiftPlanId]); - - res.status(201).json({ - ...createdPlan, - shifts: assignedShifts.map(shift => ({ - id: shift.id, - date: shift.date, - name: shift.name, - startTime: shift.start_time, - endTime: shift.end_time, - requiredEmployees: shift.required_employees, - assignedEmployees: JSON.parse(shift.assigned_employees || '[]') - })) - }); + // Return created plan + const createdPlan = await getShiftPlanById(planId); + res.status(201).json(createdPlan); } catch (error) { await db.run('ROLLBACK'); @@ -156,79 +339,167 @@ export const createShiftPlan = async (req: AuthRequest, res: Response): Promise< } }; -export const updateShiftPlan = async (req: AuthRequest, res: Response): Promise => { +export const createFromPreset = async (req: Request, res: Response): Promise => { try { - const { id } = req.params; - const { name, status, shifts } = req.body; - const userId = req.user?.userId; + const { presetName, name, startDate, endDate, isTemplate } = req.body; + const userId = (req as AuthRequest).user?.userId; - // Check if shift plan exists - const existingPlan: any = await db.get('SELECT * FROM shift_plans WHERE id = ?', [id]); - if (!existingPlan) { - res.status(404).json({ error: 'Shift plan not found' }); + if (!userId) { + res.status(401).json({ error: 'Unauthorized' }); return; } - // Check permissions (only admin/instandhalter or creator can update) - if (existingPlan.created_by !== userId && !['admin', 'instandhalter'].includes(req.user?.role || '')) { - res.status(403).json({ error: 'Insufficient permissions' }); + if (!TEMPLATE_PRESETS[presetName as keyof typeof TEMPLATE_PRESETS]) { + res.status(400).json({ error: 'Invalid preset name' }); + return; + } + + const planRequest = createPlanFromPreset( + presetName as keyof typeof TEMPLATE_PRESETS, + isTemplate, + startDate, + endDate + ); + + // Use the provided name or the preset name + planRequest.name = name || planRequest.name; + + const planId = uuidv4(); + const status = isTemplate ? 'template' : 'draft'; + + await db.run('BEGIN TRANSACTION'); + + try { + // Insert plan + await db.run( + `INSERT INTO shift_plans (id, name, description, start_date, end_date, is_template, status, created_by) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + [planId, planRequest.name, planRequest.description, startDate, endDate, isTemplate ? 1 : 0, status, userId] + ); + + // Create mapping from timeSlotKey to database timeSlotId + const timeSlotKeyToId = new Map(); + + // Insert time slots and create mapping + for (let i = 0; i < planRequest.timeSlots.length; i++) { + const timeSlot = planRequest.timeSlots[i]; + const presetTimeSlot = TEMPLATE_PRESETS[presetName as keyof typeof TEMPLATE_PRESETS].timeSlots[i]; + const timeSlotId = uuidv4(); + + await db.run( + `INSERT INTO time_slots (id, plan_id, name, start_time, end_time, description) + VALUES (?, ?, ?, ?, ?, ?)`, + [timeSlotId, planId, timeSlot.name, timeSlot.startTime, timeSlot.endTime, timeSlot.description || ''] + ); + + // Store mapping using the key from preset + timeSlotKeyToId.set(presetTimeSlot.name, timeSlotId); + } + + // Insert shifts using the mapping + for (const shift of planRequest.shifts) { + const shiftId = uuidv4(); + const timeSlotId = timeSlotKeyToId.get((shift as any).timeSlotKey); + + if (!timeSlotId) { + throw new Error(`Time slot key ${(shift as any).timeSlotKey} not found in mapping`); + } + + await db.run( + `INSERT INTO shifts (id, plan_id, day_of_week, time_slot_id, required_employees, color) + VALUES (?, ?, ?, ?, ?, ?)`, + [shiftId, planId, shift.dayOfWeek, timeSlotId, shift.requiredEmployees, shift.color || '#3498db'] + ); + } + + // If this is not a template, generate scheduled shifts + if (!isTemplate && 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) { + await db.run('ROLLBACK'); + throw error; + } + + } catch (error) { + console.error('Error creating plan from preset:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +export const updateShiftPlan = async (req: Request, res: Response): Promise => { + try { + const { id } = req.params; + const { name, description, startDate, endDate, status, timeSlots, shifts }: UpdateShiftPlanRequest = req.body; + + // 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 { - // Update shift plan - if (name !== undefined || status !== undefined) { + // Update plan + if (name !== undefined || description !== undefined || startDate !== undefined || endDate !== undefined || status !== undefined) { await db.run( `UPDATE shift_plans SET name = COALESCE(?, name), + description = COALESCE(?, description), + start_date = COALESCE(?, start_date), + end_date = COALESCE(?, end_date), status = COALESCE(?, status) WHERE id = ?`, - [name, status, id] + [name, description, startDate, endDate, status, id] ); } - // Update shifts if provided - if (shifts) { - for (const shift of shifts) { + // If updating time slots, replace all time slots + if (timeSlots) { + // Delete existing time slots + await db.run('DELETE FROM time_slots WHERE plan_id = ?', [id]); + + // Insert new time slots - always generate new IDs + for (const timeSlot of timeSlots) { + const timeSlotId = uuidv4(); await db.run( - `UPDATE assigned_shifts - SET required_employees = ?, - assigned_employees = ? - WHERE id = ? AND shift_plan_id = ?`, - [shift.requiredEmployees, JSON.stringify(shift.assignedEmployees || []), shift.id, id] + `INSERT INTO time_slots (id, plan_id, name, start_time, end_time, description) + VALUES (?, ?, ?, ?, ?, ?)`, + [timeSlotId, id, timeSlot.name, timeSlot.startTime, timeSlot.endTime, timeSlot.description || ''] + ); + } + } + + // If updating shifts, replace all shifts + if (shifts) { + // Delete existing shifts + await db.run('DELETE FROM shifts WHERE plan_id = ?', [id]); + + // Insert new shifts - use new timeSlotId (they should reference the newly created time slots) + for (const shift of shifts) { + const shiftId = uuidv4(); + await db.run( + `INSERT INTO shifts (id, plan_id, day_of_week, time_slot_id, required_employees, color) + VALUES (?, ?, ?, ?, ?, ?)`, + [shiftId, id, shift.dayOfWeek, shift.timeSlotId, shift.requiredEmployees, shift.color || '#3498db'] ); } } await db.run('COMMIT'); - // Return updated shift plan - const updatedPlan = await db.get(` - SELECT sp.*, u.name as created_by_name - FROM shift_plans sp - LEFT JOIN users u ON sp.created_by = u.id - WHERE sp.id = ? - `, [id]); - - const assignedShifts = await db.all(` - SELECT * FROM assigned_shifts - WHERE shift_plan_id = ? - ORDER BY date, start_time - `, [id]); - - res.json({ - ...updatedPlan, - shifts: assignedShifts.map(shift => ({ - id: shift.id, - date: shift.date, - startTime: shift.start_time, - endTime: shift.end_time, - requiredEmployees: shift.required_employees, - assignedEmployees: JSON.parse(shift.assigned_employees || '[]') - })) - }); + // Return updated plan + const updatedPlan = await getShiftPlanById(id); + res.json(updatedPlan); } catch (error) { await db.run('ROLLBACK'); @@ -241,26 +512,19 @@ export const updateShiftPlan = async (req: AuthRequest, res: Response): Promise< } }; -export const deleteShiftPlan = async (req: AuthRequest, res: Response): Promise => { +export const deleteShiftPlan = async (req: Request, res: Response): Promise => { try { const { id } = req.params; - const userId = req.user?.userId; - // Check if shift plan exists - const existingPlan: any = await db.get('SELECT * FROM shift_plans WHERE id = ?', [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; } - // Check permissions (only admin/instandhalter or creator can delete) - if (existingPlan.created_by !== userId && !['admin', 'instandhalter'].includes(req.user?.role || '')) { - res.status(403).json({ error: 'Insufficient permissions' }); - return; - } - await db.run('DELETE FROM shift_plans WHERE id = ?', [id]); - // Assigned shifts will be automatically deleted due to CASCADE + // Time slots, shifts, and scheduled shifts will be automatically deleted due to CASCADE res.status(204).send(); } catch (error) { @@ -269,61 +533,131 @@ export const deleteShiftPlan = async (req: AuthRequest, res: Response): Promise< } }; -// Helper function to generate shifts from template -async function generateShiftsFromTemplate(shiftPlanId: string, templateId: string, startDate: string, endDate: string): Promise { - try { - console.log(`🔄 Generiere Schichten von Vorlage ${templateId} für Plan ${shiftPlanId}`); - - // Get template shifts with time slot information - const templateShifts = await db.all(` - SELECT ts.*, tts.name as time_slot_name, tts.start_time, tts.end_time - FROM template_shifts ts - LEFT JOIN template_time_slots tts ON ts.time_slot_id = tts.id - WHERE ts.template_id = ? - ORDER BY ts.day_of_week, tts.start_time - `, [templateId]); +// Helper function to get plan by ID +async function getShiftPlanById(planId: string): Promise { + const plan = await db.get(` + SELECT sp.*, e.name as created_by_name + FROM shift_plans sp + LEFT JOIN employees e ON sp.created_by = e.id + WHERE sp.id = ? + `, [planId]); - console.log(`📋 Gefundene Template-Schichten: ${templateShifts.length}`); + if (!plan) { + return null; + } + + // Lade Zeit-Slots + const timeSlots = await db.all(` + SELECT * FROM time_slots + WHERE plan_id = ? + ORDER BY start_time + `, [planId]); + + // Lade Schichten + const shifts = await db.all(` + SELECT s.*, ts.name as time_slot_name, ts.start_time, ts.end_time + FROM shifts s + LEFT JOIN time_slots ts ON s.time_slot_id = ts.id + WHERE s.plan_id = ? + ORDER BY s.day_of_week, ts.start_time + `, [planId]); + + // Lade geplante Schichten (nur für nicht-Template Pläne) + let scheduledShifts = []; + if (!plan.is_template) { + scheduledShifts = await db.all(` + SELECT ss.*, ts.name as time_slot_name + FROM scheduled_shifts ss + LEFT JOIN time_slots ts ON ss.time_slot_id = ts.id + WHERE ss.plan_id = ? + ORDER BY ss.date, ts.start_time + `, [planId]); + } + + return { + ...plan, + isTemplate: plan.is_template === 1, + startDate: plan.start_date, + endDate: plan.end_date, + createdBy: plan.created_by, + createdAt: plan.created_at, + timeSlots: timeSlots.map(slot => ({ + id: slot.id, + planId: slot.plan_id, + name: slot.name, + startTime: slot.start_time, + endTime: slot.end_time, + description: slot.description + })), + shifts: shifts.map(shift => ({ + id: shift.id, + planId: shift.plan_id, + timeSlotId: shift.time_slot_id, + dayOfWeek: shift.day_of_week, + requiredEmployees: shift.required_employees, + color: shift.color, + timeSlot: { + id: shift.time_slot_id, + name: shift.time_slot_name, + startTime: shift.start_time, + endTime: shift.end_time + } + })), + scheduledShifts: scheduledShifts.map(shift => ({ + id: shift.id, + planId: shift.plan_id, + date: shift.date, + timeSlotId: shift.time_slot_id, + requiredEmployees: shift.required_employees, + assignedEmployees: JSON.parse(shift.assigned_employees || '[]'), + timeSlotName: shift.time_slot_name + })) + }; +} + +// Helper function to generate scheduled shifts from template +async function generateScheduledShifts(planId: string, startDate: string, endDate: string): Promise { + try { + console.log(`🔄 Generiere geplante Schichten für Plan ${planId} von ${startDate} bis ${endDate}`); + + // Get plan with shifts and time slots + const plan = await getShiftPlanById(planId); + if (!plan) { + throw new Error('Plan not found'); + } const start = new Date(startDate); const end = new Date(endDate); - // Generate shifts ONLY for days that have template shifts defined + // Generate scheduled shifts for each day in the date range for (let date = new Date(start); date <= end; date.setDate(date.getDate() + 1)) { - // Convert JS day (0=Sunday) to our format (1=Monday, 7=Sunday) - const dayOfWeek = date.getDay() === 0 ? 7 : date.getDay(); + const dayOfWeek = date.getDay() === 0 ? 7 : date.getDay(); // Convert to 1-7 (Mon-Sun) - // Find template shifts for this day of week - const shiftsForDay = templateShifts.filter(shift => shift.day_of_week === dayOfWeek); + // Find shifts for this day of week + const shiftsForDay = plan.shifts.filter((shift: any) => shift.dayOfWeek === dayOfWeek); - // Only create shifts if there are template shifts defined for this weekday - if (shiftsForDay.length > 0) { - for (const templateShift of shiftsForDay) { - const shiftId = uuidv4(); - - await db.run( - `INSERT INTO assigned_shifts (id, shift_plan_id, date, name, start_time, end_time, required_employees, assigned_employees) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, - [ - shiftId, - shiftPlanId, - date.toISOString().split('T')[0], // YYYY-MM-DD format - templateShift.time_slot_name || 'Schicht', - templateShift.start_time, - templateShift.end_time, - templateShift.required_employees, - JSON.stringify([]) - ] - ); - } - console.log(`✅ ${shiftsForDay.length} Schichten erstellt für ${date.toISOString().split('T')[0]}`); + for (const shift of shiftsForDay) { + const scheduledShiftId = uuidv4(); + + await db.run( + `INSERT INTO scheduled_shifts (id, plan_id, date, time_slot_id, required_employees, assigned_employees) + VALUES (?, ?, ?, ?, ?, ?)`, + [ + scheduledShiftId, + planId, + date.toISOString().split('T')[0], // YYYY-MM-DD format + shift.timeSlotId, + shift.requiredEmployees, + JSON.stringify([]) + ] + ); } } - console.log(`🎉 Schicht-Generierung abgeschlossen für Plan ${shiftPlanId}`); + console.log(`✅ Geplante Schichten generiert für Plan ${planId}`); } catch (error) { - console.error('❌ Fehler beim Generieren der Schichten:', error); + console.error('❌ Fehler beim Generieren der geplanten Schichten:', error); throw error; } } \ No newline at end of file diff --git a/backend/src/controllers/shiftTemplateController.ts b/backend/src/controllers/shiftTemplateController.ts index 270658d..f0bcff6 100644 --- a/backend/src/controllers/shiftTemplateController.ts +++ b/backend/src/controllers/shiftTemplateController.ts @@ -1,194 +1,290 @@ -// backend/src/controllers/shiftTemplateController.ts +// backend/src/controllers/shiftPlanController.ts import { Request, Response } from 'express'; import { v4 as uuidv4 } from 'uuid'; import { db } from '../services/databaseService.js'; -import { - CreateShiftTemplateRequest, - UpdateShiftTemplateRequest -} from '../models/ShiftTemplate.js'; import { AuthRequest } from '../middleware/auth.js'; +import { + CreateShiftPlanRequest, + UpdateShiftPlanRequest, + CreateShiftFromTemplateRequest +} from '../models/ShiftPlan.js'; -export const getTemplates = async (req: Request, res: Response): Promise => { +// Get all shift plans (including templates) +export const getShiftPlans = async (req: Request, res: Response): Promise => { try { - console.log('🔍 Lade Vorlagen...'); + console.log('🔍 Lade Schichtpläne...'); - const templates = await db.all(` - SELECT st.*, u.name as created_by_name - FROM shift_templates st - LEFT JOIN users u ON st.created_by = u.id - ORDER BY st.created_at DESC + const shiftPlans = 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 + ORDER BY sp.created_at DESC `); - console.log(`✅ ${templates.length} Vorlagen gefunden:`, templates.map(t => t.name)); - - // Für jede Vorlage die Schichten und Zeit-Slots laden - const templatesWithShifts = await Promise.all( - templates.map(async (template) => { - // Lade Schicht-Slots - const shiftSlots = await db.all(` - SELECT ts.*, tts.name as time_slot_name, tts.start_time as time_slot_start, tts.end_time as time_slot_end - FROM template_shifts ts - LEFT JOIN template_time_slots tts ON ts.time_slot_id = tts.id - WHERE ts.template_id = ? - ORDER BY ts.day_of_week, tts.start_time - `, [template.id]); + console.log(`✅ ${shiftPlans.length} Schichtpläne gefunden:`, shiftPlans.map(p => p.name)); + // Für jeden Plan die Schichten und Zeit-Slots laden + const plansWithDetails = await Promise.all( + shiftPlans.map(async (plan) => { // Lade Zeit-Slots const timeSlots = await db.all(` - SELECT * FROM template_time_slots - WHERE template_id = ? + SELECT * FROM time_slots + WHERE plan_id = ? ORDER BY start_time - `, [template.id]); + `, [plan.id]); + + // Lade Schichten + const shifts = await db.all(` + SELECT s.*, ts.name as time_slot_name, ts.start_time, ts.end_time + FROM shifts s + LEFT JOIN time_slots ts ON s.time_slot_id = ts.id + WHERE s.plan_id = ? + ORDER BY s.day_of_week, ts.start_time + `, [plan.id]); return { - ...template, - shifts: shiftSlots.map(slot => ({ - id: slot.id, - dayOfWeek: slot.day_of_week, - timeSlot: { - id: slot.time_slot_id, - name: slot.time_slot_name, - startTime: slot.time_slot_start, - endTime: slot.time_slot_end - }, - requiredEmployees: slot.required_employees, - color: slot.color - })), + ...plan, + isTemplate: plan.is_template === 1, + startDate: plan.start_date, + endDate: plan.end_date, + createdBy: plan.created_by, + createdAt: plan.created_at, timeSlots: timeSlots.map(slot => ({ id: slot.id, + planId: slot.plan_id, name: slot.name, startTime: slot.start_time, endTime: slot.end_time, description: slot.description + })), + shifts: shifts.map(shift => ({ + id: shift.id, + planId: shift.plan_id, + timeSlotId: shift.time_slot_id, + dayOfWeek: shift.day_of_week, + requiredEmployees: shift.required_employees, + color: shift.color, + timeSlot: { + id: shift.time_slot_id, + name: shift.time_slot_name, + startTime: shift.start_time, + endTime: shift.end_time + } })) }; }) ); - res.json(templatesWithShifts); + res.json(plansWithDetails); + } catch (error) { + console.error('Error fetching shift plans:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +// Get templates only (plans with is_template = true) +export const getTemplates = async (req: Request, res: Response): Promise => { + try { + console.log('🔍 Lade Vorlagen...'); + + 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 + `); + + console.log(`✅ ${templates.length} Vorlagen gefunden:`, templates.map(t => t.name)); + + const templatesWithDetails = await Promise.all( + templates.map(async (template) => { + // Lade Zeit-Slots + const timeSlots = await db.all(` + SELECT * FROM time_slots + WHERE plan_id = ? + ORDER BY start_time + `, [template.id]); + + // Lade Schichten + const shifts = await db.all(` + SELECT s.*, ts.name as time_slot_name, ts.start_time, ts.end_time + FROM shifts s + LEFT JOIN time_slots ts ON s.time_slot_id = ts.id + WHERE s.plan_id = ? + ORDER BY s.day_of_week, ts.start_time + `, [template.id]); + + return { + ...template, + isTemplate: true, + startDate: template.start_date, + endDate: template.end_date, + createdBy: template.created_by, + createdAt: template.created_at, + timeSlots: timeSlots.map(slot => ({ + id: slot.id, + planId: slot.plan_id, + name: slot.name, + startTime: slot.start_time, + endTime: slot.end_time, + description: slot.description + })), + shifts: shifts.map(shift => ({ + id: shift.id, + planId: shift.plan_id, + timeSlotId: shift.time_slot_id, + dayOfWeek: shift.day_of_week, + requiredEmployees: shift.required_employees, + color: shift.color, + timeSlot: { + id: shift.time_slot_id, + name: shift.time_slot_name, + startTime: shift.start_time, + endTime: shift.end_time + } + })) + }; + }) + ); + + res.json(templatesWithDetails); } catch (error) { console.error('Error fetching templates:', error); res.status(500).json({ error: 'Internal server error' }); } }; -export const getTemplate = async (req: Request, res: Response): Promise => { +export const getShiftPlan = async (req: Request, res: Response): Promise => { try { const { id } = req.params; - const template = await db.get(` - SELECT st.*, u.name as created_by_name - FROM shift_templates st - LEFT JOIN users u ON st.created_by = u.id - WHERE st.id = ? + const plan = await db.get(` + SELECT sp.*, e.name as created_by_name + FROM shift_plans sp + LEFT JOIN employees e ON sp.created_by = e.id + WHERE sp.id = ? `, [id]); - if (!template) { - res.status(404).json({ error: 'Template not found' }); + if (!plan) { + res.status(404).json({ error: 'Shift plan not found' }); return; } - // Lade Schicht-Slots - const shiftSlots = await db.all(` - SELECT ts.*, tts.name as time_slot_name, tts.start_time as time_slot_start, tts.end_time as time_slot_end - FROM template_shifts ts - LEFT JOIN template_time_slots tts ON ts.time_slot_id = tts.id - WHERE ts.template_id = ? - ORDER BY ts.day_of_week, tts.start_time - `, [id]); - // Lade Zeit-Slots const timeSlots = await db.all(` - SELECT * FROM template_time_slots - WHERE template_id = ? + SELECT * FROM time_slots + WHERE plan_id = ? ORDER BY start_time `, [id]); - const templateWithData = { - ...template, - shifts: shiftSlots.map(slot => ({ - id: slot.id, - dayOfWeek: slot.day_of_week, - timeSlot: { - id: slot.time_slot_id, - name: slot.time_slot_name, - startTime: slot.time_slot_start, - endTime: slot.time_slot_end - }, - requiredEmployees: slot.required_employees, - color: slot.color - })), + // Lade Schichten + const shifts = await db.all(` + SELECT s.*, ts.name as time_slot_name, ts.start_time, ts.end_time + FROM shifts s + LEFT JOIN time_slots ts ON s.time_slot_id = ts.id + WHERE s.plan_id = ? + ORDER BY s.day_of_week, ts.start_time + `, [id]); + + const planWithData = { + ...plan, + isTemplate: plan.is_template === 1, + startDate: plan.start_date, + endDate: plan.end_date, + createdBy: plan.created_by, + createdAt: plan.created_at, timeSlots: timeSlots.map(slot => ({ id: slot.id, + planId: slot.plan_id, name: slot.name, startTime: slot.start_time, endTime: slot.end_time, description: slot.description + })), + shifts: shifts.map(shift => ({ + id: shift.id, + planId: shift.plan_id, + timeSlotId: shift.time_slot_id, + dayOfWeek: shift.day_of_week, + requiredEmployees: shift.required_employees, + color: shift.color, + timeSlot: { + id: shift.time_slot_id, + name: shift.time_slot_name, + startTime: shift.start_time, + endTime: shift.end_time + } })) }; - res.json(templateWithData); + res.json(planWithData); } catch (error) { - console.error('Error fetching template:', error); + console.error('Error fetching shift plan:', error); res.status(500).json({ error: 'Internal server error' }); } }; export const createDefaultTemplate = async (userId: string): Promise => { try { - const templateId = uuidv4(); + const planId = uuidv4(); await db.run('BEGIN TRANSACTION'); try { - // Erstelle die Standard-Vorlage + // Erstelle den Standard-Plan (als Template) await db.run( - `INSERT INTO shift_templates (id, name, description, is_default, created_by) - VALUES (?, ?, ?, ?, ?)`, - [templateId, 'Standardwoche', 'Standard Vorlage mit konfigurierten Zeit-Slots', true, userId] + `INSERT INTO shift_plans (id, name, description, is_template, status, created_by) + VALUES (?, ?, ?, ?, ?, ?)`, + [planId, 'Standardwoche', 'Standard Vorlage mit konfigurierten Zeit-Slots', true, 'template', userId] ); // Füge Zeit-Slots hinzu const timeSlots = [ - { id: uuidv4(), name: 'Vormittag', startTime: '08:00', endTime: '12:00', description: 'Vormittagsschicht' }, - { id: uuidv4(), name: 'Nachmittag', startTime: '12:00', endTime: '16:00', description: 'Nachmittagsschicht' }, - { id: uuidv4(), name: 'Abend', startTime: '16:00', endTime: '20:00', description: 'Abendschicht' } + { name: 'Vormittag', startTime: '08:00', endTime: '12:00', description: 'Vormittagsschicht' }, + { name: 'Nachmittag', startTime: '11:30', endTime: '15:30', description: 'Nachmittagsschicht' } ]; - for (const slot of timeSlots) { + for (const timeSlot of timeSlots) { + const timeSlotId = uuidv4(); await db.run( - `INSERT INTO template_time_slots (id, template_id, name, start_time, end_time, description) + `INSERT INTO time_slots (id, plan_id, name, start_time, end_time, description) VALUES (?, ?, ?, ?, ?, ?)`, - [slot.id, templateId, slot.name, slot.startTime, slot.endTime, slot.description] + [timeSlotId, planId, timeSlot.name, timeSlot.startTime, timeSlot.endTime, timeSlot.description] ); } + // Get the created time slots to use their IDs + const createdTimeSlots = await db.all(` + SELECT * FROM time_slots WHERE plan_id = ? ORDER BY start_time + `, [planId]); + // Erstelle Schichten für Mo-Do mit Zeit-Slot Referenzen for (let day = 1; day <= 4; day++) { // Vormittagsschicht await db.run( - `INSERT INTO template_shifts (id, template_id, day_of_week, time_slot_id, required_employees, color) + `INSERT INTO shifts (id, plan_id, day_of_week, time_slot_id, required_employees, color) VALUES (?, ?, ?, ?, ?, ?)`, - [uuidv4(), templateId, day, timeSlots[0].id, 1, '#3498db'] + [uuidv4(), planId, day, createdTimeSlots[0].id, 2, '#3498db'] ); // Nachmittagsschicht await db.run( - `INSERT INTO template_shifts (id, template_id, day_of_week, time_slot_id, required_employees, color) + `INSERT INTO shifts (id, plan_id, day_of_week, time_slot_id, required_employees, color) VALUES (?, ?, ?, ?, ?, ?)`, - [uuidv4(), templateId, day, timeSlots[1].id, 1, '#e74c3c'] + [uuidv4(), planId, day, createdTimeSlots[1].id, 2, '#e74c3c'] ); } // Freitag nur Vormittagsschicht await db.run( - `INSERT INTO template_shifts (id, template_id, day_of_week, time_slot_id, required_employees, color) + `INSERT INTO shifts (id, plan_id, day_of_week, time_slot_id, required_employees, color) VALUES (?, ?, ?, ?, ?, ?)`, - [uuidv4(), templateId, 5, timeSlots[0].id, 1, '#3498db'] + [uuidv4(), planId, 5, createdTimeSlots[0].id, 2, '#3498db'] ); await db.run('COMMIT'); - return templateId; + return planId; } catch (error) { await db.run('ROLLBACK'); throw error; @@ -199,9 +295,9 @@ export const createDefaultTemplate = async (userId: string): Promise => } }; -export const createTemplate = async (req: Request, res: Response): Promise => { +export const createShiftPlan = async (req: Request, res: Response): Promise => { try { - const { name, description, isDefault, shifts, timeSlots }: CreateShiftTemplateRequest = req.body; + const { name, description, startDate, endDate, isTemplate, timeSlots, shifts }: CreateShiftPlanRequest = req.body; const userId = (req as AuthRequest).user?.userId; if (!userId) { @@ -209,58 +305,72 @@ export const createTemplate = async (req: Request, res: Response): Promise return; } - // Wenn diese Vorlage als Standard markiert werden soll, - // zuerst alle anderen Vorlagen auf nicht-Standard setzen - if (isDefault) { - await db.run('UPDATE shift_templates SET is_default = 0'); - } - - const templateId = uuidv4(); + const planId = uuidv4(); + const status = isTemplate ? 'template' : 'draft'; // Start transaction await db.run('BEGIN TRANSACTION'); try { - // Insert template + // Insert plan await db.run( - `INSERT INTO shift_templates (id, name, description, is_default, created_by) - VALUES (?, ?, ?, ?, ?)`, - [templateId, name, description, isDefault ? 1 : 0, userId] + `INSERT INTO shift_plans (id, name, description, start_date, end_date, is_template, status, created_by) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + [planId, name, description, startDate, endDate, isTemplate ? 1 : 0, status, userId] ); - // Insert time slots + // Create mapping for time slot IDs + const timeSlotIdMap = new Map(); + + // Insert time slots - always generate new IDs for (const timeSlot of timeSlots) { - const timeSlotId = timeSlot.id || uuidv4(); + const timeSlotId = uuidv4(); await db.run( - `INSERT INTO template_time_slots (id, template_id, name, start_time, end_time, description) + `INSERT INTO time_slots (id, plan_id, name, start_time, end_time, description) VALUES (?, ?, ?, ?, ?, ?)`, - [timeSlotId, templateId, timeSlot.name, timeSlot.startTime, timeSlot.endTime, description] + [timeSlotId, planId, timeSlot.name, timeSlot.startTime, timeSlot.endTime, timeSlot.description || ''] ); + + // Store mapping using time slot name as key (since we don't have original IDs) + timeSlotIdMap.set(timeSlot.name, timeSlotId); } - // Insert shifts + // Insert shifts - use the mapping to find correct timeSlotId for (const shift of shifts) { const shiftId = uuidv4(); + + // Find timeSlotId by matching with time slot names + let finalTimeSlotId = ''; + for (const [name, id] of timeSlotIdMap.entries()) { + // This is a simple matching logic - you might need to adjust based on your data structure + if (shift.timeSlotId.includes(name) || name.includes(shift.timeSlotId)) { + finalTimeSlotId = id; + break; + } + } + + // If no match found, use the first time slot (fallback) + if (!finalTimeSlotId && timeSlotIdMap.size > 0) { + finalTimeSlotId = Array.from(timeSlotIdMap.values())[0]; + } + await db.run( - `INSERT INTO template_shifts (id, template_id, day_of_week, time_slot_id, required_employees, color) + `INSERT INTO shifts (id, plan_id, day_of_week, time_slot_id, required_employees, color) VALUES (?, ?, ?, ?, ?, ?)`, - [shiftId, templateId, shift.dayOfWeek, shift.timeSlot.id, shift.requiredEmployees, shift.color || '#3498db'] + [shiftId, planId, shift.dayOfWeek, finalTimeSlotId, shift.requiredEmployees, shift.color || '#3498db'] ); } - // If this is set as default, remove default from other templates - if (isDefault) { - await db.run( - `UPDATE shift_templates SET is_default = 0 WHERE id != ? AND is_default = 1`, - [templateId] - ); + // If this is not a template, generate scheduled shifts + if (!isTemplate && startDate && endDate) { + await generateScheduledShifts(planId, startDate, endDate); } await db.run('COMMIT'); - // Return created template - const createdTemplate = await getTemplateById(templateId); - res.status(201).json(createdTemplate); + // Return created plan + const createdPlan = await getShiftPlanById(planId); + res.status(201).json(createdPlan); } catch (error) { await db.run('ROLLBACK'); @@ -268,56 +378,144 @@ export const createTemplate = async (req: Request, res: Response): Promise } } catch (error) { - console.error('Error creating template:', error); + console.error('Error creating shift plan:', error); res.status(500).json({ error: 'Internal server error' }); } }; -export const updateTemplate = async (req: Request, res: Response): Promise => { +export const createFromTemplate = async (req: Request, res: Response): Promise => { + try { + const { templatePlanId, name, startDate, endDate, description }: CreateShiftFromTemplateRequest = req.body; + const userId = (req as AuthRequest).user?.userId; + + if (!userId) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + + // Get the template plan + const templatePlan = await getShiftPlanById(templatePlanId); + if (!templatePlan) { + res.status(404).json({ error: 'Template plan not found' }); + return; + } + + if (!templatePlan.isTemplate) { + res.status(400).json({ error: 'Specified plan is not a template' }); + return; + } + + const planId = uuidv4(); + + await db.run('BEGIN TRANSACTION'); + + 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(` + 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) { + await db.run('ROLLBACK'); + throw error; + } + + } catch (error) { + console.error('Error creating plan from template:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +export const updateShiftPlan = async (req: Request, res: Response): Promise => { try { const { id } = req.params; - const { name, description, isDefault, shifts, timeSlots }: UpdateShiftTemplateRequest = req.body; + const { name, description, startDate, endDate, status, timeSlots, shifts }: UpdateShiftPlanRequest = req.body; - // Check if template exists - const existingTemplate = await db.get('SELECT * FROM shift_templates WHERE id = ?', [id]); - if (!existingTemplate) { - res.status(404).json({ error: 'Template not found' }); + // 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 { - // Wenn diese Vorlage als Standard markiert werden soll, - // zuerst alle anderen Vorlagen auf nicht-Standard setzen - if (isDefault) { - await db.run('UPDATE shift_templates SET is_default = 0'); - } - - // Update template - if (name !== undefined || description !== undefined || isDefault !== undefined) { + // Update plan + if (name !== undefined || description !== undefined || startDate !== undefined || endDate !== undefined || status !== undefined) { await db.run( - `UPDATE shift_templates + `UPDATE shift_plans SET name = COALESCE(?, name), description = COALESCE(?, description), - is_default = COALESCE(?, is_default) + start_date = COALESCE(?, start_date), + end_date = COALESCE(?, end_date), + status = COALESCE(?, status) WHERE id = ?`, - [name, description, isDefault ? 1 : 0, id] + [name, description, startDate, endDate, status, id] ); } // If updating time slots, replace all time slots if (timeSlots) { // Delete existing time slots - await db.run('DELETE FROM template_time_slots WHERE template_id = ?', [id]); + await db.run('DELETE FROM time_slots WHERE plan_id = ?', [id]); // Insert new time slots for (const timeSlot of timeSlots) { - const timeSlotId = timeSlot.id || uuidv4(); + const timeSlotId = uuidv4(); await db.run( - `INSERT INTO template_time_slots (id, template_id, name, start_time, end_time, description) + `INSERT INTO time_slots (id, plan_id, name, start_time, end_time, description) VALUES (?, ?, ?, ?, ?, ?)`, - [timeSlotId, id, timeSlot.name, timeSlot.startTime, timeSlot.endTime, description] + [timeSlotId, id, timeSlot.name, timeSlot.startTime, timeSlot.endTime, timeSlot.description || ''] ); } } @@ -325,32 +523,24 @@ export const updateTemplate = async (req: Request, res: Response): Promise // If updating shifts, replace all shifts if (shifts) { // Delete existing shifts - await db.run('DELETE FROM template_shifts WHERE template_id = ?', [id]); + await db.run('DELETE FROM shifts WHERE plan_id = ?', [id]); // Insert new shifts for (const shift of shifts) { const shiftId = uuidv4(); await db.run( - `INSERT INTO template_shifts (id, template_id, day_of_week, time_slot_id, required_employees, color) + `INSERT INTO shifts (id, plan_id, day_of_week, time_slot_id, required_employees, color) VALUES (?, ?, ?, ?, ?, ?)`, - [shiftId, id, shift.dayOfWeek, shift.timeSlot.id, shift.requiredEmployees, shift.color || '#3498db'] + [shiftId, id, shift.dayOfWeek, shift.timeSlotId, shift.requiredEmployees, shift.color || '#3498db'] ); } } - // If this is set as default, remove default from other templates - if (isDefault) { - await db.run( - `UPDATE shift_templates SET is_default = 0 WHERE id != ? AND is_default = 1`, - [id] - ); - } - await db.run('COMMIT'); - // Return updated template - const updatedTemplate = await getTemplateById(id); - res.json(updatedTemplate); + // Return updated plan + const updatedPlan = await getShiftPlanById(id); + res.json(updatedPlan); } catch (error) { await db.run('ROLLBACK'); @@ -358,81 +548,136 @@ export const updateTemplate = async (req: Request, res: Response): Promise } } catch (error) { - console.error('Error updating template:', error); + console.error('Error updating shift plan:', error); res.status(500).json({ error: 'Internal server error' }); } }; -export const deleteTemplate = async (req: Request, res: Response): Promise => { +export const deleteShiftPlan = async (req: Request, res: Response): Promise => { try { const { id } = req.params; - // Check if template exists - const existingTemplate = await db.get('SELECT * FROM shift_templates WHERE id = ?', [id]); - if (!existingTemplate) { - res.status(404).json({ error: 'Template not found' }); + // 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('DELETE FROM shift_templates WHERE id = ?', [id]); - // Template shifts and time slots will be automatically deleted due to CASCADE + await db.run('DELETE FROM shift_plans WHERE id = ?', [id]); + // Time slots, shifts, and scheduled shifts will be automatically deleted due to CASCADE res.status(204).send(); } catch (error) { - console.error('Error deleting template:', error); + console.error('Error deleting shift plan:', error); res.status(500).json({ error: 'Internal server error' }); } }; -// Helper function to get template by ID -async function getTemplateById(templateId: string): Promise { - const template = await db.get(` - SELECT st.*, u.name as created_by_name - FROM shift_templates st - LEFT JOIN users u ON st.created_by = u.id - WHERE st.id = ? - `, [templateId]); +// Helper function to get plan by ID +async function getShiftPlanById(planId: string): Promise { + const plan = await db.get(` + SELECT sp.*, e.name as created_by_name + FROM shift_plans sp + LEFT JOIN employees e ON sp.created_by = e.id + WHERE sp.id = ? + `, [planId]); - if (!template) { + if (!plan) { return null; } - // Lade Schicht-Slots - const shiftSlots = await db.all(` - SELECT ts.*, tts.name as time_slot_name, tts.start_time as time_slot_start, tts.end_time as time_slot_end - FROM template_shifts ts - LEFT JOIN template_time_slots tts ON ts.time_slot_id = tts.id - WHERE ts.template_id = ? - ORDER BY ts.day_of_week, tts.start_time - `, [templateId]); - // Lade Zeit-Slots const timeSlots = await db.all(` - SELECT * FROM template_time_slots - WHERE template_id = ? + SELECT * FROM time_slots + WHERE plan_id = ? ORDER BY start_time - `, [templateId]); + `, [planId]); + + // Lade Schichten + const shifts = await db.all(` + SELECT s.*, ts.name as time_slot_name, ts.start_time, ts.end_time + FROM shifts s + LEFT JOIN time_slots ts ON s.time_slot_id = ts.id + WHERE s.plan_id = ? + ORDER BY s.day_of_week, ts.start_time + `, [planId]); return { - ...template, - shifts: shiftSlots.map(slot => ({ - id: slot.id, - dayOfWeek: slot.day_of_week, - timeSlot: { - id: slot.time_slot_id, - name: slot.time_slot_name, - startTime: slot.time_slot_start, - endTime: slot.time_slot_end - }, - requiredEmployees: slot.required_employees, - color: slot.color - })), + ...plan, + isTemplate: plan.is_template === 1, + startDate: plan.start_date, + endDate: plan.end_date, + createdBy: plan.created_by, + createdAt: plan.created_at, timeSlots: timeSlots.map(slot => ({ id: slot.id, + planId: slot.plan_id, name: slot.name, startTime: slot.start_time, endTime: slot.end_time, description: slot.description + })), + shifts: shifts.map(shift => ({ + id: shift.id, + planId: shift.plan_id, + timeSlotId: shift.time_slot_id, + dayOfWeek: shift.day_of_week, + requiredEmployees: shift.required_employees, + color: shift.color, + timeSlot: { + id: shift.time_slot_id, + name: shift.time_slot_name, + startTime: shift.start_time, + endTime: shift.end_time + } })) }; +} + +// Helper function to generate scheduled shifts from template +async function generateScheduledShifts(planId: string, startDate: string, endDate: string): Promise { + try { + console.log(`🔄 Generiere geplante Schichten für Plan ${planId} von ${startDate} bis ${endDate}`); + + // Get plan with shifts and time slots + const plan = await getShiftPlanById(planId); + if (!plan) { + throw new Error('Plan not found'); + } + + const start = new Date(startDate); + const end = new Date(endDate); + + // Generate scheduled shifts for each day in the date range + for (let date = new Date(start); date <= end; date.setDate(date.getDate() + 1)) { + const dayOfWeek = date.getDay() === 0 ? 7 : date.getDay(); // Convert to 1-7 (Mon-Sun) + + // Find shifts for this day of week + const shiftsForDay = plan.shifts.filter((shift: any) => shift.dayOfWeek === dayOfWeek); + + for (const shift of shiftsForDay) { + const scheduledShiftId = uuidv4(); + + await db.run( + `INSERT INTO scheduled_shifts (id, plan_id, date, time_slot_id, required_employees, assigned_employees) + VALUES (?, ?, ?, ?, ?, ?)`, + [ + scheduledShiftId, + planId, + date.toISOString().split('T')[0], // YYYY-MM-DD format + shift.timeSlotId, + shift.requiredEmployees, + JSON.stringify([]) + ] + ); + } + } + + console.log(`✅ Geplante Schichten generiert für Plan ${planId}`); + + } catch (error) { + console.error('❌ Fehler beim Generieren der geplanten Schichten:', error); + throw error; + } } \ No newline at end of file diff --git a/backend/src/models/ShiftPlan.ts b/backend/src/models/ShiftPlan.ts index 5c8a4c4..51d5d5d 100644 --- a/backend/src/models/ShiftPlan.ts +++ b/backend/src/models/ShiftPlan.ts @@ -24,6 +24,7 @@ export interface TimeSlot { } export interface Shift { + timeSlot: any; id: string; planId: string; timeSlotId: string; @@ -76,6 +77,7 @@ export interface UpdateShiftPlanRequest { description?: string; startDate?: string; endDate?: string; + isTemplate?: boolean; status?: 'draft' | 'published' | 'archived' | 'template'; timeSlots?: Omit[]; shifts?: Omit[]; @@ -99,121 +101,6 @@ export interface UpdateAvailabilityRequest { availabilities: Omit[]; } -// Default time slots for ZEBRA (specific workplace) -export const DEFAULT_ZEBRA_TIME_SLOTS: Omit[] = [ - { - name: 'Vormittag', - startTime: '08:00', - endTime: '12:00', - description: 'Vormittagsschicht' - }, - { - name: 'Nachmittag', - startTime: '11:30', - endTime: '15:30', - description: 'Nachmittagsschicht' - }, -]; - -// Default time slots for general use -export const DEFAULT_TIME_SLOTS: Omit[] = [ - { - name: 'Vormittag', - startTime: '08:00', - endTime: '12:00', - description: 'Vormittagsschicht' - }, - { - name: 'Nachmittag', - startTime: '11:30', - endTime: '15:30', - description: 'Nachmittagsschicht' - }, - { - name: 'Abend', - startTime: '14:00', - endTime: '18:00', - description: 'Abendschicht' - }, -]; - -// Helper functions -export function validateRequiredEmployees(shift: Shift | ScheduledShift): string[] { - const errors: string[] = []; - - if (shift.requiredEmployees < 1) { - errors.push('Required employees must be at least 1'); - } - - if (shift.requiredEmployees > 10) { - errors.push('Required employees cannot exceed 10'); - } - - return errors; -} - - -export function isTemplate(plan: ShiftPlan): boolean { - return plan.isTemplate || plan.status === 'template'; -} - -export function hasDateRange(plan: ShiftPlan): boolean { - return !isTemplate(plan) && !!plan.startDate && !!plan.endDate; -} - -export function validatePlanDates(plan: ShiftPlan): string[] { - const errors: string[] = []; - - if (!isTemplate(plan)) { - if (!plan.startDate) errors.push('Start date is required for non-template plans'); - if (!plan.endDate) errors.push('End date is required for non-template plans'); - if (plan.startDate && plan.endDate && plan.startDate > plan.endDate) { - errors.push('Start date must be before end date'); - } - } - - return errors; -} - -// Type guards -export function isScheduledShift(shift: Shift | ScheduledShift): shift is ScheduledShift { - return 'date' in shift; -} - -// Template presets for quick setup -// Default shifts for ZEBRA standard week template with variable required employees -export const DEFAULT_ZEBRA_SHIFTS: Omit[] = [ - // Monday-Thursday: Morning + Afternoon - ...Array.from({ length: 4 }, (_, i) => i + 1).flatMap(day => [ - { timeSlotId: 'morning', dayOfWeek: day, requiredEmployees: 2, color: '#3498db' }, - { timeSlotId: 'afternoon', dayOfWeek: day, requiredEmployees: 2, color: '#e74c3c' } - ]), - // Friday: Morning only - { timeSlotId: 'morning', dayOfWeek: 5, requiredEmployees: 2, color: '#3498db' } -]; - -// Default shifts for general standard week template with variable required employees -export const DEFAULT_SHIFTS: Omit[] = [ - // Monday-Friday: Morning + Afternoon + Evening - ...Array.from({ length: 5 }, (_, i) => i + 1).flatMap(day => [ - { timeSlotId: 'morning', dayOfWeek: day, requiredEmployees: 2, color: '#3498db' }, - { timeSlotId: 'afternoon', dayOfWeek: day, requiredEmployees: 2, color: '#e74c3c' }, - { timeSlotId: 'evening', dayOfWeek: day, requiredEmployees: 1, color: '#2ecc71' } // Only 1 for evening - ]) -]; - -// Template presets for quick creation -export const TEMPLATE_PRESETS = { - ZEBRA_STANDARD: { - name: 'ZEBRA Standardwoche', - description: 'Standard Vorlage für ZEBRA: Mo-Do Vormittag+Nachmittag, Fr nur Vormittag', - timeSlots: DEFAULT_ZEBRA_TIME_SLOTS, - shifts: DEFAULT_ZEBRA_SHIFTS - }, - GENERAL_STANDARD: { - name: 'Standard Wochenplan', - description: 'Standard Vorlage: Mo-Fr Vormittag+Nachmittag+Abend', - timeSlots: DEFAULT_TIME_SLOTS, - shifts: DEFAULT_SHIFTS - } -} as const; \ No newline at end of file +export interface UpdateRequiredEmployeesRequest { + requiredEmployees: number; +} \ No newline at end of file diff --git a/backend/src/routes/shiftTemplates.ts b/backend/src/routes/shiftTemplates.ts index dcf390d..b6a2c9b 100644 --- a/backend/src/routes/shiftTemplates.ts +++ b/backend/src/routes/shiftTemplates.ts @@ -2,20 +2,21 @@ import express from 'express'; import { authMiddleware } from '../middleware/auth.js'; import { - getTemplates, - getTemplate, - createTemplate, - updateTemplate, - deleteTemplate + getTemplates, + getShiftPlans, + getShiftPlan, + createFromTemplate, + updateShiftPlan, + deleteShiftPlan } from '../controllers/shiftTemplateController.js'; const router = express.Router(); router.use(authMiddleware); router.get('/', getTemplates); -router.get('/:id', getTemplate); -router.post('/', createTemplate); -router.put('/:id', updateTemplate); -router.delete('/:id', deleteTemplate); +router.get('/:id', getShiftPlan); +router.post('/', createFromTemplate); +router.put('/:id', updateShiftPlan); +router.delete('/:id', deleteShiftPlan); export default router; \ No newline at end of file diff --git a/backend/src/scripts/checkTemplates.ts b/backend/src/scripts/checkTemplates.ts index 56c217a..02d0173 100644 --- a/backend/src/scripts/checkTemplates.ts +++ b/backend/src/scripts/checkTemplates.ts @@ -1,12 +1,12 @@ import { db } from '../services/databaseService.js'; -import { TemplateShift } from '../models/ShiftTemplate.js'; +import { ShiftPlan } from '../models/ShiftPlan.js'; async function checkTemplates() { try { - const templates = await db.all( - `SELECT st.*, u.name as created_by_name - FROM shift_templates st - LEFT JOIN users u ON st.created_by = u.id` + const templates = await db.all( + `SELECT sp.*, u.name as created_by_name + FROM shift_plans sp + LEFT JOIN users u ON sp.created_by = u.id` ); console.log('Templates:', templates); diff --git a/backend/src/scripts/setupDefaultTemplate.ts b/backend/src/scripts/setupDefaultTemplate.ts index 9551ee2..8e9697d 100644 --- a/backend/src/scripts/setupDefaultTemplate.ts +++ b/backend/src/scripts/setupDefaultTemplate.ts @@ -1,7 +1,7 @@ // backend/src/scripts/setupDefaultTemplate.ts import { v4 as uuidv4 } from 'uuid'; import { db } from '../services/databaseService.js'; -import { DEFAULT_TIME_SLOTS, TemplateShift } from '../models/ShiftTemplate.js'; +import { DEFAULT_ZEBRA_TIME_SLOTS, TemplateShift } from '../models/ShiftPlan.js'; interface AdminUser { id: string;