mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-12-01 15:05:45 +01:00
frontend with ony errors
This commit is contained in:
@@ -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'}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user