can add and delete shiftplans from pressets

This commit is contained in:
2025-10-12 20:53:03 +02:00
parent 1317781d7b
commit fda519d401
9 changed files with 181 additions and 193 deletions

View File

@@ -1,5 +1,5 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { Employee } from '../../../backend/src/models/employee'; import { Employee } from '../models/Employee';
interface LoginRequest { interface LoginRequest {
email: string; email: string;

View File

@@ -68,7 +68,7 @@ export const TEMPLATE_PRESETS = {
timeSlots: DEFAULT_ZEBRA_TIME_SLOTS, timeSlots: DEFAULT_ZEBRA_TIME_SLOTS,
shifts: DEFAULT_ZEBRA_SHIFTS shifts: DEFAULT_ZEBRA_SHIFTS
}, },
ZEBRA_MINIMAL: { /*ZEBRA_MINIMAL: {
name: 'ZEBRA Minimal', name: 'ZEBRA Minimal',
description: 'ZEBRA mit minimaler Besetzung', description: 'ZEBRA mit minimaler Besetzung',
timeSlots: DEFAULT_ZEBRA_TIME_SLOTS, timeSlots: DEFAULT_ZEBRA_TIME_SLOTS,
@@ -89,14 +89,14 @@ export const TEMPLATE_PRESETS = {
{ timeSlotId: 'afternoon', dayOfWeek: day, requiredEmployees: 3, color: '#e74c3c' } { timeSlotId: 'afternoon', dayOfWeek: day, requiredEmployees: 3, color: '#e74c3c' }
]) ])
] ]
}, },*/
GENERAL_STANDARD: { GENERAL_STANDARD: {
name: 'Standard Wochenplan', name: 'Standard Wochenplan',
description: 'Standard Vorlage: Mo-Fr Vormittag+Nachmittag+Abend', description: 'Standard Vorlage: Mo-Fr Vormittag+Nachmittag+Abend',
timeSlots: DEFAULT_TIME_SLOTS, timeSlots: DEFAULT_TIME_SLOTS,
shifts: DEFAULT_SHIFTS shifts: DEFAULT_SHIFTS
}, },
ZEBRA_PART_TIME: { /*ZEBRA_PART_TIME: {
name: 'ZEBRA Teilzeit', name: 'ZEBRA Teilzeit',
description: 'ZEBRA Vorlage mit reduzierten Schichten', description: 'ZEBRA Vorlage mit reduzierten Schichten',
timeSlots: DEFAULT_ZEBRA_TIME_SLOTS, timeSlots: DEFAULT_ZEBRA_TIME_SLOTS,
@@ -106,7 +106,7 @@ export const TEMPLATE_PRESETS = {
timeSlotId: 'morning', dayOfWeek: day, requiredEmployees: 1, color: '#3498db' timeSlotId: 'morning', dayOfWeek: day, requiredEmployees: 1, color: '#3498db'
})) }))
] ]
} } */
} as const; } as const;
// Helper function to create plan from preset // Helper function to create plan from preset

View File

@@ -1,15 +1,14 @@
// frontend/src/pages/ShiftPlans/ShiftPlanCreate.tsx // frontend/src/pages/ShiftPlans/ShiftPlanCreate.tsx
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { shiftTemplateService } from '../../services/shiftTemplateService';
import { shiftPlanService } from '../../services/shiftPlanService'; import { shiftPlanService } from '../../services/shiftPlanService';
import styles from './ShiftPlanCreate.module.css'; import styles from './ShiftPlanCreate.module.css';
import { TimeSlot, Shift } from '../../models/ShiftPlan';
export interface TemplateShift { // Interface für Template Presets
id: string; interface TemplatePreset {
name: string; name: string;
isDefault?: boolean; label: string;
description: string;
} }
const ShiftPlanCreate: React.FC = () => { const ShiftPlanCreate: React.FC = () => {
@@ -19,37 +18,31 @@ const ShiftPlanCreate: React.FC = () => {
const [planName, setPlanName] = useState(''); const [planName, setPlanName] = useState('');
const [startDate, setStartDate] = useState(''); const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState(''); const [endDate, setEndDate] = useState('');
const [selectedTemplate, setSelectedTemplate] = useState(''); const [selectedPreset, setSelectedPreset] = useState('');
const [templates, setTemplates] = useState<TemplateShift[]>([]); const [presets, setPresets] = useState<TemplatePreset[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
loadTemplates(); loadTemplatePresets();
}, []); }, []);
useEffect(() => { const loadTemplatePresets = async () => {
// Template aus URL-Parameter setzen, falls vorhanden
const templateId = searchParams.get('template');
if (templateId) {
setSelectedTemplate(templateId);
}
}, [searchParams]);
const loadTemplates = async () => {
try { try {
const data = await shiftTemplateService.getTemplates(); console.log('🔄 Lade verfügbare Vorlagen-Presets...');
setTemplates(data); const data = await shiftPlanService.getTemplatePresets();
console.log('✅ Presets geladen:', data);
// Wenn keine Template-ID in der URL ist, setze die Standard-Vorlage setPresets(data);
if (!searchParams.get('template')) {
if (!searchParams.get('template') && data.length > 0) { // Setze das erste Preset als Standard, falls vorhanden
setSelectedTemplate(data[0].id); if (data.length > 0) {
} setSelectedPreset(data[0].name);
} }
} catch (error) { } catch (error) {
console.error('Fehler beim Laden der Vorlagen:', error); console.error('Fehler beim Laden der Vorlagen-Presets:', error);
setError('Vorlagen konnten nicht geladen werden'); setError('Vorlagen-Presets konnten nicht geladen werden');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -57,6 +50,7 @@ const ShiftPlanCreate: React.FC = () => {
const handleCreate = async () => { const handleCreate = async () => {
try { try {
// Validierung
if (!planName.trim()) { if (!planName.trim()) {
setError('Bitte geben Sie einen Namen für den Schichtplan ein'); setError('Bitte geben Sie einen Namen für den Schichtplan ein');
return; return;
@@ -73,53 +67,53 @@ const ShiftPlanCreate: React.FC = () => {
setError('Das Enddatum muss nach dem Startdatum liegen'); setError('Das Enddatum muss nach dem Startdatum liegen');
return; return;
} }
if (!selectedPreset) {
let timeSlots: Omit<TimeSlot, 'id' | 'planId'>[] = []; setError('Bitte wählen Sie eine Vorlage aus');
let shifts: Omit<Shift, 'id' | 'planId'>[] = []; return;
// If a template is selected, load its data
if (selectedTemplate) {
try {
const template = await shiftTemplateService.getTemplate(selectedTemplate);
timeSlots = template.timeSlots.map(slot => ({
name: slot.name,
startTime: slot.startTime,
endTime: slot.endTime,
description: slot.description
}));
shifts = template.shifts.map(shift => ({
timeSlotId: shift.timeSlotId,
dayOfWeek: shift.dayOfWeek,
requiredEmployees: shift.requiredEmployees,
color: shift.color
}));
} catch (error) {
console.error('Fehler beim Laden der Vorlage:', error);
setError('Die ausgewählte Vorlage konnte nicht geladen werden');
return;
}
} }
await shiftPlanService.createShiftPlan({ console.log('🔄 Erstelle Schichtplan aus Preset...', {
presetName: selectedPreset,
name: planName, name: planName,
startDate, startDate,
endDate, endDate
isTemplate: false,
templateId: selectedTemplate || undefined,
timeSlots,
shifts
}); });
// Nach erfolgreicher Erstellung zur Liste der Schichtpläne navigieren // Erstelle den Plan aus dem ausgewählten Preset
navigate('/shift-plans'); const createdPlan = await shiftPlanService.createFromPreset({
presetName: selectedPreset,
name: planName,
startDate: startDate,
endDate: endDate,
isTemplate: false
});
console.log('✅ Plan erstellt:', createdPlan);
// Erfolgsmeldung und Weiterleitung
setSuccess('Schichtplan erfolgreich erstellt!');
setTimeout(() => {
navigate(`/shift-plans/${createdPlan.id}`);
}, 1500);
} catch (error) { } catch (error) {
console.error('Fehler beim Erstellen des Schichtplans:', error); const err = error as Error;
setError('Der Schichtplan konnte nicht erstellt werden. Bitte versuchen Sie es später erneut.'); console.error('❌ Fehler beim Erstellen des Plans:', err);
} setError(`Plan konnte nicht erstellt werden: ${err.message}`);
}
};
const getSelectedPresetDescription = () => {
const preset = presets.find(p => p.name === selectedPreset);
return preset ? preset.description : '';
}; };
if (loading) { if (loading) {
return <div>Lade Vorlagen...</div>; return (
<div className={styles.container}>
<div className={styles.loading}>Lade Vorlagen...</div>
</div>
);
} }
return ( return (
@@ -136,6 +130,12 @@ const ShiftPlanCreate: React.FC = () => {
{error} {error}
</div> </div>
)} )}
{success && (
<div className={styles.success}>
{success}
</div>
)}
<div className={styles.form}> <div className={styles.form}>
<div className={styles.formGroup}> <div className={styles.formGroup}>
@@ -145,6 +145,7 @@ const ShiftPlanCreate: React.FC = () => {
value={planName} value={planName}
onChange={(e) => setPlanName(e.target.value)} onChange={(e) => setPlanName(e.target.value)}
placeholder="z.B. KW 42 2025" placeholder="z.B. KW 42 2025"
className={styles.input}
/> />
</div> </div>
@@ -155,6 +156,7 @@ const ShiftPlanCreate: React.FC = () => {
type="date" type="date"
value={startDate} value={startDate}
onChange={(e) => setStartDate(e.target.value)} onChange={(e) => setStartDate(e.target.value)}
className={styles.input}
/> />
</div> </div>
@@ -164,6 +166,7 @@ const ShiftPlanCreate: React.FC = () => {
type="date" type="date"
value={endDate} value={endDate}
onChange={(e) => setEndDate(e.target.value)} onChange={(e) => setEndDate(e.target.value)}
className={styles.input}
/> />
</div> </div>
</div> </div>
@@ -171,29 +174,37 @@ const ShiftPlanCreate: React.FC = () => {
<div className={styles.formGroup}> <div className={styles.formGroup}>
<label>Vorlage verwenden:</label> <label>Vorlage verwenden:</label>
<select <select
value={selectedTemplate} value={selectedPreset}
onChange={(e) => setSelectedTemplate(e.target.value)} onChange={(e) => setSelectedPreset(e.target.value)}
className={templates.length === 0 ? styles.empty : ''} className={`${styles.select} ${presets.length === 0 ? styles.empty : ''}`}
> >
<option value="">Keine Vorlage</option> <option value="">Bitte wählen...</option>
{templates.map(template => ( {presets.map(preset => (
<option key={template.id} value={template.id}> <option key={preset.name} value={preset.name}>
{template.name} {template.isDefault ? '(Standard)' : ''} {preset.label}
</option> </option>
))} ))}
</select> </select>
{templates.length === 0 && (
{selectedPreset && (
<div className={styles.presetDescription}>
{getSelectedPresetDescription()}
</div>
)}
{presets.length === 0 && (
<p className={styles.noTemplates}> <p className={styles.noTemplates}>
Keine Vorlagen verfügbar. Keine Vorlagen verfügbar.
<button onClick={() => navigate('/shift-templates/new')} className={styles.linkButton}>
Neue Vorlage erstellen
</button>
</p> </p>
)} )}
</div> </div>
<div className={styles.actions}> <div className={styles.actions}>
<button onClick={handleCreate} className={styles.createButton} disabled={!selectedTemplate}> <button
onClick={handleCreate}
className={styles.createButton}
disabled={!selectedPreset || !planName.trim() || !startDate || !endDate}
>
Schichtplan erstellen Schichtplan erstellen
</button> </button>
</div> </div>

View File

@@ -3,7 +3,7 @@ import React, { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext'; import { useAuth } from '../../contexts/AuthContext';
import { shiftPlanService } from '../../services/shiftPlanService'; import { shiftPlanService } from '../../services/shiftPlanService';
import { ShiftPlan } from '../../../../backend/src/models/shiftPlan'; import { ShiftPlan } from '../../models/ShiftPlan';
import { useNotification } from '../../contexts/NotificationContext'; import { useNotification } from '../../contexts/NotificationContext';
import { formatDate } from '../../utils/foramatters'; import { formatDate } from '../../utils/foramatters';

View File

@@ -1,5 +1,5 @@
// frontend/src/services/authService.ts // frontend/src/services/authService.ts
import { Employee } from '../../../backend/src/models/employee'; import { Employee } from '../models/Employee';
const API_BASE = 'http://localhost:3002/api'; const API_BASE = 'http://localhost:3002/api';
export interface LoginRequest { export interface LoginRequest {

View File

@@ -1,5 +1,5 @@
// frontend/src/services/employeeService.ts // frontend/src/services/employeeService.ts
import { Employee, CreateEmployeeRequest, UpdateEmployeeRequest, EmployeeAvailability } from '../../../backend/src/models/employee'; import { Employee, CreateEmployeeRequest, UpdateEmployeeRequest, EmployeeAvailability } from '../models/Employee';
const API_BASE_URL = 'http://localhost:3002/api'; const API_BASE_URL = 'http://localhost:3002/api';

View File

@@ -1,9 +1,28 @@
// frontend/src/services/shiftPlanService.ts // frontend/src/services/shiftPlanService.ts
import { authService } from './authService'; import { authService } from './authService';
import { ShiftPlan, CreateShiftPlanRequest, Shift } from '../models/ShiftPlan'; import { ShiftPlan, CreateShiftPlanRequest, Shift, CreateShiftFromTemplateRequest } from '../models/ShiftPlan';
import { TEMPLATE_PRESETS } from '../models/defaults/shiftPlanDefaults';
const API_BASE = 'http://localhost:3002/api/shift-plans'; const API_BASE = 'http://localhost:3002/api/shift-plans';
// Helper function to get auth headers
const getAuthHeaders = () => {
const token = localStorage.getItem('token');
return {
'Content-Type': 'application/json',
...(token && { 'Authorization': `Bearer ${token}` })
};
};
// Helper function to handle responses
const handleResponse = async (response: Response) => {
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
return response.json();
};
export const shiftPlanService = { export const shiftPlanService = {
async getShiftPlans(): Promise<ShiftPlan[]> { async getShiftPlans(): Promise<ShiftPlan[]> {
const response = await fetch(API_BASE, { const response = await fetch(API_BASE, {
@@ -101,5 +120,70 @@ export const shiftPlanService = {
} }
throw new Error('Fehler beim Löschen des Schichtplans'); throw new Error('Fehler beim Löschen des Schichtplans');
} }
} },
getTemplates: async (): Promise<ShiftPlan[]> => {
const response = await fetch(`${API_BASE}/templates`, {
headers: getAuthHeaders()
});
return handleResponse(response);
},
// Get specific template or plan
getTemplate: async (id: string): Promise<ShiftPlan> => {
const response = await fetch(`${API_BASE}/${id}`, {
headers: getAuthHeaders()
});
return handleResponse(response);
},
// Create plan from template
createFromTemplate: async (data: CreateShiftFromTemplateRequest): Promise<ShiftPlan> => {
const response = await fetch(`${API_BASE}/from-template`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(data),
});
return handleResponse(response);
},
// Create new plan
createPlan: async (data: CreateShiftPlanRequest): Promise<ShiftPlan> => {
const response = await fetch(`${API_BASE}`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(data),
});
return handleResponse(response);
},
createFromPreset: async (data: {
presetName: string;
name: string;
startDate: string;
endDate: string;
isTemplate?: boolean;
}): Promise<ShiftPlan> => {
const response = await fetch(`${API_BASE}/from-preset`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(data),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
return response.json();
},
getTemplatePresets: async (): Promise<{name: string, label: string, description: string}[]> => {
// name = label
return Object.entries(TEMPLATE_PRESETS).map(([key, preset]) => ({
name: key,
label: preset.name,
description: preset.description
}));
},
}; };

View File

@@ -1,105 +0,0 @@
// frontend/src/services/shiftTemplateService.ts
import { ShiftPlan } from '../../../backend/src/models/shiftPlan.js';
import { authService } from './authService';
const API_BASE = 'http://localhost:3002/api/shift-templates';
export const shiftTemplateService = {
async getTemplates(): Promise<ShiftPlan[]> {
const response = await fetch(API_BASE, {
headers: {
'Content-Type': 'application/json',
...authService.getAuthHeaders()
}
});
if (!response.ok) {
if (response.status === 401) {
authService.logout();
throw new Error('Nicht authorisiert - bitte erneut anmelden');
}
throw new Error('Fehler beim Laden der Vorlagen');
}
const templates = await response.json();
return templates;
},
async getTemplate(id: string): Promise<ShiftPlan> {
const response = await fetch(`${API_BASE}/${id}`, {
headers: {
'Content-Type': 'application/json',
...authService.getAuthHeaders()
}
});
if (!response.ok) {
if (response.status === 401) {
authService.logout();
throw new Error('Nicht authorisiert - bitte erneut anmelden');
}
throw new Error('Vorlage nicht gefunden');
}
return response.json();
},
async createTemplate(template: Omit<ShiftPlan, 'id' | 'createdAt' | 'createdBy'>): Promise<ShiftPlan> {
const response = await fetch(API_BASE, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...authService.getAuthHeaders()
},
body: JSON.stringify(template)
});
if (!response.ok) {
if (response.status === 401) {
authService.logout();
throw new Error('Nicht authorisiert - bitte erneut anmelden');
}
throw new Error('Fehler beim Erstellen der Vorlage');
}
return response.json();
},
async updateTemplate(id: string, template: Partial<ShiftPlan>): Promise<ShiftPlan> {
const response = await fetch(`${API_BASE}/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...authService.getAuthHeaders()
},
body: JSON.stringify(template)
});
if (!response.ok) {
if (response.status === 401) {
authService.logout();
throw new Error('Nicht authorisiert - bitte erneut anmelden');
}
throw new Error('Fehler beim Aktualisieren der Vorlage');
}
return response.json();
},
async deleteTemplate(id: string): Promise<void> {
const response = await fetch(`${API_BASE}/${id}`, {
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 Vorlage');
}
}
};

View File

@@ -1,6 +1,4 @@
// frontend/src/shared/utils.ts // frontend/src/shared/utils.ts
// import { ScheduledShift } from '../../../backend/src/models/shiftPlan.js';
// Shared date and time formatting utilities // Shared date and time formatting utilities
export const formatDate = (dateString: string | undefined): string => { export const formatDate = (dateString: string | undefined): string => {
if (!dateString) return 'Kein Datum'; if (!dateString) return 'Kein Datum';