mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-12-01 06:55:45 +01:00
fixed most of dependencies errors
This commit is contained in:
@@ -2,148 +2,331 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { db } from '../services/databaseService.js';
|
import { db } from '../services/databaseService.js';
|
||||||
|
import {
|
||||||
|
CreateShiftPlanRequest,
|
||||||
|
UpdateShiftPlanRequest,
|
||||||
|
ShiftPlan
|
||||||
|
} from '../models/ShiftPlan.js';
|
||||||
import { AuthRequest } from '../middleware/auth.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<void> => {
|
export const getShiftPlans = async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const userId = req.user?.userId;
|
console.log('🔍 Lade Schichtpläne...');
|
||||||
const userRole = req.user?.role;
|
|
||||||
|
|
||||||
let query = `
|
const shiftPlans = await db.all<any>(`
|
||||||
SELECT sp.*, u.name as created_by_name
|
SELECT sp.*, e.name as created_by_name
|
||||||
FROM shift_plans sp
|
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
|
console.log(`✅ ${shiftPlans.length} Schichtpläne gefunden:`, shiftPlans.map(p => p.name));
|
||||||
if (userRole === 'user') {
|
|
||||||
query += ` WHERE sp.status = 'published'`;
|
// 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<any>(`
|
||||||
|
SELECT * FROM time_slots
|
||||||
|
WHERE plan_id = ?
|
||||||
|
ORDER BY start_time
|
||||||
|
`, [plan.id]);
|
||||||
|
|
||||||
|
// Lade Schichten
|
||||||
|
const shifts = await db.all<any>(`
|
||||||
|
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]);
|
||||||
|
|
||||||
|
// Lade geplante Schichten (nur für nicht-Template Pläne)
|
||||||
|
let scheduledShifts = [];
|
||||||
|
if (!plan.is_template) {
|
||||||
|
scheduledShifts = await db.all<any>(`
|
||||||
|
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]);
|
||||||
}
|
}
|
||||||
|
|
||||||
query += ` ORDER BY sp.created_at DESC`;
|
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
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const shiftPlans = await db.all<ShiftPlan>(query);
|
res.json(plansWithDetails);
|
||||||
|
|
||||||
res.json(shiftPlans);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching shift plans:', error);
|
console.error('Error fetching shift plans:', error);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getShiftPlan = async (req: AuthRequest, res: Response): Promise<void> => {
|
export const getShiftPlan = async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const userId = req.user?.userId;
|
|
||||||
const userRole = req.user?.role;
|
|
||||||
|
|
||||||
let query = `
|
const plan = await db.get<any>(`
|
||||||
SELECT sp.*, u.name as created_by_name
|
SELECT sp.*, e.name as created_by_name
|
||||||
FROM shift_plans sp
|
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 = ?
|
WHERE sp.id = ?
|
||||||
`;
|
`, [id]);
|
||||||
|
|
||||||
// Regular users can only see published plans
|
if (!plan) {
|
||||||
if (userRole === 'user') {
|
|
||||||
query += ` AND sp.status = 'published'`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shiftPlan = await db.get<ShiftPlan>(query, [id]);
|
|
||||||
|
|
||||||
if (!shiftPlan) {
|
|
||||||
res.status(404).json({ error: 'Shift plan not found' });
|
res.status(404).json({ error: 'Shift plan not found' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load assigned shifts
|
// Lade Zeit-Slots
|
||||||
const assignedShifts = await db.all<any>(`
|
const timeSlots = await db.all<any>(`
|
||||||
SELECT * FROM assigned_shifts
|
SELECT * FROM time_slots
|
||||||
WHERE shift_plan_id = ?
|
WHERE plan_id = ?
|
||||||
ORDER BY date, start_time
|
ORDER BY start_time
|
||||||
`, [id]);
|
`, [id]);
|
||||||
|
|
||||||
const shiftPlanWithShifts = {
|
// Lade Schichten
|
||||||
...shiftPlan,
|
const shifts = await db.all<any>(`
|
||||||
shifts: assignedShifts.map(shift => ({
|
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<any>(`
|
||||||
|
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,
|
id: shift.id,
|
||||||
date: shift.date,
|
planId: shift.plan_id,
|
||||||
name: shift.name,
|
timeSlotId: shift.time_slot_id,
|
||||||
startTime: shift.start_time,
|
dayOfWeek: shift.day_of_week,
|
||||||
endTime: shift.end_time,
|
|
||||||
requiredEmployees: shift.required_employees,
|
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) {
|
} catch (error) {
|
||||||
console.error('Error fetching shift plan:', error);
|
console.error('Error fetching shift plan:', error);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createShiftPlan = async (req: AuthRequest, res: Response): Promise<void> => {
|
export const createDefaultTemplate = async (userId: string): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
const { name, startDate, endDate, templateId }: CreateShiftPlanRequest = req.body;
|
const planId = uuidv4();
|
||||||
const userId = req.user?.userId;
|
|
||||||
|
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<void> => {
|
||||||
|
try {
|
||||||
|
const { name, description, startDate, endDate, isTemplate, timeSlots, shifts }: CreateShiftPlanRequest = req.body;
|
||||||
|
const userId = (req as AuthRequest).user?.userId;
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
res.status(401).json({ error: 'Unauthorized' });
|
res.status(401).json({ error: 'Unauthorized' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!name || !startDate || !endDate) {
|
const planId = uuidv4();
|
||||||
res.status(400).json({ error: 'Name, start date and end date are required' });
|
const status = isTemplate ? 'template' : 'draft';
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shiftPlanId = uuidv4();
|
|
||||||
|
|
||||||
|
// Start transaction
|
||||||
await db.run('BEGIN TRANSACTION');
|
await db.run('BEGIN TRANSACTION');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create shift plan
|
// Insert plan
|
||||||
await db.run(
|
await db.run(
|
||||||
`INSERT INTO shift_plans (id, name, start_date, end_date, template_id, status, created_by)
|
`INSERT INTO shift_plans (id, name, description, start_date, end_date, is_template, status, created_by)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[shiftPlanId, name, startDate, endDate, templateId, 'draft', userId]
|
[planId, name, description, startDate, endDate, isTemplate ? 1 : 0, status, userId]
|
||||||
);
|
);
|
||||||
|
|
||||||
// If template is provided, generate shifts from template
|
// Create mapping for time slot IDs
|
||||||
if (templateId) {
|
const timeSlotIdMap = new Map<string, string>();
|
||||||
await generateShiftsFromTemplate(shiftPlanId, templateId, startDate, endDate);
|
|
||||||
|
// 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');
|
await db.run('COMMIT');
|
||||||
|
|
||||||
// Return created shift plan
|
// Return created plan
|
||||||
const createdPlan = await db.get<ShiftPlan>(`
|
const createdPlan = await getShiftPlanById(planId);
|
||||||
SELECT sp.*, u.name as created_by_name
|
res.status(201).json(createdPlan);
|
||||||
FROM shift_plans sp
|
|
||||||
LEFT JOIN users u ON sp.created_by = u.id
|
|
||||||
WHERE sp.id = ?
|
|
||||||
`, [shiftPlanId]);
|
|
||||||
|
|
||||||
const assignedShifts = await db.all<any>(`
|
|
||||||
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 || '[]')
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await db.run('ROLLBACK');
|
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<void> => {
|
export const createFromPreset = async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { presetName, name, startDate, endDate, isTemplate } = req.body;
|
||||||
const { name, status, shifts } = req.body;
|
const userId = (req as AuthRequest).user?.userId;
|
||||||
const userId = req.user?.userId;
|
|
||||||
|
|
||||||
// Check if shift plan exists
|
if (!userId) {
|
||||||
const existingPlan: any = await db.get('SELECT * FROM shift_plans WHERE id = ?', [id]);
|
res.status(401).json({ error: 'Unauthorized' });
|
||||||
if (!existingPlan) {
|
|
||||||
res.status(404).json({ error: 'Shift plan not found' });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check permissions (only admin/instandhalter or creator can update)
|
if (!TEMPLATE_PRESETS[presetName as keyof typeof TEMPLATE_PRESETS]) {
|
||||||
if (existingPlan.created_by !== userId && !['admin', 'instandhalter'].includes(req.user?.role || '')) {
|
res.status(400).json({ error: 'Invalid preset name' });
|
||||||
res.status(403).json({ error: 'Insufficient permissions' });
|
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<string, string>();
|
||||||
|
|
||||||
|
// 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<void> => {
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.run('BEGIN TRANSACTION');
|
await db.run('BEGIN TRANSACTION');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Update shift plan
|
// Update plan
|
||||||
if (name !== undefined || status !== undefined) {
|
if (name !== undefined || description !== undefined || startDate !== undefined || endDate !== undefined || status !== undefined) {
|
||||||
await db.run(
|
await db.run(
|
||||||
`UPDATE shift_plans
|
`UPDATE shift_plans
|
||||||
SET name = COALESCE(?, name),
|
SET name = COALESCE(?, name),
|
||||||
|
description = COALESCE(?, description),
|
||||||
|
start_date = COALESCE(?, start_date),
|
||||||
|
end_date = COALESCE(?, end_date),
|
||||||
status = COALESCE(?, status)
|
status = COALESCE(?, status)
|
||||||
WHERE id = ?`,
|
WHERE id = ?`,
|
||||||
[name, status, id]
|
[name, description, startDate, endDate, status, id]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update shifts if provided
|
// If updating time slots, replace all time slots
|
||||||
if (shifts) {
|
if (timeSlots) {
|
||||||
for (const shift of shifts) {
|
// 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(
|
await db.run(
|
||||||
`UPDATE assigned_shifts
|
`INSERT INTO time_slots (id, plan_id, name, start_time, end_time, description)
|
||||||
SET required_employees = ?,
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
assigned_employees = ?
|
[timeSlotId, id, timeSlot.name, timeSlot.startTime, timeSlot.endTime, timeSlot.description || '']
|
||||||
WHERE id = ? AND shift_plan_id = ?`,
|
);
|
||||||
[shift.requiredEmployees, JSON.stringify(shift.assignedEmployees || []), shift.id, id]
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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');
|
await db.run('COMMIT');
|
||||||
|
|
||||||
// Return updated shift plan
|
// Return updated plan
|
||||||
const updatedPlan = await db.get<ShiftPlan>(`
|
const updatedPlan = await getShiftPlanById(id);
|
||||||
SELECT sp.*, u.name as created_by_name
|
res.json(updatedPlan);
|
||||||
FROM shift_plans sp
|
|
||||||
LEFT JOIN users u ON sp.created_by = u.id
|
|
||||||
WHERE sp.id = ?
|
|
||||||
`, [id]);
|
|
||||||
|
|
||||||
const assignedShifts = await db.all<any>(`
|
|
||||||
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 || '[]')
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await db.run('ROLLBACK');
|
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<void> => {
|
export const deleteShiftPlan = async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const userId = req.user?.userId;
|
|
||||||
|
|
||||||
// Check if shift plan exists
|
// Check if plan exists
|
||||||
const existingPlan: any = await db.get('SELECT * FROM shift_plans WHERE id = ?', [id]);
|
const existingPlan = await db.get('SELECT * FROM shift_plans WHERE id = ?', [id]);
|
||||||
if (!existingPlan) {
|
if (!existingPlan) {
|
||||||
res.status(404).json({ error: 'Shift plan not found' });
|
res.status(404).json({ error: 'Shift plan not found' });
|
||||||
return;
|
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]);
|
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();
|
res.status(204).send();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -269,61 +533,131 @@ export const deleteShiftPlan = async (req: AuthRequest, res: Response): Promise<
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to generate shifts from template
|
// Helper function to get plan by ID
|
||||||
async function generateShiftsFromTemplate(shiftPlanId: string, templateId: string, startDate: string, endDate: string): Promise<void> {
|
async function getShiftPlanById(planId: string): Promise<any> {
|
||||||
|
const plan = await db.get<any>(`
|
||||||
|
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 (!plan) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lade Zeit-Slots
|
||||||
|
const timeSlots = await db.all<any>(`
|
||||||
|
SELECT * FROM time_slots
|
||||||
|
WHERE plan_id = ?
|
||||||
|
ORDER BY start_time
|
||||||
|
`, [planId]);
|
||||||
|
|
||||||
|
// Lade Schichten
|
||||||
|
const shifts = await db.all<any>(`
|
||||||
|
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<any>(`
|
||||||
|
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<void> {
|
||||||
try {
|
try {
|
||||||
console.log(`🔄 Generiere Schichten von Vorlage ${templateId} für Plan ${shiftPlanId}`);
|
console.log(`🔄 Generiere geplante Schichten für Plan ${planId} von ${startDate} bis ${endDate}`);
|
||||||
|
|
||||||
// Get template shifts with time slot information
|
// Get plan with shifts and time slots
|
||||||
const templateShifts = await db.all<any>(`
|
const plan = await getShiftPlanById(planId);
|
||||||
SELECT ts.*, tts.name as time_slot_name, tts.start_time, tts.end_time
|
if (!plan) {
|
||||||
FROM template_shifts ts
|
throw new Error('Plan not found');
|
||||||
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]);
|
|
||||||
|
|
||||||
console.log(`📋 Gefundene Template-Schichten: ${templateShifts.length}`);
|
|
||||||
|
|
||||||
const start = new Date(startDate);
|
const start = new Date(startDate);
|
||||||
const end = new Date(endDate);
|
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)) {
|
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(); // Convert to 1-7 (Mon-Sun)
|
||||||
const dayOfWeek = date.getDay() === 0 ? 7 : date.getDay();
|
|
||||||
|
|
||||||
// Find template shifts for this day of week
|
// Find shifts for this day of week
|
||||||
const shiftsForDay = templateShifts.filter(shift => shift.day_of_week === dayOfWeek);
|
const shiftsForDay = plan.shifts.filter((shift: any) => shift.dayOfWeek === dayOfWeek);
|
||||||
|
|
||||||
// Only create shifts if there are template shifts defined for this weekday
|
for (const shift of shiftsForDay) {
|
||||||
if (shiftsForDay.length > 0) {
|
const scheduledShiftId = uuidv4();
|
||||||
for (const templateShift of shiftsForDay) {
|
|
||||||
const shiftId = uuidv4();
|
|
||||||
|
|
||||||
await db.run(
|
await db.run(
|
||||||
`INSERT INTO assigned_shifts (id, shift_plan_id, date, name, start_time, end_time, required_employees, assigned_employees)
|
`INSERT INTO scheduled_shifts (id, plan_id, date, time_slot_id, required_employees, assigned_employees)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
[
|
[
|
||||||
shiftId,
|
scheduledShiftId,
|
||||||
shiftPlanId,
|
planId,
|
||||||
date.toISOString().split('T')[0], // YYYY-MM-DD format
|
date.toISOString().split('T')[0], // YYYY-MM-DD format
|
||||||
templateShift.time_slot_name || 'Schicht',
|
shift.timeSlotId,
|
||||||
templateShift.start_time,
|
shift.requiredEmployees,
|
||||||
templateShift.end_time,
|
|
||||||
templateShift.required_employees,
|
|
||||||
JSON.stringify([])
|
JSON.stringify([])
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
console.log(`✅ ${shiftsForDay.length} Schichten erstellt für ${date.toISOString().split('T')[0]}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`🎉 Schicht-Generierung abgeschlossen für Plan ${shiftPlanId}`);
|
console.log(`✅ Geplante Schichten generiert für Plan ${planId}`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Fehler beim Generieren der Schichten:', error);
|
console.error('❌ Fehler beim Generieren der geplanten Schichten:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,194 +1,290 @@
|
|||||||
// backend/src/controllers/shiftTemplateController.ts
|
// backend/src/controllers/shiftPlanController.ts
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { db } from '../services/databaseService.js';
|
import { db } from '../services/databaseService.js';
|
||||||
import {
|
|
||||||
CreateShiftTemplateRequest,
|
|
||||||
UpdateShiftTemplateRequest
|
|
||||||
} from '../models/ShiftTemplate.js';
|
|
||||||
import { AuthRequest } from '../middleware/auth.js';
|
import { AuthRequest } from '../middleware/auth.js';
|
||||||
|
import {
|
||||||
|
CreateShiftPlanRequest,
|
||||||
|
UpdateShiftPlanRequest,
|
||||||
|
CreateShiftFromTemplateRequest
|
||||||
|
} from '../models/ShiftPlan.js';
|
||||||
|
|
||||||
export const getTemplates = async (req: Request, res: Response): Promise<void> => {
|
// Get all shift plans (including templates)
|
||||||
|
export const getShiftPlans = async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
console.log('🔍 Lade Vorlagen...');
|
console.log('🔍 Lade Schichtpläne...');
|
||||||
|
|
||||||
const templates = await db.all<any>(`
|
const shiftPlans = await db.all<any>(`
|
||||||
SELECT st.*, u.name as created_by_name
|
SELECT sp.*, e.name as created_by_name
|
||||||
FROM shift_templates st
|
FROM shift_plans sp
|
||||||
LEFT JOIN users u ON st.created_by = u.id
|
LEFT JOIN employees e ON sp.created_by = e.id
|
||||||
ORDER BY st.created_at DESC
|
ORDER BY sp.created_at DESC
|
||||||
`);
|
`);
|
||||||
|
|
||||||
console.log(`✅ ${templates.length} Vorlagen gefunden:`, templates.map(t => t.name));
|
console.log(`✅ ${shiftPlans.length} Schichtpläne gefunden:`, shiftPlans.map(p => p.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<any>(`
|
|
||||||
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]);
|
|
||||||
|
|
||||||
|
// Für jeden Plan die Schichten und Zeit-Slots laden
|
||||||
|
const plansWithDetails = await Promise.all(
|
||||||
|
shiftPlans.map(async (plan) => {
|
||||||
// Lade Zeit-Slots
|
// Lade Zeit-Slots
|
||||||
const timeSlots = await db.all<any>(`
|
const timeSlots = await db.all<any>(`
|
||||||
SELECT * FROM template_time_slots
|
SELECT * FROM time_slots
|
||||||
WHERE template_id = ?
|
WHERE plan_id = ?
|
||||||
ORDER BY start_time
|
ORDER BY start_time
|
||||||
`, [template.id]);
|
`, [plan.id]);
|
||||||
|
|
||||||
|
// Lade Schichten
|
||||||
|
const shifts = await db.all<any>(`
|
||||||
|
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 {
|
return {
|
||||||
...template,
|
...plan,
|
||||||
shifts: shiftSlots.map(slot => ({
|
isTemplate: plan.is_template === 1,
|
||||||
id: slot.id,
|
startDate: plan.start_date,
|
||||||
dayOfWeek: slot.day_of_week,
|
endDate: plan.end_date,
|
||||||
timeSlot: {
|
createdBy: plan.created_by,
|
||||||
id: slot.time_slot_id,
|
createdAt: plan.created_at,
|
||||||
name: slot.time_slot_name,
|
|
||||||
startTime: slot.time_slot_start,
|
|
||||||
endTime: slot.time_slot_end
|
|
||||||
},
|
|
||||||
requiredEmployees: slot.required_employees,
|
|
||||||
color: slot.color
|
|
||||||
})),
|
|
||||||
timeSlots: timeSlots.map(slot => ({
|
timeSlots: timeSlots.map(slot => ({
|
||||||
id: slot.id,
|
id: slot.id,
|
||||||
|
planId: slot.plan_id,
|
||||||
name: slot.name,
|
name: slot.name,
|
||||||
startTime: slot.start_time,
|
startTime: slot.start_time,
|
||||||
endTime: slot.end_time,
|
endTime: slot.end_time,
|
||||||
description: slot.description
|
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<void> => {
|
||||||
|
try {
|
||||||
|
console.log('🔍 Lade Vorlagen...');
|
||||||
|
|
||||||
|
const templates = await db.all<any>(`
|
||||||
|
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<any>(`
|
||||||
|
SELECT * FROM time_slots
|
||||||
|
WHERE plan_id = ?
|
||||||
|
ORDER BY start_time
|
||||||
|
`, [template.id]);
|
||||||
|
|
||||||
|
// Lade Schichten
|
||||||
|
const shifts = await db.all<any>(`
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error('Error fetching templates:', error);
|
console.error('Error fetching templates:', error);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTemplate = async (req: Request, res: Response): Promise<void> => {
|
export const getShiftPlan = async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
const template = await db.get<any>(`
|
const plan = await db.get<any>(`
|
||||||
SELECT st.*, u.name as created_by_name
|
SELECT sp.*, e.name as created_by_name
|
||||||
FROM shift_templates st
|
FROM shift_plans sp
|
||||||
LEFT JOIN users u ON st.created_by = u.id
|
LEFT JOIN employees e ON sp.created_by = e.id
|
||||||
WHERE st.id = ?
|
WHERE sp.id = ?
|
||||||
`, [id]);
|
`, [id]);
|
||||||
|
|
||||||
if (!template) {
|
if (!plan) {
|
||||||
res.status(404).json({ error: 'Template not found' });
|
res.status(404).json({ error: 'Shift plan not found' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lade Schicht-Slots
|
|
||||||
const shiftSlots = await db.all<any>(`
|
|
||||||
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
|
// Lade Zeit-Slots
|
||||||
const timeSlots = await db.all<any>(`
|
const timeSlots = await db.all<any>(`
|
||||||
SELECT * FROM template_time_slots
|
SELECT * FROM time_slots
|
||||||
WHERE template_id = ?
|
WHERE plan_id = ?
|
||||||
ORDER BY start_time
|
ORDER BY start_time
|
||||||
`, [id]);
|
`, [id]);
|
||||||
|
|
||||||
const templateWithData = {
|
// Lade Schichten
|
||||||
...template,
|
const shifts = await db.all<any>(`
|
||||||
shifts: shiftSlots.map(slot => ({
|
SELECT s.*, ts.name as time_slot_name, ts.start_time, ts.end_time
|
||||||
id: slot.id,
|
FROM shifts s
|
||||||
dayOfWeek: slot.day_of_week,
|
LEFT JOIN time_slots ts ON s.time_slot_id = ts.id
|
||||||
timeSlot: {
|
WHERE s.plan_id = ?
|
||||||
id: slot.time_slot_id,
|
ORDER BY s.day_of_week, ts.start_time
|
||||||
name: slot.time_slot_name,
|
`, [id]);
|
||||||
startTime: slot.time_slot_start,
|
|
||||||
endTime: slot.time_slot_end
|
const planWithData = {
|
||||||
},
|
...plan,
|
||||||
requiredEmployees: slot.required_employees,
|
isTemplate: plan.is_template === 1,
|
||||||
color: slot.color
|
startDate: plan.start_date,
|
||||||
})),
|
endDate: plan.end_date,
|
||||||
|
createdBy: plan.created_by,
|
||||||
|
createdAt: plan.created_at,
|
||||||
timeSlots: timeSlots.map(slot => ({
|
timeSlots: timeSlots.map(slot => ({
|
||||||
id: slot.id,
|
id: slot.id,
|
||||||
|
planId: slot.plan_id,
|
||||||
name: slot.name,
|
name: slot.name,
|
||||||
startTime: slot.start_time,
|
startTime: slot.start_time,
|
||||||
endTime: slot.end_time,
|
endTime: slot.end_time,
|
||||||
description: slot.description
|
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) {
|
} catch (error) {
|
||||||
console.error('Error fetching template:', error);
|
console.error('Error fetching shift plan:', error);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createDefaultTemplate = async (userId: string): Promise<string> => {
|
export const createDefaultTemplate = async (userId: string): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
const templateId = uuidv4();
|
const planId = uuidv4();
|
||||||
|
|
||||||
await db.run('BEGIN TRANSACTION');
|
await db.run('BEGIN TRANSACTION');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Erstelle die Standard-Vorlage
|
// Erstelle den Standard-Plan (als Template)
|
||||||
await db.run(
|
await db.run(
|
||||||
`INSERT INTO shift_templates (id, name, description, is_default, created_by)
|
`INSERT INTO shift_plans (id, name, description, is_template, status, created_by)
|
||||||
VALUES (?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
[templateId, 'Standardwoche', 'Standard Vorlage mit konfigurierten Zeit-Slots', true, userId]
|
[planId, 'Standardwoche', 'Standard Vorlage mit konfigurierten Zeit-Slots', true, 'template', userId]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Füge Zeit-Slots hinzu
|
// Füge Zeit-Slots hinzu
|
||||||
const timeSlots = [
|
const timeSlots = [
|
||||||
{ id: uuidv4(), name: 'Vormittag', startTime: '08:00', endTime: '12:00', description: 'Vormittagsschicht' },
|
{ name: 'Vormittag', startTime: '08:00', endTime: '12:00', description: 'Vormittagsschicht' },
|
||||||
{ id: uuidv4(), name: 'Nachmittag', startTime: '12:00', endTime: '16:00', description: 'Nachmittagsschicht' },
|
{ name: 'Nachmittag', startTime: '11:30', endTime: '15:30', description: 'Nachmittagsschicht' }
|
||||||
{ id: uuidv4(), name: 'Abend', startTime: '16:00', endTime: '20:00', description: 'Abendschicht' }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const slot of timeSlots) {
|
for (const timeSlot of timeSlots) {
|
||||||
|
const timeSlotId = uuidv4();
|
||||||
await db.run(
|
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 (?, ?, ?, ?, ?, ?)`,
|
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<any>(`
|
||||||
|
SELECT * FROM time_slots WHERE plan_id = ? ORDER BY start_time
|
||||||
|
`, [planId]);
|
||||||
|
|
||||||
// Erstelle Schichten für Mo-Do mit Zeit-Slot Referenzen
|
// Erstelle Schichten für Mo-Do mit Zeit-Slot Referenzen
|
||||||
for (let day = 1; day <= 4; day++) {
|
for (let day = 1; day <= 4; day++) {
|
||||||
// Vormittagsschicht
|
// Vormittagsschicht
|
||||||
await db.run(
|
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 (?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
[uuidv4(), templateId, day, timeSlots[0].id, 1, '#3498db']
|
[uuidv4(), planId, day, createdTimeSlots[0].id, 2, '#3498db']
|
||||||
);
|
);
|
||||||
|
|
||||||
// Nachmittagsschicht
|
// Nachmittagsschicht
|
||||||
await db.run(
|
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 (?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
[uuidv4(), templateId, day, timeSlots[1].id, 1, '#e74c3c']
|
[uuidv4(), planId, day, createdTimeSlots[1].id, 2, '#e74c3c']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Freitag nur Vormittagsschicht
|
// Freitag nur Vormittagsschicht
|
||||||
await db.run(
|
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 (?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
[uuidv4(), templateId, 5, timeSlots[0].id, 1, '#3498db']
|
[uuidv4(), planId, 5, createdTimeSlots[0].id, 2, '#3498db']
|
||||||
);
|
);
|
||||||
|
|
||||||
await db.run('COMMIT');
|
await db.run('COMMIT');
|
||||||
return templateId;
|
return planId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await db.run('ROLLBACK');
|
await db.run('ROLLBACK');
|
||||||
throw error;
|
throw error;
|
||||||
@@ -199,9 +295,9 @@ export const createDefaultTemplate = async (userId: string): Promise<string> =>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createTemplate = async (req: Request, res: Response): Promise<void> => {
|
export const createShiftPlan = async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
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;
|
const userId = (req as AuthRequest).user?.userId;
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
@@ -209,58 +305,72 @@ export const createTemplate = async (req: Request, res: Response): Promise<void>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wenn diese Vorlage als Standard markiert werden soll,
|
const planId = uuidv4();
|
||||||
// zuerst alle anderen Vorlagen auf nicht-Standard setzen
|
const status = isTemplate ? 'template' : 'draft';
|
||||||
if (isDefault) {
|
|
||||||
await db.run('UPDATE shift_templates SET is_default = 0');
|
|
||||||
}
|
|
||||||
|
|
||||||
const templateId = uuidv4();
|
|
||||||
|
|
||||||
// Start transaction
|
// Start transaction
|
||||||
await db.run('BEGIN TRANSACTION');
|
await db.run('BEGIN TRANSACTION');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Insert template
|
// Insert plan
|
||||||
await db.run(
|
await db.run(
|
||||||
`INSERT INTO shift_templates (id, name, description, is_default, created_by)
|
`INSERT INTO shift_plans (id, name, description, start_date, end_date, is_template, status, created_by)
|
||||||
VALUES (?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[templateId, name, description, isDefault ? 1 : 0, userId]
|
[planId, name, description, startDate, endDate, isTemplate ? 1 : 0, status, userId]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Insert time slots
|
// Create mapping for time slot IDs
|
||||||
|
const timeSlotIdMap = new Map<string, string>();
|
||||||
|
|
||||||
|
// Insert time slots - always generate new IDs
|
||||||
for (const timeSlot of timeSlots) {
|
for (const timeSlot of timeSlots) {
|
||||||
const timeSlotId = timeSlot.id || uuidv4();
|
const timeSlotId = uuidv4();
|
||||||
await db.run(
|
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 (?, ?, ?, ?, ?, ?)`,
|
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) {
|
for (const shift of shifts) {
|
||||||
const shiftId = uuidv4();
|
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(
|
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 (?, ?, ?, ?, ?, ?)`,
|
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 this is not a template, generate scheduled shifts
|
||||||
if (isDefault) {
|
if (!isTemplate && startDate && endDate) {
|
||||||
await db.run(
|
await generateScheduledShifts(planId, startDate, endDate);
|
||||||
`UPDATE shift_templates SET is_default = 0 WHERE id != ? AND is_default = 1`,
|
|
||||||
[templateId]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.run('COMMIT');
|
await db.run('COMMIT');
|
||||||
|
|
||||||
// Return created template
|
// Return created plan
|
||||||
const createdTemplate = await getTemplateById(templateId);
|
const createdPlan = await getShiftPlanById(planId);
|
||||||
res.status(201).json(createdTemplate);
|
res.status(201).json(createdPlan);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await db.run('ROLLBACK');
|
await db.run('ROLLBACK');
|
||||||
@@ -268,56 +378,144 @@ export const createTemplate = async (req: Request, res: Response): Promise<void>
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating template:', error);
|
console.error('Error creating shift plan:', error);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateTemplate = async (req: Request, res: Response): Promise<void> => {
|
export const createFromTemplate = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
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<any>(`
|
||||||
|
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<void> => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
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
|
// Check if plan exists
|
||||||
const existingTemplate = await db.get('SELECT * FROM shift_templates WHERE id = ?', [id]);
|
const existingPlan = await db.get('SELECT * FROM shift_plans WHERE id = ?', [id]);
|
||||||
if (!existingTemplate) {
|
if (!existingPlan) {
|
||||||
res.status(404).json({ error: 'Template not found' });
|
res.status(404).json({ error: 'Shift plan not found' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.run('BEGIN TRANSACTION');
|
await db.run('BEGIN TRANSACTION');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Wenn diese Vorlage als Standard markiert werden soll,
|
// Update plan
|
||||||
// zuerst alle anderen Vorlagen auf nicht-Standard setzen
|
if (name !== undefined || description !== undefined || startDate !== undefined || endDate !== undefined || status !== undefined) {
|
||||||
if (isDefault) {
|
|
||||||
await db.run('UPDATE shift_templates SET is_default = 0');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update template
|
|
||||||
if (name !== undefined || description !== undefined || isDefault !== undefined) {
|
|
||||||
await db.run(
|
await db.run(
|
||||||
`UPDATE shift_templates
|
`UPDATE shift_plans
|
||||||
SET name = COALESCE(?, name),
|
SET name = COALESCE(?, name),
|
||||||
description = COALESCE(?, description),
|
description = COALESCE(?, description),
|
||||||
is_default = COALESCE(?, is_default)
|
start_date = COALESCE(?, start_date),
|
||||||
|
end_date = COALESCE(?, end_date),
|
||||||
|
status = COALESCE(?, status)
|
||||||
WHERE id = ?`,
|
WHERE id = ?`,
|
||||||
[name, description, isDefault ? 1 : 0, id]
|
[name, description, startDate, endDate, status, id]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If updating time slots, replace all time slots
|
// If updating time slots, replace all time slots
|
||||||
if (timeSlots) {
|
if (timeSlots) {
|
||||||
// Delete existing time slots
|
// 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
|
// Insert new time slots
|
||||||
for (const timeSlot of timeSlots) {
|
for (const timeSlot of timeSlots) {
|
||||||
const timeSlotId = timeSlot.id || uuidv4();
|
const timeSlotId = uuidv4();
|
||||||
await db.run(
|
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 (?, ?, ?, ?, ?, ?)`,
|
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<void>
|
|||||||
// If updating shifts, replace all shifts
|
// If updating shifts, replace all shifts
|
||||||
if (shifts) {
|
if (shifts) {
|
||||||
// Delete existing 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
|
// Insert new shifts
|
||||||
for (const shift of shifts) {
|
for (const shift of shifts) {
|
||||||
const shiftId = uuidv4();
|
const shiftId = uuidv4();
|
||||||
await db.run(
|
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 (?, ?, ?, ?, ?, ?)`,
|
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');
|
await db.run('COMMIT');
|
||||||
|
|
||||||
// Return updated template
|
// Return updated plan
|
||||||
const updatedTemplate = await getTemplateById(id);
|
const updatedPlan = await getShiftPlanById(id);
|
||||||
res.json(updatedTemplate);
|
res.json(updatedPlan);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await db.run('ROLLBACK');
|
await db.run('ROLLBACK');
|
||||||
@@ -358,81 +548,136 @@ export const updateTemplate = async (req: Request, res: Response): Promise<void>
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating template:', error);
|
console.error('Error updating shift plan:', error);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteTemplate = async (req: Request, res: Response): Promise<void> => {
|
export const deleteShiftPlan = async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
// Check if template exists
|
// Check if plan exists
|
||||||
const existingTemplate = await db.get('SELECT * FROM shift_templates WHERE id = ?', [id]);
|
const existingPlan = await db.get('SELECT * FROM shift_plans WHERE id = ?', [id]);
|
||||||
if (!existingTemplate) {
|
if (!existingPlan) {
|
||||||
res.status(404).json({ error: 'Template not found' });
|
res.status(404).json({ error: 'Shift plan not found' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.run('DELETE FROM shift_templates WHERE id = ?', [id]);
|
await db.run('DELETE FROM shift_plans WHERE id = ?', [id]);
|
||||||
// Template shifts and time slots will be automatically deleted due to CASCADE
|
// Time slots, shifts, and scheduled shifts will be automatically deleted due to CASCADE
|
||||||
|
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting template:', error);
|
console.error('Error deleting shift plan:', error);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to get template by ID
|
// Helper function to get plan by ID
|
||||||
async function getTemplateById(templateId: string): Promise<any> {
|
async function getShiftPlanById(planId: string): Promise<any> {
|
||||||
const template = await db.get<any>(`
|
const plan = await db.get<any>(`
|
||||||
SELECT st.*, u.name as created_by_name
|
SELECT sp.*, e.name as created_by_name
|
||||||
FROM shift_templates st
|
FROM shift_plans sp
|
||||||
LEFT JOIN users u ON st.created_by = u.id
|
LEFT JOIN employees e ON sp.created_by = e.id
|
||||||
WHERE st.id = ?
|
WHERE sp.id = ?
|
||||||
`, [templateId]);
|
`, [planId]);
|
||||||
|
|
||||||
if (!template) {
|
if (!plan) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lade Schicht-Slots
|
|
||||||
const shiftSlots = await db.all<any>(`
|
|
||||||
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
|
// Lade Zeit-Slots
|
||||||
const timeSlots = await db.all<any>(`
|
const timeSlots = await db.all<any>(`
|
||||||
SELECT * FROM template_time_slots
|
SELECT * FROM time_slots
|
||||||
WHERE template_id = ?
|
WHERE plan_id = ?
|
||||||
ORDER BY start_time
|
ORDER BY start_time
|
||||||
`, [templateId]);
|
`, [planId]);
|
||||||
|
|
||||||
|
// Lade Schichten
|
||||||
|
const shifts = await db.all<any>(`
|
||||||
|
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 {
|
return {
|
||||||
...template,
|
...plan,
|
||||||
shifts: shiftSlots.map(slot => ({
|
isTemplate: plan.is_template === 1,
|
||||||
id: slot.id,
|
startDate: plan.start_date,
|
||||||
dayOfWeek: slot.day_of_week,
|
endDate: plan.end_date,
|
||||||
timeSlot: {
|
createdBy: plan.created_by,
|
||||||
id: slot.time_slot_id,
|
createdAt: plan.created_at,
|
||||||
name: slot.time_slot_name,
|
|
||||||
startTime: slot.time_slot_start,
|
|
||||||
endTime: slot.time_slot_end
|
|
||||||
},
|
|
||||||
requiredEmployees: slot.required_employees,
|
|
||||||
color: slot.color
|
|
||||||
})),
|
|
||||||
timeSlots: timeSlots.map(slot => ({
|
timeSlots: timeSlots.map(slot => ({
|
||||||
id: slot.id,
|
id: slot.id,
|
||||||
|
planId: slot.plan_id,
|
||||||
name: slot.name,
|
name: slot.name,
|
||||||
startTime: slot.start_time,
|
startTime: slot.start_time,
|
||||||
endTime: slot.end_time,
|
endTime: slot.end_time,
|
||||||
description: slot.description
|
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<void> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ export interface TimeSlot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Shift {
|
export interface Shift {
|
||||||
|
timeSlot: any;
|
||||||
id: string;
|
id: string;
|
||||||
planId: string;
|
planId: string;
|
||||||
timeSlotId: string;
|
timeSlotId: string;
|
||||||
@@ -76,6 +77,7 @@ export interface UpdateShiftPlanRequest {
|
|||||||
description?: string;
|
description?: string;
|
||||||
startDate?: string;
|
startDate?: string;
|
||||||
endDate?: string;
|
endDate?: string;
|
||||||
|
isTemplate?: boolean;
|
||||||
status?: 'draft' | 'published' | 'archived' | 'template';
|
status?: 'draft' | 'published' | 'archived' | 'template';
|
||||||
timeSlots?: Omit<TimeSlot, 'id' | 'planId'>[];
|
timeSlots?: Omit<TimeSlot, 'id' | 'planId'>[];
|
||||||
shifts?: Omit<Shift, 'id' | 'planId'>[];
|
shifts?: Omit<Shift, 'id' | 'planId'>[];
|
||||||
@@ -99,121 +101,6 @@ export interface UpdateAvailabilityRequest {
|
|||||||
availabilities: Omit<EmployeeAvailability, 'id' | 'employeeId'>[];
|
availabilities: Omit<EmployeeAvailability, 'id' | 'employeeId'>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default time slots for ZEBRA (specific workplace)
|
export interface UpdateRequiredEmployeesRequest {
|
||||||
export const DEFAULT_ZEBRA_TIME_SLOTS: Omit<TimeSlot, 'id' | 'planId'>[] = [
|
requiredEmployees: number;
|
||||||
{
|
|
||||||
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<TimeSlot, 'id' | 'planId'>[] = [
|
|
||||||
{
|
|
||||||
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<Shift, 'id' | 'planId'>[] = [
|
|
||||||
// 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<Shift, 'id' | 'planId'>[] = [
|
|
||||||
// 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;
|
|
||||||
@@ -3,19 +3,20 @@ import express from 'express';
|
|||||||
import { authMiddleware } from '../middleware/auth.js';
|
import { authMiddleware } from '../middleware/auth.js';
|
||||||
import {
|
import {
|
||||||
getTemplates,
|
getTemplates,
|
||||||
getTemplate,
|
getShiftPlans,
|
||||||
createTemplate,
|
getShiftPlan,
|
||||||
updateTemplate,
|
createFromTemplate,
|
||||||
deleteTemplate
|
updateShiftPlan,
|
||||||
|
deleteShiftPlan
|
||||||
} from '../controllers/shiftTemplateController.js';
|
} from '../controllers/shiftTemplateController.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.use(authMiddleware);
|
router.use(authMiddleware);
|
||||||
router.get('/', getTemplates);
|
router.get('/', getTemplates);
|
||||||
router.get('/:id', getTemplate);
|
router.get('/:id', getShiftPlan);
|
||||||
router.post('/', createTemplate);
|
router.post('/', createFromTemplate);
|
||||||
router.put('/:id', updateTemplate);
|
router.put('/:id', updateShiftPlan);
|
||||||
router.delete('/:id', deleteTemplate);
|
router.delete('/:id', deleteShiftPlan);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { db } from '../services/databaseService.js';
|
import { db } from '../services/databaseService.js';
|
||||||
import { TemplateShift } from '../models/ShiftTemplate.js';
|
import { ShiftPlan } from '../models/ShiftPlan.js';
|
||||||
|
|
||||||
async function checkTemplates() {
|
async function checkTemplates() {
|
||||||
try {
|
try {
|
||||||
const templates = await db.all<TemplateShift>(
|
const templates = await db.all<ShiftPlan>(
|
||||||
`SELECT st.*, u.name as created_by_name
|
`SELECT sp.*, u.name as created_by_name
|
||||||
FROM shift_templates st
|
FROM shift_plans sp
|
||||||
LEFT JOIN users u ON st.created_by = u.id`
|
LEFT JOIN users u ON sp.created_by = u.id`
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('Templates:', templates);
|
console.log('Templates:', templates);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// backend/src/scripts/setupDefaultTemplate.ts
|
// backend/src/scripts/setupDefaultTemplate.ts
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { db } from '../services/databaseService.js';
|
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 {
|
interface AdminUser {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user