frontend with ony errors

This commit is contained in:
2025-10-12 00:59:57 +02:00
parent 75d4d86ef3
commit 90d8ae5140
31 changed files with 869 additions and 1481 deletions

View File

@@ -1,6 +1,6 @@
// frontend/src/pages/Employees/EmployeeManagement.tsx
import React, { useState, useEffect } from 'react';
import { Employee } from '../../../../backend/src/models/employee';
import { Employee } from '../../models/Employee';
import { employeeService } from '../../services/employeeService';
import EmployeeList from './components/EmployeeList';
import EmployeeForm from './components/EmployeeForm';
@@ -220,7 +220,6 @@ const EmployeeManagement: React.FC = () => {
onEdit={handleEditEmployee}
onDelete={handleDeleteEmployee} // Jetzt mit Employee-Objekt
onManageAvailability={handleManageAvailability}
currentUserRole={hasRole(['admin']) ? 'admin' : 'instandhalter'}
/>
)}

View File

@@ -1,9 +1,9 @@
// frontend/src/pages/Employees/components/AvailabilityManager.tsx
import React, { useState, useEffect } from 'react';
import { Employee, EmployeeAvailability } from '../../../../../backend/src/models/employee';
import { employeeService } from '../../../services/employeeService';
import { shiftPlanService } from '../../../services/shiftPlanService';
import { ShiftPlan, TimeSlot, Shift } from '../../../../../backend/src/models/shiftPlan';
import { Employee, EmployeeAvailability } from '../../../models/Employee';
import { ShiftPlan, TimeSlot, Shift } from '../../../models/ShiftPlan';
interface AvailabilityManagerProps {
employee: Employee;
@@ -180,11 +180,13 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
// 3. Extract time slots from plans
let extractedTimeSlots = extractTimeSlotsFromPlans(plans);
// 4. Fallback to default slots if none found
/* 4. Fallback to default slots if none found
if (extractedTimeSlots.length === 0) {
console.log('⚠️ KEINE ZEIT-SLOTS GEFUNDEN, VERWENDE STANDARD-SLOTS');
extractedTimeSlots = getDefaultTimeSlots();
}
}*/
console.log('✅ GEFUNDENE ZEIT-SLOTS:', extractedTimeSlots.length, extractedTimeSlots);
setTimeSlots(extractedTimeSlots);
setShiftPlans(plans);

View File

@@ -1,6 +1,7 @@
// frontend/src/pages/Employees/components/EmployeeForm.tsx - KORRIGIERT
import React, { useState, useEffect } from 'react';
import { Employee, CreateEmployeeRequest, UpdateEmployeeRequest } from '../../../types/employee';
import { Employee, CreateEmployeeRequest, UpdateEmployeeRequest } from '../../../models/Employee';
import { ROLE_CONFIG, EMPLOYEE_TYPE_CONFIG } from '../../../models/defaults/employeeDefaults';
import { employeeService } from '../../../services/employeeService';
import { useAuth } from '../../../contexts/AuthContext';
@@ -11,34 +12,6 @@ interface EmployeeFormProps {
onCancel: () => void;
}
// Rollen Definition
const ROLE_OPTIONS = [
{ value: 'user', label: 'Mitarbeiter', description: 'Kann eigene Schichten einsehen' },
{ value: 'instandhalter', label: 'Instandhalter', description: 'Kann Schichtpläne erstellen und Mitarbeiter verwalten' },
{ value: 'admin', label: 'Administrator', description: 'Voller Zugriff auf alle Funktionen' }
] as const;
// Mitarbeiter Typen Definition
const EMPLOYEE_TYPE_OPTIONS = [
{
value: 'chef',
label: '👨‍💼 Chef/Administrator',
description: 'Vollzugriff auf alle Funktionen und Mitarbeiterverwaltung',
color: '#e74c3c'
},
{
value: 'erfahren',
label: '👴 Erfahren',
description: 'Langjährige Erfahrung, kann komplexe Aufgaben übernehmen',
color: '#3498db'
},
{
value: 'neuling',
label: '👶 Neuling',
description: 'Benötigt Einarbeitung und Unterstützung',
color: '#27ae60'
}
] as const;
const EmployeeForm: React.FC<EmployeeFormProps> = ({
mode,
@@ -50,9 +23,9 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
name: '',
email: '',
password: '',
role: 'user' as 'admin' | 'instandhalter' | 'user',
employeeType: 'neuling' as 'chef' | 'neuling' | 'erfahren',
isSufficientlyIndependent: false,
role: 'user' as 'admin' | 'maintenance' | 'user',
employeeType: 'trainee' as 'manager' | 'trainee' | 'experienced',
canWorkAlone: false,
isActive: true
});
const [loading, setLoading] = useState(false);
@@ -61,22 +34,20 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
useEffect(() => {
if (mode === 'edit' && employee) {
console.log('📝 Lade Mitarbeiter-Daten:', employee);
setFormData({
name: employee.name,
email: employee.email,
password: '', // Passwort wird beim Bearbeiten nicht angezeigt
role: employee.role,
employeeType: employee.employeeType,
isSufficientlyIndependent: employee.isSufficientlyIndependent,
canWorkAlone: employee.canWorkAlone,
isActive: employee.isActive
});
}
}, [mode, employee]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value, type } = e.target;
console.log(`🔄 Feld geändert: ${name} = ${value}`);
setFormData(prev => ({
...prev,
@@ -84,25 +55,14 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
}));
};
const handleRoleChange = (roleValue: 'admin' | 'instandhalter' | 'user') => {
console.log(`🔄 Rolle geändert: ${roleValue}`);
setFormData(prev => ({
...prev,
role: roleValue
}));
};
const handleEmployeeTypeChange = (employeeType: 'chef' | 'neuling' | 'erfahren') => {
console.log(`🔄 Mitarbeiter-Typ geändert: ${employeeType}`);
const handleEmployeeTypeChange = (employeeType: 'manager' | 'trainee' | 'experienced') => {
// Manager and experienced can work alone, trainee cannot
const canWorkAlone = employeeType === 'manager' || employeeType === 'experienced';
// Automatische Werte basierend auf Typ
const isSufficientlyIndependent = employeeType === 'chef' ? true :
employeeType === 'erfahren' ? true : false;
setFormData(prev => ({
...prev,
employeeType,
isSufficientlyIndependent
canWorkAlone
}));
};
@@ -111,8 +71,6 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
setLoading(true);
setError('');
console.log('📤 Sende Formulardaten:', formData);
try {
if (mode === 'create') {
const createData: CreateEmployeeRequest = {
@@ -121,52 +79,37 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
password: formData.password,
role: formData.role,
employeeType: formData.employeeType,
isSufficientlyIndependent: formData.isSufficientlyIndependent,
contractType: 'small', // Default value
canWorkAlone: formData.canWorkAlone
};
console.log(' Erstelle Mitarbeiter:', createData);
await employeeService.createEmployee(createData);
} else if (employee) {
const updateData: UpdateEmployeeRequest = {
name: formData.name.trim(),
role: formData.role,
employeeType: formData.employeeType,
isSufficientlyIndependent: formData.isSufficientlyIndependent,
contractType: employee.contractType, // Keep the existing contract type
canWorkAlone: formData.canWorkAlone,
isActive: formData.isActive,
};
console.log('✏️ Aktualisiere Mitarbeiter:', updateData);
await employeeService.updateEmployee(employee.id, updateData);
}
console.log('✅ Erfolg - rufe onSuccess auf');
onSuccess();
} catch (err: any) {
console.error('❌ Fehler beim Speichern:', err);
setError(err.message || `Fehler beim ${mode === 'create' ? 'Erstellen' : 'Aktualisieren'} des Mitarbeiters`);
} finally {
setLoading(false);
}
};
const isFormValid = () => {
if (mode === 'create') {
return formData.name.trim() &&
formData.email.trim() &&
formData.password.length >= 6;
}
return formData.name.trim() && formData.email.trim();
};
const isFormValid = mode === 'create'
? formData.name.trim() && formData.email.trim() && formData.password.length >= 6
: formData.name.trim() && formData.email.trim();
const getAvailableRoles = () => {
if (hasRole(['admin'])) {
return ROLE_OPTIONS;
}
if (hasRole(['instandhalter'])) {
return ROLE_OPTIONS.filter(role => role.value !== 'admin');
}
return ROLE_OPTIONS.filter(role => role.value === 'user');
};
const availableRoles = getAvailableRoles();
const availableRoles = hasRole(['admin'])
? ROLE_CONFIG
: ROLE_CONFIG.filter(role => role.value !== 'admin');
return (
<div style={{
@@ -294,7 +237,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
<h3 style={{ margin: '0 0 15px 0', color: '#495057' }}>👥 Mitarbeiter Kategorie</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{EMPLOYEE_TYPE_OPTIONS.map(type => (
{EMPLOYEE_TYPE_CONFIG.map(type => (
<div
key={type.value}
style={{
@@ -352,18 +295,6 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
</div>
))}
</div>
{/* Debug-Anzeige */}
<div style={{
marginTop: '15px',
padding: '10px',
backgroundColor: '#e8f4fd',
border: '1px solid #b6d7e8',
borderRadius: '4px',
fontSize: '12px'
}}>
<strong>Debug:</strong> Ausgewählter Typ: <code>{formData.employeeType}</code>
</div>
</div>
{/* Eigenständigkeit */}
@@ -386,29 +317,29 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
}}>
<input
type="checkbox"
name="isSufficientlyIndependent"
id="isSufficientlyIndependent"
checked={formData.isSufficientlyIndependent}
name="canWorkAlone"
id="canWorkAlone"
checked={formData.canWorkAlone}
onChange={handleChange}
disabled={formData.employeeType === 'chef'}
disabled={formData.employeeType === 'manager'}
style={{
width: '20px',
height: '20px',
opacity: formData.employeeType === 'chef' ? 0.5 : 1
opacity: formData.employeeType === 'manager' ? 0.5 : 1
}}
/>
<div style={{ flex: 1 }}>
<label htmlFor="isSufficientlyIndependent" style={{
<label htmlFor="canWorkAlone" style={{
fontWeight: 'bold',
color: '#2c3e50',
display: 'block',
opacity: formData.employeeType === 'chef' ? 0.5 : 1
opacity: formData.employeeType === 'manager' ? 0.5 : 1
}}>
Als ausreichend eigenständig markieren
{formData.employeeType === 'chef' && ' (Automatisch für Chefs)'}
{formData.employeeType === 'manager' && ' (Automatisch für Chefs)'}
</label>
<div style={{ fontSize: '14px', color: '#7f8c8d' }}>
{formData.employeeType === 'chef'
{formData.employeeType === 'manager'
? 'Chefs sind automatisch als eigenständig markiert.'
: 'Dieser Mitarbeiter kann komplexe Aufgaben eigenständig lösen und benötigt keine ständige Betreuung.'
}
@@ -416,14 +347,14 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
</div>
<div style={{
padding: '6px 12px',
backgroundColor: formData.isSufficientlyIndependent ? '#27ae60' : '#e74c3c',
backgroundColor: formData.canWorkAlone ? '#27ae60' : '#e74c3c',
color: 'white',
borderRadius: '15px',
fontSize: '12px',
fontWeight: 'bold',
opacity: formData.employeeType === 'chef' ? 0.7 : 1
opacity: formData.employeeType === 'manager' ? 0.7 : 1
}}>
{formData.isSufficientlyIndependent ? 'EIGENSTÄNDIG' : 'BETREUUNG'}
{formData.canWorkAlone ? 'EIGENSTÄNDIG' : 'BETREUUNG'}
</div>
</div>
</div>
@@ -451,14 +382,14 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
backgroundColor: formData.role === role.value ? '#fef9e7' : 'white',
cursor: 'pointer'
}}
onClick={() => handleRoleChange(role.value)}
onClick={() => setFormData(prev => ({ ...prev, role: role.value }))}
>
<input
type="radio"
name="role"
value={role.value}
checked={formData.role === role.value}
onChange={() => handleRoleChange(role.value)}
onChange={() => setFormData(prev => ({ ...prev, role: role.value }))}
style={{
marginRight: '10px',
marginTop: '2px'
@@ -537,14 +468,14 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
<button
type="submit"
disabled={loading || !isFormValid()}
disabled={loading || !isFormValid}
style={{
padding: '12px 24px',
backgroundColor: loading ? '#bdc3c7' : (isFormValid() ? '#27ae60' : '#95a5a6'),
backgroundColor: loading ? '#bdc3c7' : (isFormValid ? '#27ae60' : '#95a5a6'),
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: (loading || !isFormValid()) ? 'not-allowed' : 'pointer',
cursor: (loading || !isFormValid) ? 'not-allowed' : 'pointer',
fontWeight: 'bold'
}}
>

View File

@@ -1,6 +1,7 @@
// frontend/src/pages/Employees/components/EmployeeList.tsx - KORRIGIERT
import React, { useState } from 'react';
import { Employee } from '../../../types/employee';
import { ROLE_CONFIG, EMPLOYEE_TYPE_CONFIG } from '../../../../../backend/src/models/defaults/employeeDefaults';
import { Employee } from '../../../../../backend/src/models/employee';
import { useAuth } from '../../../contexts/AuthContext';
interface EmployeeListProps {
@@ -8,26 +9,22 @@ interface EmployeeListProps {
onEdit: (employee: Employee) => void;
onDelete: (employee: Employee) => void;
onManageAvailability: (employee: Employee) => void;
currentUserRole: 'admin' | 'instandhalter';
}
const EmployeeList: React.FC<EmployeeListProps> = ({
employees,
onEdit,
onDelete,
onManageAvailability,
currentUserRole
onManageAvailability
}) => {
const [filter, setFilter] = useState<'all' | 'active' | 'inactive'>('active');
const [searchTerm, setSearchTerm] = useState('');
const { user: currentUser } = useAuth();
const { user: currentUser, hasRole } = useAuth();
const filteredEmployees = employees.filter(employee => {
// Status-Filter
if (filter === 'active' && !employee.isActive) return false;
if (filter === 'inactive' && employee.isActive) return false;
// Suchfilter
if (searchTerm) {
const term = searchTerm.toLowerCase();
return (
@@ -41,28 +38,25 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
return true;
});
const getRoleBadgeColor = (role: string) => {
switch (role) {
case 'admin': return '#e74c3c';
case 'instandhalter': return '#3498db';
case 'user': return '#27ae60';
default: return '#95a5a6';
}
// Simplified permission checks
const canDeleteEmployee = (employee: Employee): boolean => {
if (!hasRole(['admin'])) return false;
if (employee.id === currentUser?.id) return false;
if (employee.role === 'admin' && !hasRole(['admin'])) return false;
return true;
};
const getEmployeeTypeBadge = (type: string) => {
switch (type) {
case 'chef': return { text: '👨‍💼 CHEF', color: '#e74c3c', bgColor: '#fadbd8' };
case 'erfahren': return { text: '👴 ERFAHREN', color: '#3498db', bgColor: '#d6eaf8' };
case 'neuling': return { text: '👶 NEULING', color: '#27ae60', bgColor: '#d5f4e6' };
default: return { text: 'UNBEKANNT', color: '#95a5a6', bgColor: '#ecf0f1' };
const canEditEmployee = (employee: Employee): boolean => {
if (hasRole(['admin'])) return true;
if (hasRole(['maintenance'])) {
return employee.role === 'user' || employee.id === currentUser?.id;
}
return false;
};
const getIndependenceBadge = (isIndependent: boolean) => {
return isIndependent
? { text: '✅ Eigenständig', color: '#27ae60', bgColor: '#d5f4e6' }
: { text: '❌ Betreuung', color: '#e74c3c', bgColor: '#fadbd8' };
// Using shared configuration for consistent styling
const getEmployeeTypeBadge = (type: keyof typeof EMPLOYEE_TYPE_CONFIG) => {
return EMPLOYEE_TYPE_CONFIG[type] || EMPLOYEE_TYPE_CONFIG.trainee;
};
const getStatusBadge = (isActive: boolean) => {
@@ -71,31 +65,10 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
: { text: 'Inaktiv', color: '#e74c3c', bgColor: '#fadbd8' };
};
// Kann Benutzer löschen?
const canDeleteEmployee = (employee: Employee): boolean => {
// Nur Admins können löschen
if (currentUserRole !== 'admin') return false;
// Kann sich nicht selbst löschen
if (employee.id === currentUser?.id) return false;
// Admins können nur von Admins gelöscht werden
if (employee.role === 'admin' && currentUserRole !== 'admin') return false;
return true;
};
// Kann Benutzer bearbeiten?
const canEditEmployee = (employee: Employee): boolean => {
// Admins können alle bearbeiten
if (currentUserRole === 'admin') return true;
// Instandhalter können nur User und sich selbst bearbeiten
if (currentUserRole === 'instandhalter') {
return employee.role === 'user' || employee.id === currentUser?.id;
}
return false;
const getIndependenceBadge = (canWorkAlone: boolean) => {
return canWorkAlone
? { text: '✅ Eigenständig', color: '#27ae60', bgColor: '#d5f4e6' }
: { text: '❌ Betreuung', color: '#e74c3c', bgColor: '#fadbd8' };
};
if (employees.length === 0) {
@@ -197,8 +170,8 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
{filteredEmployees.map(employee => {
const employeeType = getEmployeeTypeBadge(employee.employeeType);
const independence = getIndependenceBadge(employee.isSufficientlyIndependent);
const roleColor = getRoleBadgeColor(employee.role);
const independence = getIndependenceBadge(employee.canWorkAlone);
const roleColor = '#d5f4e6'; // Default color
const status = getStatusBadge(employee.isActive);
const canEdit = canEditEmployee(employee);
const canDelete = canDeleteEmployee(employee);
@@ -239,7 +212,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
<div style={{ textAlign: 'center' }}>
<span
style={{
backgroundColor: employeeType.bgColor,
backgroundColor: employeeType.color,
color: employeeType.color,
padding: '6px 12px',
borderRadius: '15px',
@@ -248,7 +221,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
display: 'inline-block'
}}
>
{employeeType.text}
{employeeType.label}
</span>
</div>
@@ -284,7 +257,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
}}
>
{employee.role === 'admin' ? 'ADMIN' :
employee.role === 'instandhalter' ? 'INSTANDHALTER' : 'MITARBEITER'}
employee.role === 'maintenance' ? 'INSTANDHALTER' : 'MITARBEITER'}
</span>
</div>
@@ -322,7 +295,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
flexWrap: 'wrap'
}}>
{/* Verfügbarkeit Button */}
{(currentUserRole === 'admin' || currentUserRole === 'instandhalter') && (
{(employee.role === 'admin' || employee.role === 'maintenance') && (
<button
onClick={() => onManageAvailability(employee)}
style={{
@@ -385,7 +358,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
)}
{/* Platzhalter für Symmetrie */}
{!canEdit && !canDelete && (currentUserRole !== 'admin' && currentUserRole !== 'instandhalter') && (
{!canEdit && !canDelete && (employee.role !== 'admin' && employee.role !== 'maintenance') && (
<div style={{ width: '32px', height: '32px' }}></div>
)}
</div>

View File

@@ -3,9 +3,14 @@ import React, { useState, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { shiftTemplateService } from '../../services/shiftTemplateService';
import { shiftPlanService } from '../../services/shiftPlanService';
import { TemplateShift } from '../../types/shiftTemplate';
import styles from './ShiftPlanCreate.module.css';
export interface TemplateShift {
id: string;
name: string;
isDefault?: boolean;
}
const ShiftPlanCreate: React.FC = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();

View File

@@ -1,8 +1,10 @@
// 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 { shiftPlanService } from '../../services/shiftPlanService';
import { ShiftPlan, Shift } from '../../../../backend/src/models/shiftPlan';
import { useNotification } from '../../contexts/NotificationContext';
import { getTimeSlotById } from '../../models/helpers/shiftPlanHelpers';
const ShiftPlanEdit: React.FC = () => {
const { id } = useParams<{ id: string }>();
@@ -10,12 +12,10 @@ const ShiftPlanEdit: React.FC = () => {
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>>({
const [editingShift, setEditingShift] = useState<Shift | null>(null);
const [newShift, setNewShift] = useState<Partial<ScheduledShift>>({
date: '',
name: '',
startTime: '',
endTime: '',
timeSlotId: '',
requiredEmployees: 1
});
@@ -41,16 +41,10 @@ const ShiftPlanEdit: React.FC = () => {
}
};
const handleUpdateShift = async (shift: ShiftPlanShift) => {
const handleUpdateShift = async (shift: Shift) => {
if (!shiftPlan || !id) return;
try {
await shiftPlanService.updateShiftPlanShift(id, shift);
showNotification({
type: 'success',
title: 'Erfolg',
message: 'Schicht wurde aktualisiert.'
});
loadShiftPlan();
setEditingShift(null);
} catch (error) {
@@ -66,23 +60,16 @@ const ShiftPlanEdit: React.FC = () => {
const handleAddShift = async () => {
if (!shiftPlan || !id) return;
if (!newShift.date || !newShift.name || !newShift.startTime || !newShift.endTime || !newShift.requiredEmployees) {
if (!getTimeSlotById(shiftPlan, newShift.timeSlotId?) || !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',
@@ -112,12 +99,6 @@ const ShiftPlanEdit: React.FC = () => {
}
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);

View File

@@ -2,8 +2,10 @@
import React, { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
import { shiftPlanService, ShiftPlan } from '../../services/shiftPlanService';
import { shiftPlanService } from '../../services/shiftPlanService';
import { ShiftPlan } from '../../../../backend/src/models/shiftPlan';
import { useNotification } from '../../contexts/NotificationContext';
import { formatDate } from '../../utils/foramatters';
const ShiftPlanList: React.FC = () => {
const { hasRole } = useAuth();
@@ -55,14 +57,6 @@ const ShiftPlanList: React.FC = () => {
}
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
};
if (loading) {
return <div>Lade Schichtpläne...</div>;
}

View File

@@ -3,8 +3,10 @@ import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
import { shiftPlanService } from '../../services/shiftPlanService';
import { ShiftPlan, Shift, TimeSlot } from '../../../../backend/src/models/shiftPlan.js';
import { getTimeSlotById } from '../../models/helpers/shiftPlanHelpers';
import { ShiftPlan, TimeSlot } from '../../models/ShiftPlan';
import { useNotification } from '../../contexts/NotificationContext';
import { formatDate, formatTime } from '../../utils/foramatters';
const ShiftPlanView: React.FC = () => {
const { id } = useParams<{ id: string }>();
@@ -36,94 +38,44 @@ const ShiftPlanView: React.FC = () => {
}
};
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
// Simplified timetable data generation
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.timeSlot.name}|${shift.timeSlot.startTime}|${shift.timeSlot.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 => {
// Use timeSlots directly since shifts reference them
const timetableShifts = shiftPlan.timeSlots.map(timeSlot => {
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.timeSlot.name === shiftType.name &&
shift.timeSlot.startTime === shiftType.startTime &&
shift.timeSlot.endTime === shiftType.endTime;
});
const shiftsOnDay = shiftPlan.shifts.filter(shift =>
shift.dayOfWeek === weekday.id &&
shift.timeSlotId === timeSlot.id
);
if (shiftsOnDay.length === 0) {
weekdayData[weekday] = '';
weekdayData[weekday.id] = '';
} else {
const totalAssigned = shiftsOnDay.reduce((sum, shift) => sum + shift.timeSlot.assignedEmployees.length, 0);
const totalRequired = shiftsOnDay.reduce((sum, shift) => sum + shift.requiredEmployees, 0);
weekdayData[weekday] = `${totalAssigned}/${totalRequired}`;
const totalRequired = shiftsOnDay.reduce((sum, shift) =>
sum + shift.requiredEmployees, 0);
// For now, show required count since we don't have assigned employees in Shift
weekdayData[weekday.id] = `0/${totalRequired}`;
}
});
return {
...shiftType,
displayName: `${shiftType.name} (${formatTime(shiftType.startTime)}${formatTime(shiftType.endTime)})`,
...timeSlot,
displayName: `${timeSlot.name} (${formatTime(timeSlot.startTime)}${formatTime(timeSlot.endTime)})`,
weekdayData
};
});
return {
shifts: timetableShifts,
weekdays: weekdays.map(day => ({
id: day,
name: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'][day === 7 ? 0 : day]
}))
};
return { shifts: timetableShifts, weekdays };
};
if (loading) {
return <div>Lade Schichtplan...</div>;
}
if (!shiftPlan) {
return <div>Schichtplan nicht gefunden</div>;
}
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={{

View File

@@ -1,97 +0,0 @@
.editorContainer {
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.title {
font-size: 24px;
color: #2c3e50;
margin: 0;
}
.buttons {
display: flex;
gap: 10px;
}
.previewButton {
padding: 8px 16px;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.previewButton:hover {
background-color: #2980b9;
}
.saveButton {
padding: 8px 16px;
background-color: #2ecc71;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.saveButton:hover {
background-color: #27ae60;
}
.formGroup {
margin-bottom: 20px;
}
.formGroup label {
display: block;
margin-bottom: 8px;
color: #34495e;
font-weight: 500;
}
.formGroup input[type="text"],
.formGroup textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #bdc3c7;
border-radius: 4px;
font-size: 14px;
}
.formGroup textarea {
min-height: 100px;
resize: vertical;
}
.defaultCheckbox {
margin-top: 15px;
display: flex;
align-items: center;
gap: 8px;
}
.defaultCheckbox input[type="checkbox"] {
width: 16px;
height: 16px;
}
.defaultCheckbox label {
margin: 0;
font-size: 14px;
}
.previewContainer {
margin-top: 30px;
border-top: 1px solid #ddd;
padding-top: 20px;
}

View File

@@ -1,206 +0,0 @@
// frontend/src/pages/ShiftTemplates/ShiftTemplateEditor.tsx
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { TemplateShiftSlot, TemplateShift, TemplateShiftTimeSlot, 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<TemplateShiftSlot, 'id'> {
id?: string;
isPreview?: boolean;
}
const defaultShift: ExtendedTemplateShift = {
dayOfWeek: 1, // Montag
timeSlot: { id: '', name: '', startTime: '', endTime: '' },
requiredEmployees: 1,
color: '#3498db'
};
const ShiftTemplateEditor: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const isEditing = !!id;
const [template, setTemplate] = useState<Omit<TemplateShift, 'id' | 'createdAt' | 'createdBy'>>({
name: '',
description: '',
shifts: [],
isDefault: false
});
const [loading, setLoading] = useState(isEditing);
const [saving, setSaving] = useState(false);
const [showPreview, setShowPreview] = useState(false);
useEffect(() => {
if (isEditing) {
loadTemplate();
}
}, [id]);
const loadTemplate = async () => {
try {
if (!id) return;
const data = await shiftTemplateService.getTemplate(id);
setTemplate({
name: data.name,
description: data.description,
shifts: data.shifts,
isDefault: data.isDefault
});
} catch (error) {
console.error('Fehler beim Laden:', error);
alert('Vorlage konnte nicht geladen werden');
} finally {
setLoading(false);
}
};
const handleSave = async () => {
if (!template.name.trim()) {
alert('Bitte geben Sie einen Namen für die Vorlage ein');
return;
}
setSaving(true);
try {
if (isEditing && id) {
await shiftTemplateService.updateTemplate(id, template);
} else {
await shiftTemplateService.createTemplate(template);
}
navigate('/shift-templates');
} catch (error) {
console.error('Speichern fehlgeschlagen:', error);
alert('Fehler beim Speichern der Vorlage');
} finally {
setSaving(false);
}
};
const addShift = (dayOfWeek: number) => {
const newShift: TemplateShiftSlot = {
...defaultShift,
id: Date.now().toString(),
dayOfWeek,
timeSlot: { ...defaultShift.timeSlot, id: Date.now().toString() },
requiredEmployees: defaultShift.requiredEmployees,
color: defaultShift.color
};
setTemplate(prev => ({
...prev,
shifts: [...prev.shifts, newShift]
}));
};
const updateShift = (shiftId: string, updates: Partial<TemplateShift>) => {
setTemplate(prev => ({
...prev,
shifts: prev.shifts.map(shift =>
shift.id === shiftId ? { ...shift, ...updates } : shift
)
}));
};
const removeShift = (shiftId: string) => {
setTemplate(prev => ({
...prev,
shifts: prev.shifts.filter(shift => shift.id !== shiftId)
}));
};
// Preview-Daten für die DefaultTemplateView vorbereiten
const previewTemplate: TemplateShift = {
id: 'preview',
name: template.name || 'Vorschau',
description: template.description,
shifts: template.shifts.map(shift => ({
...shift,
id: shift.id || 'preview-' + Date.now()
})),
createdBy: 'preview',
createdAt: new Date().toISOString(),
isDefault: template.isDefault
};
if (loading) return <div>Lade Vorlage...</div>;
return (
<div className={styles.editorContainer}>
<div className={styles.header}>
<h1 className={styles.title}>{isEditing ? 'Vorlage bearbeiten' : 'Neue Vorlage erstellen'}</h1>
<div className={styles.buttons}>
<button
className={styles.previewButton}
onClick={() => setShowPreview(!showPreview)}
>
{showPreview ? 'Editor anzeigen' : 'Vorschau'}
</button>
<button
className={styles.saveButton}
onClick={handleSave}
disabled={saving}
>
{saving ? 'Speichern...' : 'Speichern'}
</button>
</div>
</div>
{showPreview ? (
<DefaultTemplateView template={previewTemplate} />
) : (
<>
<div className={styles.formGroup}>
<label>Vorlagenname *</label>
<input
type="text"
value={template.name}
onChange={(e) => setTemplate(prev => ({ ...prev, name: e.target.value }))}
placeholder="z.B. Standard Woche, Teilzeit Modell, etc."
/>
</div>
<div className={styles.formGroup}>
<label>Beschreibung</label>
<textarea
value={template.description || ''}
onChange={(e) => setTemplate(prev => ({ ...prev, description: e.target.value }))}
placeholder="Beschreibung der Vorlage (optional)"
/>
</div>
<div className={styles.defaultCheckbox}>
<input
type="checkbox"
id="isDefault"
checked={template.isDefault}
onChange={(e) => setTemplate(prev => ({ ...prev, isDefault: e.target.checked }))}
/>
<label htmlFor="isDefault">Als Standardvorlage festlegen</label>
</div>
<div style={{ marginTop: '30px' }}>
<h2>Schichten pro Wochentag</h2>
<div style={{ display: 'grid', gap: '20px', marginTop: '20px' }}>
{DEFAULT_DAYS.map(day => (
<ShiftDayEditor
key={day.id}
day={day}
shifts={template.shifts.filter(s => s.dayOfWeek === day.id)}
onAddShift={() => addShift(day.id)}
onUpdateShift={updateShift}
onRemoveShift={removeShift}
/>
))}
</div>
</div>
</>
)}
</div>
);
};
export default ShiftTemplateEditor;

View File

@@ -1,101 +0,0 @@
.templateList {
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.createButton {
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.createButton:hover {
background-color: #0056b3;
}
.templateGrid {
display: grid;
gap: 15px;
}
.templateCard {
border: 1px solid #ddd;
padding: 15px;
border-radius: 8px;
background-color: #f9f9f9;
}
.templateHeader {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.templateInfo h3 {
margin: 0 0 5px 0;
}
.templateInfo p {
margin: 0 0 10px 0;
color: #666;
}
.templateMeta {
font-size: 14px;
color: #888;
}
.defaultBadge {
color: green;
margin-left: 10px;
}
.actionButtons {
display: flex;
gap: 10px;
}
.viewButton {
padding: 5px 10px;
border: 1px solid #007bff;
color: #007bff;
background: white;
cursor: pointer;
}
.useButton {
padding: 5px 10px;
background-color: #28a745;
color: white;
border: none;
cursor: pointer;
}
.deleteButton {
padding: 5px 10px;
background-color: #dc3545;
color: white;
border: none;
cursor: pointer;
}
.viewButton:hover {
background-color: #e6f0ff;
}
.useButton:hover {
background-color: #218838;
}
.deleteButton:hover {
background-color: #c82333;
}

View File

@@ -1,136 +0,0 @@
// frontend/src/pages/ShiftTemplates/ShiftTemplateList.tsx
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
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<TemplateShift[]>([]);
const [loading, setLoading] = useState(true);
const { hasRole } = useAuth();
const [selectedTemplate, setSelectedTemplate] = useState<TemplateShift | null>(null);
useEffect(() => {
loadTemplates();
}, []);
const loadTemplates = async () => {
try {
const data = await shiftTemplateService.getTemplates();
setTemplates(data);
// Setze die Standard-Vorlage als ausgewählt
const defaultTemplate = data.find(t => t.isDefault);
if (defaultTemplate) {
setSelectedTemplate(defaultTemplate);
}
} catch (error) {
console.error('Fehler:', error);
} finally {
setLoading(false);
}
};
const handleDelete = async (id: string) => {
if (!window.confirm('Vorlage wirklich löschen?')) return;
try {
await shiftTemplateService.deleteTemplate(id);
setTemplates(templates.filter(t => t.id !== id));
if (selectedTemplate?.id === id) {
setSelectedTemplate(null);
}
} catch (error) {
console.error('Löschen fehlgeschlagen:', error);
}
};
if (loading) return <div>Lade Vorlagen...</div>;
return (
<div className={styles.templateList}>
<div className={styles.header}>
<h1>Schichtplan Vorlagen</h1>
{hasRole(['admin', 'instandhalter']) && (
<Link to="/shift-templates/new">
<button className={styles.createButton}>
Neue Vorlage
</button>
</Link>
)}
</div>
<div className={styles.templateGrid}>
{templates.length === 0 ? (
<div style={{ textAlign: 'center', padding: '40px', color: '#666' }}>
<p>Noch keine Vorlagen vorhanden.</p>
{hasRole(['admin', 'instandhalter']) && (
<Link to="/shift-templates/new">
<button className={styles.createButton}>
Erste Vorlage erstellen
</button>
</Link>
)}
</div>
) : (
templates.map(template => (
<div key={template.id} className={styles.templateCard}>
<div className={styles.templateHeader}>
<div className={styles.templateInfo}>
<h3>{template.name}</h3>
{template.description && (
<p>{template.description}</p>
)}
<div className={styles.templateMeta}>
{template.shifts.length} Schichttypen Erstellt am {new Date(template.createdAt).toLocaleDateString('de-DE')}
{template.isDefault && <span className={styles.defaultBadge}> Standard</span>}
</div>
</div>
<div className={styles.actionButtons}>
<button
className={styles.viewButton}
onClick={() => setSelectedTemplate(template)}
>
Vorschau
</button>
{hasRole(['admin', 'instandhalter']) && (
<>
<Link to={`/shift-templates/${template.id}`}>
<button className={styles.viewButton}>
Bearbeiten
</button>
</Link>
<Link to={`/shift-plans/new?template=${template.id}`}>
<button className={styles.useButton}>
Verwenden
</button>
</Link>
<button
onClick={() => handleDelete(template.id)}
className={styles.deleteButton}
>
Löschen
</button>
</>
)}
</div>
</div>
</div>
))
)}
</div>
{selectedTemplate && (
<div style={{ marginTop: '30px' }}>
<DefaultTemplateView template={selectedTemplate} />
</div>
)}
</div>
);
};
export default ShiftTemplateList;

View File

@@ -1,48 +0,0 @@
.defaultTemplateView {
padding: 20px;
}
.weekView {
display: flex;
gap: 20px;
margin-top: 20px;
overflow-x: auto;
}
.dayColumn {
min-width: 200px;
background: #f5f6fa;
padding: 15px;
border-radius: 8px;
}
.dayColumn h3 {
margin: 0 0 15px 0;
color: #2c3e50;
text-align: center;
}
.shiftsContainer {
display: flex;
flex-direction: column;
gap: 10px;
}
.shiftCard {
background: white;
padding: 12px;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.shiftCard h4 {
margin: 0 0 8px 0;
color: #34495e;
font-size: 0.9em;
}
.shiftCard p {
margin: 0;
color: #7f8c8d;
font-size: 0.85em;
}

View File

@@ -1,55 +0,0 @@
// frontend/src/pages/ShiftTemplates/components/DefaultTemplateView.tsx
import React from 'react';
import { TemplateShift } from '../../../types/shiftTemplate';
import styles from './DefaultTemplateView.module.css';
interface DefaultTemplateViewProps {
template: TemplateShift;
}
const DefaultTemplateView: React.FC<DefaultTemplateViewProps> = ({ template }) => {
// Gruppiere Schichten nach Wochentag
const shiftsByDay = template.shifts.reduce((acc, shift) => {
const day = shift.dayOfWeek;
if (!acc[day]) {
acc[day] = [];
}
acc[day].push(shift);
return acc;
}, {} as Record<number, typeof template.shifts>);
// Funktion zum Formatieren der Zeit
const formatTime = (time: string) => {
return time.substring(0, 5); // Zeigt nur HH:MM
};
// Wochentagsnamen
const dayNames = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
return (
<div className={styles.defaultTemplateView}>
<h2>{template.name}</h2>
{template.description && <p>{template.description}</p>}
<div className={styles.weekView}>
{[1, 2, 3, 4, 5].map(dayIndex => (
<div key={dayIndex} className={styles.dayColumn}>
<h3>{dayNames[dayIndex]}</h3>
<div className={styles.shiftsContainer}>
{shiftsByDay[dayIndex]?.map(shift => (
<div key={shift.id} className={styles.shiftCard}>
<h4>{shift.timeSlot.name}</h4>
<p>
{formatTime(shift.timeSlot.startTime)} - {formatTime(shift.timeSlot.endTime)}
</p>
</div>
))}
</div>
</div>
))}
</div>
</div>
);
};
export default DefaultTemplateView;

View File

@@ -1,137 +0,0 @@
.dayEditor {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.dayHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.dayName {
font-size: 18px;
color: #2c3e50;
margin: 0;
}
.addButton {
padding: 6px 12px;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
gap: 6px;
}
.addButton:hover {
background-color: #2980b9;
}
.addButton svg {
width: 16px;
height: 16px;
}
.shiftsGrid {
display: grid;
gap: 15px;
}
.shiftCard {
background: white;
padding: 15px;
border-radius: 6px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.shiftHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.shiftTitle {
color: #2c3e50;
font-size: 16px;
font-weight: 500;
margin: 0;
}
.deleteButton {
padding: 4px 8px;
background-color: #e74c3c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.deleteButton:hover {
background-color: #c0392b;
}
.formGroup {
margin-bottom: 12px;
}
.formGroup label {
display: block;
margin-bottom: 4px;
color: #34495e;
font-size: 14px;
}
.formGroup input {
width: 100%;
padding: 6px 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.formGroup input[type="time"] {
width: auto;
}
.timeInputs {
display: flex;
gap: 15px;
align-items: center;
}
.colorPicker {
width: 100px;
}
.requiredEmployees {
display: flex;
gap: 10px;
align-items: center;
}
.requiredEmployees input {
width: 60px;
text-align: center;
}
.requiredEmployees button {
padding: 4px 8px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
}
.requiredEmployees button:hover {
background: #f5f6fa;
}

View File

@@ -1,125 +0,0 @@
// frontend/src/pages/ShiftTemplates/components/ShiftDayEditor.tsx
import React from 'react';
import { TemplateShiftSlot } from '../../../types/shiftTemplate';
import styles from './ShiftDayEditor.module.css';
interface ShiftDayEditorProps {
day: { id: number; name: string };
shifts: TemplateShiftSlot[];
onAddShift: () => void;
onUpdateShift: (shiftId: string, updates: Partial<TemplateShiftSlot>) => void;
onRemoveShift: (shiftId: string) => void;
}
const ShiftDayEditor: React.FC<ShiftDayEditorProps> = ({
day,
shifts,
onAddShift,
onUpdateShift,
onRemoveShift
}) => {
return (
<div className={styles.dayEditor}>
<div className={styles.dayHeader}>
<h3 className={styles.dayName}>{day.name}</h3>
<button className={styles.addButton} onClick={onAddShift}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" width="16" height="16">
<path fillRule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clipRule="evenodd" />
</svg>
Schicht hinzufügen
</button>
</div>
{shifts.length === 0 ? (
<div style={{ textAlign: 'center', padding: '20px', color: '#999', fontStyle: 'italic' }}>
Keine Schichten für {day.name}
</div>
) : (
<div className={styles.shiftsGrid}>
{shifts.map(shift => (
<div key={shift.id} className={styles.shiftCard}>
<div className={styles.shiftHeader}>
<h4 className={styles.shiftTitle}>Schicht bearbeiten</h4>
<button
className={styles.deleteButton}
onClick={() => onRemoveShift(shift.id)}
title="Schicht löschen"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" width="16" height="16">
<path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
Löschen
</button>
</div>
<div className={styles.formGroup}>
<input
type="text"
value={shift.timeSlot.name}
onChange={(e) => onUpdateShift(shift.id, { timeSlot: { ...shift.timeSlot, name: e.target.value } })}
placeholder="Schichtname"
/>
</div>
<div className={styles.timeInputs}>
<div className={styles.formGroup}>
<label>Start</label>
<input
type="time"
value={shift.timeSlot.startTime}
onChange={(e) => onUpdateShift(shift.id, { timeSlot: { ...shift.timeSlot, startTime: e.target.value } })}
/>
</div>
<div className={styles.formGroup}>
<label>Ende</label>
<input
type="time"
value={shift.timeSlot.endTime}
onChange={(e) => onUpdateShift(shift.id, { timeSlot: { ...shift.timeSlot, endTime: e.target.value } })}
/>
</div>
</div>
<div className={styles.formGroup}>
<label>Benötigte Mitarbeiter</label>
<div className={styles.requiredEmployees}>
<button
onClick={() => onUpdateShift(shift.id, { requiredEmployees: Math.max(1, shift.requiredEmployees - 1) })}
>
-
</button>
<input
type="number"
min="1"
value={shift.requiredEmployees}
onChange={(e) => onUpdateShift(shift.id, { requiredEmployees: parseInt(e.target.value) || 1 })}
/>
<button
onClick={() => onUpdateShift(shift.id, { requiredEmployees: shift.requiredEmployees + 1 })}
>
+
</button>
</div>
</div>
{shift.color && (
<div className={styles.formGroup}>
<label>Farbe</label>
<input
type="color"
value={shift.color}
onChange={(e) => onUpdateShift(shift.id, { color: e.target.value })}
className={styles.colorPicker}
/>
</div>
)}
</div>
))}
</div>
)}
</div>
);
};
export default ShiftDayEditor;