diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index 9b87313..d0ede86 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -1,5 +1,5 @@ import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; -import { Employee } from '../../../backend/src/models/employee'; +import { Employee } from '../models/Employee'; interface LoginRequest { email: string; diff --git a/frontend/src/models/defaults/shiftPlanDefaults.ts b/frontend/src/models/defaults/shiftPlanDefaults.ts index 5deacd5..afe02a6 100644 --- a/frontend/src/models/defaults/shiftPlanDefaults.ts +++ b/frontend/src/models/defaults/shiftPlanDefaults.ts @@ -68,7 +68,7 @@ export const TEMPLATE_PRESETS = { timeSlots: DEFAULT_ZEBRA_TIME_SLOTS, shifts: DEFAULT_ZEBRA_SHIFTS }, - ZEBRA_MINIMAL: { + /*ZEBRA_MINIMAL: { name: 'ZEBRA Minimal', description: 'ZEBRA mit minimaler Besetzung', timeSlots: DEFAULT_ZEBRA_TIME_SLOTS, @@ -89,14 +89,14 @@ export const TEMPLATE_PRESETS = { { timeSlotId: 'afternoon', dayOfWeek: day, requiredEmployees: 3, color: '#e74c3c' } ]) ] - }, + },*/ GENERAL_STANDARD: { name: 'Standard Wochenplan', description: 'Standard Vorlage: Mo-Fr Vormittag+Nachmittag+Abend', timeSlots: DEFAULT_TIME_SLOTS, shifts: DEFAULT_SHIFTS }, - ZEBRA_PART_TIME: { + /*ZEBRA_PART_TIME: { name: 'ZEBRA Teilzeit', description: 'ZEBRA Vorlage mit reduzierten Schichten', timeSlots: DEFAULT_ZEBRA_TIME_SLOTS, @@ -106,7 +106,7 @@ export const TEMPLATE_PRESETS = { timeSlotId: 'morning', dayOfWeek: day, requiredEmployees: 1, color: '#3498db' })) ] - } + } */ } as const; // Helper function to create plan from preset diff --git a/frontend/src/pages/ShiftPlans/ShiftPlanCreate.tsx b/frontend/src/pages/ShiftPlans/ShiftPlanCreate.tsx index d990aa9..a9f9b55 100644 --- a/frontend/src/pages/ShiftPlans/ShiftPlanCreate.tsx +++ b/frontend/src/pages/ShiftPlans/ShiftPlanCreate.tsx @@ -1,15 +1,14 @@ // frontend/src/pages/ShiftPlans/ShiftPlanCreate.tsx import React, { useState, useEffect } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; -import { shiftTemplateService } from '../../services/shiftTemplateService'; import { shiftPlanService } from '../../services/shiftPlanService'; import styles from './ShiftPlanCreate.module.css'; -import { TimeSlot, Shift } from '../../models/ShiftPlan'; -export interface TemplateShift { - id: string; +// Interface für Template Presets +interface TemplatePreset { name: string; - isDefault?: boolean; + label: string; + description: string; } const ShiftPlanCreate: React.FC = () => { @@ -19,37 +18,31 @@ const ShiftPlanCreate: React.FC = () => { const [planName, setPlanName] = useState(''); const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); - const [selectedTemplate, setSelectedTemplate] = useState(''); - const [templates, setTemplates] = useState([]); + const [selectedPreset, setSelectedPreset] = useState(''); + const [presets, setPresets] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); useEffect(() => { - loadTemplates(); + loadTemplatePresets(); }, []); - useEffect(() => { - // Template aus URL-Parameter setzen, falls vorhanden - const templateId = searchParams.get('template'); - if (templateId) { - setSelectedTemplate(templateId); - } - }, [searchParams]); - - const loadTemplates = async () => { + const loadTemplatePresets = async () => { try { - const data = await shiftTemplateService.getTemplates(); - setTemplates(data); + console.log('🔄 Lade verfügbare Vorlagen-Presets...'); + const data = await shiftPlanService.getTemplatePresets(); + console.log('✅ Presets geladen:', data); - // Wenn keine Template-ID in der URL ist, setze die Standard-Vorlage - if (!searchParams.get('template')) { - if (!searchParams.get('template') && data.length > 0) { - setSelectedTemplate(data[0].id); - } + setPresets(data); + + // Setze das erste Preset als Standard, falls vorhanden + if (data.length > 0) { + setSelectedPreset(data[0].name); } } catch (error) { - console.error('Fehler beim Laden der Vorlagen:', error); - setError('Vorlagen konnten nicht geladen werden'); + console.error('❌ Fehler beim Laden der Vorlagen-Presets:', error); + setError('Vorlagen-Presets konnten nicht geladen werden'); } finally { setLoading(false); } @@ -57,6 +50,7 @@ const ShiftPlanCreate: React.FC = () => { const handleCreate = async () => { try { + // Validierung if (!planName.trim()) { setError('Bitte geben Sie einen Namen für den Schichtplan ein'); return; @@ -73,53 +67,53 @@ const ShiftPlanCreate: React.FC = () => { setError('Das Enddatum muss nach dem Startdatum liegen'); return; } - - let timeSlots: Omit[] = []; - let shifts: Omit[] = []; - - // 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; - } + if (!selectedPreset) { + setError('Bitte wählen Sie eine Vorlage aus'); + return; } - await shiftPlanService.createShiftPlan({ + console.log('🔄 Erstelle Schichtplan aus Preset...', { + presetName: selectedPreset, name: planName, startDate, - endDate, - isTemplate: false, - templateId: selectedTemplate || undefined, - timeSlots, - shifts + endDate }); - // Nach erfolgreicher Erstellung zur Liste der Schichtpläne navigieren - navigate('/shift-plans'); + // Erstelle den Plan aus dem ausgewählten Preset + 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) { - console.error('Fehler beim Erstellen des Schichtplans:', error); - setError('Der Schichtplan konnte nicht erstellt werden. Bitte versuchen Sie es später erneut.'); - } + const err = error as Error; + 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) { - return
Lade Vorlagen...
; + return ( +
+
Lade Vorlagen...
+
+ ); } return ( @@ -136,6 +130,12 @@ const ShiftPlanCreate: React.FC = () => { {error} )} + + {success && ( +
+ {success} +
+ )}
@@ -145,6 +145,7 @@ const ShiftPlanCreate: React.FC = () => { value={planName} onChange={(e) => setPlanName(e.target.value)} placeholder="z.B. KW 42 2025" + className={styles.input} />
@@ -155,6 +156,7 @@ const ShiftPlanCreate: React.FC = () => { type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} + className={styles.input} />
@@ -164,6 +166,7 @@ const ShiftPlanCreate: React.FC = () => { type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} + className={styles.input} /> @@ -171,29 +174,37 @@ const ShiftPlanCreate: React.FC = () => {
- {templates.length === 0 && ( + + {selectedPreset && ( +
+ {getSelectedPresetDescription()} +
+ )} + + {presets.length === 0 && (

- Keine Vorlagen verfügbar. - + Keine Vorlagen verfügbar.

)}
-
diff --git a/frontend/src/pages/ShiftPlans/ShiftPlanList.tsx b/frontend/src/pages/ShiftPlans/ShiftPlanList.tsx index 9cd7e4a..d3e46a5 100644 --- a/frontend/src/pages/ShiftPlans/ShiftPlanList.tsx +++ b/frontend/src/pages/ShiftPlans/ShiftPlanList.tsx @@ -3,7 +3,7 @@ import React, { useState, useEffect } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { useAuth } from '../../contexts/AuthContext'; import { shiftPlanService } from '../../services/shiftPlanService'; -import { ShiftPlan } from '../../../../backend/src/models/shiftPlan'; +import { ShiftPlan } from '../../models/ShiftPlan'; import { useNotification } from '../../contexts/NotificationContext'; import { formatDate } from '../../utils/foramatters'; diff --git a/frontend/src/services/authService.ts b/frontend/src/services/authService.ts index a9fac60..03ec5fa 100644 --- a/frontend/src/services/authService.ts +++ b/frontend/src/services/authService.ts @@ -1,5 +1,5 @@ // frontend/src/services/authService.ts -import { Employee } from '../../../backend/src/models/employee'; +import { Employee } from '../models/Employee'; const API_BASE = 'http://localhost:3002/api'; export interface LoginRequest { diff --git a/frontend/src/services/employeeService.ts b/frontend/src/services/employeeService.ts index 672b20c..710d73d 100644 --- a/frontend/src/services/employeeService.ts +++ b/frontend/src/services/employeeService.ts @@ -1,5 +1,5 @@ // 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'; diff --git a/frontend/src/services/shiftPlanService.ts b/frontend/src/services/shiftPlanService.ts index 55fb404..e423ff2 100644 --- a/frontend/src/services/shiftPlanService.ts +++ b/frontend/src/services/shiftPlanService.ts @@ -1,9 +1,28 @@ // frontend/src/services/shiftPlanService.ts 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'; +// 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 = { async getShiftPlans(): Promise { const response = await fetch(API_BASE, { @@ -101,5 +120,70 @@ export const shiftPlanService = { } throw new Error('Fehler beim Löschen des Schichtplans'); } - } + }, + + getTemplates: async (): Promise => { + const response = await fetch(`${API_BASE}/templates`, { + headers: getAuthHeaders() + }); + return handleResponse(response); + }, + + // Get specific template or plan + getTemplate: async (id: string): Promise => { + const response = await fetch(`${API_BASE}/${id}`, { + headers: getAuthHeaders() + }); + return handleResponse(response); + }, + + // Create plan from template + createFromTemplate: async (data: CreateShiftFromTemplateRequest): Promise => { + 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 => { + 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 => { + 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 + })); + }, }; \ No newline at end of file diff --git a/frontend/src/services/shiftTemplateService.ts b/frontend/src/services/shiftTemplateService.ts deleted file mode 100644 index ede1cd7..0000000 --- a/frontend/src/services/shiftTemplateService.ts +++ /dev/null @@ -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 { - 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 { - 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): Promise { - 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): Promise { - 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 { - 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'); - } - } -}; \ No newline at end of file diff --git a/frontend/src/utils/foramatters.ts b/frontend/src/utils/foramatters.ts index 05c08b0..b86544c 100644 --- a/frontend/src/utils/foramatters.ts +++ b/frontend/src/utils/foramatters.ts @@ -1,6 +1,4 @@ // frontend/src/shared/utils.ts -// import { ScheduledShift } from '../../../backend/src/models/shiftPlan.js'; - // Shared date and time formatting utilities export const formatDate = (dateString: string | undefined): string => { if (!dateString) return 'Kein Datum';