ein runtime error

This commit is contained in:
2025-10-16 00:17:30 +02:00
parent d60a6d9fae
commit 7b2256c0ed
10 changed files with 119 additions and 68 deletions

View File

@@ -60,7 +60,7 @@ CREATE TABLE IF NOT EXISTS scheduled_shifts (
required_employees INTEGER NOT NULL CHECK (required_employees >= 1 AND required_employees <= 10) DEFAULT 2,
assigned_employees TEXT DEFAULT '[]', -- JSON array of employee IDs
FOREIGN KEY (plan_id) REFERENCES shift_plans(id) ON DELETE CASCADE,
FOREIGN KEY (time_slot_id) REFERENCES time_slots(id),
FOREIGN KEY (time_slot_id) REFERENCES time_slots(id) ON DELETE CASCADE,
UNIQUE(plan_id, date, time_slot_id)
);
@@ -89,7 +89,7 @@ CREATE TABLE IF NOT EXISTS employee_availability (
notes TEXT,
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE,
FOREIGN KEY (plan_id) REFERENCES shift_plans(id) ON DELETE CASCADE,
FOREIGN KEY (time_slot_id) REFERENCES time_slots(id),
FOREIGN KEY (time_slot_id) REFERENCES time_slots(id) ON DELETE CASCADE,
UNIQUE(employee_id, plan_id, day_of_week, time_slot_id)
);

View File

@@ -78,7 +78,7 @@ export function calculateTotalRequiredEmployees(plan: ShiftPlan): number {
return plan.shifts.reduce((total, shift) => total + shift.requiredEmployees, 0);
}
export function getScheduledShiftByDateAndTime(
/*export function getScheduledShiftByDateAndTime(
plan: ShiftPlan,
date: string,
timeSlotId: string
@@ -86,7 +86,7 @@ export function getScheduledShiftByDateAndTime(
return plan.scheduledShifts?.find(shift =>
shift.date === date && shift.timeSlotId === timeSlotId
);
}
}*/
export function canPublishPlan(plan: ShiftPlan): { canPublish: boolean; errors: string[] } {
const errors: string[] = [];

View File

@@ -0,0 +1,30 @@
// backend/src/routes/scheduledShifts.ts
import express from 'express';
import { authMiddleware, requireRole } from '../middleware/auth.js';
import {
regenerateScheduledShifts,
generateScheduledShiftsForPlan,
getScheduledShift,
getScheduledShiftsFromPlan,
updateScheduledShift
} from '../controllers/shiftPlanController.js';
const router = express.Router();
router.use(authMiddleware);
router.post('/:id/generate-shifts', requireRole(['admin', 'instandhalter']), generateScheduledShiftsForPlan);
router.post('/:id/regenerate-shifts', requireRole(['admin', 'instandhalter']), regenerateScheduledShifts);
// GET all scheduled shifts for a plan
router.get('/plan/:planId', requireRole(['admin']), getScheduledShiftsFromPlan);
// GET specific scheduled shift
router.get('/:id', requireRole(['admin']), getScheduledShift);
// UPDATE scheduled shift
router.put('/:id', requireRole(['admin']), updateScheduledShift);
export default router;

View File

@@ -7,15 +7,8 @@ import {
createShiftPlan,
updateShiftPlan,
deleteShiftPlan,
//getTemplates,
//createFromTemplate,
createFromPreset,
revertToDraft,
regenerateScheduledShifts,
generateScheduledShiftsForPlan,
getScheduledShift,
getScheduledShiftsFromPlan,
updateScheduledShift
} from '../controllers/shiftPlanController.js';
const router = express.Router();
@@ -51,21 +44,4 @@ router.delete('/:id', requireRole(['admin', 'instandhalter']), deleteShiftPlan);
// PUT revert published plan to draft
router.put('/:id/revert-to-draft', requireRole(['admin', 'instandhalter']), revertToDraft);
// SCHEDULED SHIFTS
router.post('/:id/generate-shifts', requireRole(['admin', 'instandhalter']), generateScheduledShiftsForPlan);
router.post('/:id/regenerate-shifts', requireRole(['admin', 'instandhalter']), regenerateScheduledShifts);
// GET all scheduled shifts for a plan (for debugging)
router.get('/plan/:planId', requireRole(['admin']), getScheduledShiftsFromPlan);
// GET specific scheduled shift
router.get('/:id', requireRole(['admin']), getScheduledShift);
// UPDATE scheduled shift
router.put('/:id', requireRole(['admin']), updateScheduledShift);
export default router;

View File

@@ -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 scheduledShifts from './routes/scheduledShifts.js';
const app = express();
const PORT = 3002;
@@ -21,6 +22,7 @@ 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', scheduledShifts);
// Error handling middleware should come after routes
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {

View File

@@ -11,7 +11,6 @@ export interface ShiftPlan {
createdAt: string;
timeSlots: TimeSlot[];
shifts: Shift[];
scheduledShifts?: ScheduledShift[]; // Only for non-template plans with dates
}
export interface TimeSlot {

View File

@@ -1,4 +1,5 @@
// backend/src/models/helpers/shiftPlanHelpers.ts
import { shiftAssignmentService } from '../../services/shiftAssignmentService.js';
import { ShiftPlan, Shift, ScheduledShift, TimeSlot } from '../ShiftPlan.js';
// Validation helpers
@@ -78,15 +79,16 @@ export function calculateTotalRequiredEmployees(plan: ShiftPlan): number {
return plan.shifts.reduce((total, shift) => total + shift.requiredEmployees, 0);
}
export function getScheduledShiftByDateAndTime(
/*export async function getScheduledShiftByDateAndTime(
plan: ShiftPlan,
date: string,
timeSlotId: string
): ScheduledShift | undefined {
return plan.scheduledShifts?.find(shift =>
shift.date === date && shift.timeSlotId === timeSlotId
): Promise<ScheduledShift | undefined> {
const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(plan.id);
return scheduledShifts.find(
shift => shift.date === date && shift.timeSlotId === timeSlotId
);
}
}*/
export function canPublishPlan(plan: ShiftPlan): { canPublish: boolean; errors: string[] } {
const errors: string[] = [];

View File

@@ -4,8 +4,9 @@ import { Link } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
import { shiftPlanService } from '../../services/shiftPlanService';
import { employeeService } from '../../services/employeeService';
import { ShiftPlan } from '../../models/ShiftPlan';
import { ShiftPlan, ScheduledShift } from '../../models/ShiftPlan';
import { Employee } from '../../models/Employee';
import { shiftAssignmentService } from '../../services/shiftAssignmentService';
interface DashboardData {
currentShiftPlan: ShiftPlan | null;
@@ -29,6 +30,7 @@ interface DashboardData {
const Dashboard: React.FC = () => {
const { user, hasRole } = useAuth();
const [loading, setLoading] = useState(true);
const [currentPlanShifts, setCurrentPlanShifts] = useState<ScheduledShift[]>([]);
const [data, setData] = useState<DashboardData>({
currentShiftPlan: null,
upcomingShifts: [],
@@ -53,13 +55,25 @@ const Dashboard: React.FC = () => {
const [shiftPlans, employees] = await Promise.all([
shiftPlanService.getShiftPlans(),
employeeService.getEmployees()
employeeService.getEmployees(),
]);
// Find current shift plan
const today = new Date().toISOString().split('T')[0];
const currentPlan = findCurrentShiftPlan(shiftPlans, today);
// Load shifts for current plan
if (currentPlan) {
const shifts = await shiftAssignmentService.getScheduledShiftsForPlan(currentPlan.id);
setCurrentPlanShifts(shifts);
} else {
setCurrentPlanShifts([]);
}
console.log('📊 Loaded data:', {
plans: shiftPlans.length,
employees: employees.length,
plansWithShifts: shiftPlans.filter(p => p.scheduledShifts && p.scheduledShifts.length > 0).length
currentPlanShifts
});
// Debug: Log plan details
@@ -68,14 +82,14 @@ const Dashboard: React.FC = () => {
status: plan.status,
startDate: plan.startDate,
endDate: plan.endDate,
scheduledShifts: plan.scheduledShifts?.length || 0,
//scheduledShifts: plan.scheduledShifts?.length || 0,
isTemplate: plan.isTemplate
});
});
// Find current shift plan (published and current date within range)
const today = new Date().toISOString().split('T')[0];
const currentPlan = findCurrentShiftPlan(shiftPlans, today);
//const today = new Date().toISOString().split('T')[0];
//const currentPlan = findCurrentShiftPlan(shiftPlans, today);
// Get user's upcoming shifts
const userShifts = await loadUserUpcomingShifts(shiftPlans, today);
@@ -96,7 +110,7 @@ const Dashboard: React.FC = () => {
console.log('✅ Dashboard data loaded:', {
currentPlan: currentPlan?.name,
userShifts: userShifts.length,
//userShifts: userShifts.length,
teamStats,
recentPlans: recentPlans.length
});
@@ -142,13 +156,14 @@ const Dashboard: React.FC = () => {
// Check each plan for user assignments
for (const plan of shiftPlans) {
if (plan.status !== 'published' || !plan.scheduledShifts || plan.scheduledShifts.length === 0) {
const scheduledShifts = (await shiftAssignmentService.getScheduledShiftsForPlan(plan.id));
if (plan.status !== 'published' || scheduledShifts.length === 0) {
continue;
}
console.log(`🔍 Checking plan ${plan.name} for user shifts:`, plan.scheduledShifts.length);
console.log(`🔍 Checking plan ${plan.name} for user shifts:`, scheduledShifts.length);
for (const scheduledShift of plan.scheduledShifts) {
for (const scheduledShift of scheduledShifts) {
// Ensure assignedEmployees is an array
const assignedEmployees = Array.isArray(scheduledShift.assignedEmployees)
? scheduledShift.assignedEmployees
@@ -237,16 +252,19 @@ const Dashboard: React.FC = () => {
return `${start} - ${end}`;
};
const calculatePlanProgress = (plan: ShiftPlan): { covered: number; total: number; percentage: number } => {
if (!plan.scheduledShifts || plan.scheduledShifts.length === 0) {
const calculatePlanProgress = (plan: ShiftPlan, shifts: ScheduledShift[]): {
covered: number; total: number; percentage: number
} => {
if (!plan.id || shifts.length === 0) {
console.log(`📊 Plan ${plan.name} has no scheduled shifts`);
return { covered: 0, total: 0, percentage: 0 };
}
const totalShifts = plan.scheduledShifts.length;
const coveredShifts = plan.scheduledShifts.filter(shift => {
const assigned = Array.isArray(shift.assignedEmployees) ? shift.assignedEmployees : [];
return assigned.length > 0;
const currentDate = new Date();
const totalShifts = shifts.length;
const coveredShifts = shifts.filter(shift => {
const shiftDate = new Date(shift.date);
return currentDate > shiftDate;
}).length;
const percentage = totalShifts > 0 ? Math.round((coveredShifts / totalShifts) * 100) : 0;
@@ -302,7 +320,7 @@ const Dashboard: React.FC = () => {
<div><strong>End Date:</strong> {data.currentShiftPlan.endDate}</div>
<div><strong>Shifts Defined:</strong> {data.currentShiftPlan.shifts?.length || 0}</div>
<div><strong>Time Slots:</strong> {data.currentShiftPlan.timeSlots?.length || 0}</div>
<div><strong>Scheduled Shifts:</strong> {data.currentShiftPlan.scheduledShifts?.length || 0}</div>
<div><strong>Scheduled Shifts:</strong> {data.currentShiftPlan.shifts.length || 0}</div>
{data.currentShiftPlan.shifts && data.currentShiftPlan.shifts.length > 0 && (
<div style={{ marginTop: '10px' }}>
@@ -319,7 +337,9 @@ const Dashboard: React.FC = () => {
);
};
const progress = data.currentShiftPlan ? calculatePlanProgress(data.currentShiftPlan) : { covered: 0, total: 0, percentage: 0 };
const progress = data.currentShiftPlan
? calculatePlanProgress(data.currentShiftPlan, currentPlanShifts)
: { covered: 0, total: 0, percentage: 0 };
return (
<div>

View File

@@ -10,6 +10,7 @@ import { ShiftPlan, TimeSlot } from '../../models/ShiftPlan';
import { Employee, EmployeeAvailability } from '../../models/Employee';
import { useNotification } from '../../contexts/NotificationContext';
import { formatDate, formatTime } from '../../utils/foramatters';
import { isScheduledShift } from '../../models/helpers';
// Local interface extensions (same as AvailabilityManager)
interface ExtendedTimeSlot extends TimeSlot {
@@ -148,7 +149,7 @@ const ShiftPlanView: React.FC = () => {
return date.getDay() === 0 ? 7 : date.getDay();
};
const debugManagerAvailability = () => {
/*const debugManagerAvailability = () => {
if (!shiftPlan || !employees.length || !availabilities.length) return;
const manager = employees.find(emp => emp.role === 'admin');
@@ -196,7 +197,7 @@ const ShiftPlanView: React.FC = () => {
});
}
});
};
};*/
const handlePreviewAssignment = async () => {
if (!shiftPlan) return;
@@ -282,13 +283,15 @@ const ShiftPlanView: React.FC = () => {
console.log('🔄 Starting to publish assignments...');
const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
// Debug: Check if scheduled shifts exist
if (!shiftPlan.scheduledShifts || shiftPlan.scheduledShifts.length === 0) {
if (!scheduledShifts || scheduledShifts.length === 0) {
throw new Error('No scheduled shifts found in the plan');
}
// Update scheduled shifts with assignments
const updatePromises = shiftPlan.scheduledShifts.map(async (scheduledShift) => {
const updatePromises = scheduledShifts.map(async (scheduledShift) => {
const assignedEmployees = assignmentResult.assignments[scheduledShift.id] || [];
console.log(`📝 Updating shift ${scheduledShift.id} with`, assignedEmployees.length, 'employees');
@@ -375,10 +378,11 @@ const ShiftPlanView: React.FC = () => {
message: 'Schichtplan wurde erfolgreich zurück in den Entwurfsstatus gesetzt. Alle Daten wurden neu geladen.'
});
const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
console.log('Scheduled shifts after revert:', {
hasScheduledShifts: !!shiftPlan.scheduledShifts,
count: shiftPlan.scheduledShifts?.length || 0,
firstFew: shiftPlan.scheduledShifts?.slice(0, 3)
hasScheduledShifts: !! scheduledShifts,
count: scheduledShifts.length || 0,
firstFew: scheduledShifts?.slice(0, 3)
});
} catch (error) {
@@ -419,8 +423,15 @@ const ShiftPlanView: React.FC = () => {
};
// Render timetable using the same structure as AvailabilityManager
const renderTimetable = () => {
const renderTimetable = async () => {
const { days, allTimeSlots, timeSlotsByDay } = getTimetableData();
if (!shiftPlan?.id) {
console.warn("Shift plan ID is missing");
return []; // safely exit
}
const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
if (days.length === 0 || allTimeSlots.length === 0) {
return (
@@ -524,9 +535,11 @@ const ShiftPlanView: React.FC = () => {
let assignedEmployees: string[] = [];
let displayText = '';
if (shiftPlan?.status === 'published' && shiftPlan.scheduledShifts) {
if (shiftPlan?.status === 'published' && scheduledShifts) {
// For published plans, use actual assignments from scheduled shifts
const scheduledShift = shiftPlan.scheduledShifts.find(scheduled => {
const scheduledShift = scheduledShifts.find(scheduled => {
const scheduledDayOfWeek = getDayOfWeek(scheduled.date);
return scheduledDayOfWeek === weekday.id &&
scheduled.timeSlotId === timeSlot.id;
@@ -541,7 +554,7 @@ const ShiftPlanView: React.FC = () => {
}
} else if (assignmentResult) {
// For draft with preview, use assignment result
const scheduledShift = shiftPlan?.scheduledShifts?.find(scheduled => {
const scheduledShift = scheduledShifts?.find(scheduled => {
const scheduledDayOfWeek = getDayOfWeek(scheduled.date);
return scheduledDayOfWeek === weekday.id &&
scheduled.timeSlotId === timeSlot.id;
@@ -594,8 +607,9 @@ const ShiftPlanView: React.FC = () => {
if (loading) return <div>Lade Schichtplan...</div>;
if (!shiftPlan) return <div>Schichtplan nicht gefunden</div>;
const availabilityStatus = getAvailabilityStatus();
const { days, allTimeSlots } = getTimetableData();
const availabilityStatus = getAvailabilityStatus();
return (
<div style={{ padding: '20px' }}>

View File

@@ -5,6 +5,7 @@ import { authService } from './authService';
import { scheduleWithManager } from './scheduling/shiftScheduler';
import { transformToSchedulingData } from './scheduling/dataAdapter';
import { AssignmentResult, WeeklyPattern } from './scheduling/types';
import { isScheduledShift } from '../models/helpers';
const API_BASE_URL = 'http://localhost:3002/api/scheduled-shifts';
@@ -130,7 +131,7 @@ export class ShiftAssignmentService {
console.log('🔄 Starting enhanced scheduling algorithm...');
// Get defined shifts for the first week
const definedShifts = this.getDefinedShifts(shiftPlan);
const definedShifts = await this.getDefinedShifts(shiftPlan);
const firstWeekShifts = this.getFirstWeekShifts(definedShifts);
console.log('📊 First week analysis:', {
@@ -399,8 +400,15 @@ export class ShiftAssignmentService {
// ========== EXISTING HELPER METHODS ==========
private static getDefinedShifts(shiftPlan: ShiftPlan): ScheduledShift[] {
if (!shiftPlan.scheduledShifts) return [];
static async getDefinedShifts(shiftPlan: ShiftPlan): Promise<ScheduledShift[]> {
let scheduledShifts: ScheduledShift[] = [];
try {
scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
} catch (err) {
console.error("Failed to load scheduled shifts:", err);
return [];
}
if (scheduledShifts.length) return [];
const definedShiftPatterns = new Set(
shiftPlan.shifts.map(shift =>
@@ -408,7 +416,7 @@ export class ShiftAssignmentService {
)
);
const definedShifts = shiftPlan.scheduledShifts.filter(scheduledShift => {
const definedShifts = scheduledShifts.filter(scheduledShift => {
const dayOfWeek = this.getDayOfWeek(scheduledShift.date);
const pattern = `${dayOfWeek}-${scheduledShift.timeSlotId}`;
return definedShiftPatterns.has(pattern);