diff --git a/backend/src/controllers/employeeController.ts b/backend/src/controllers/employeeController.ts index 69ee828..a6631ad 100644 --- a/backend/src/controllers/employeeController.ts +++ b/backend/src/controllers/employeeController.ts @@ -10,7 +10,10 @@ export const getEmployees = async (req: AuthRequest, res: Response): Promise(` + const { includeInactive } = req.query; + const includeInactiveFlag = includeInactive === 'true'; + + let query = ` SELECT id, email, name, role, is_active as isActive, employee_type as employeeType, @@ -19,9 +22,15 @@ export const getEmployees = async (req: AuthRequest, res: Response): Promise(query); console.log('✅ Employees found:', employees.length); res.json(employees); diff --git a/backend/src/routes/scheduledShifts.ts b/backend/src/routes/scheduledShifts.ts new file mode 100644 index 0000000..f24ab75 --- /dev/null +++ b/backend/src/routes/scheduledShifts.ts @@ -0,0 +1,117 @@ +// backend/src/routes/scheduledShifts.ts - COMPLETE REWRITE +import express from 'express'; +import { authMiddleware, requireRole } from '../middleware/auth.js'; +import { db } from '../services/databaseService.js'; + +const router = express.Router(); + +router.use(authMiddleware); + +// GET all scheduled shifts for a plan (for debugging) +router.get('/plan/:planId', async (req, res) => { + 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' }); + } +}); + +// GET specific scheduled shift +router.get('/:id', async (req, res) => { + try { + const { id } = req.params; + + const shift = await db.get( + 'SELECT * FROM scheduled_shifts WHERE id = ?', + [id] + ) as any; + + if (!shift) { + return 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 }); + } +}); + +// UPDATE scheduled shift +router.put('/:id', requireRole(['admin', 'instandhalter']), async (req, res) => { + try { + const { id } = req.params; + const { assignedEmployees } = req.body; + + console.log('🔄 Updating scheduled shift:', { + id, + assignedEmployees, + body: req.body + }); + + if (!Array.isArray(assignedEmployees)) { + return 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); + return 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 }); + } +}); + +export default router; \ No newline at end of file diff --git a/backend/src/server.ts b/backend/src/server.ts index 3a57f36..5af4814 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -8,6 +8,7 @@ import authRoutes from './routes/auth.js'; import employeeRoutes from './routes/employees.js'; import shiftPlanRoutes from './routes/shiftPlans.js'; import setupRoutes from './routes/setup.js'; +import scheduledShiftsRouter from './routes/scheduledShifts.js'; const app = express(); const PORT = 3002; @@ -21,6 +22,18 @@ app.use('/api/setup', setupRoutes); app.use('/api/auth', authRoutes); app.use('/api/employees', employeeRoutes); app.use('/api/shift-plans', shiftPlanRoutes); +app.use('/api/scheduled-shifts', scheduledShiftsRouter); + +// Error handling middleware should come after routes +app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => { + console.error('Unhandled error:', err); + res.status(500).json({ error: 'Internal server error' }); +}); + +// 404 handler for API routes +app.use('/api/*', (req, res) => { + res.status(404).json({ error: 'API endpoint not found' }); +}); // Health route app.get('/api/health', (req: any, res: any) => { diff --git a/frontend/src/pages/Employees/EmployeeManagement.tsx b/frontend/src/pages/Employees/EmployeeManagement.tsx index 11788ca..cd53f70 100644 --- a/frontend/src/pages/Employees/EmployeeManagement.tsx +++ b/frontend/src/pages/Employees/EmployeeManagement.tsx @@ -28,7 +28,7 @@ const EmployeeManagement: React.FC = () => { console.log('🔄 Loading employees...'); // Add cache-busting parameter to prevent browser caching - const data = await employeeService.getEmployees(); + const data = await employeeService.getEmployees(true); console.log('✅ Employees loaded:', data); setEmployees(data); diff --git a/frontend/src/pages/Employees/components/EmployeeList.tsx b/frontend/src/pages/Employees/components/EmployeeList.tsx index 16e5e3f..e2fc475 100644 --- a/frontend/src/pages/Employees/components/EmployeeList.tsx +++ b/frontend/src/pages/Employees/components/EmployeeList.tsx @@ -321,7 +321,7 @@ const EmployeeList: React.FC = ({ flexWrap: 'wrap' }}> {/* Verfügbarkeit Button */} - {(employee.role === 'admin' || employee.role === 'maintenance') && ( + {(employee.role === 'admin' || employee.role === 'maintenance' || employee.role === 'user') && ( - )} - + marginTop: '5px', + overflow: 'hidden' + }}> +
+
+ + + {hasRole(['admin', 'instandhalter']) && ( +
+ + + {!canPublish() && ( +
+ {getAvailabilityStatus().percentage === 100 + ? 'Bereit zur Veröffentlichung' + : `${getAvailabilityStatus().total - getAvailabilityStatus().completed} Mitarbeiter müssen noch Verfügbarkeit eintragen`} +
+ )} +
+ )} + - + )} + + {/* Assignment Preview Modal */} + {showAssignmentPreview && assignmentResult && ( +
+
+

Wochenmuster-Zuordnung

+ + {/* Show weekly pattern info */} + {assignmentResult.pattern && ( +
+

Wochenmuster erstellt

+

+ Der Algorithmus hat ein Muster für {assignmentResult.pattern.weekShifts.length} Schichten in der ersten Woche erstellt + und dieses für alle {Math.ceil(Object.keys(assignmentResult.assignments).length / assignmentResult.pattern.weekShifts.length)} Wochen im Plan wiederholt. +

+
+ Wochenmuster-Statistik: +
- Schichten pro Woche: {assignmentResult.pattern.weekShifts.length}
+
- Zuweisungen pro Woche: {Object.values(assignmentResult.pattern.assignments).flat().length}
+
- Gesamtzuweisungen: {Object.values(assignmentResult.assignments).flat().length}
+
+
+ )} + + {assignmentResult.violations.length > 0 && ( +
+

Warnungen:

+
    + {assignmentResult.violations.map((violation, index) => ( +
  • + {violation} +
  • + ))} +
+
+ )} + +
+

Zusammenfassung:

+

+ {assignmentResult.success + ? '✅ Alle Schichten können zugeordnet werden!' + : '⚠️ Es gibt Probleme bei der Zuordnung die manuell behoben werden müssen.'} +

+
+ +
+ + + + + +
+
+
+ )}
{ padding: '20px', boxShadow: '0 2px 4px rgba(0,0,0,0.1)' }}> -
-
Zeitraum: {formatDate(shiftPlan.startDate)} - {formatDate(shiftPlan.endDate)}
-
Status: - {shiftPlan.status === 'published' ? 'Veröffentlicht' : 'Entwurf'} -
-
- {/* Timetable */}

Schichtplan

diff --git a/frontend/src/services/employeeService.ts b/frontend/src/services/employeeService.ts index ce9ac38..925ca65 100644 --- a/frontend/src/services/employeeService.ts +++ b/frontend/src/services/employeeService.ts @@ -12,13 +12,13 @@ const getAuthHeaders = () => { }; export class EmployeeService { - async getEmployees(): Promise { + async getEmployees(includeInactive: boolean = false): Promise { console.log('🔄 Fetching employees from API...'); const token = localStorage.getItem('token'); console.log('🔑 Token exists:', !!token); - const response = await fetch(`${API_BASE_URL}/employees`, { + const response = await fetch(`${API_BASE_URL}/employees?includeInactive=${includeInactive}`, { headers: getAuthHeaders(), }); diff --git a/frontend/src/services/shiftAssignmentService.ts b/frontend/src/services/shiftAssignmentService.ts new file mode 100644 index 0000000..ec62866 --- /dev/null +++ b/frontend/src/services/shiftAssignmentService.ts @@ -0,0 +1,406 @@ +// frontend/src/services/shiftAssignmentService.ts - WEEKLY PATTERN VERSION +import { ShiftPlan, ScheduledShift } from '../models/ShiftPlan'; +import { Employee, EmployeeAvailability } from '../models/Employee'; + +export interface AssignmentResult { + assignments: { [shiftId: string]: string[] }; + violations: string[]; + success: boolean; + pattern: WeeklyPattern; +} + +export interface WeeklyPattern { + weekShifts: ScheduledShift[]; + assignments: { [shiftId: string]: string[] }; + weekNumber: number; +} + +export class ShiftAssignmentService { + + static async assignShifts( + shiftPlan: ShiftPlan, + employees: Employee[], + availabilities: EmployeeAvailability[], + constraints: any = {} + ): Promise { + + console.log('🔄 Starting weekly pattern assignment...'); + + // Get defined shifts + const definedShifts = this.getDefinedShifts(shiftPlan); + const activeEmployees = employees.filter(emp => emp.isActive); + + console.log('📊 Plan analysis:'); + console.log('- Total shifts in plan:', definedShifts.length); + console.log('- Active employees:', activeEmployees.length); + + // STRATEGY: Create weekly pattern and repeat + const weeklyPattern = await this.createWeeklyPattern( + definedShifts, + activeEmployees, + availabilities, + constraints.enforceNoTraineeAlone + ); + + console.log('🎯 Weekly pattern created for', weeklyPattern.weekShifts.length, 'shifts'); + + // Apply pattern to all weeks in the plan + const assignments = this.applyWeeklyPattern(definedShifts, weeklyPattern); + + const violations = this.findViolations(assignments, activeEmployees, definedShifts, constraints.enforceNoTraineeAlone); + + console.log('📊 Weekly pattern assignment completed:'); + console.log('- Pattern shifts:', weeklyPattern.weekShifts.length); + console.log('- Total plan shifts:', definedShifts.length); + console.log('- Assignments made:', Object.values(assignments).flat().length); + console.log('- Violations:', violations.length); + + return { + assignments, + violations, + success: violations.length === 0, + pattern: weeklyPattern + }; + } + + private static async createWeeklyPattern( + definedShifts: ScheduledShift[], + employees: Employee[], + availabilities: EmployeeAvailability[], + enforceNoTraineeAlone: boolean + ): Promise { + + // Get first week of shifts (7 days from the start) + const firstWeekShifts = this.getFirstWeekShifts(definedShifts); + + console.log('📅 First week analysis:'); + console.log('- Shifts in first week:', firstWeekShifts.length); + + // Fix: Use Array.from instead of spread operator with Set + const uniqueDays = Array.from(new Set(firstWeekShifts.map(s => this.getDayOfWeek(s.date)))).sort(); + console.log('- Days covered:', uniqueDays); + + // Build availability map + const availabilityMap = this.buildAvailabilityMap(availabilities); + const employeeMap = new Map(employees.map(emp => [emp.id, emp])); + + // Initialize assignment state for first week + const weeklyAssignments: { [shiftId: string]: string[] } = {}; + const employeeAssignmentCount: { [employeeId: string]: number } = {}; + + employees.forEach(emp => { + employeeAssignmentCount[emp.id] = 0; + }); + + firstWeekShifts.forEach(shift => { + weeklyAssignments[shift.id] = []; + }); + + // Sort employees by capacity and experience + const sortedEmployees = [...employees].sort((a, b) => { + const aCapacity = this.getMaxAssignments(a); + const bCapacity = this.getMaxAssignments(b); + const aIsManager = a.role === 'admin'; + const bIsManager = b.role === 'admin'; + + if (aIsManager !== bIsManager) return aIsManager ? -1 : 1; + if (a.employeeType !== b.employeeType) { + if (a.employeeType === 'experienced') return -1; + if (b.employeeType === 'experienced') return 1; + } + return bCapacity - aCapacity; + }); + + // Sort shifts by priority (those with fewer available employees first) + const sortedShifts = [...firstWeekShifts].sort((a, b) => { + const aAvailable = this.countAvailableEmployees(a, employees, availabilityMap); + const bAvailable = this.countAvailableEmployees(b, employees, availabilityMap); + return aAvailable - bAvailable; + }); + + // Assign employees to first week shifts + for (const employee of sortedEmployees) { + const maxAssignments = this.getMaxAssignments(employee); + + // Get available shifts for this employee in first week + const availableShifts = sortedShifts + .filter(shift => { + if (employeeAssignmentCount[employee.id] >= maxAssignments) return false; + if (weeklyAssignments[shift.id].length >= shift.requiredEmployees) return false; + + const dayOfWeek = this.getDayOfWeek(shift.date); + const shiftKey = `${dayOfWeek}-${shift.timeSlotId}`; + + return this.isEmployeeAvailable(employee, shiftKey, availabilityMap) && + this.isAssignmentCompatible(employee, weeklyAssignments[shift.id], employeeMap, enforceNoTraineeAlone); + }) + .sort((a, b) => { + // Prefer shifts with fewer current assignments + return weeklyAssignments[a.id].length - weeklyAssignments[b.id].length; + }); + + // Assign to available shifts until capacity is reached + for (const shift of availableShifts) { + if (employeeAssignmentCount[employee.id] >= maxAssignments) break; + + weeklyAssignments[shift.id].push(employee.id); + employeeAssignmentCount[employee.id]++; + console.log(`✅ Assigned ${employee.name} to weekly pattern shift`); + + if (employeeAssignmentCount[employee.id] >= maxAssignments) break; + } + } + + // Ensure all shifts in first week have at least one employee + for (const shift of firstWeekShifts) { + if (weeklyAssignments[shift.id].length === 0) { + const dayOfWeek = this.getDayOfWeek(shift.date); + const shiftKey = `${dayOfWeek}-${shift.timeSlotId}`; + + const availableEmployees = employees + .filter(emp => + this.isEmployeeAvailable(emp, shiftKey, availabilityMap) && + this.canAssignByContract(emp, employeeAssignmentCount) + ) + .sort((a, b) => { + const aPref = availabilityMap.get(a.id)?.get(shiftKey) || 3; + const bPref = availabilityMap.get(b.id)?.get(shiftKey) || 3; + const aCount = employeeAssignmentCount[a.id] || 0; + const bCount = employeeAssignmentCount[b.id] || 0; + + if (aPref !== bPref) return aPref - bPref; + return aCount - bCount; + }); + + if (availableEmployees.length > 0) { + const bestCandidate = availableEmployees[0]; + weeklyAssignments[shift.id].push(bestCandidate.id); + employeeAssignmentCount[bestCandidate.id]++; + console.log(`🆘 Emergency assigned ${bestCandidate.name} to weekly pattern`); + } + } + } + + return { + weekShifts: firstWeekShifts, + assignments: weeklyAssignments, + weekNumber: 1 + }; + } + + private static applyWeeklyPattern( + allShifts: ScheduledShift[], + weeklyPattern: WeeklyPattern + ): { [shiftId: string]: string[] } { + + const assignments: { [shiftId: string]: string[] } = {}; + + // Group all shifts by week + const shiftsByWeek = this.groupShiftsByWeek(allShifts); + + console.log('📅 Applying weekly pattern to', Object.keys(shiftsByWeek).length, 'weeks'); + + // For each week, apply the pattern from week 1 + Object.entries(shiftsByWeek).forEach(([weekKey, weekShifts]) => { + const weekNumber = parseInt(weekKey); + + weekShifts.forEach(shift => { + // Find the corresponding shift in the weekly pattern + const patternShift = this.findMatchingPatternShift(shift, weeklyPattern.weekShifts); + + if (patternShift) { + // Use the same assignment as the pattern shift + assignments[shift.id] = [...weeklyPattern.assignments[patternShift.id]]; + } else { + // No matching pattern shift, leave empty + assignments[shift.id] = []; + } + }); + }); + + return assignments; + } + + private static groupShiftsByWeek(shifts: ScheduledShift[]): { [weekNumber: string]: ScheduledShift[] } { + const weeks: { [weekNumber: string]: ScheduledShift[] } = {}; + + shifts.forEach(shift => { + const weekNumber = this.getWeekNumber(shift.date); + if (!weeks[weekNumber]) { + weeks[weekNumber] = []; + } + weeks[weekNumber].push(shift); + }); + + return weeks; + } + + private static getFirstWeekShifts(shifts: ScheduledShift[]): ScheduledShift[] { + if (shifts.length === 0) return []; + + // Sort by date and get the first 7 days + const sortedShifts = [...shifts].sort((a, b) => a.date.localeCompare(b.date)); + const firstDate = new Date(sortedShifts[0].date); + const firstWeekEnd = new Date(firstDate); + firstWeekEnd.setDate(firstWeekEnd.getDate() + 6); // 7 days total + + return sortedShifts.filter(shift => { + const shiftDate = new Date(shift.date); + return shiftDate >= firstDate && shiftDate <= firstWeekEnd; + }); + } + + private static findMatchingPatternShift( + shift: ScheduledShift, + patternShifts: ScheduledShift[] + ): ScheduledShift | null { + const shiftDayOfWeek = this.getDayOfWeek(shift.date); + const shiftTimeSlot = shift.timeSlotId; + + return patternShifts.find(patternShift => + this.getDayOfWeek(patternShift.date) === shiftDayOfWeek && + patternShift.timeSlotId === shiftTimeSlot + ) || null; + } + + private static getWeekNumber(dateString: string): number { + const date = new Date(dateString); + const firstDayOfYear = new Date(date.getFullYear(), 0, 1); + const pastDaysOfYear = (date.getTime() - firstDayOfYear.getTime()) / 86400000; + return Math.ceil((pastDaysOfYear + firstDayOfYear.getDay() + 1) / 7); + } + + // ========== EXISTING HELPER METHODS ========== + + private static getDefinedShifts(shiftPlan: ShiftPlan): ScheduledShift[] { + if (!shiftPlan.scheduledShifts) return []; + + const definedShiftPatterns = new Set( + shiftPlan.shifts.map(shift => + `${shift.dayOfWeek}-${shift.timeSlotId}` + ) + ); + + const definedShifts = shiftPlan.scheduledShifts.filter(scheduledShift => { + const dayOfWeek = this.getDayOfWeek(scheduledShift.date); + const pattern = `${dayOfWeek}-${scheduledShift.timeSlotId}`; + return definedShiftPatterns.has(pattern); + }); + + return definedShifts; + } + + private static countAvailableEmployees( + scheduledShift: ScheduledShift, + employees: Employee[], + availabilityMap: Map> + ): number { + const dayOfWeek = this.getDayOfWeek(scheduledShift.date); + const shiftKey = `${dayOfWeek}-${scheduledShift.timeSlotId}`; + + return employees.filter(emp => + this.isEmployeeAvailable(emp, shiftKey, availabilityMap) + ).length; + } + + private static isAssignmentCompatible( + candidate: Employee, + currentAssignments: string[], + employeeMap: Map, + enforceNoTraineeAlone: boolean + ): boolean { + if (!enforceNoTraineeAlone || currentAssignments.length === 0) return true; + + const currentEmployees = currentAssignments.map(id => employeeMap.get(id)).filter(Boolean) as Employee[]; + + if (candidate.employeeType === 'trainee') { + const hasExperiencedOrChef = currentEmployees.some(emp => + emp.employeeType === 'experienced' || emp.role === 'admin' + ); + return hasExperiencedOrChef; + } + + return true; + } + + private static buildAvailabilityMap(availabilities: EmployeeAvailability[]): Map> { + const map = new Map>(); + + availabilities.forEach(avail => { + if (!map.has(avail.employeeId)) { + map.set(avail.employeeId, new Map()); + } + + const shiftKey = `${avail.dayOfWeek}-${avail.timeSlotId}`; + map.get(avail.employeeId)!.set(shiftKey, avail.preferenceLevel); + }); + + return map; + } + + private static isEmployeeAvailable( + employee: Employee, + shiftKey: string, + availabilityMap: Map> + ): boolean { + if (!employee.isActive) return false; + + const employeeAvailability = availabilityMap.get(employee.id); + if (!employeeAvailability) return false; + + const preference = employeeAvailability.get(shiftKey); + return preference !== undefined && preference !== 3; + } + + private static canAssignByContract( + employee: Employee, + assignmentCount: { [employeeId: string]: number } + ): boolean { + const currentCount = assignmentCount[employee.id] || 0; + const maxAssignments = this.getMaxAssignments(employee); + return currentCount < maxAssignments; + } + + private static getMaxAssignments(employee: Employee): number { + switch (employee.contractType) { + case 'small': return 1; + case 'large': return 2; + default: return 999; + } + } + + private static findViolations( + assignments: { [shiftId: string]: string[] }, + employees: Employee[], + definedShifts: ScheduledShift[], + enforceNoTraineeAlone: boolean + ): string[] { + const violations: string[] = []; + const employeeMap = new Map(employees.map(emp => [emp.id, emp])); + + definedShifts.forEach(scheduledShift => { + const assignedEmployeeIds = assignments[scheduledShift.id] || []; + + if (assignedEmployeeIds.length === 0) { + violations.push(`Shift has no assigned employees`); + return; + } + + const assignedEmployees = assignedEmployeeIds.map(id => employeeMap.get(id)).filter(Boolean) as Employee[]; + + if (enforceNoTraineeAlone && assignedEmployees.length === 1) { + const soloEmployee = assignedEmployees[0]; + if (soloEmployee.employeeType === 'trainee') { + violations.push(`Trainee ${soloEmployee.name} is working alone`); + } + } + }); + + return violations; + } + + private static getDayOfWeek(dateString: string): number { + const date = new Date(dateString); + return date.getDay() === 0 ? 7 : date.getDay(); + } +} \ No newline at end of file diff --git a/frontend/src/services/shiftPlanService.ts b/frontend/src/services/shiftPlanService.ts index b915a2c..ee62431 100644 --- a/frontend/src/services/shiftPlanService.ts +++ b/frontend/src/services/shiftPlanService.ts @@ -1,6 +1,6 @@ // frontend/src/services/shiftPlanService.ts import { authService } from './authService'; -import { ShiftPlan, CreateShiftPlanRequest, Shift, CreateShiftFromTemplateRequest } from '../models/ShiftPlan'; +import { ShiftPlan, CreateShiftPlanRequest, ScheduledShift, CreateShiftFromTemplateRequest } from '../models/ShiftPlan'; import { TEMPLATE_PRESETS } from '../models/defaults/shiftPlanDefaults'; const API_BASE = 'http://localhost:3002/api/shift-plans'; @@ -186,4 +186,105 @@ export const shiftPlanService = { description: preset.description })); }, + + async updateScheduledShift(id: string, updates: { assignedEmployees: string[] }): Promise { + try { + console.log('🔄 Updating scheduled shift via API:', { id, updates }); + + const response = await fetch(`/api/scheduled-shifts/${id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('token')}` + }, + body: JSON.stringify(updates) + }); + + // First, check if we got any response + if (!response.ok) { + // Try to get error message from response + const responseText = await response.text(); + console.error('❌ Server response:', responseText); + + let errorMessage = `HTTP ${response.status}: ${response.statusText}`; + + // Try to parse as JSON if possible + try { + const errorData = JSON.parse(responseText); + errorMessage = errorData.error || errorMessage; + } catch (e) { + // If not JSON, use the text as is + errorMessage = responseText || errorMessage; + } + + throw new Error(errorMessage); + } + + // Try to parse successful response + const responseText = await response.text(); + let result; + try { + result = responseText ? JSON.parse(responseText) : {}; + } catch (e) { + console.warn('⚠️ Response was not JSON, but request succeeded'); + result = { message: 'Update successful' }; + } + + console.log('✅ Scheduled shift updated successfully:', result); + + } catch (error) { + console.error('❌ Error updating scheduled shift:', error); + throw error; + } + }, + + async getScheduledShift(id: string): Promise { + try { + const response = await fetch(`/api/scheduled-shifts/${id}`, { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}` + } + }); + + if (!response.ok) { + const responseText = await response.text(); + let errorMessage = `HTTP ${response.status}: ${response.statusText}`; + + try { + const errorData = JSON.parse(responseText); + errorMessage = errorData.error || errorMessage; + } catch (e) { + errorMessage = responseText || errorMessage; + } + + throw new Error(errorMessage); + } + + const responseText = await response.text(); + return responseText ? JSON.parse(responseText) : {}; + } catch (error) { + console.error('Error fetching scheduled shift:', error); + throw error; + } + }, + + // New method to get all scheduled shifts for a plan + async getScheduledShiftsForPlan(planId: string): Promise { + try { + const response = await fetch(`/api/scheduled-shifts/plan/${planId}`, { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}` + } + }); + + if (!response.ok) { + throw new Error(`Failed to fetch scheduled shifts: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error('Error fetching scheduled shifts for plan:', error); + throw error; + } + } }; \ No newline at end of file