set template shift struc

This commit is contained in:
2025-10-10 23:42:11 +02:00
parent 168f2cfae3
commit 6247461754
21 changed files with 1627 additions and 369 deletions

View File

@@ -308,7 +308,7 @@ async function generateShiftsFromTemplate(shiftPlanId: string, templateId: strin
const start = new Date(startDate);
const end = new Date(endDate);
// Generate shifts for each day in the date range
// Generate shifts ONLY for days that have template shifts defined
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();
@@ -316,6 +316,8 @@ async function generateShiftsFromTemplate(shiftPlanId: string, templateId: strin
// Find template shifts for this day of week
const shiftsForDay = templateShifts.filter(shift => shift.day_of_week === dayOfWeek);
// Only create shifts if there are template shifts defined for this weekday
if (shiftsForDay.length > 0) {
for (const templateShift of shiftsForDay) {
const shiftId = uuidv4();
@@ -336,3 +338,4 @@ async function generateShiftsFromTemplate(shiftPlanId: string, templateId: strin
}
}
}
}

View File

@@ -2,37 +2,63 @@
import { Request, Response } from 'express';
import { v4 as uuidv4 } from 'uuid';
import { db } from '../services/databaseService.js';
import { ShiftTemplate, CreateShiftTemplateRequest, UpdateShiftTemplateRequest } from '../models/ShiftTemplate.js';
import {
ShiftTemplate,
TemplateShiftSlot,
TemplateShiftTimeRange,
CreateShiftTemplateRequest,
UpdateShiftTemplateRequest
} from '../models/ShiftTemplate.js';
import { AuthRequest } from '../middleware/auth.js';
export const getTemplates = async (req: Request, res: Response): Promise<void> => {
try {
const templates = await db.all<ShiftTemplate>(`
const templates = await db.all<any>(`
SELECT st.*, u.name as created_by_name
FROM shift_templates st
LEFT JOIN users u ON st.created_by = u.id
ORDER BY st.created_at DESC
`);
// Für jede Vorlage die Schichten laden
// Für jede Vorlage die Schichten und Zeit-Slots laden
const templatesWithShifts = await Promise.all(
templates.map(async (template) => {
const shifts = await db.all<any>(`
SELECT * FROM template_shifts
// Lade Schicht-Slots
const shiftSlots = await db.all<any>(`
SELECT ts.*, tts.name as time_range_name, tts.start_time as time_range_start, tts.end_time as time_range_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]);
// Lade Zeit-Slots
const timeSlots = await db.all<any>(`
SELECT * FROM template_time_slots
WHERE template_id = ?
ORDER BY day_of_week, start_time
ORDER BY start_time
`, [template.id]);
return {
...template,
shifts: shifts.map(shift => ({
id: shift.id,
dayOfWeek: shift.day_of_week,
name: shift.name,
startTime: shift.start_time,
endTime: shift.end_time,
requiredEmployees: shift.required_employees,
color: shift.color
shifts: shiftSlots.map(slot => ({
id: slot.id,
dayOfWeek: slot.day_of_week,
timeRange: {
id: slot.time_slot_id,
name: slot.time_range_name,
startTime: slot.time_range_start,
endTime: slot.time_range_end
},
requiredEmployees: slot.required_employees,
color: slot.color
})),
timeSlots: timeSlots.map(slot => ({
id: slot.id,
name: slot.name,
startTime: slot.start_time,
endTime: slot.end_time,
description: slot.description
}))
};
})
@@ -49,7 +75,7 @@ export const getTemplate = async (req: Request, res: Response): Promise<void> =>
try {
const { id } = req.params;
const template = await db.get<ShiftTemplate>(`
const template = await db.get<any>(`
SELECT st.*, u.name as created_by_name
FROM shift_templates st
LEFT JOIN users u ON st.created_by = u.id
@@ -61,26 +87,46 @@ export const getTemplate = async (req: Request, res: Response): Promise<void> =>
return;
}
const shifts = await db.all<any>(`
SELECT * FROM template_shifts
WHERE template_id = ?
ORDER BY day_of_week, start_time
// Lade Schicht-Slots
const shiftSlots = await db.all<any>(`
SELECT ts.*, tts.name as time_range_name, tts.start_time as time_range_start, tts.end_time as time_range_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]);
const templateWithShifts = {
// Lade Zeit-Slots
const timeSlots = await db.all<any>(`
SELECT * FROM template_time_slots
WHERE template_id = ?
ORDER BY start_time
`, [id]);
const templateWithData = {
...template,
shifts: shifts.map(shift => ({
id: shift.id,
dayOfWeek: shift.day_of_week,
name: shift.name,
startTime: shift.start_time,
endTime: shift.end_time,
requiredEmployees: shift.required_employees,
color: shift.color
shifts: shiftSlots.map(slot => ({
id: slot.id,
dayOfWeek: slot.day_of_week,
timeRange: {
id: slot.time_slot_id,
name: slot.time_range_name,
startTime: slot.time_range_start,
endTime: slot.time_range_end
},
requiredEmployees: slot.required_employees,
color: slot.color
})),
timeSlots: timeSlots.map(slot => ({
id: slot.id,
name: slot.name,
startTime: slot.start_time,
endTime: slot.end_time,
description: slot.description
}))
};
res.json(templateWithShifts);
res.json(templateWithData);
} catch (error) {
console.error('Error fetching template:', error);
res.status(500).json({ error: 'Internal server error' });
@@ -98,32 +144,46 @@ export const createDefaultTemplate = async (userId: string): Promise<string> =>
await db.run(
`INSERT INTO shift_templates (id, name, description, is_default, created_by)
VALUES (?, ?, ?, ?, ?)`,
[templateId, 'Standardwoche', 'Mo-Do: 2 Schichten, Fr: 1 Schicht', true, userId]
[templateId, 'Standardwoche', 'Standard Vorlage mit konfigurierten Zeit-Slots', true, userId]
);
// Vormittagsschicht Mo-Do
for (let day = 1; day <= 4; day++) {
// Füge Zeit-Slots hinzu
const timeSlots = [
{ id: uuidv4(), name: 'Vormittag', startTime: '08:00', endTime: '12:00', description: 'Vormittagsschicht' },
{ id: uuidv4(), name: 'Nachmittag', startTime: '12:00', endTime: '16:00', description: 'Nachmittagsschicht' },
{ id: uuidv4(), name: 'Abend', startTime: '16:00', endTime: '20:00', description: 'Abendschicht' }
];
for (const slot of timeSlots) {
await db.run(
`INSERT INTO template_shifts (id, template_id, day_of_week, name, start_time, end_time, required_employees)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[uuidv4(), templateId, day, 'Vormittagsschicht', '08:00', '12:00', 1]
`INSERT INTO template_time_slots (id, template_id, name, start_time, end_time, description)
VALUES (?, ?, ?, ?, ?, ?)`,
[slot.id, templateId, slot.name, slot.startTime, slot.endTime, slot.description]
);
}
// Nachmittagsschicht Mo-Do
// Erstelle Schichten für Mo-Do mit Zeit-Slot Referenzen
for (let day = 1; day <= 4; day++) {
// Vormittagsschicht
await db.run(
`INSERT INTO template_shifts (id, template_id, day_of_week, name, start_time, end_time, required_employees)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[uuidv4(), templateId, day, 'Nachmittagsschicht', '11:30', '15:30', 1]
`INSERT INTO template_shifts (id, template_id, day_of_week, time_slot_id, required_employees, color)
VALUES (?, ?, ?, ?, ?, ?)`,
[uuidv4(), templateId, day, timeSlots[0].id, 1, '#3498db']
);
// Nachmittagsschicht
await db.run(
`INSERT INTO template_shifts (id, template_id, day_of_week, time_slot_id, required_employees, color)
VALUES (?, ?, ?, ?, ?, ?)`,
[uuidv4(), templateId, day, timeSlots[1].id, 1, '#e74c3c']
);
}
// Freitag nur Vormittagsschicht
await db.run(
`INSERT INTO template_shifts (id, template_id, day_of_week, name, start_time, end_time, required_employees)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[uuidv4(), templateId, 5, 'Vormittagsschicht', '08:00', '12:00', 1]
`INSERT INTO template_shifts (id, template_id, day_of_week, time_slot_id, required_employees, color)
VALUES (?, ?, ?, ?, ?, ?)`,
[uuidv4(), templateId, 5, timeSlots[0].id, 1, '#3498db']
);
await db.run('COMMIT');
@@ -140,7 +200,7 @@ export const createDefaultTemplate = async (userId: string): Promise<string> =>
export const createTemplate = async (req: Request, res: Response): Promise<void> => {
try {
const { name, description, isDefault, shifts }: CreateShiftTemplateRequest = req.body;
const { name, description, isDefault, shifts, timeSlots }: CreateShiftTemplateRequest = req.body;
const userId = (req as AuthRequest).user?.userId;
if (!userId) {
@@ -167,13 +227,23 @@ export const createTemplate = async (req: Request, res: Response): Promise<void>
[templateId, name, description, isDefault ? 1 : 0, userId]
);
// Insert time slots
for (const timeSlot of timeSlots) {
const timeSlotId = timeSlot.id || uuidv4();
await db.run(
`INSERT INTO template_time_slots (id, template_id, name, start_time, end_time, description)
VALUES (?, ?, ?, ?, ?, ?)`,
[timeSlotId, templateId, timeSlot.name, timeSlot.startTime, timeSlot.endTime, timeSlot.description]
);
}
// Insert shifts
for (const shift of shifts) {
const shiftId = uuidv4();
await db.run(
`INSERT INTO template_shifts (id, template_id, day_of_week, name, start_time, end_time, required_employees, color)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[shiftId, templateId, shift.dayOfWeek, shift.name, shift.startTime, shift.endTime, shift.requiredEmployees, shift.color || '#3498db']
`INSERT INTO template_shifts (id, template_id, day_of_week, time_slot_id, required_employees, color)
VALUES (?, ?, ?, ?, ?, ?)`,
[shiftId, templateId, shift.dayOfWeek, shift.timeRange.id, shift.requiredEmployees, shift.color || '#3498db']
);
}
@@ -188,31 +258,8 @@ export const createTemplate = async (req: Request, res: Response): Promise<void>
await db.run('COMMIT');
// Return created template
const createdTemplate = await db.get<ShiftTemplate>(`
SELECT st.*, u.name as created_by_name
FROM shift_templates st
LEFT JOIN users u ON st.created_by = u.id
WHERE st.id = ?
`, [templateId]);
const templateShifts = await db.all<any>(`
SELECT * FROM template_shifts
WHERE template_id = ?
ORDER BY day_of_week, start_time
`, [templateId]);
res.status(201).json({
...createdTemplate,
shifts: templateShifts.map(shift => ({
id: shift.id,
dayOfWeek: shift.day_of_week,
name: shift.name,
startTime: shift.start_time,
endTime: shift.end_time,
requiredEmployees: shift.required_employees,
color: shift.color
}))
});
const createdTemplate = await getTemplateById(templateId);
res.status(201).json(createdTemplate);
} catch (error) {
await db.run('ROLLBACK');
@@ -228,7 +275,7 @@ export const createTemplate = async (req: Request, res: Response): Promise<void>
export const updateTemplate = async (req: Request, res: Response): Promise<void> => {
try {
const { id } = req.params;
const { name, description, isDefault, shifts }: UpdateShiftTemplateRequest = req.body;
const { name, description, isDefault, shifts, timeSlots }: UpdateShiftTemplateRequest = req.body;
// Check if template exists
const existingTemplate = await db.get('SELECT * FROM shift_templates WHERE id = ?', [id]);
@@ -258,6 +305,22 @@ export const updateTemplate = async (req: Request, res: Response): Promise<void>
);
}
// If updating time slots, replace all time slots
if (timeSlots) {
// Delete existing time slots
await db.run('DELETE FROM template_time_slots WHERE template_id = ?', [id]);
// Insert new time slots
for (const timeSlot of timeSlots) {
const timeSlotId = timeSlot.id || uuidv4();
await db.run(
`INSERT INTO template_time_slots (id, template_id, name, start_time, end_time, description)
VALUES (?, ?, ?, ?, ?, ?)`,
[timeSlotId, id, timeSlot.name, timeSlot.startTime, timeSlot.endTime, timeSlot.description]
);
}
}
// If updating shifts, replace all shifts
if (shifts) {
// Delete existing shifts
@@ -267,9 +330,9 @@ export const updateTemplate = async (req: Request, res: Response): Promise<void>
for (const shift of shifts) {
const shiftId = uuidv4();
await db.run(
`INSERT INTO template_shifts (id, template_id, day_of_week, name, start_time, end_time, required_employees, color)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[shiftId, id, shift.dayOfWeek, shift.name, shift.startTime, shift.endTime, shift.requiredEmployees, shift.color || '#3498db']
`INSERT INTO template_shifts (id, template_id, day_of_week, time_slot_id, required_employees, color)
VALUES (?, ?, ?, ?, ?, ?)`,
[shiftId, id, shift.dayOfWeek, shift.timeRange.id, shift.requiredEmployees, shift.color || '#3498db']
);
}
}
@@ -285,31 +348,8 @@ export const updateTemplate = async (req: Request, res: Response): Promise<void>
await db.run('COMMIT');
// Return updated template
const updatedTemplate = await db.get<ShiftTemplate>(`
SELECT st.*, u.name as created_by_name
FROM shift_templates st
LEFT JOIN users u ON st.created_by = u.id
WHERE st.id = ?
`, [id]);
const templateShifts = await db.all<any>(`
SELECT * FROM template_shifts
WHERE template_id = ?
ORDER BY day_of_week, start_time
`, [id]);
res.json({
...updatedTemplate,
shifts: templateShifts.map(shift => ({
id: shift.id,
dayOfWeek: shift.day_of_week,
name: shift.name,
startTime: shift.start_time,
endTime: shift.end_time,
requiredEmployees: shift.required_employees,
color: shift.color
}))
});
const updatedTemplate = await getTemplateById(id);
res.json(updatedTemplate);
} catch (error) {
await db.run('ROLLBACK');
@@ -334,7 +374,7 @@ export const deleteTemplate = async (req: Request, res: Response): Promise<void>
}
await db.run('DELETE FROM shift_templates WHERE id = ?', [id]);
// Template shifts will be automatically deleted due to CASCADE
// Template shifts and time slots will be automatically deleted due to CASCADE
res.status(204).send();
} catch (error) {
@@ -342,3 +382,56 @@ export const deleteTemplate = async (req: Request, res: Response): Promise<void>
res.status(500).json({ error: 'Internal server error' });
}
};
// Helper function to get template by ID
async function getTemplateById(templateId: string): Promise<any> {
const template = await db.get<any>(`
SELECT st.*, u.name as created_by_name
FROM shift_templates st
LEFT JOIN users u ON st.created_by = u.id
WHERE st.id = ?
`, [templateId]);
if (!template) {
return null;
}
// Lade Schicht-Slots
const shiftSlots = await db.all<any>(`
SELECT ts.*, tts.name as time_range_name, tts.start_time as time_range_start, tts.end_time as time_range_end
FROM template_shifts ts
LEFT JOIN template_time_slots tts ON ts.time_slot_id = tts.id
WHERE ts.template_id = ?
ORDER BY ts.day_of_week, tts.start_time
`, [templateId]);
// Lade Zeit-Slots
const timeSlots = await db.all<any>(`
SELECT * FROM template_time_slots
WHERE template_id = ?
ORDER BY start_time
`, [templateId]);
return {
...template,
shifts: shiftSlots.map(slot => ({
id: slot.id,
dayOfWeek: slot.day_of_week,
timeRange: {
id: slot.time_slot_id,
name: slot.time_range_name,
startTime: slot.time_range_start,
endTime: slot.time_range_end
},
requiredEmployees: slot.required_employees,
color: slot.color
})),
timeSlots: timeSlots.map(slot => ({
id: slot.id,
name: slot.name,
startTime: slot.start_time,
endTime: slot.end_time,
description: slot.description
}))
};
}

View File

@@ -27,7 +27,7 @@ CREATE TABLE IF NOT EXISTS shift_templates (
CREATE TABLE IF NOT EXISTS template_shifts (
id TEXT PRIMARY KEY,
template_id TEXT NOT NULL,
day_of_week INTEGER NOT NULL CHECK (day_of_week >= 1 AND day_of_week <= 5),
day_of_week INTEGER NOT NULL CHECK (day_of_week >= 1 AND day_of_week <= 7),
name TEXT NOT NULL,
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
@@ -51,6 +51,7 @@ CREATE TABLE IF NOT EXISTS shift_plans (
CREATE TABLE IF NOT EXISTS assigned_shifts (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
shift_plan_id TEXT NOT NULL,
date TEXT NOT NULL,
start_time TEXT NOT NULL,
@@ -64,7 +65,7 @@ CREATE TABLE IF NOT EXISTS assigned_shifts (
CREATE TABLE IF NOT EXISTS employee_availabilities (
id TEXT PRIMARY KEY,
employee_id TEXT NOT NULL,
day_of_week INTEGER NOT NULL CHECK (day_of_week >= 1 AND day_of_week <= 5),
day_of_week INTEGER NOT NULL CHECK (day_of_week >= 0 AND day_of_week <= 6),
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
is_available BOOLEAN DEFAULT FALSE,

View File

@@ -1,17 +1,37 @@
// backend/src/models/Shift.ts
export interface ShiftTemplate {
export interface Shift {
id: string;
name: string;
shifts: TemplateShift[];
description?: string;
isDefault: boolean;
createdBy: string;
createdAt: string;
shifts: ShiftSlot[];
}
export interface TemplateShift {
dayOfWeek: number; // 0-6
export interface ShiftSlot {
id: string;
shiftId: string;
dayOfWeek: number;
name: string;
startTime: string; // "08:00"
endTime: string; // "12:00"
startTime: string;
endTime: string;
requiredEmployees: number;
color?: string;
}
export interface CreateShiftRequest {
name: string;
description?: string;
isDefault: boolean;
shifts: Omit<ShiftSlot, 'id' | 'shiftId'>[];
}
export interface UpdateShiftSlotRequest {
name?: string;
description?: string;
isDefault?: boolean;
shifts?: Omit<ShiftSlot, 'id' | 'shiftId'>[];
}
export interface ShiftPlan {

View File

@@ -1,30 +1,35 @@
// backend/src/models/ShiftTemplate.ts
export interface ShiftTemplate {
export interface TemplateShift {
id: string;
name: string;
description?: string;
isDefault: boolean;
createdBy: string;
createdAt: string;
shifts: TemplateShift[];
shifts: TemplateShiftSlot[];
}
export interface TemplateShift {
export interface TemplateShiftSlot {
id: string;
templateId: string;
dayOfWeek: number;
name: string;
startTime: string;
endTime: string;
timeRange: TemplateShiftTimeRange;
requiredEmployees: number;
color?: string;
}
export interface TemplateShiftTimeRange {
id: string;
name: string; // e.g., "Frühschicht", "Spätschicht"
startTime: string;
endTime: string;
}
export interface CreateShiftTemplateRequest {
name: string;
description?: string;
isDefault: boolean;
shifts: Omit<TemplateShift, 'id' | 'templateId'>[];
timeSlots: Omit<TemplateShiftTimeRange, 'id'>[];
}
export interface UpdateShiftTemplateRequest {
@@ -32,4 +37,5 @@ export interface UpdateShiftTemplateRequest {
description?: string;
isDefault?: boolean;
shifts?: Omit<TemplateShift, 'id' | 'templateId'>[];
timeSlots?: Omit<TemplateShiftTimeRange, 'id'>[];
}

View File

@@ -1,17 +1,64 @@
import { db } from '../services/databaseService.js';
import { readFile } from 'fs/promises';
import { readFile, readdir } from 'fs/promises';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Helper function to ensure migrations are tracked
async function ensureMigrationTable() {
await db.exec(`
CREATE TABLE IF NOT EXISTS applied_migrations (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
}
// Helper function to check if a migration has been applied
async function isMigrationApplied(migrationName: string): Promise<boolean> {
const result = await db.get<{ count: number }>(
'SELECT COUNT(*) as count FROM applied_migrations WHERE name = ?',
[migrationName]
);
return (result?.count ?? 0) > 0;
}
// Helper function to mark a migration as applied
async function markMigrationAsApplied(migrationName: string) {
await db.run(
'INSERT INTO applied_migrations (id, name) VALUES (?, ?)',
[crypto.randomUUID(), migrationName]
);
}
export async function applyMigration() {
try {
console.log('📦 Starting database migration...');
// Read the migration file
const migrationPath = join(__dirname, '../database/migrations/002_add_employee_fields.sql');
// Ensure migration tracking table exists
await ensureMigrationTable();
// Get all migration files
const migrationsDir = join(__dirname, '../database/migrations');
const files = await readdir(migrationsDir);
// Sort files to ensure consistent order
const migrationFiles = files
.filter(f => f.endsWith('.sql'))
.sort();
// Process each migration file
for (const migrationFile of migrationFiles) {
if (await isMigrationApplied(migrationFile)) {
console.log(` Migration ${migrationFile} already applied, skipping...`);
continue;
}
console.log(`📄 Applying migration: ${migrationFile}`);
const migrationPath = join(migrationsDir, migrationFile);
const migrationSQL = await readFile(migrationPath, 'utf-8');
// Split into individual statements
@@ -20,6 +67,10 @@ export async function applyMigration() {
.map(s => s.trim())
.filter(s => s.length > 0);
// Start transaction for this migration
await db.run('BEGIN TRANSACTION');
try {
// Execute each statement
for (const statement of statements) {
try {
@@ -35,7 +86,18 @@ export async function applyMigration() {
}
}
console.log('✅ Migration completed successfully');
// Mark migration as applied
await markMigrationAsApplied(migrationFile);
await db.run('COMMIT');
console.log(`✅ Migration ${migrationFile} applied successfully`);
} catch (error) {
await db.run('ROLLBACK');
throw error;
}
}
console.log('✅ All migrations completed successfully');
} catch (error) {
console.error('❌ Migration failed:', error);
throw error;

View File

@@ -1,9 +1,9 @@
import { db } from '../services/databaseService.js';
import { ShiftTemplate } from '../models/ShiftTemplate.js';
import { TemplateShift } from '../models/ShiftTemplate.js';
async function checkTemplates() {
try {
const templates = await db.all<ShiftTemplate>(
const templates = await db.all<TemplateShift>(
`SELECT st.*, u.name as created_by_name
FROM shift_templates st
LEFT JOIN users u ON st.created_by = u.id`

View File

@@ -55,7 +55,7 @@ export async function setupDefaultTemplate(): Promise<void> {
console.log('Standard-Vorlage erstellt:', templateId);
// Vormittagsschicht Mo-Do
for (let day = 1; day <= 4; day++) {
for (let day = 1; day <= 5; day++) {
await db.run(
`INSERT INTO template_shifts (id, template_id, day_of_week, name, start_time, end_time, required_employees)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
@@ -63,7 +63,7 @@ export async function setupDefaultTemplate(): Promise<void> {
);
}
console.log('Vormittagsschichten Mo-Do erstellt');
console.log('Vormittagsschichten Mo-Fr erstellt');
// Nachmittagsschicht Mo-Do
for (let day = 1; day <= 4; day++) {
@@ -76,15 +76,6 @@ export async function setupDefaultTemplate(): Promise<void> {
console.log('Nachmittagsschichten Mo-Do erstellt');
// Freitag nur Vormittagsschicht
await db.run(
`INSERT INTO template_shifts (id, template_id, day_of_week, name, start_time, end_time, required_employees)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[uuidv4(), templateId, 5, 'Vormittagsschicht', '08:00', '12:00', 1]
);
console.log('Freitag Vormittagsschicht erstellt');
await db.run('COMMIT');
console.log('Standard-Vorlage erfolgreich initialisiert');
} catch (error) {

View File

@@ -9,6 +9,8 @@ import Login from './pages/Auth/Login';
import Dashboard from './pages/Dashboard/Dashboard';
import ShiftPlanList from './pages/ShiftPlans/ShiftPlanList';
import ShiftPlanCreate from './pages/ShiftPlans/ShiftPlanCreate';
import ShiftPlanEdit from './pages/ShiftPlans/ShiftPlanEdit';
import ShiftPlanView from './pages/ShiftPlans/ShiftPlanView';
import EmployeeManagement from './pages/Employees/EmployeeManagement';
import Settings from './pages/Settings/Settings';
import Help from './pages/Help/Help';
@@ -93,6 +95,16 @@ const AppContent: React.FC = () => {
<ShiftPlanCreate />
</ProtectedRoute>
} />
<Route path="/shift-plans/:id/edit" element={
<ProtectedRoute roles={['admin', 'instandhalter']}>
<ShiftPlanEdit />
</ProtectedRoute>
} />
<Route path="/shift-plans/:id" element={
<ProtectedRoute>
<ShiftPlanView />
</ProtectedRoute>
} />
<Route path="/employees" element={
<ProtectedRoute roles={['admin', 'instandhalter']}>
<EmployeeManagement />

View File

@@ -2,6 +2,7 @@
import React, { useState, useEffect } from 'react';
import { Employee, Availability } from '../../../types/employee';
import { employeeService } from '../../../services/employeeService';
import { shiftPlanService, ShiftPlan, ShiftPlanShift } from '../../../services/shiftPlanService';
interface AvailabilityManagerProps {
employee: Employee;
@@ -9,12 +10,18 @@ interface AvailabilityManagerProps {
onCancel: () => void;
}
// Verfügbarkeits-Level
export type AvailabilityLevel = 1 | 2 | 3; // 1: bevorzugt, 2: möglich, 3: nicht möglich
const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
employee,
onSave,
onCancel
}) => {
const [availabilities, setAvailabilities] = useState<Availability[]>([]);
const [shiftPlans, setShiftPlans] = useState<ShiftPlan[]>([]);
const [selectedPlanId, setSelectedPlanId] = useState<string>('');
const [selectedPlan, setSelectedPlan] = useState<ShiftPlan | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
@@ -29,53 +36,112 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
{ id: 0, name: 'Sonntag' }
];
const defaultTimeSlots = [
{ name: 'Vormittag', start: '08:00', end: '12:00' },
{ name: 'Nachmittag', start: '12:00', end: '16:00' },
{ name: 'Abend', start: '16:00', end: '20:00' }
// Verfügbarkeits-Level mit Farben und Beschreibungen
const availabilityLevels = [
{ level: 1 as AvailabilityLevel, label: 'Bevorzugt', color: '#27ae60', bgColor: '#d5f4e6', description: 'Ideale Zeit' },
{ level: 2 as AvailabilityLevel, label: 'Möglich', color: '#f39c12', bgColor: '#fef5e7', description: 'Akzeptable Zeit' },
{ level: 3 as AvailabilityLevel, label: 'Nicht möglich', color: '#e74c3c', bgColor: '#fadbd8', description: 'Nicht verfügbar' }
];
useEffect(() => {
loadAvailabilities();
loadData();
}, [employee.id]);
const loadAvailabilities = async () => {
useEffect(() => {
if (selectedPlanId) {
loadSelectedPlan();
}
}, [selectedPlanId]);
const loadData = async () => {
try {
setLoading(true);
const data = await employeeService.getAvailabilities(employee.id);
setAvailabilities(data);
} catch (err: any) {
// Falls keine Verfügbarkeiten existieren, erstelle Standard-Einträge
const defaultAvailabilities = daysOfWeek.flatMap(day =>
defaultTimeSlots.map(slot => ({
id: `temp-${day.id}-${slot.name}`,
// Load availabilities
try {
const availData = await employeeService.getAvailabilities(employee.id);
setAvailabilities(availData);
} catch (err) {
// Falls keine Verfügbarkeiten existieren, erstelle Standard-Einträge (Level 3: nicht möglich)
const defaultAvailabilities: Availability[] = daysOfWeek.flatMap(day => [
{
id: `temp-${day.id}-morning`,
employeeId: employee.id,
dayOfWeek: day.id,
startTime: slot.start,
endTime: slot.end,
isAvailable: false
}))
);
startTime: '08:00',
endTime: '12:00',
isAvailable: false,
availabilityLevel: 3 as AvailabilityLevel
},
{
id: `temp-${day.id}-afternoon`,
employeeId: employee.id,
dayOfWeek: day.id,
startTime: '12:00',
endTime: '16:00',
isAvailable: false,
availabilityLevel: 3 as AvailabilityLevel
},
{
id: `temp-${day.id}-evening`,
employeeId: employee.id,
dayOfWeek: day.id,
startTime: '16:00',
endTime: '20:00',
isAvailable: false,
availabilityLevel: 3 as AvailabilityLevel
}
]);
setAvailabilities(defaultAvailabilities);
}
// Load shift plans
const plans = await shiftPlanService.getShiftPlans();
setShiftPlans(plans);
// Auto-select the first published plan or the first draft
if (plans.length > 0) {
const publishedPlan = plans.find(plan => plan.status === 'published');
const firstPlan = publishedPlan || plans[0];
setSelectedPlanId(firstPlan.id);
}
} catch (err: any) {
console.error('Error loading data:', err);
setError('Daten konnten nicht geladen werden');
} finally {
setLoading(false);
}
};
const handleAvailabilityChange = (id: string, isAvailable: boolean) => {
const loadSelectedPlan = async () => {
try {
const plan = await shiftPlanService.getShiftPlan(selectedPlanId);
setSelectedPlan(plan);
} catch (err: any) {
console.error('Error loading shift plan:', err);
setError('Schichtplan konnte nicht geladen werden');
}
};
const handleAvailabilityLevelChange = (dayId: number, timeSlot: string, level: AvailabilityLevel) => {
setAvailabilities(prev =>
prev.map(avail =>
avail.id === id ? { ...avail, isAvailable } : avail
avail.dayOfWeek === dayId && getTimeSlotName(avail.startTime, avail.endTime) === timeSlot
? {
...avail,
availabilityLevel: level,
isAvailable: level !== 3
}
: avail
)
);
};
const handleTimeChange = (id: string, field: 'startTime' | 'endTime', value: string) => {
setAvailabilities(prev =>
prev.map(avail =>
avail.id === id ? { ...avail, [field]: value } : avail
)
);
const getTimeSlotName = (startTime: string, endTime: string): string => {
if (startTime === '08:00' && endTime === '12:00') return 'Vormittag';
if (startTime === '12:00' && endTime === '16:00') return 'Nachmittag';
if (startTime === '16:00' && endTime === '20:00') return 'Abend';
return `${startTime}-${endTime}`;
};
const handleSave = async () => {
@@ -92,8 +158,91 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
}
};
const getAvailabilitiesForDay = (dayId: number) => {
return availabilities.filter(avail => avail.dayOfWeek === dayId);
// Get availability level for a specific shift
const getAvailabilityForShift = (shift: ShiftPlanShift): AvailabilityLevel => {
const shiftDate = new Date(shift.date);
const dayOfWeek = shiftDate.getDay(); // 0 = Sunday, 1 = Monday, etc.
// Find matching availability for this day and time
const matchingAvailabilities = availabilities.filter(avail =>
avail.dayOfWeek === dayOfWeek &&
avail.availabilityLevel !== 3 && // Nur Level 1 und 2 berücksichtigen
isTimeOverlap(avail.startTime, avail.endTime, shift.startTime, shift.endTime)
);
if (matchingAvailabilities.length === 0) {
return 3; // Nicht möglich, wenn keine Übereinstimmung
}
// Nehme das beste (niedrigste) Verfügbarkeits-Level
const minLevel = Math.min(...matchingAvailabilities.map(avail => avail.availabilityLevel));
return minLevel as AvailabilityLevel;
};
// Helper function to check time overlap
const isTimeOverlap = (availStart: string, availEnd: string, shiftStart: string, shiftEnd: string): boolean => {
const availStartMinutes = timeToMinutes(availStart);
const availEndMinutes = timeToMinutes(availEnd);
const shiftStartMinutes = timeToMinutes(shiftStart);
const shiftEndMinutes = timeToMinutes(shiftEnd);
return shiftStartMinutes < availEndMinutes && shiftEndMinutes > availStartMinutes;
};
const timeToMinutes = (time: string): number => {
const [hours, minutes] = time.split(':').map(Number);
return hours * 60 + minutes;
};
// Group shifts by weekday for timetable display
const getTimetableData = () => {
if (!selectedPlan) return { shiftsByDay: {}, weekdays: [] };
const shiftsByDay: Record<number, ShiftPlanShift[]> = {};
// Initialize empty arrays for each day
daysOfWeek.forEach(day => {
shiftsByDay[day.id] = [];
});
// Group shifts by weekday
selectedPlan.shifts.forEach(shift => {
const shiftDate = new Date(shift.date);
const dayOfWeek = shiftDate.getDay(); // 0 = Sunday, 1 = Monday, etc.
shiftsByDay[dayOfWeek].push(shift);
});
// Remove duplicate shifts (same name and time on same day)
Object.keys(shiftsByDay).forEach(day => {
const dayNum = parseInt(day);
const uniqueShifts: ShiftPlanShift[] = [];
const seen = new Set();
shiftsByDay[dayNum].forEach(shift => {
const key = `${shift.name}|${shift.startTime}|${shift.endTime}`;
if (!seen.has(key)) {
seen.add(key);
uniqueShifts.push(shift);
}
});
shiftsByDay[dayNum] = uniqueShifts;
});
return {
shiftsByDay,
weekdays: daysOfWeek
};
};
const timetableData = getTimetableData();
// Get availability for a specific day and time slot
const getAvailabilityForDayAndSlot = (dayId: number, timeSlot: string): AvailabilityLevel => {
const availability = availabilities.find(avail =>
avail.dayOfWeek === dayId && getTimeSlotName(avail.startTime, avail.endTime) === timeSlot
);
return availability?.availabilityLevel || 3;
};
if (loading) {
@@ -106,7 +255,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
return (
<div style={{
maxWidth: '800px',
maxWidth: '1200px',
margin: '0 auto',
backgroundColor: 'white',
padding: '30px',
@@ -128,7 +277,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
{employee.name}
</h3>
<p style={{ margin: 0, color: '#7f8c8d' }}>
Legen Sie fest, an welchen Tagen und Zeiten {employee.name} verfügbar ist.
Legen Sie die Verfügbarkeit für {employee.name} fest (1: bevorzugt, 2: möglich, 3: nicht möglich).
</p>
</div>
@@ -145,132 +294,234 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
</div>
)}
{/* Verfügbarkeiten Tabelle */}
{/* Verfügbarkeits-Legende */}
<div style={{
marginBottom: '30px',
padding: '20px',
backgroundColor: '#f8f9fa',
borderRadius: '8px',
border: '1px solid #e9ecef'
}}>
<h4 style={{ margin: '0 0 15px 0', color: '#495057' }}>
Verfügbarkeits-Level
</h4>
<div style={{ display: 'flex', gap: '20px', flexWrap: 'wrap' }}>
{availabilityLevels.map(level => (
<div key={level.level} style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<div
style={{
width: '20px',
height: '20px',
backgroundColor: level.bgColor,
border: `2px solid ${level.color}`,
borderRadius: '4px'
}}
/>
<div>
<div style={{ fontWeight: 'bold', color: level.color }}>
{level.level}: {level.label}
</div>
<div style={{ fontSize: '12px', color: '#666' }}>
{level.description}
</div>
</div>
</div>
))}
</div>
</div>
{/* Schichtplan Auswahl */}
<div style={{
marginBottom: '30px',
padding: '20px',
backgroundColor: '#f8f9fa',
borderRadius: '8px',
border: '1px solid #e9ecef'
}}>
<h4 style={{ margin: '0 0 15px 0', color: '#495057' }}>
Verfügbarkeit für Schichtplan prüfen
</h4>
<div style={{ display: 'flex', gap: '15px', alignItems: 'center', flexWrap: 'wrap' }}>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
Schichtplan auswählen:
</label>
<select
value={selectedPlanId}
onChange={(e) => setSelectedPlanId(e.target.value)}
style={{
padding: '8px 12px',
border: '1px solid #ddd',
borderRadius: '4px',
minWidth: '250px'
}}
>
<option value="">Bitte auswählen...</option>
{shiftPlans.map(plan => (
<option key={plan.id} value={plan.id}>
{plan.name} ({plan.status === 'published' ? 'Veröffentlicht' : 'Entwurf'})
</option>
))}
</select>
</div>
{selectedPlan && (
<div style={{ fontSize: '14px', color: '#666' }}>
Zeitraum: {new Date(selectedPlan.startDate).toLocaleDateString('de-DE')} - {new Date(selectedPlan.endDate).toLocaleDateString('de-DE')}
</div>
)}
</div>
</div>
{/* Verfügbarkeits-Timetable mit Dropdown-Menüs */}
{selectedPlan && (
<div style={{
marginBottom: '30px',
border: '1px solid #e0e0e0',
borderRadius: '8px',
overflow: 'hidden',
marginBottom: '30px'
overflow: 'hidden'
}}>
{daysOfWeek.map((day, dayIndex) => {
const dayAvailabilities = getAvailabilitiesForDay(day.id);
const isLastDay = dayIndex === daysOfWeek.length - 1;
return (
<div key={day.id} style={{
borderBottom: isLastDay ? 'none' : '1px solid #f0f0f0'
}}>
{/* Tag Header */}
<div style={{
backgroundColor: '#f8f9fa',
backgroundColor: '#2c3e50',
color: 'white',
padding: '15px 20px',
fontWeight: 'bold',
color: '#2c3e50',
borderBottom: '1px solid #e0e0e0'
fontWeight: 'bold'
}}>
{day.name}
Verfügbarkeit für: {selectedPlan.name}
</div>
{/* Zeit-Slots */}
<div style={{ padding: '15px 20px' }}>
{dayAvailabilities.map((availability, availabilityIndex) => {
const isLastAvailability = availabilityIndex === dayAvailabilities.length - 1;
<div style={{ overflowX: 'auto' }}>
<table style={{
width: '100%',
borderCollapse: 'collapse',
backgroundColor: 'white'
}}>
<thead>
<tr style={{ backgroundColor: '#f8f9fa' }}>
<th style={{
padding: '12px 16px',
textAlign: 'left',
border: '1px solid #dee2e6',
fontWeight: 'bold',
minWidth: '150px'
}}>
Zeit
</th>
{timetableData.weekdays.map(weekday => (
<th key={weekday.id} style={{
padding: '12px 16px',
textAlign: 'center',
border: '1px solid #dee2e6',
fontWeight: 'bold',
minWidth: '150px'
}}>
{weekday.name}
</th>
))}
</tr>
</thead>
<tbody>
{['Vormittag', 'Nachmittag', 'Abend'].map((timeSlot, timeIndex) => (
<tr key={timeSlot} style={{
backgroundColor: timeIndex % 2 === 0 ? 'white' : '#f8f9fa'
}}>
<td style={{
padding: '12px 16px',
border: '1px solid #dee2e6',
fontWeight: '500',
backgroundColor: '#f8f9fa'
}}>
{timeSlot}
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
{timeSlot === 'Vormittag' ? '08:00-12:00' :
timeSlot === 'Nachmittag' ? '12:00-16:00' : '16:00-20:00'}
</div>
</td>
{timetableData.weekdays.map(weekday => {
const currentLevel = getAvailabilityForDayAndSlot(weekday.id, timeSlot);
const levelConfig = availabilityLevels.find(l => l.level === currentLevel);
return (
<div
key={availability.id}
style={{
display: 'grid',
gridTemplateColumns: '1fr auto auto auto',
gap: '15px',
alignItems: 'center',
padding: '10px 0',
borderBottom: isLastAvailability ? 'none' : '1px solid #f8f9fa'
}}
>
{/* Verfügbarkeit Toggle */}
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<input
type="checkbox"
id={`avail-${availability.id}`}
checked={availability.isAvailable}
onChange={(e) => handleAvailabilityChange(availability.id, e.target.checked)}
style={{ width: '18px', height: '18px' }}
/>
<label
htmlFor={`avail-${availability.id}`}
<td key={weekday.id} style={{
padding: '12px 16px',
border: '1px solid #dee2e6',
textAlign: 'center',
backgroundColor: levelConfig?.bgColor
}}>
<select
value={currentLevel}
onChange={(e) => handleAvailabilityLevelChange(weekday.id, timeSlot, parseInt(e.target.value) as AvailabilityLevel)}
style={{
padding: '8px 12px',
border: `2px solid ${levelConfig?.color || '#ddd'}`,
borderRadius: '6px',
backgroundColor: levelConfig?.bgColor || 'white',
color: levelConfig?.color || '#333',
fontWeight: 'bold',
color: availability.isAvailable ? '#27ae60' : '#95a5a6'
minWidth: '120px',
cursor: 'pointer',
textAlign: 'center'
}}
>
{availability.isAvailable ? 'Verfügbar' : 'Nicht verfügbar'}
</label>
</div>
{/* Startzeit */}
<div>
<label style={{ fontSize: '12px', color: '#7f8c8d', display: 'block', marginBottom: '4px' }}>
Von
</label>
<input
type="time"
value={availability.startTime}
onChange={(e) => handleTimeChange(availability.id, 'startTime', e.target.value)}
disabled={!availability.isAvailable}
{availabilityLevels.map(level => (
<option
key={level.level}
value={level.level}
style={{
padding: '6px 8px',
border: `1px solid ${availability.isAvailable ? '#ddd' : '#f0f0f0'}`,
borderRadius: '4px',
backgroundColor: availability.isAvailable ? 'white' : '#f8f9fa',
color: availability.isAvailable ? '#333' : '#999'
}}
/>
</div>
{/* Endzeit */}
<div>
<label style={{ fontSize: '12px', color: '#7f8c8d', display: 'block', marginBottom: '4px' }}>
Bis
</label>
<input
type="time"
value={availability.endTime}
onChange={(e) => handleTimeChange(availability.id, 'endTime', e.target.value)}
disabled={!availability.isAvailable}
style={{
padding: '6px 8px',
border: `1px solid ${availability.isAvailable ? '#ddd' : '#f0f0f0'}`,
borderRadius: '4px',
backgroundColor: availability.isAvailable ? 'white' : '#f8f9fa',
color: availability.isAvailable ? '#333' : '#999'
}}
/>
</div>
{/* Status Badge */}
<div>
<span
style={{
backgroundColor: availability.isAvailable ? '#d5f4e6' : '#fadbd8',
color: availability.isAvailable ? '#27ae60' : '#e74c3c',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '12px',
backgroundColor: level.bgColor,
color: level.color,
fontWeight: 'bold'
}}
>
{availability.isAvailable ? 'Aktiv' : 'Inaktiv'}
{level.level}: {level.label}
</option>
))}
</select>
<div style={{
fontSize: '11px',
color: levelConfig?.color,
marginTop: '4px',
fontWeight: 'bold'
}}>
{levelConfig?.description}
</div>
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
{/* Legende */}
<div style={{
padding: '12px 16px',
backgroundColor: '#e8f4fd',
borderTop: '1px solid #b8d4f0',
fontSize: '14px',
color: '#2c3e50'
}}>
<strong>Legende:</strong>
{availabilityLevels.map(level => (
<span key={level.level} style={{ marginLeft: '15px', display: 'inline-flex', alignItems: 'center', gap: '5px' }}>
<div
style={{
width: '12px',
height: '12px',
backgroundColor: level.bgColor,
border: `1px solid ${level.color}`,
borderRadius: '2px'
}}
/>
<strong style={{ color: level.color }}>{level.level}</strong>: {level.label}
</span>
))}
</div>
</div>
);
})}
</div>
</div>
);
})}
</div>
)}
{/* Info Text */}
<div style={{
@@ -282,8 +533,10 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
}}>
<h4 style={{ margin: '0 0 8px 0', color: '#2c3e50' }}>💡 Information</h4>
<p style={{ margin: 0, color: '#546e7a', fontSize: '14px' }}>
Verfügbarkeiten bestimmen, wann dieser Mitarbeiter für Schichten eingeplant werden kann.
Nur als "verfügbar" markierte Zeitfenster werden bei der automatischen Schichtplanung berücksichtigt.
<strong>1: Bevorzugt</strong> - Ideale Zeit für diesen Mitarbeiter<br/>
<strong>2: Möglich</strong> - Akzeptable Zeit, falls benötigt<br/>
<strong>3: Nicht möglich</strong> - Mitarbeiter ist nicht verfügbar<br/>
Das System priorisiert Mitarbeiter mit Level 1 für Schichtzuweisungen.
</p>
</div>

View File

@@ -3,7 +3,7 @@ import React, { useState, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { shiftTemplateService } from '../../services/shiftTemplateService';
import { shiftPlanService } from '../../services/shiftPlanService';
import { ShiftTemplate } from '../../types/shiftTemplate';
import { TemplateShift } from '../../types/shiftTemplate';
import styles from './ShiftPlanCreate.module.css';
const ShiftPlanCreate: React.FC = () => {
@@ -14,7 +14,7 @@ const ShiftPlanCreate: React.FC = () => {
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const [selectedTemplate, setSelectedTemplate] = useState('');
const [templates, setTemplates] = useState<ShiftTemplate[]>([]);
const [templates, setTemplates] = useState<TemplateShift[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

View File

@@ -0,0 +1,430 @@
// frontend/src/pages/ShiftPlans/ShiftPlanEdit.tsx
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { shiftPlanService, ShiftPlan, ShiftPlanShift } from '../../services/shiftPlanService';
import { useNotification } from '../../contexts/NotificationContext';
const ShiftPlanEdit: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { showNotification } = useNotification();
const [shiftPlan, setShiftPlan] = useState<ShiftPlan | null>(null);
const [loading, setLoading] = useState(true);
const [editingShift, setEditingShift] = useState<ShiftPlanShift | null>(null);
const [newShift, setNewShift] = useState<Partial<ShiftPlanShift>>({
date: '',
name: '',
startTime: '',
endTime: '',
requiredEmployees: 1
});
useEffect(() => {
loadShiftPlan();
}, [id]);
const loadShiftPlan = async () => {
if (!id) return;
try {
const plan = await shiftPlanService.getShiftPlan(id);
setShiftPlan(plan);
} catch (error) {
console.error('Error loading shift plan:', error);
showNotification({
type: 'error',
title: 'Fehler',
message: 'Der Schichtplan konnte nicht geladen werden.'
});
navigate('/shift-plans');
} finally {
setLoading(false);
}
};
const handleUpdateShift = async (shift: ShiftPlanShift) => {
if (!shiftPlan || !id) return;
try {
await shiftPlanService.updateShiftPlanShift(id, shift);
showNotification({
type: 'success',
title: 'Erfolg',
message: 'Schicht wurde aktualisiert.'
});
loadShiftPlan();
setEditingShift(null);
} catch (error) {
console.error('Error updating shift:', error);
showNotification({
type: 'error',
title: 'Fehler',
message: 'Die Schicht konnte nicht aktualisiert werden.'
});
}
};
const handleAddShift = async () => {
if (!shiftPlan || !id) return;
if (!newShift.date || !newShift.name || !newShift.startTime || !newShift.endTime || !newShift.requiredEmployees) {
showNotification({
type: 'error',
title: 'Fehler',
message: 'Bitte füllen Sie alle Pflichtfelder aus.'
});
return;
}
try {
await shiftPlanService.addShiftPlanShift(id, {
date: newShift.date,
name: newShift.name,
startTime: newShift.startTime,
endTime: newShift.endTime,
requiredEmployees: Number(newShift.requiredEmployees)
});
showNotification({
type: 'success',
title: 'Erfolg',
message: 'Neue Schicht wurde hinzugefügt.'
});
setNewShift({
date: '',
name: '',
startTime: '',
endTime: '',
requiredEmployees: 1
});
loadShiftPlan();
} catch (error) {
console.error('Error adding shift:', error);
showNotification({
type: 'error',
title: 'Fehler',
message: 'Die Schicht konnte nicht hinzugefügt werden.'
});
}
};
const handleDeleteShift = async (shiftId: string) => {
if (!window.confirm('Möchten Sie diese Schicht wirklich löschen?')) {
return;
}
try {
await shiftPlanService.deleteShiftPlanShift(id!, shiftId);
showNotification({
type: 'success',
title: 'Erfolg',
message: 'Schicht wurde gelöscht.'
});
loadShiftPlan();
} catch (error) {
console.error('Error deleting shift:', error);
showNotification({
type: 'error',
title: 'Fehler',
message: 'Die Schicht konnte nicht gelöscht werden.'
});
}
};
const handlePublish = async () => {
if (!shiftPlan || !id) return;
try {
await shiftPlanService.updateShiftPlan(id, {
...shiftPlan,
status: 'published'
});
showNotification({
type: 'success',
title: 'Erfolg',
message: 'Schichtplan wurde veröffentlicht.'
});
loadShiftPlan();
} catch (error) {
console.error('Error publishing shift plan:', error);
showNotification({
type: 'error',
title: 'Fehler',
message: 'Der Schichtplan konnte nicht veröffentlicht werden.'
});
}
};
if (loading) {
return <div>Lade Schichtplan...</div>;
}
if (!shiftPlan) {
return <div>Schichtplan nicht gefunden</div>;
}
// Group shifts by date
const shiftsByDate = shiftPlan.shifts.reduce((acc, shift) => {
if (!acc[shift.date]) {
acc[shift.date] = [];
}
acc[shift.date].push(shift);
return acc;
}, {} as Record<string, typeof shiftPlan.shifts>);
return (
<div style={{ padding: '20px' }}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '30px'
}}>
<h1>{shiftPlan.name} bearbeiten</h1>
<div>
{shiftPlan.status === 'draft' && (
<button
onClick={handlePublish}
style={{
padding: '8px 16px',
backgroundColor: '#2ecc71',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
marginRight: '10px'
}}
>
Veröffentlichen
</button>
)}
<button
onClick={() => navigate('/shift-plans')}
style={{
padding: '8px 16px',
backgroundColor: '#95a5a6',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Zurück
</button>
</div>
</div>
{/* Add new shift form */}
<div style={{
backgroundColor: 'white',
borderRadius: '8px',
padding: '20px',
marginBottom: '20px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
<h3>Neue Schicht hinzufügen</h3>
<div style={{ display: 'grid', gap: '15px', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))' }}>
<div>
<label>Datum</label>
<input
type="date"
value={newShift.date}
onChange={(e) => setNewShift({ ...newShift, date: e.target.value })}
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
/>
</div>
<div>
<label>Name</label>
<input
type="text"
value={newShift.name}
onChange={(e) => setNewShift({ ...newShift, name: e.target.value })}
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
/>
</div>
<div>
<label>Startzeit</label>
<input
type="time"
value={newShift.startTime}
onChange={(e) => setNewShift({ ...newShift, startTime: e.target.value })}
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
/>
</div>
<div>
<label>Endzeit</label>
<input
type="time"
value={newShift.endTime}
onChange={(e) => setNewShift({ ...newShift, endTime: e.target.value })}
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
/>
</div>
<div>
<label>Benötigte Mitarbeiter</label>
<input
type="number"
min="1"
value={newShift.requiredEmployees}
onChange={(e) => setNewShift({ ...newShift, requiredEmployees: parseInt(e.target.value) })}
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
/>
</div>
</div>
<button
onClick={handleAddShift}
disabled={!newShift.date || !newShift.name || !newShift.startTime || !newShift.endTime}
style={{
marginTop: '15px',
padding: '8px 16px',
backgroundColor: '#3498db',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Schicht hinzufügen
</button>
</div>
{/* Existing shifts */}
<div style={{ display: 'grid', gap: '20px' }}>
{Object.entries(shiftsByDate).map(([date, shifts]) => (
<div key={date} style={{
backgroundColor: 'white',
borderRadius: '8px',
padding: '20px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
<h3 style={{ marginTop: 0 }}>{new Date(date).toLocaleDateString('de-DE', { weekday: 'long', day: '2-digit', month: '2-digit', year: 'numeric' })}</h3>
<div style={{ display: 'grid', gap: '15px' }}>
{shifts.map(shift => (
<div key={shift.id} style={{
backgroundColor: '#f8f9fa',
padding: '15px',
borderRadius: '6px',
boxShadow: '0 1px 3px rgba(0,0,0,0.05)'
}}>
{editingShift?.id === shift.id ? (
<div style={{ display: 'grid', gap: '10px', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))' }}>
<div>
<label>Name</label>
<input
type="text"
value={editingShift.name}
onChange={(e) => setEditingShift({ ...editingShift, name: e.target.value })}
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
/>
</div>
<div>
<label>Startzeit</label>
<input
type="time"
value={editingShift.startTime}
onChange={(e) => setEditingShift({ ...editingShift, startTime: e.target.value })}
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
/>
</div>
<div>
<label>Endzeit</label>
<input
type="time"
value={editingShift.endTime}
onChange={(e) => setEditingShift({ ...editingShift, endTime: e.target.value })}
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
/>
</div>
<div>
<label>Benötigte Mitarbeiter</label>
<input
type="number"
min="1"
value={editingShift.requiredEmployees}
onChange={(e) => setEditingShift({ ...editingShift, requiredEmployees: parseInt(e.target.value) })}
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
/>
</div>
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-end' }}>
<button
onClick={() => handleUpdateShift(editingShift)}
style={{
padding: '8px 16px',
backgroundColor: '#2ecc71',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Speichern
</button>
<button
onClick={() => setEditingShift(null)}
style={{
padding: '8px 16px',
backgroundColor: '#95a5a6',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Abbrechen
</button>
</div>
</div>
) : (
<>
<div style={{ fontWeight: 'bold', marginBottom: '5px' }}>
{shift.name}
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ fontSize: '14px', color: '#666' }}>
<span>Zeit: {shift.startTime.substring(0, 5)} - {shift.endTime.substring(0, 5)}</span>
<span style={{ margin: '0 15px' }}>|</span>
<span>Benötigte Mitarbeiter: {shift.requiredEmployees}</span>
<span style={{ margin: '0 15px' }}>|</span>
<span>Zugewiesen: {shift.assignedEmployees.length}/{shift.requiredEmployees}</span>
</div>
<div>
<button
onClick={() => setEditingShift(shift)}
style={{
padding: '6px 12px',
backgroundColor: '#f1c40f',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
marginRight: '8px'
}}
>
Bearbeiten
</button>
<button
onClick={() => handleDeleteShift(shift.id)}
style={{
padding: '6px 12px',
backgroundColor: '#e74c3c',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Löschen
</button>
</div>
</div>
</>
)}
</div>
))}
</div>
</div>
))}
</div>
</div>
);
};
export default ShiftPlanEdit;

View File

@@ -0,0 +1,292 @@
// frontend/src/pages/ShiftPlans/ShiftPlanView.tsx
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
import { shiftPlanService, ShiftPlan } from '../../services/shiftPlanService';
import { useNotification } from '../../contexts/NotificationContext';
const ShiftPlanView: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { hasRole } = useAuth();
const { showNotification } = useNotification();
const [shiftPlan, setShiftPlan] = useState<ShiftPlan | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadShiftPlan();
}, [id]);
const loadShiftPlan = async () => {
if (!id) return;
try {
const plan = await shiftPlanService.getShiftPlan(id);
setShiftPlan(plan);
} catch (error) {
console.error('Error loading shift plan:', error);
showNotification({
type: 'error',
title: 'Fehler',
message: 'Der Schichtplan konnte nicht geladen werden.'
});
navigate('/shift-plans');
} finally {
setLoading(false);
}
};
const formatDate = (dateString: string | undefined): string => {
if (!dateString) return 'Kein Datum';
const date = new Date(dateString);
if (isNaN(date.getTime())) {
return 'Ungültiges Datum';
}
return date.toLocaleDateString('de-DE', {
weekday: 'long',
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
};
const formatTime = (timeString: string) => {
return timeString.substring(0, 5);
};
// Get unique shift types and their staffing per weekday
const getTimetableData = () => {
if (!shiftPlan) return { shifts: [], weekdays: [] };
// Get all unique shift types (name + time combination)
const shiftTypes = Array.from(new Set(
shiftPlan.shifts.map(shift =>
`${shift.name}|${shift.startTime}|${shift.endTime}`
)
)).map(shiftKey => {
const [name, startTime, endTime] = shiftKey.split('|');
return { name, startTime, endTime };
});
// Weekdays (1=Monday, 7=Sunday)
const weekdays = [1, 2, 3, 4, 5, 6, 7];
// For each shift type and weekday, calculate staffing
const timetableShifts = shiftTypes.map(shiftType => {
const weekdayData: Record<number, string> = {};
weekdays.forEach(weekday => {
// Find all shifts of this type on this weekday
const shiftsOnDay = shiftPlan.shifts.filter(shift => {
const date = new Date(shift.date);
const dayOfWeek = date.getDay() === 0 ? 7 : date.getDay(); // Convert to 1-7 (Mon-Sun)
return dayOfWeek === weekday &&
shift.name === shiftType.name &&
shift.startTime === shiftType.startTime &&
shift.endTime === shiftType.endTime;
});
if (shiftsOnDay.length === 0) {
weekdayData[weekday] = '';
} else {
const totalAssigned = shiftsOnDay.reduce((sum, shift) => sum + shift.assignedEmployees.length, 0);
const totalRequired = shiftsOnDay.reduce((sum, shift) => sum + shift.requiredEmployees, 0);
weekdayData[weekday] = `${totalAssigned}/${totalRequired}`;
}
});
return {
...shiftType,
displayName: `${shiftType.name} (${formatTime(shiftType.startTime)}${formatTime(shiftType.endTime)})`,
weekdayData
};
});
return {
shifts: timetableShifts,
weekdays: weekdays.map(day => ({
id: day,
name: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'][day === 7 ? 0 : day]
}))
};
};
if (loading) {
return <div>Lade Schichtplan...</div>;
}
if (!shiftPlan) {
return <div>Schichtplan nicht gefunden</div>;
}
const timetableData = getTimetableData();
return (
<div style={{ padding: '20px' }}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '30px'
}}>
<div>
<h1>{shiftPlan.name}</h1>
<p style={{ color: '#666', marginTop: '5px' }}>
Zeitraum: {formatDate(shiftPlan.startDate)} - {formatDate(shiftPlan.endDate)}
</p>
<p style={{ color: '#666', marginTop: '5px' }}>
Status: <span style={{
color: shiftPlan.status === 'published' ? '#2ecc71' : '#f1c40f',
fontWeight: 'bold'
}}>
{shiftPlan.status === 'published' ? 'Veröffentlicht' : 'Entwurf'}
</span>
</p>
</div>
<div>
{hasRole(['admin', 'instandhalter']) && (
<button
onClick={() => navigate(`/shift-plans/${id}/edit`)}
style={{
padding: '8px 16px',
backgroundColor: '#f1c40f',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
marginRight: '10px'
}}
>
Bearbeiten
</button>
)}
<button
onClick={() => navigate('/shift-plans')}
style={{
padding: '8px 16px',
backgroundColor: '#95a5a6',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Zurück
</button>
</div>
</div>
<div style={{
backgroundColor: 'white',
borderRadius: '8px',
padding: '20px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
<div style={{ marginBottom: '20px' }}>
<div>Zeitraum: {formatDate(shiftPlan.startDate)} - {formatDate(shiftPlan.endDate)}</div>
<div>Status: <span style={{
color: shiftPlan.status === 'published' ? '#2ecc71' : '#f1c40f',
fontWeight: 'bold'
}}>
{shiftPlan.status === 'published' ? 'Veröffentlicht' : 'Entwurf'}
</span></div>
</div>
{/* Timetable */}
<div style={{ marginTop: '30px' }}>
<h3>Schichtplan</h3>
{timetableData.shifts.length === 0 ? (
<div style={{
textAlign: 'center',
padding: '40px',
color: '#666',
fontStyle: 'italic'
}}>
Keine Schichten für diesen Zeitraum konfiguriert
</div>
) : (
<div style={{
overflowX: 'auto',
marginTop: '20px'
}}>
<table style={{
width: '100%',
borderCollapse: 'collapse',
backgroundColor: 'white'
}}>
<thead>
<tr style={{ backgroundColor: '#f8f9fa' }}>
<th style={{
padding: '12px 16px',
textAlign: 'left',
border: '1px solid #dee2e6',
fontWeight: 'bold',
minWidth: '200px'
}}>
Schicht (Zeit)
</th>
{timetableData.weekdays.map(weekday => (
<th key={weekday.id} style={{
padding: '12px 16px',
textAlign: 'center',
border: '1px solid #dee2e6',
fontWeight: 'bold',
minWidth: '80px'
}}>
{weekday.name}
</th>
))}
</tr>
</thead>
<tbody>
{timetableData.shifts.map((shift, index) => (
<tr key={index} style={{
backgroundColor: index % 2 === 0 ? 'white' : '#f8f9fa'
}}>
<td style={{
padding: '12px 16px',
border: '1px solid #dee2e6',
fontWeight: '500'
}}>
{shift.displayName}
</td>
{timetableData.weekdays.map(weekday => (
<td key={weekday.id} style={{
padding: '12px 16px',
border: '1px solid #dee2e6',
textAlign: 'center',
color: shift.weekdayData[weekday.id] ? '#2c3e50' : '#bdc3c7'
}}>
{shift.weekdayData[weekday.id] || ''}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* Summary */}
{timetableData.shifts.length > 0 && (
<div style={{
marginTop: '20px',
padding: '12px 16px',
backgroundColor: '#e8f4fd',
borderRadius: '4px',
border: '1px solid #b8d4f0',
fontSize: '14px'
}}>
<strong>Legende:</strong> Angezeigt wird "zugewiesene/benötigte Mitarbeiter" pro Schicht und Wochentag
</div>
)}
</div>
</div>
);
};
export default ShiftPlanView;

View File

@@ -1,22 +1,20 @@
// frontend/src/pages/ShiftTemplates/ShiftTemplateEditor.tsx
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { ShiftTemplate, TemplateShift, DEFAULT_DAYS } from '../../types/shiftTemplate';
import { TemplateShiftSlot, TemplateShift, TemplateShiftTimeRange, DEFAULT_DAYS } from '../../types/shiftTemplate';
import { shiftTemplateService } from '../../services/shiftTemplateService';
import ShiftDayEditor from './components/ShiftDayEditor';
import DefaultTemplateView from './components/DefaultTemplateView';
import styles from './ShiftTemplateEditor.module.css';
interface ExtendedTemplateShift extends Omit<TemplateShift, 'id'> {
interface ExtendedTemplateShift extends Omit<TemplateShiftSlot, 'id'> {
id?: string;
isPreview?: boolean;
}
const defaultShift: ExtendedTemplateShift = {
dayOfWeek: 1, // Montag
name: '',
startTime: '08:00',
endTime: '12:00',
timeRange: { id: '', name: '', startTime: '', endTime: '' },
requiredEmployees: 1,
color: '#3498db'
};
@@ -26,7 +24,7 @@ const ShiftTemplateEditor: React.FC = () => {
const navigate = useNavigate();
const isEditing = !!id;
const [template, setTemplate] = useState<Omit<ShiftTemplate, 'id' | 'createdAt' | 'createdBy'>>({
const [template, setTemplate] = useState<Omit<TemplateShift, 'id' | 'createdAt' | 'createdBy'>>({
name: '',
description: '',
shifts: [],
@@ -83,11 +81,13 @@ const ShiftTemplateEditor: React.FC = () => {
};
const addShift = (dayOfWeek: number) => {
const newShift: TemplateShift = {
const newShift: TemplateShiftSlot = {
...defaultShift,
id: Date.now().toString(),
dayOfWeek,
name: `Schicht ${template.shifts.filter(s => s.dayOfWeek === dayOfWeek).length + 1}`
timeRange: { ...defaultShift.timeRange, id: Date.now().toString() },
requiredEmployees: defaultShift.requiredEmployees,
color: defaultShift.color
};
setTemplate(prev => ({
@@ -113,7 +113,7 @@ const ShiftTemplateEditor: React.FC = () => {
};
// Preview-Daten für die DefaultTemplateView vorbereiten
const previewTemplate: ShiftTemplate = {
const previewTemplate: TemplateShift = {
id: 'preview',
name: template.name || 'Vorschau',
description: template.description,

View File

@@ -1,17 +1,17 @@
// frontend/src/pages/ShiftTemplates/ShiftTemplateList.tsx
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { ShiftTemplate } from '../../types/shiftTemplate';
import { TemplateShift } from '../../types/shiftTemplate';
import { shiftTemplateService } from '../../services/shiftTemplateService';
import { useAuth } from '../../contexts/AuthContext';
import DefaultTemplateView from './components/DefaultTemplateView';
import styles from './ShiftTemplateList.module.css';
const ShiftTemplateList: React.FC = () => {
const [templates, setTemplates] = useState<ShiftTemplate[]>([]);
const [templates, setTemplates] = useState<TemplateShift[]>([]);
const [loading, setLoading] = useState(true);
const { hasRole } = useAuth();
const [selectedTemplate, setSelectedTemplate] = useState<ShiftTemplate | null>(null);
const [selectedTemplate, setSelectedTemplate] = useState<TemplateShift | null>(null);
useEffect(() => {
loadTemplates();

View File

@@ -1,10 +1,10 @@
// frontend/src/pages/ShiftTemplates/components/DefaultTemplateView.tsx
import React from 'react';
import { ShiftTemplate } from '../../../types/shiftTemplate';
import { TemplateShift } from '../../../types/shiftTemplate';
import styles from './DefaultTemplateView.module.css';
interface DefaultTemplateViewProps {
template: ShiftTemplate;
template: TemplateShift;
}
const DefaultTemplateView: React.FC<DefaultTemplateViewProps> = ({ template }) => {
@@ -38,9 +38,9 @@ const DefaultTemplateView: React.FC<DefaultTemplateViewProps> = ({ template }) =
<div className={styles.shiftsContainer}>
{shiftsByDay[dayIndex]?.map(shift => (
<div key={shift.id} className={styles.shiftCard}>
<h4>{shift.name}</h4>
<h4>{shift.timeRange.name}</h4>
<p>
{formatTime(shift.startTime)} - {formatTime(shift.endTime)}
{formatTime(shift.timeRange.startTime)} - {formatTime(shift.timeRange.endTime)}
</p>
</div>
))}

View File

@@ -1,13 +1,13 @@
// frontend/src/pages/ShiftTemplates/components/ShiftDayEditor.tsx
import React from 'react';
import { TemplateShift } from '../../../types/shiftTemplate';
import { TemplateShiftSlot } from '../../../types/shiftTemplate';
import styles from './ShiftDayEditor.module.css';
interface ShiftDayEditorProps {
day: { id: number; name: string };
shifts: TemplateShift[];
shifts: TemplateShiftSlot[];
onAddShift: () => void;
onUpdateShift: (shiftId: string, updates: Partial<TemplateShift>) => void;
onUpdateShift: (shiftId: string, updates: Partial<TemplateShiftSlot>) => void;
onRemoveShift: (shiftId: string) => void;
}
@@ -55,8 +55,8 @@ const ShiftDayEditor: React.FC<ShiftDayEditorProps> = ({
<div className={styles.formGroup}>
<input
type="text"
value={shift.name}
onChange={(e) => onUpdateShift(shift.id, { name: e.target.value })}
value={shift.timeRange.name}
onChange={(e) => onUpdateShift(shift.id, { timeRange: { ...shift.timeRange, name: e.target.value } })}
placeholder="Schichtname"
/>
</div>
@@ -66,8 +66,8 @@ const ShiftDayEditor: React.FC<ShiftDayEditorProps> = ({
<label>Start</label>
<input
type="time"
value={shift.startTime}
onChange={(e) => onUpdateShift(shift.id, { startTime: e.target.value })}
value={shift.timeRange.startTime}
onChange={(e) => onUpdateShift(shift.id, { timeRange: { ...shift.timeRange, startTime: e.target.value } })}
/>
</div>
@@ -75,8 +75,8 @@ const ShiftDayEditor: React.FC<ShiftDayEditorProps> = ({
<label>Ende</label>
<input
type="time"
value={shift.endTime}
onChange={(e) => onUpdateShift(shift.id, { endTime: e.target.value })}
value={shift.timeRange.endTime}
onChange={(e) => onUpdateShift(shift.id, { timeRange: { ...shift.timeRange, endTime: e.target.value } })}
/>
</div>
</div>

View File

@@ -50,7 +50,20 @@ export const shiftPlanService = {
throw new Error('Fehler beim Laden der Schichtpläne');
}
return response.json();
const data = await response.json();
// Convert snake_case to camelCase
return data.map((plan: any) => ({
id: plan.id,
name: plan.name,
startDate: plan.start_date, // Convert here
endDate: plan.end_date, // Convert here
templateId: plan.template_id,
status: plan.status,
createdBy: plan.created_by,
createdAt: plan.created_at,
shifts: plan.shifts || []
}));
},
async getShiftPlan(id: string): Promise<ShiftPlan> {
@@ -69,7 +82,20 @@ export const shiftPlanService = {
throw new Error('Schichtplan nicht gefunden');
}
return response.json();
const data = await response.json();
// Convert snake_case to camelCase
return {
id: data.id,
name: data.name,
startDate: data.start_date, // Convert here
endDate: data.end_date, // Convert here
templateId: data.template_id,
status: data.status,
createdBy: data.created_by,
createdAt: data.created_at,
shifts: data.shifts || []
};
},
async createShiftPlan(plan: CreateShiftPlanRequest): Promise<ShiftPlan> {
@@ -130,5 +156,60 @@ export const shiftPlanService = {
}
throw new Error('Fehler beim Löschen des Schichtplans');
}
},
async updateShiftPlanShift(planId: string, shift: ShiftPlanShift): Promise<void> {
const response = await fetch(`${API_BASE}/${planId}/shifts/${shift.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...authService.getAuthHeaders()
},
body: JSON.stringify(shift)
});
if (!response.ok) {
if (response.status === 401) {
authService.logout();
throw new Error('Nicht authorisiert - bitte erneut anmelden');
}
throw new Error('Fehler beim Aktualisieren der Schicht');
}
},
async addShiftPlanShift(planId: string, shift: Omit<ShiftPlanShift, 'id' | 'shiftPlanId' | 'assignedEmployees'>): Promise<void> {
const response = await fetch(`${API_BASE}/${planId}/shifts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...authService.getAuthHeaders()
},
body: JSON.stringify(shift)
});
if (!response.ok) {
if (response.status === 401) {
authService.logout();
throw new Error('Nicht authorisiert - bitte erneut anmelden');
}
throw new Error('Fehler beim Hinzufügen der Schicht');
}
},
async deleteShiftPlanShift(planId: string, shiftId: string): Promise<void> {
const response = await fetch(`${API_BASE}/${planId}/shifts/${shiftId}`, {
method: 'DELETE',
headers: {
...authService.getAuthHeaders()
}
});
if (!response.ok) {
if (response.status === 401) {
authService.logout();
throw new Error('Nicht authorisiert - bitte erneut anmelden');
}
throw new Error('Fehler beim Löschen der Schicht');
}
}
};

View File

@@ -1,11 +1,11 @@
// frontend/src/services/shiftTemplateService.ts
import { ShiftTemplate, TemplateShift } from '../types/shiftTemplate';
import { TemplateShift } from '../types/shiftTemplate';
import { authService } from './authService';
const API_BASE = 'http://localhost:3002/api/shift-templates';
const API_BASE = 'http://localhost:3001/api/shift-templates';
export const shiftTemplateService = {
async getTemplates(): Promise<ShiftTemplate[]> {
async getTemplates(): Promise<TemplateShift[]> {
const response = await fetch(API_BASE, {
headers: {
'Content-Type': 'application/json',
@@ -23,14 +23,14 @@ export const shiftTemplateService = {
const templates = await response.json();
// Sortiere die Vorlagen so, dass die Standard-Vorlage immer zuerst kommt
return templates.sort((a: ShiftTemplate, b: ShiftTemplate) => {
return templates.sort((a: TemplateShift, b: TemplateShift) => {
if (a.isDefault && !b.isDefault) return -1;
if (!a.isDefault && b.isDefault) return 1;
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
});
},
async getTemplate(id: string): Promise<ShiftTemplate> {
async getTemplate(id: string): Promise<TemplateShift> {
const response = await fetch(`${API_BASE}/${id}`, {
headers: {
'Content-Type': 'application/json',
@@ -49,7 +49,7 @@ export const shiftTemplateService = {
return response.json();
},
async createTemplate(template: Omit<ShiftTemplate, 'id' | 'createdAt' | 'createdBy'>): Promise<ShiftTemplate> {
async createTemplate(template: Omit<TemplateShift, 'id' | 'createdAt' | 'createdBy'>): Promise<TemplateShift> {
// Wenn diese Vorlage als Standard markiert ist,
// fragen wir den Benutzer, ob er wirklich die Standard-Vorlage ändern möchte
if (template.isDefault) {
@@ -81,7 +81,7 @@ export const shiftTemplateService = {
return response.json();
},
async updateTemplate(id: string, template: Partial<ShiftTemplate>): Promise<ShiftTemplate> {
async updateTemplate(id: string, template: Partial<TemplateShift>): Promise<TemplateShift> {
const response = await fetch(`${API_BASE}/${id}`, {
method: 'PUT',
headers: {

View File

@@ -35,4 +35,5 @@ export interface Availability {
startTime: string;
endTime: string;
isAvailable: boolean;
availabilityLevel: 1 | 2 | 3; // 1: bevorzugt, 2: möglich, 3: nicht möglich
}

View File

@@ -1,28 +1,41 @@
// frontend/src/types/shiftTemplate.ts
export interface ShiftTemplate {
export interface TemplateShift {
id: string;
name: string;
description?: string;
shifts: TemplateShift[];
isDefault: boolean;
createdBy: string;
createdAt: string;
isDefault: boolean;
shifts: TemplateShiftSlot[];
}
export interface TemplateShift {
export interface TemplateShiftSlot {
id: string;
dayOfWeek: number; // 1-5 (Montag=1, Dienstag=2, ...)
name: string;
startTime: string; // "08:00"
endTime: string; // "12:00"
dayOfWeek: number;
timeRange: TemplateShiftTimeRange;
requiredEmployees: number;
color?: string; // Für visuelle Darstellung
color?: string;
}
export interface TemplateShiftTimeRange {
id: string;
name: string; // e.g., "Frühschicht", "Spätschicht"
startTime: string;
endTime: string;
}
export const DEFAULT_TIME_SLOTS: TemplateShiftTimeRange[] = [
{ id: 'morning', name: 'Vormittag', startTime: '08:00', endTime: '12:00' },
{ id: 'afternoon', name: 'Nachmittag', startTime: '11:30', endTime: '15:30' },
];
export const DEFAULT_DAYS = [
{ id: 1, name: 'Montag' },
{ id: 2, name: 'Dienstag' },
{ id: 3, name: 'Donnerstag' },
{ id: 4, name: 'Mittwoch' },
{ id: 5, name: 'Freitag' },
{ id: 6, name: 'Samstag' },
{ id: 7, name: 'Sonntag' }
];