mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-12-01 06:55:45 +01:00
reworked scheduling
This commit is contained in:
@@ -282,12 +282,7 @@ const Dashboard: React.FC = () => {
|
||||
percentage
|
||||
};
|
||||
};
|
||||
|
||||
// Add refresh functionality
|
||||
const handleRefresh = () => {
|
||||
loadDashboardData();
|
||||
};
|
||||
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||
@@ -370,8 +365,6 @@ const Dashboard: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PlanDebugInfo />
|
||||
|
||||
{/* Quick Actions - Nur für Admins/Instandhalter */}
|
||||
{hasRole(['admin', 'instandhalter']) && (
|
||||
<div style={{ marginBottom: '30px' }}>
|
||||
|
||||
@@ -437,6 +437,8 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
|
||||
await employeeService.updateAvailabilities(employee.id, requestData);
|
||||
console.log('✅ VERFÜGBARKEITEN ERFOLGREICH GESPEICHERT');
|
||||
|
||||
window.dispatchEvent(new CustomEvent('availabilitiesChanged'));
|
||||
|
||||
onSave();
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// frontend/src/pages/Employees/components/EmployeeList.tsx - KORRIGIERT
|
||||
// frontend/src/pages/Employees/components/EmployeeList.tsx
|
||||
import React, { useState } from 'react';
|
||||
import { ROLE_CONFIG, EMPLOYEE_TYPE_CONFIG } from '../../../models/defaults/employeeDefaults';
|
||||
import { Employee } from '../../../models/Employee';
|
||||
@@ -11,6 +11,9 @@ interface EmployeeListProps {
|
||||
onManageAvailability: (employee: Employee) => void;
|
||||
}
|
||||
|
||||
type SortField = 'name' | 'employeeType' | 'canWorkAlone' | 'role' | 'lastLogin';
|
||||
type SortDirection = 'asc' | 'desc';
|
||||
|
||||
const EmployeeList: React.FC<EmployeeListProps> = ({
|
||||
employees,
|
||||
onEdit,
|
||||
@@ -19,8 +22,11 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
||||
}) => {
|
||||
const [filter, setFilter] = useState<'all' | 'active' | 'inactive'>('active');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [sortField, setSortField] = useState<SortField>('name');
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
|
||||
const { user: currentUser, hasRole } = useAuth();
|
||||
|
||||
// Filter employees based on active/inactive and search term
|
||||
const filteredEmployees = employees.filter(employee => {
|
||||
if (filter === 'active' && !employee.isActive) return false;
|
||||
if (filter === 'inactive' && employee.isActive) return false;
|
||||
@@ -38,6 +44,60 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
||||
return true;
|
||||
});
|
||||
|
||||
// Sort employees based on selected field and direction
|
||||
const sortedEmployees = [...filteredEmployees].sort((a, b) => {
|
||||
let aValue: any;
|
||||
let bValue: any;
|
||||
|
||||
switch (sortField) {
|
||||
case 'name':
|
||||
aValue = a.name.toLowerCase();
|
||||
bValue = b.name.toLowerCase();
|
||||
break;
|
||||
case 'employeeType':
|
||||
aValue = a.employeeType;
|
||||
bValue = b.employeeType;
|
||||
break;
|
||||
case 'canWorkAlone':
|
||||
aValue = a.canWorkAlone;
|
||||
bValue = b.canWorkAlone;
|
||||
break;
|
||||
case 'role':
|
||||
aValue = a.role;
|
||||
bValue = b.role;
|
||||
break;
|
||||
case 'lastLogin':
|
||||
// Handle null values for lastLogin (put them at the end)
|
||||
aValue = a.lastLogin ? new Date(a.lastLogin).getTime() : 0;
|
||||
bValue = b.lastLogin ? new Date(b.lastLogin).getTime() : 0;
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (sortDirection === 'asc') {
|
||||
return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
|
||||
} else {
|
||||
return aValue > bValue ? -1 : aValue < bValue ? 1 : 0;
|
||||
}
|
||||
});
|
||||
|
||||
const handleSort = (field: SortField) => {
|
||||
if (sortField === field) {
|
||||
// Toggle direction if same field
|
||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
// New field, default to ascending
|
||||
setSortField(field);
|
||||
setSortDirection('asc');
|
||||
}
|
||||
};
|
||||
|
||||
const getSortIndicator = (field: SortField) => {
|
||||
if (sortField !== field) return '↕';
|
||||
return sortDirection === 'asc' ? '↑' : '↓';
|
||||
};
|
||||
|
||||
// Simplified permission checks
|
||||
const canDeleteEmployee = (employee: Employee): boolean => {
|
||||
if (!hasRole(['admin'])) return false;
|
||||
@@ -78,8 +138,8 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
||||
|
||||
const getIndependenceBadge = (canWorkAlone: boolean) => {
|
||||
return canWorkAlone
|
||||
? { text: '✅ Eigenständig', color: '#27ae60', bgColor: '#d5f4e6' }
|
||||
: { text: '❌ Betreuung', color: '#e74c3c', bgColor: '#fadbd8' };
|
||||
? { text: 'Eigenständig', color: '#27ae60', bgColor: '#d5f4e6' }
|
||||
: { text: 'Betreuung', color: '#e74c3c', bgColor: '#fadbd8' };
|
||||
};
|
||||
|
||||
type Role = typeof ROLE_CONFIG[number]['value'];
|
||||
@@ -161,7 +221,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
||||
</div>
|
||||
|
||||
<div style={{ color: '#7f8c8d', fontSize: '14px' }}>
|
||||
{filteredEmployees.length} von {employees.length} Mitarbeitern
|
||||
{sortedEmployees.length} von {employees.length} Mitarbeitern
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -185,16 +245,41 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
||||
color: '#2c3e50',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<div>Name & E-Mail</div>
|
||||
<div style={{ textAlign: 'center' }}>Typ</div>
|
||||
<div style={{ textAlign: 'center' }}>Eigenständigkeit</div>
|
||||
<div style={{ textAlign: 'center' }}>Rolle</div>
|
||||
<div
|
||||
onClick={() => handleSort('name')}
|
||||
style={{ cursor: 'pointer', userSelect: 'none', display: 'flex', alignItems: 'center', gap: '5px' }}
|
||||
>
|
||||
Name & E-Mail {getSortIndicator('name')}
|
||||
</div>
|
||||
<div
|
||||
onClick={() => handleSort('employeeType')}
|
||||
style={{ textAlign: 'center', cursor: 'pointer', userSelect: 'none', display: 'flex', alignItems: 'center', gap: '5px', justifyContent: 'center' }}
|
||||
>
|
||||
Typ {getSortIndicator('employeeType')}
|
||||
</div>
|
||||
<div
|
||||
onClick={() => handleSort('canWorkAlone')}
|
||||
style={{ textAlign: 'center', cursor: 'pointer', userSelect: 'none', display: 'flex', alignItems: 'center', gap: '5px', justifyContent: 'center' }}
|
||||
>
|
||||
Eigenständigkeit {getSortIndicator('canWorkAlone')}
|
||||
</div>
|
||||
<div
|
||||
onClick={() => handleSort('role')}
|
||||
style={{ textAlign: 'center', cursor: 'pointer', userSelect: 'none', display: 'flex', alignItems: 'center', gap: '5px', justifyContent: 'center' }}
|
||||
>
|
||||
Rolle {getSortIndicator('role')}
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>Status</div>
|
||||
<div style={{ textAlign: 'center' }}>Letzter Login</div>
|
||||
<div
|
||||
onClick={() => handleSort('lastLogin')}
|
||||
style={{ textAlign: 'center', cursor: 'pointer', userSelect: 'none', display: 'flex', alignItems: 'center', gap: '5px', justifyContent: 'center' }}
|
||||
>
|
||||
Letzter Login {getSortIndicator('lastLogin')}
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>Aktionen</div>
|
||||
</div>
|
||||
|
||||
{filteredEmployees.map(employee => {
|
||||
{sortedEmployees.map(employee => {
|
||||
const employeeType = getEmployeeTypeBadge(employee.employeeType);
|
||||
const independence = getIndependenceBadge(employee.canWorkAlone);
|
||||
const roleColor = getRoleBadge(employee.role);
|
||||
|
||||
@@ -206,102 +206,7 @@ const Help: React.FC = () => {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Network Visualization */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
|
||||
gap: '15px',
|
||||
marginTop: '30px'
|
||||
}}>
|
||||
{/* Employees */}
|
||||
<div style={{
|
||||
backgroundColor: '#e8f4fd',
|
||||
padding: '15px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #b8d4f0'
|
||||
}}>
|
||||
<h4 style={{ margin: '0 0 10px 0', color: '#3498db' }}>👥 Mitarbeiter</h4>
|
||||
<div style={{ fontSize: '14px', lineHeight: '1.4' }}>
|
||||
<div>• Manager (1)</div>
|
||||
<div>• Erfahrene ({currentStage >= 1 ? '3' : '0'})</div>
|
||||
<div>• Neue ({currentStage >= 1 ? '2' : '0'})</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Shifts */}
|
||||
<div style={{
|
||||
backgroundColor: '#fff3cd',
|
||||
padding: '15px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ffeaa7'
|
||||
}}>
|
||||
<h4 style={{ margin: '0 0 10px 0', color: '#f39c12' }}>📅 Schichten</h4>
|
||||
<div style={{ fontSize: '14px', lineHeight: '1.4' }}>
|
||||
<div>• Vormittag (5)</div>
|
||||
<div>• Nachmittag (4)</div>
|
||||
<div>• Manager-Schichten (3)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current Actions */}
|
||||
<div style={{
|
||||
backgroundColor: '#d4edda',
|
||||
padding: '15px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #c3e6cb'
|
||||
}}>
|
||||
<h4 style={{ margin: '0 0 10px 0', color: '#27ae60' }}>⚡ Aktive Aktionen</h4>
|
||||
<div style={{ fontSize: '14px', lineHeight: '1.4' }}>
|
||||
{currentStage === 0 && (
|
||||
<>
|
||||
<div>• Grundzuweisung läuft</div>
|
||||
<div>• Erfahrene priorisieren</div>
|
||||
</>
|
||||
)}
|
||||
{currentStage === 1 && (
|
||||
<>
|
||||
<div>• Manager wird zugewiesen</div>
|
||||
<div>• Erfahrene suchen</div>
|
||||
</>
|
||||
)}
|
||||
{currentStage === 2 && (
|
||||
<>
|
||||
<div>• Überbesetzung prüfen</div>
|
||||
<div>• Pool-Verwaltung aktiv</div>
|
||||
</>
|
||||
)}
|
||||
{currentStage === 3 && (
|
||||
<>
|
||||
<div>• Finale Validierung</div>
|
||||
<div>• Bericht generieren</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Problems & Solutions */}
|
||||
<div style={{
|
||||
backgroundColor: '#f8d7da',
|
||||
padding: '15px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #f5c6cb'
|
||||
}}>
|
||||
<h4 style={{ margin: '0 0 10px 0', color: '#e74c3c' }}>🔍 Probleme & Lösungen</h4>
|
||||
<div style={{ fontSize: '14px', lineHeight: '1.4' }}>
|
||||
{currentStage >= 2 ? (
|
||||
<>
|
||||
<div style={{ color: '#27ae60' }}>✅ 2 Probleme behoben</div>
|
||||
<div style={{ color: '#e74c3c' }}>❌ 0 kritische Probleme</div>
|
||||
<div style={{ color: '#f39c12' }}>⚠️ 1 Warnung</div>
|
||||
</>
|
||||
) : (
|
||||
<div>Noch keine Probleme analysiert</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Business Rules */}
|
||||
<div style={{
|
||||
@@ -312,7 +217,7 @@ const Help: React.FC = () => {
|
||||
boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
|
||||
border: '1px solid #e0e0e0'
|
||||
}}>
|
||||
<h2 style={{ color: '#2c3e50', marginBottom: '20px' }}>📋 Geschäftsregeln</h2>
|
||||
<h2 style={{ color: '#2c3e50', marginBottom: '20px' }}>📋 Validierungs Regeln</h2>
|
||||
<div style={{ display: 'grid', gap: '10px' }}>
|
||||
{businessRules.map((rule, index) => (
|
||||
<div
|
||||
@@ -383,7 +288,10 @@ const Help: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
marginTop: '25px',
|
||||
padding: '20px',
|
||||
backgroundColor: '#e8f4fd',
|
||||
@@ -398,7 +306,6 @@ const Help: React.FC = () => {
|
||||
<li>Planen Sie Manager-Verfügbarkeit im Voraus</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@keyframes pulse {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { employeeService } from '../../services/employeeService';
|
||||
import { useNotification } from '../../contexts/NotificationContext';
|
||||
import AvailabilityManager from '../Employees/components/AvailabilityManager';
|
||||
import { Employee } from '../../models/Employee';
|
||||
import { styles } from './type/SettingsType';
|
||||
|
||||
const Settings: React.FC = () => {
|
||||
const { user: currentUser, updateUser } = useAuth();
|
||||
@@ -148,7 +149,12 @@ const Settings: React.FC = () => {
|
||||
};
|
||||
|
||||
if (!currentUser) {
|
||||
return <div>Nicht eingeloggt</div>;
|
||||
return <div style={{
|
||||
textAlign: 'center',
|
||||
padding: '3rem',
|
||||
color: '#666',
|
||||
fontSize: '1.1rem'
|
||||
}}>Nicht eingeloggt</div>;
|
||||
}
|
||||
|
||||
if (showAvailabilityManager) {
|
||||
@@ -161,395 +167,390 @@ const Settings: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Style constants for consistency
|
||||
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
|
||||
<h1>⚙️ Einstellungen</h1>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
borderBottom: '1px solid #e0e0e0',
|
||||
marginBottom: '30px'
|
||||
}}>
|
||||
<button
|
||||
onClick={() => setActiveTab('profile')}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
backgroundColor: activeTab === 'profile' ? '#3498db' : 'transparent',
|
||||
color: activeTab === 'profile' ? 'white' : '#333',
|
||||
border: 'none',
|
||||
borderBottom: activeTab === 'profile' ? '3px solid #3498db' : 'none',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
👤 Profil
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('password')}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
backgroundColor: activeTab === 'password' ? '#3498db' : 'transparent',
|
||||
color: activeTab === 'password' ? 'white' : '#333',
|
||||
border: 'none',
|
||||
borderBottom: activeTab === 'password' ? '3px solid #3498db' : 'none',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
🔒 Passwort
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('availability')}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
backgroundColor: activeTab === 'availability' ? '#3498db' : 'transparent',
|
||||
color: activeTab === 'availability' ? 'white' : '#333',
|
||||
border: 'none',
|
||||
borderBottom: activeTab === 'availability' ? '3px solid #3498db' : 'none',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
📅 Verfügbarkeit
|
||||
</button>
|
||||
<div style={styles.container}>
|
||||
{/* Left Sidebar with Tabs */}
|
||||
<div style={styles.sidebar}>
|
||||
<div style={styles.header}>
|
||||
<h1 style={styles.title}>Einstellungen</h1>
|
||||
<div style={styles.subtitle}>Verwalten Sie Ihre Kontoeinstellungen und Präferenzen</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.tabs}>
|
||||
<button
|
||||
onClick={() => setActiveTab('profile')}
|
||||
style={{
|
||||
...styles.tab,
|
||||
...(activeTab === 'profile' ? styles.tabActive : {})
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (activeTab !== 'profile') {
|
||||
e.currentTarget.style.background = styles.tabHover.background;
|
||||
e.currentTarget.style.color = styles.tabHover.color;
|
||||
e.currentTarget.style.transform = styles.tabHover.transform;
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (activeTab !== 'profile') {
|
||||
e.currentTarget.style.background = styles.tab.background;
|
||||
e.currentTarget.style.color = styles.tab.color;
|
||||
e.currentTarget.style.transform = 'none';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span style={{ color: '#cda8f0', fontSize: '24px' }}>{'\u{1F464}\u{FE0E}'}</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<span style={{ fontSize: '0.95rem', fontWeight: 500 }}>Profil</span>
|
||||
<span style={{ fontSize: '0.8rem', opacity: 0.7, marginTop: '2px' }}>Persönliche Informationen</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('password')}
|
||||
style={{
|
||||
...styles.tab,
|
||||
...(activeTab === 'password' ? styles.tabActive : {})
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (activeTab !== 'password') {
|
||||
e.currentTarget.style.background = styles.tabHover.background;
|
||||
e.currentTarget.style.color = styles.tabHover.color;
|
||||
e.currentTarget.style.transform = styles.tabHover.transform;
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (activeTab !== 'password') {
|
||||
e.currentTarget.style.background = styles.tab.background;
|
||||
e.currentTarget.style.color = styles.tab.color;
|
||||
e.currentTarget.style.transform = 'none';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '1.2rem', width: '24px' }}>🔒</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<span style={{ fontSize: '0.95rem', fontWeight: 500 }}>Passwort</span>
|
||||
<span style={{ fontSize: '0.8rem', opacity: 0.7, marginTop: '2px' }}>Sicherheitseinstellungen</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('availability')}
|
||||
style={{
|
||||
...styles.tab,
|
||||
...(activeTab === 'availability' ? styles.tabActive : {})
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (activeTab !== 'availability') {
|
||||
e.currentTarget.style.background = styles.tabHover.background;
|
||||
e.currentTarget.style.color = styles.tabHover.color;
|
||||
e.currentTarget.style.transform = styles.tabHover.transform;
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (activeTab !== 'availability') {
|
||||
e.currentTarget.style.background = styles.tab.background;
|
||||
e.currentTarget.style.color = styles.tab.color;
|
||||
e.currentTarget.style.transform = 'none';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '1.2rem', width: '24px' }}>📅</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<span style={{ fontSize: '0.95rem', fontWeight: 500 }}>Verfügbarkeit</span>
|
||||
<span style={{ fontSize: '0.8rem', opacity: 0.7, marginTop: '2px' }}>Schichtplanung</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Profile Tab */}
|
||||
{activeTab === 'profile' && (
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
padding: '30px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #e0e0e0',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
||||
}}>
|
||||
<h2 style={{ marginTop: 0, color: '#2c3e50' }}>Profilinformationen</h2>
|
||||
|
||||
<form onSubmit={handleProfileUpdate}>
|
||||
<div style={{ display: 'grid', gap: '20px' }}>
|
||||
{/* Read-only information */}
|
||||
<div style={{
|
||||
padding: '15px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #e9ecef'
|
||||
}}>
|
||||
<h4 style={{ margin: '0 0 15px 0', color: '#495057' }}>Systeminformationen</h4>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '15px' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}>
|
||||
E-Mail
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={currentUser.email}
|
||||
disabled
|
||||
style={{
|
||||
width: '95%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
color: '#666'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}>
|
||||
Rolle
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={currentUser.role}
|
||||
disabled
|
||||
style={{
|
||||
width: '95%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
color: '#666'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '15px', marginTop: '15px' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}>
|
||||
Mitarbeiter Typ
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={currentUser.employeeType}
|
||||
disabled
|
||||
style={{
|
||||
width: '95%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
color: '#666'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}>
|
||||
Vertragstyp
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={currentUser.contractType}
|
||||
disabled
|
||||
style={{
|
||||
width: '95%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
color: '#666'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Right Content Area */}
|
||||
<div style={styles.content}>
|
||||
{/* Profile Tab */}
|
||||
{activeTab === 'profile' && (
|
||||
<>
|
||||
<div style={styles.section}>
|
||||
<h2 style={styles.sectionTitle}>Profilinformationen</h2>
|
||||
<p style={styles.sectionDescription}>
|
||||
Verwalten Sie Ihre persönlichen Informationen und Kontaktdaten
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: '10px',
|
||||
padding: '15px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #e9ecef',
|
||||
}}
|
||||
>
|
||||
<label
|
||||
style={{
|
||||
display: 'block',
|
||||
marginBottom: '8px',
|
||||
fontWeight: 'bold',
|
||||
color: '#2c3e50',
|
||||
}}
|
||||
>
|
||||
Vollständiger Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={profileForm.name}
|
||||
onChange={handleProfileChange}
|
||||
required
|
||||
style={{
|
||||
width: '97.5%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
fontSize: '16px',
|
||||
}}
|
||||
placeholder="Ihr vollständiger Name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '15px',
|
||||
justifyContent: 'flex-end',
|
||||
marginTop: '30px',
|
||||
paddingTop: '20px',
|
||||
borderTop: '1px solid #f0f0f0'
|
||||
}}>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !profileForm.name.trim()}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
backgroundColor: loading ? '#bdc3c7' : (!profileForm.name.trim() ? '#95a5a6' : '#27ae60'),
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
cursor: (loading || !profileForm.name.trim()) ? 'not-allowed' : 'pointer',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
{loading ? '⏳ Wird gespeichert...' : 'Profil aktualisieren'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Password Tab */}
|
||||
{activeTab === 'password' && (
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
padding: '30px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #e0e0e0',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
||||
}}>
|
||||
<h2 style={{ marginTop: 0, color: '#2c3e50' }}>Passwort ändern</h2>
|
||||
|
||||
<form onSubmit={handlePasswordUpdate}>
|
||||
<div style={{ display: 'grid', gap: '20px', maxWidth: '400px' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
|
||||
Aktuelles Passwort *
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="currentPassword"
|
||||
value={passwordForm.currentPassword}
|
||||
onChange={handlePasswordChange}
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
fontSize: '16px'
|
||||
}}
|
||||
placeholder="Aktuelles Passwort"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
|
||||
Neues Passwort *
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="newPassword"
|
||||
value={passwordForm.newPassword}
|
||||
onChange={handlePasswordChange}
|
||||
required
|
||||
minLength={6}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
fontSize: '16px'
|
||||
}}
|
||||
placeholder="Mindestens 6 Zeichen"
|
||||
/>
|
||||
<div style={{ fontSize: '12px', color: '#7f8c8d', marginTop: '5px' }}>
|
||||
Das Passwort muss mindestens 6 Zeichen lang sein.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
|
||||
Neues Passwort bestätigen *
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="confirmPassword"
|
||||
value={passwordForm.confirmPassword}
|
||||
onChange={handlePasswordChange}
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
fontSize: '16px'
|
||||
}}
|
||||
placeholder="Passwort wiederholen"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '15px',
|
||||
justifyContent: 'flex-end',
|
||||
marginTop: '30px',
|
||||
paddingTop: '20px',
|
||||
borderTop: '1px solid #f0f0f0'
|
||||
}}>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
backgroundColor: loading ? '#bdc3c7' : (!passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword ? '#95a5a6' : '#3498db'),
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
cursor: (loading || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword) ? 'not-allowed' : 'pointer',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
{loading ? '⏳ Wird geändert...' : 'Passwort ändern'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Availability Tab */}
|
||||
{activeTab === 'availability' && (
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
padding: '30px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #e0e0e0',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
||||
}}>
|
||||
<h2 style={{ marginTop: 0, color: '#2c3e50' }}>Meine Verfügbarkeit</h2>
|
||||
|
||||
<div style={{
|
||||
padding: '30px',
|
||||
textAlign: 'center',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '8px',
|
||||
border: '2px dashed #dee2e6'
|
||||
}}>
|
||||
<div style={{ fontSize: '48px', marginBottom: '20px' }}>📅</div>
|
||||
<h3 style={{ color: '#2c3e50' }}>Verfügbarkeit verwalten</h3>
|
||||
<p style={{ color: '#6c757d', marginBottom: '25px' }}>
|
||||
Hier können Sie Ihre persönliche Verfügbarkeit für Schichtpläne festlegen.
|
||||
Legen Sie für jeden Tag und jede Schicht fest, ob Sie bevorzugt, möglicherweise
|
||||
oder nicht verfügbar sind.
|
||||
</p>
|
||||
|
||||
<button
|
||||
onClick={() => setShowAvailabilityManager(true)}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
backgroundColor: '#3498db',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '16px'
|
||||
}}
|
||||
>
|
||||
Verfügbarkeit bearbeiten
|
||||
</button>
|
||||
<form onSubmit={handleProfileUpdate} style={{ marginTop: '2rem' }}>
|
||||
<div style={styles.formGrid}>
|
||||
{/* Read-only information */}
|
||||
<div style={styles.infoCard}>
|
||||
<h4 style={styles.infoCardTitle}>Systeminformationen</h4>
|
||||
<div style={styles.infoGrid}>
|
||||
<div style={styles.field}>
|
||||
<label style={styles.fieldLabel}>
|
||||
E-Mail
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={currentUser.email}
|
||||
disabled
|
||||
style={styles.fieldInputDisabled}
|
||||
/>
|
||||
</div>
|
||||
<div style={styles.field}>
|
||||
<label style={styles.fieldLabel}>
|
||||
Rolle
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={currentUser.role}
|
||||
disabled
|
||||
style={styles.fieldInputDisabled}
|
||||
/>
|
||||
</div>
|
||||
<div style={styles.field}>
|
||||
<label style={styles.fieldLabel}>
|
||||
Mitarbeiter Typ
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={currentUser.employeeType}
|
||||
disabled
|
||||
style={styles.fieldInputDisabled}
|
||||
/>
|
||||
</div>
|
||||
<div style={styles.field}>
|
||||
<label style={styles.fieldLabel}>
|
||||
Vertragstyp
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={currentUser.contractType}
|
||||
disabled
|
||||
style={styles.fieldInputDisabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={styles.infoCard}>
|
||||
{/* Editable name field */}
|
||||
<div style={{ ...styles.field, marginTop: '1rem' }}>
|
||||
<label style={styles.fieldLabel}>
|
||||
Vollständiger Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={profileForm.name}
|
||||
onChange={handleProfileChange}
|
||||
required
|
||||
style={{
|
||||
...styles.fieldInput,
|
||||
width: '95%'
|
||||
}}
|
||||
placeholder="Ihr vollständiger Name"
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#1a1325';
|
||||
e.target.style.boxShadow = '0 0 0 3px rgba(26, 19, 37, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#e8e8e8';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
marginTop: '20px',
|
||||
padding: '15px',
|
||||
backgroundColor: '#e8f4fd',
|
||||
border: '1px solid #b6d7e8',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
color: '#2c3e50',
|
||||
textAlign: 'left'
|
||||
}}>
|
||||
<strong>💡 Informationen:</strong>
|
||||
<ul style={{ margin: '8px 0 0 20px', padding: 0 }}>
|
||||
<li><strong>Bevorzugt:</strong> Sie möchten diese Schicht arbeiten</li>
|
||||
<li><strong>Möglich:</strong> Sie können diese Schicht arbeiten</li>
|
||||
<li><strong>Nicht möglich:</strong> Sie können diese Schicht nicht arbeiten</li>
|
||||
</ul>
|
||||
<div style={styles.actions}>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !profileForm.name.trim()}
|
||||
style={{
|
||||
...styles.button,
|
||||
...styles.buttonPrimary,
|
||||
...((loading || !profileForm.name.trim()) ? styles.buttonDisabled : {})
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!loading && profileForm.name.trim()) {
|
||||
e.currentTarget.style.background = styles.buttonPrimaryHover.background;
|
||||
e.currentTarget.style.transform = styles.buttonPrimaryHover.transform;
|
||||
e.currentTarget.style.boxShadow = styles.buttonPrimaryHover.boxShadow;
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!loading && profileForm.name.trim()) {
|
||||
e.currentTarget.style.background = styles.buttonPrimary.background;
|
||||
e.currentTarget.style.transform = 'none';
|
||||
e.currentTarget.style.boxShadow = styles.buttonPrimary.boxShadow;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? '⏳ Wird gespeichert...' : 'Profil aktualisieren'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Password Tab */}
|
||||
{activeTab === 'password' && (
|
||||
<>
|
||||
<div style={styles.section}>
|
||||
<h2 style={styles.sectionTitle}>Passwort ändern</h2>
|
||||
<p style={styles.sectionDescription}>
|
||||
Aktualisieren Sie Ihr Passwort für erhöhte Sicherheit
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handlePasswordUpdate} style={{ marginTop: '2rem' }}>
|
||||
<div style={styles.formGridCompact}>
|
||||
<div style={styles.field}>
|
||||
<label style={styles.fieldLabel}>
|
||||
Aktuelles Passwort *
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="currentPassword"
|
||||
value={passwordForm.currentPassword}
|
||||
onChange={handlePasswordChange}
|
||||
required
|
||||
style={styles.fieldInput}
|
||||
placeholder="Aktuelles Passwort"
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#1a1325';
|
||||
e.target.style.boxShadow = '0 0 0 3px rgba(26, 19, 37, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#e8e8e8';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={styles.field}>
|
||||
<label style={styles.fieldLabel}>
|
||||
Neues Passwort *
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="newPassword"
|
||||
value={passwordForm.newPassword}
|
||||
onChange={handlePasswordChange}
|
||||
required
|
||||
minLength={6}
|
||||
style={styles.fieldInput}
|
||||
placeholder="Mindestens 6 Zeichen"
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#1a1325';
|
||||
e.target.style.boxShadow = '0 0 0 3px rgba(26, 19, 37, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#e8e8e8';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
/>
|
||||
<div style={styles.fieldHint}>
|
||||
Das Passwort muss mindestens 6 Zeichen lang sein.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.field}>
|
||||
<label style={styles.fieldLabel}>
|
||||
Neues Passwort bestätigen *
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="confirmPassword"
|
||||
value={passwordForm.confirmPassword}
|
||||
onChange={handlePasswordChange}
|
||||
required
|
||||
style={styles.fieldInput}
|
||||
placeholder="Passwort wiederholen"
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#1a1325';
|
||||
e.target.style.boxShadow = '0 0 0 3px rgba(26, 19, 37, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#e8e8e8';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.actions}>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword}
|
||||
style={{
|
||||
...styles.button,
|
||||
...styles.buttonPrimary,
|
||||
...((loading || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword) ? styles.buttonDisabled : {})
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!loading && passwordForm.currentPassword && passwordForm.newPassword && passwordForm.confirmPassword) {
|
||||
e.currentTarget.style.background = styles.buttonPrimaryHover.background;
|
||||
e.currentTarget.style.transform = styles.buttonPrimaryHover.transform;
|
||||
e.currentTarget.style.boxShadow = styles.buttonPrimaryHover.boxShadow;
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!loading && passwordForm.currentPassword && passwordForm.newPassword && passwordForm.confirmPassword) {
|
||||
e.currentTarget.style.background = styles.buttonPrimary.background;
|
||||
e.currentTarget.style.transform = 'none';
|
||||
e.currentTarget.style.boxShadow = styles.buttonPrimary.boxShadow;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? '⏳ Wird geändert...' : 'Passwort ändern'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Availability Tab */}
|
||||
{activeTab === 'availability' && (
|
||||
<>
|
||||
<div style={styles.section}>
|
||||
<h2 style={styles.sectionTitle}>Meine Verfügbarkeit</h2>
|
||||
<p style={styles.sectionDescription}>
|
||||
Legen Sie Ihre persönliche Verfügbarkeit für Schichtpläne fest
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={styles.availabilityCard}>
|
||||
<div style={styles.availabilityIcon}>📅</div>
|
||||
<h3 style={styles.availabilityTitle}>Verfügbarkeit verwalten</h3>
|
||||
<p style={styles.availabilityDescription}>
|
||||
Hier können Sie Ihre persönliche Verfügbarkeit für Schichtpläne festlegen.
|
||||
Legen Sie für jeden Tag und jede Schicht fest, ob Sie bevorzugt, möglicherweise
|
||||
oder nicht verfügbar sind.
|
||||
</p>
|
||||
|
||||
<button
|
||||
onClick={() => setShowAvailabilityManager(true)}
|
||||
style={{
|
||||
...styles.button,
|
||||
...styles.buttonPrimary,
|
||||
marginBottom: '2rem'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = styles.buttonPrimaryHover.background;
|
||||
e.currentTarget.style.transform = styles.buttonPrimaryHover.transform;
|
||||
e.currentTarget.style.boxShadow = styles.buttonPrimaryHover.boxShadow;
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = styles.buttonPrimary.background;
|
||||
e.currentTarget.style.transform = 'none';
|
||||
e.currentTarget.style.boxShadow = styles.buttonPrimary.boxShadow;
|
||||
}}
|
||||
>
|
||||
Verfügbarkeit bearbeiten
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
236
frontend/src/pages/Settings/type/SettingsType.tsx
Normal file
236
frontend/src/pages/Settings/type/SettingsType.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
export const styles = {
|
||||
container: {
|
||||
display: 'flex',
|
||||
minHeight: 'calc(100vh - 120px)',
|
||||
background: '#FBFAF6',
|
||||
padding: '2rem',
|
||||
maxWidth: '1200px',
|
||||
margin: '0 auto',
|
||||
gap: '2rem',
|
||||
},
|
||||
sidebar: {
|
||||
width: '280px',
|
||||
background: '#FBFAF6',
|
||||
borderRadius: '16px',
|
||||
border: '1px solid rgba(255, 255, 255, 0.8)',
|
||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.06), 0 1px 4px rgba(0, 0, 0, 0.04)',
|
||||
padding: '1.5rem',
|
||||
height: 'fit-content',
|
||||
position: 'sticky' as const,
|
||||
top: '2rem',
|
||||
},
|
||||
header: {
|
||||
marginBottom: '2rem',
|
||||
paddingBottom: '1.5rem',
|
||||
borderBottom: '1px solid rgba(26, 19, 37, 0.1)',
|
||||
},
|
||||
title: {
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: 600,
|
||||
color: '#161718',
|
||||
margin: '0 0 0.5rem 0',
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: '0.95rem',
|
||||
color: '#666',
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.5,
|
||||
},
|
||||
tabs: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
gap: '0.5rem',
|
||||
},
|
||||
tab: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1rem',
|
||||
padding: '1rem 1.25rem',
|
||||
background: 'transparent',
|
||||
color: '#666',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 500,
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
textAlign: 'left' as const,
|
||||
width: '100%',
|
||||
},
|
||||
tabActive: {
|
||||
background: '#51258f',
|
||||
color: '#FBFAF6',
|
||||
boxShadow: '0 4px 12px rgba(26, 19, 37, 0.15)',
|
||||
},
|
||||
tabHover: {
|
||||
background: 'rgba(81, 37, 143, 0.1)',
|
||||
color: '#1a1325',
|
||||
transform: 'translateX(4px)',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
background: '#FBFAF6',
|
||||
padding: '2.5rem',
|
||||
borderRadius: '16px',
|
||||
border: '1px solid rgba(255, 255, 255, 0.8)',
|
||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.06), 0 1px 4px rgba(0, 0, 0, 0.04)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
minHeight: '200px',
|
||||
},
|
||||
section: {
|
||||
marginBottom: '2rem',
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: '1.75rem',
|
||||
fontWeight: 600,
|
||||
color: '#161718',
|
||||
margin: '0 0 0.5rem 0',
|
||||
},
|
||||
sectionDescription: {
|
||||
color: '#666',
|
||||
fontSize: '1rem',
|
||||
margin: 0,
|
||||
lineHeight: 1.5,
|
||||
},
|
||||
formGrid: {
|
||||
display: 'grid',
|
||||
gap: '1.5rem',
|
||||
},
|
||||
formGridCompact: {
|
||||
display: 'grid',
|
||||
gap: '1.5rem',
|
||||
maxWidth: '500px',
|
||||
},
|
||||
infoCard: {
|
||||
padding: '1.5rem',
|
||||
background: 'rgba(26, 19, 37, 0.02)',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid rgba(26, 19, 37, 0.1)',
|
||||
},
|
||||
infoCardTitle: {
|
||||
fontSize: '1rem',
|
||||
fontWeight: 600,
|
||||
color: '#1a1325',
|
||||
margin: '0 0 1rem 0',
|
||||
},
|
||||
infoGrid: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '1rem',
|
||||
},
|
||||
field: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
gap: '0.5rem',
|
||||
},
|
||||
fieldLabel: {
|
||||
fontSize: '0.9rem',
|
||||
fontWeight: 600,
|
||||
color: '#161718',
|
||||
},
|
||||
fieldInput: {
|
||||
padding: '0.875rem 1rem',
|
||||
border: '1.5px solid #e8e8e8',
|
||||
borderRadius: '8px',
|
||||
fontSize: '0.95rem',
|
||||
background: '#FBFAF6',
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
color: '#161718',
|
||||
},
|
||||
fieldInputDisabled: {
|
||||
padding: '0.875rem 1rem',
|
||||
border: '1.5px solid rgba(26, 19, 37, 0.1)',
|
||||
borderRadius: '8px',
|
||||
fontSize: '0.95rem',
|
||||
background: 'rgba(26, 19, 37, 0.05)',
|
||||
color: '#666',
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
fieldHint: {
|
||||
fontSize: '0.8rem',
|
||||
color: '#888',
|
||||
marginTop: '0.25rem',
|
||||
},
|
||||
actions: {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
marginTop: '2.5rem',
|
||||
paddingTop: '1.5rem',
|
||||
borderTop: '1px solid rgba(26, 19, 37, 0.1)',
|
||||
},
|
||||
button: {
|
||||
padding: '0.875rem 2rem',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
fontSize: '0.95rem',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
position: 'relative' as const,
|
||||
overflow: 'hidden' as const,
|
||||
},
|
||||
buttonPrimary: {
|
||||
background: '#1a1325',
|
||||
color: '#FBFAF6',
|
||||
boxShadow: '0 2px 8px rgba(26, 19, 37, 0.2)',
|
||||
},
|
||||
buttonPrimaryHover: {
|
||||
background: '#24163a',
|
||||
transform: 'translateY(-1px)',
|
||||
boxShadow: '0 4px 16px rgba(26, 19, 37, 0.3)',
|
||||
},
|
||||
buttonDisabled: {
|
||||
background: '#ccc',
|
||||
color: '#666',
|
||||
cursor: 'not-allowed',
|
||||
transform: 'none',
|
||||
boxShadow: 'none',
|
||||
},
|
||||
availabilityCard: {
|
||||
padding: '3rem 2rem',
|
||||
textAlign: 'center' as const,
|
||||
background: 'rgba(26, 19, 37, 0.03)',
|
||||
borderRadius: '16px',
|
||||
border: '2px dashed rgba(26, 19, 37, 0.1)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
},
|
||||
availabilityIcon: {
|
||||
fontSize: '3rem',
|
||||
marginBottom: '1.5rem',
|
||||
opacity: 0.8,
|
||||
},
|
||||
availabilityTitle: {
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: 600,
|
||||
color: '#161718',
|
||||
margin: '0 0 1rem 0',
|
||||
},
|
||||
availabilityDescription: {
|
||||
color: '#666',
|
||||
marginBottom: '2rem',
|
||||
lineHeight: 1.6,
|
||||
maxWidth: '500px',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
infoHint: {
|
||||
padding: '1.25rem',
|
||||
background: 'rgba(26, 19, 37, 0.05)',
|
||||
border: '1px solid rgba(26, 19, 37, 0.1)',
|
||||
borderRadius: '12px',
|
||||
fontSize: '0.9rem',
|
||||
color: '#161718',
|
||||
textAlign: 'left' as const,
|
||||
maxWidth: '400px',
|
||||
margin: '0 auto',
|
||||
},
|
||||
infoList: {
|
||||
margin: '0.75rem 0 0 1rem',
|
||||
padding: 0,
|
||||
listStyle: 'none',
|
||||
},
|
||||
infoListItem: {
|
||||
marginBottom: '0.5rem',
|
||||
position: 'relative' as const,
|
||||
paddingLeft: '1rem',
|
||||
},
|
||||
};
|
||||
@@ -5,12 +5,11 @@ import { useAuth } from '../../contexts/AuthContext';
|
||||
import { shiftPlanService } from '../../services/shiftPlanService';
|
||||
import { employeeService } from '../../services/employeeService';
|
||||
import { shiftAssignmentService, ShiftAssignmentService } from '../../services/shiftAssignmentService';
|
||||
import { AssignmentResult } from '../../services/scheduling';
|
||||
import { IntelligentShiftScheduler, SchedulingResult, AssignmentResult } from '../../services/scheduling';
|
||||
import { ShiftPlan, TimeSlot, ScheduledShift } from '../../models/ShiftPlan';
|
||||
import { Employee, EmployeeAvailability } from '../../models/Employee';
|
||||
import { useNotification } from '../../contexts/NotificationContext';
|
||||
import { formatDate, formatTime } from '../../utils/foramatters';
|
||||
import { isScheduledShift } from '../../models/helpers';
|
||||
|
||||
// Local interface extensions (same as AvailabilityManager)
|
||||
interface ExtendedTimeSlot extends TimeSlot {
|
||||
@@ -36,125 +35,301 @@ const ShiftPlanView: React.FC = () => {
|
||||
const [shiftPlan, setShiftPlan] = useState<ShiftPlan | null>(null);
|
||||
const [employees, setEmployees] = useState<Employee[]>([]);
|
||||
const [availabilities, setAvailabilities] = useState<EmployeeAvailability[]>([]);
|
||||
const [assignmentResult, setAssignmentResult] = useState<AssignmentResult | null>(null);
|
||||
const [assignmentResult, setAssignmentResult] = useState<AssignmentResult | null>(null); // Add this line
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [publishing, setPublishing] = useState(false);
|
||||
const [scheduledShifts, setScheduledShifts] = useState<ScheduledShift[]>([]);
|
||||
const [reverting, setReverting] = useState(false);
|
||||
const [showAssignmentPreview, setShowAssignmentPreview] = useState(false);
|
||||
const [recreating, setRecreating] = useState(false);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
loadShiftPlanData();
|
||||
debugScheduledShifts();
|
||||
|
||||
// Event Listener für Verfügbarkeits-Änderungen
|
||||
const handleAvailabilityChange = () => {
|
||||
console.log('📢 Verfügbarkeiten wurden geändert - lade Daten neu...');
|
||||
reloadAvailabilities();
|
||||
};
|
||||
|
||||
// Globales Event für Verfügbarkeits-Änderungen
|
||||
window.addEventListener('availabilitiesChanged', handleAvailabilityChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('availabilitiesChanged', handleAvailabilityChange);
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
const loadShiftPlanData = async () => {
|
||||
if (!id) return;
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
// Seite ist wieder sichtbar - Daten neu laden
|
||||
console.log('🔄 Seite ist wieder sichtbar - lade Daten neu...');
|
||||
reloadAvailabilities();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
(window as any).debugRenderLogic = debugRenderLogic;
|
||||
return () => { (window as any).debugRenderLogic = undefined; };
|
||||
}, [shiftPlan, scheduledShifts]);
|
||||
|
||||
const loadShiftPlanData = async () => {
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Load plan and employees first
|
||||
const [plan, employeesData] = await Promise.all([
|
||||
shiftPlanService.getShiftPlan(id),
|
||||
employeeService.getEmployees(),
|
||||
]);
|
||||
|
||||
setShiftPlan(plan);
|
||||
setEmployees(employeesData.filter(emp => emp.isActive));
|
||||
|
||||
// CRITICAL: Load scheduled shifts and verify they exist
|
||||
const shiftsData = await shiftAssignmentService.getScheduledShiftsForPlan(id);
|
||||
console.log('📋 Loaded scheduled shifts:', shiftsData.length);
|
||||
|
||||
if (shiftsData.length === 0) {
|
||||
console.warn('⚠️ No scheduled shifts found for plan:', id);
|
||||
showNotification({
|
||||
type: 'warning',
|
||||
title: 'Keine Schichten gefunden',
|
||||
message: 'Der Schichtplan hat keine generierten Schichten. Bitte überprüfen Sie die Plan-Konfiguration.'
|
||||
});
|
||||
}
|
||||
|
||||
setScheduledShifts(shiftsData);
|
||||
|
||||
// Load availabilities
|
||||
const availabilityPromises = employeesData
|
||||
.filter(emp => emp.isActive)
|
||||
.map(emp => employeeService.getAvailabilities(emp.id));
|
||||
|
||||
const allAvailabilities = await Promise.all(availabilityPromises);
|
||||
const flattenedAvailabilities = allAvailabilities.flat();
|
||||
|
||||
const planAvailabilities = flattenedAvailabilities.filter(
|
||||
availability => availability.planId === id
|
||||
);
|
||||
|
||||
setAvailabilities(planAvailabilities);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading shift plan data:', error);
|
||||
showNotification({
|
||||
type: 'error',
|
||||
title: 'Fehler',
|
||||
message: 'Daten konnten nicht geladen werden.'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRecreateAssignments = async () => {
|
||||
if (!shiftPlan) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const [plan, employeesData, shiftsData] = await Promise.all([
|
||||
shiftPlanService.getShiftPlan(id),
|
||||
employeeService.getEmployees(),
|
||||
shiftAssignmentService.getScheduledShiftsForPlan(id) // Load shifts here
|
||||
]);
|
||||
setRecreating(true);
|
||||
|
||||
if (!window.confirm('Möchten Sie die aktuellen Zuweisungen wirklich zurücksetzen? Alle vorhandenen Zuweisungen werden gelöscht.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
setShiftPlan(plan);
|
||||
setEmployees(employeesData.filter(emp => emp.isActive));
|
||||
setScheduledShifts(shiftsData);
|
||||
console.log('🔄 STARTING COMPLETE ASSIGNMENT CLEARING PROCESS');
|
||||
|
||||
// Load availabilities for all employees
|
||||
const availabilityPromises = employeesData
|
||||
.filter(emp => emp.isActive)
|
||||
.map(emp => employeeService.getAvailabilities(emp.id));
|
||||
// STEP 1: Get current scheduled shifts
|
||||
const currentScheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
||||
console.log(`📋 Found ${currentScheduledShifts.length} shifts to clear`);
|
||||
|
||||
// STEP 2: Clear ALL assignments by setting empty arrays
|
||||
const clearPromises = currentScheduledShifts.map(async (scheduledShift) => {
|
||||
console.log(`🗑️ Clearing assignments for shift: ${scheduledShift.id}`);
|
||||
await shiftAssignmentService.updateScheduledShift(scheduledShift.id, {
|
||||
assignedEmployees: [] // EMPTY ARRAY - this clears the assignments
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.all(clearPromises);
|
||||
console.log('✅ All assignments cleared from database');
|
||||
|
||||
// STEP 3: Update plan status to draft
|
||||
await shiftPlanService.updateShiftPlan(shiftPlan.id, {
|
||||
status: 'draft'
|
||||
});
|
||||
console.log('📝 Plan status set to draft');
|
||||
|
||||
// STEP 4: CRITICAL - Force reload of scheduled shifts to get EMPTY assignments
|
||||
const refreshedShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
||||
setScheduledShifts(refreshedShifts); // Update state with EMPTY assignments
|
||||
|
||||
const allAvailabilities = await Promise.all(availabilityPromises);
|
||||
const flattenedAvailabilities = allAvailabilities.flat();
|
||||
|
||||
// Filter availabilities to only include those for the current shift plan
|
||||
const planAvailabilities = flattenedAvailabilities.filter(
|
||||
availability => availability.planId === id
|
||||
);
|
||||
|
||||
setAvailabilities(planAvailabilities);
|
||||
debugAvailabilities();
|
||||
// STEP 5: Clear any previous assignment results
|
||||
setAssignmentResult(null);
|
||||
setShowAssignmentPreview(false);
|
||||
|
||||
// STEP 6: Force complete data refresh
|
||||
await loadShiftPlanData();
|
||||
|
||||
console.log('🎯 ASSIGNMENT CLEARING COMPLETE - Table should now be empty');
|
||||
|
||||
showNotification({
|
||||
type: 'success',
|
||||
title: 'Zuweisungen gelöscht',
|
||||
message: 'Alle Zuweisungen wurden erfolgreich gelöscht. Die Tabelle sollte jetzt leer sein.'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading shift plan data:', error);
|
||||
console.error('❌ Error clearing assignments:', error);
|
||||
showNotification({
|
||||
type: 'error',
|
||||
title: 'Fehler',
|
||||
message: 'Daten konnten nicht geladen werden.'
|
||||
message: `Löschen der Zuweisungen fehlgeschlagen: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}`
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRecreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const debugAvailabilities = () => {
|
||||
if (!shiftPlan || !employees.length || !availabilities.length) return;
|
||||
|
||||
console.log('🔍 AVAILABILITY ANALYSIS:', {
|
||||
totalAvailabilities: availabilities.length,
|
||||
employeesWithAvailabilities: new Set(availabilities.map(a => a.employeeId)).size,
|
||||
totalEmployees: employees.length,
|
||||
availabilityByEmployee: employees.map(emp => {
|
||||
const empAvailabilities = availabilities.filter(a => a.employeeId === emp.id);
|
||||
return {
|
||||
employee: emp.name,
|
||||
availabilities: empAvailabilities.length,
|
||||
preferences: empAvailabilities.map(a => ({
|
||||
day: a.dayOfWeek,
|
||||
timeSlot: a.timeSlotId,
|
||||
preference: a.preferenceLevel
|
||||
}))
|
||||
};
|
||||
})
|
||||
});
|
||||
|
||||
// Prüfe spezifisch für Manager/Admin
|
||||
const manager = employees.find(emp => emp.role === 'admin');
|
||||
if (manager) {
|
||||
const managerAvailabilities = availabilities.filter(a => a.employeeId === manager.id);
|
||||
console.log('🔍 MANAGER AVAILABILITIES:', {
|
||||
manager: manager.name,
|
||||
availabilities: managerAvailabilities.length,
|
||||
details: managerAvailabilities.map(a => ({
|
||||
day: a.dayOfWeek,
|
||||
timeSlot: a.timeSlotId,
|
||||
preference: a.preferenceLevel
|
||||
}))
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const debugScheduledShifts = async () => {
|
||||
const debugRenderLogic = () => {
|
||||
if (!shiftPlan) return;
|
||||
|
||||
try {
|
||||
const shifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
||||
console.log('🔍 SCHEDULED SHIFTS IN DATABASE:', {
|
||||
total: shifts.length,
|
||||
shifts: shifts.map(s => ({
|
||||
id: s.id,
|
||||
date: s.date,
|
||||
timeSlotId: s.timeSlotId,
|
||||
requiredEmployees: s.requiredEmployees
|
||||
}))
|
||||
console.log('🔍 RENDER LOGIC DEBUG:');
|
||||
console.log('=====================');
|
||||
|
||||
const { days, allTimeSlots, timeSlotsByDay } = getExtendedTimetableData();
|
||||
|
||||
console.log('📊 TABLE STRUCTURE:');
|
||||
console.log('- Days in table:', days.length);
|
||||
console.log('- TimeSlots in table:', allTimeSlots.length);
|
||||
console.log('- Days with data:', Object.keys(timeSlotsByDay).length);
|
||||
|
||||
// Zeige die tatsächliche Struktur der Tabelle
|
||||
console.log('\n📅 ACTUAL TABLE DAYS:');
|
||||
days.forEach(day => {
|
||||
const slotsForDay = timeSlotsByDay[day.id] || [];
|
||||
console.log(`- ${day.name}: ${slotsForDay.length} time slots`);
|
||||
});
|
||||
|
||||
console.log('\n⏰ ACTUAL TIME SLOTS:');
|
||||
allTimeSlots.forEach(slot => {
|
||||
console.log(`- ${slot.name} (${slot.startTime}-${slot.endTime})`);
|
||||
});
|
||||
|
||||
// Prüfe wie viele Scheduled Shifts tatsächlich gerendert werden
|
||||
console.log('\n🔍 SCHEDULED SHIFTS RENDER ANALYSIS:');
|
||||
|
||||
let totalRenderedShifts = 0;
|
||||
let shiftsWithAssignments = 0;
|
||||
|
||||
days.forEach(day => {
|
||||
const slotsForDay = timeSlotsByDay[day.id] || [];
|
||||
slotsForDay.forEach(timeSlot => {
|
||||
totalRenderedShifts++;
|
||||
|
||||
// Finde den entsprechenden Scheduled Shift
|
||||
const scheduledShift = scheduledShifts.find(scheduled => {
|
||||
const scheduledDayOfWeek = getDayOfWeek(scheduled.date);
|
||||
return scheduledDayOfWeek === day.id &&
|
||||
scheduled.timeSlotId === timeSlot.id;
|
||||
});
|
||||
|
||||
if (scheduledShift && scheduledShift.assignedEmployees && scheduledShift.assignedEmployees.length > 0) {
|
||||
shiftsWithAssignments++;
|
||||
}
|
||||
});
|
||||
|
||||
// Check if we have any shifts at all
|
||||
if (shifts.length === 0) {
|
||||
console.error('❌ NO SCHEDULED SHIFTS IN DATABASE - This is the problem!');
|
||||
console.log('💡 Solution: Regenerate scheduled shifts for this plan');
|
||||
});
|
||||
|
||||
console.log(`- Total shifts in table: ${totalRenderedShifts}`);
|
||||
console.log(`- Shifts with assignments: ${shiftsWithAssignments}`);
|
||||
console.log(`- Total scheduled shifts: ${scheduledShifts.length}`);
|
||||
console.log(`- Coverage: ${Math.round((totalRenderedShifts / scheduledShifts.length) * 100)}%`);
|
||||
|
||||
// Problem-Analyse
|
||||
if (totalRenderedShifts < scheduledShifts.length) {
|
||||
console.log('\n🚨 PROBLEM: Table is not showing all scheduled shifts!');
|
||||
console.log('💡 The table structure (days × timeSlots) is smaller than actual scheduled shifts');
|
||||
|
||||
// Zeige die fehlenden Shifts
|
||||
const missingShifts = scheduledShifts.filter(scheduled => {
|
||||
const dayOfWeek = getDayOfWeek(scheduled.date);
|
||||
const timeSlotExists = allTimeSlots.some(ts => ts.id === scheduled.timeSlotId);
|
||||
const dayExists = days.some(day => day.id === dayOfWeek);
|
||||
|
||||
return !(timeSlotExists && dayExists);
|
||||
});
|
||||
|
||||
if (missingShifts.length > 0) {
|
||||
console.log(`❌ ${missingShifts.length} shifts cannot be rendered in table:`);
|
||||
missingShifts.slice(0, 5).forEach(shift => {
|
||||
const dayOfWeek = getDayOfWeek(shift.date);
|
||||
const timeSlot = shiftPlan.timeSlots?.find(ts => ts.id === shift.timeSlotId);
|
||||
console.log(` - ${shift.date} (Day ${dayOfWeek}): ${timeSlot?.name || 'Unknown'} - ${shift.assignedEmployees?.length || 0} assignments`);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error loading scheduled shifts:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const getExtendedTimetableData = () => {
|
||||
if (!shiftPlan || !shiftPlan.timeSlots) {
|
||||
return { days: [], timeSlotsByDay: {}, allTimeSlots: [] };
|
||||
}
|
||||
|
||||
// Verwende alle Tage die tatsächlich in scheduledShifts vorkommen
|
||||
const allDaysInScheduledShifts = [...new Set(scheduledShifts.map(s => getDayOfWeek(s.date)))].sort();
|
||||
|
||||
const days = allDaysInScheduledShifts.map(dayId => {
|
||||
return weekdays.find(day => day.id === dayId) || { id: dayId, name: `Tag ${dayId}` };
|
||||
});
|
||||
|
||||
// Verwende alle TimeSlots die tatsächlich in scheduledShifts vorkommen
|
||||
const allTimeSlotIdsInScheduledShifts = [...new Set(scheduledShifts.map(s => s.timeSlotId))];
|
||||
|
||||
const allTimeSlots = allTimeSlotIdsInScheduledShifts
|
||||
.map(id => shiftPlan.timeSlots?.find(ts => ts.id === id))
|
||||
.filter(Boolean)
|
||||
.map(timeSlot => ({
|
||||
...timeSlot!,
|
||||
displayName: `${timeSlot!.name} (${formatTime(timeSlot!.startTime)}-${formatTime(timeSlot!.endTime)})`
|
||||
}))
|
||||
.sort((a, b) => a.startTime.localeCompare(b.startTime));
|
||||
|
||||
// TimeSlots pro Tag
|
||||
const timeSlotsByDay: Record<number, ExtendedTimeSlot[]> = {};
|
||||
|
||||
days.forEach(day => {
|
||||
const timeSlotIdsForDay = new Set(
|
||||
scheduledShifts
|
||||
.filter(shift => getDayOfWeek(shift.date) === day.id)
|
||||
.map(shift => shift.timeSlotId)
|
||||
);
|
||||
|
||||
timeSlotsByDay[day.id] = allTimeSlots
|
||||
.filter(timeSlot => timeSlotIdsForDay.has(timeSlot.id))
|
||||
.sort((a, b) => a.startTime.localeCompare(b.startTime));
|
||||
});
|
||||
|
||||
/*console.log('🔄 Extended timetable data:', {
|
||||
days: days.length,
|
||||
timeSlots: allTimeSlots.length,
|
||||
totalScheduledShifts: scheduledShifts.length
|
||||
});*/
|
||||
|
||||
return { days, timeSlotsByDay, allTimeSlots };
|
||||
};
|
||||
|
||||
// Extract plan-specific shifts using the same logic as AvailabilityManager
|
||||
const getTimetableData = () => {
|
||||
const getTimetableData = () => {
|
||||
if (!shiftPlan || !shiftPlan.shifts || !shiftPlan.timeSlots) {
|
||||
return { days: [], timeSlotsByDay: {}, allTimeSlots: [] };
|
||||
}
|
||||
@@ -216,174 +391,46 @@ const ShiftPlanView: React.FC = () => {
|
||||
return date.getDay() === 0 ? 7 : date.getDay();
|
||||
};
|
||||
|
||||
/*const debugManagerAvailability = () => {
|
||||
if (!shiftPlan || !employees.length || !availabilities.length) return;
|
||||
|
||||
const manager = employees.find(emp => emp.role === 'admin');
|
||||
if (!manager) {
|
||||
console.log('❌ Kein Manager (admin) gefunden');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔍 Manager-Analyse:', {
|
||||
manager: manager.name,
|
||||
managerId: manager.id,
|
||||
totalAvailabilities: availabilities.length,
|
||||
managerAvailabilities: availabilities.filter(a => a.employeeId === manager.id).length
|
||||
});
|
||||
|
||||
// Prüfe speziell die leeren Manager-Schichten
|
||||
const emptyManagerShifts = [
|
||||
'a8ef4ce0-adfd-4ec3-8c58-efa0f7347f9f',
|
||||
'a496a8d6-f7a0-4d77-96de-c165379378c4',
|
||||
'ea2d73d1-8354-4833-8c87-40f318ce8be0',
|
||||
'90eb5454-2ae2-4445-86b7-a6e0e2cf0b22'
|
||||
];
|
||||
|
||||
emptyManagerShifts.forEach(shiftId => {
|
||||
const scheduledShift = shiftPlan.scheduledShifts?.find(s => s.id === shiftId);
|
||||
if (scheduledShift) {
|
||||
const dayOfWeek = getDayOfWeek(scheduledShift.date);
|
||||
const shiftKey = `${dayOfWeek}-${scheduledShift.timeSlotId}`;
|
||||
|
||||
const managerAvailability = availabilities.find(a =>
|
||||
a.employeeId === manager.id &&
|
||||
a.dayOfWeek === dayOfWeek &&
|
||||
a.timeSlotId === scheduledShift.timeSlotId
|
||||
);
|
||||
|
||||
console.log(`📊 Schicht ${shiftId}:`, {
|
||||
date: scheduledShift.date,
|
||||
dayOfWeek,
|
||||
timeSlotId: scheduledShift.timeSlotId,
|
||||
shiftKey,
|
||||
managerAvailability: managerAvailability ? managerAvailability.preferenceLevel : 'NICHT GEFUNDEN',
|
||||
status: managerAvailability ?
|
||||
(managerAvailability.preferenceLevel === 3 ? '❌ NICHT VERFÜGBAR' : '✅ VERFÜGBAR') :
|
||||
'❌ KEINE VERFÜGBARKEITSDATEN'
|
||||
});
|
||||
}
|
||||
});
|
||||
};*/
|
||||
|
||||
const debugAssignments = async () => {
|
||||
if (!shiftPlan) return;
|
||||
|
||||
try {
|
||||
const shifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
||||
console.log('🔍 DEBUG - Scheduled Shifts nach Veröffentlichung:', {
|
||||
totalShifts: shifts.length,
|
||||
shiftsWithAssignments: shifts.filter(s => s.assignedEmployees && s.assignedEmployees.length > 0).length,
|
||||
allShifts: shifts.map(s => ({
|
||||
id: s.id,
|
||||
date: s.date,
|
||||
timeSlotId: s.timeSlotId,
|
||||
assignedEmployees: s.assignedEmployees,
|
||||
assignedCount: s.assignedEmployees?.length || 0
|
||||
}))
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Debug error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePreviewAssignment = async () => {
|
||||
if (!shiftPlan) return;
|
||||
debugScheduledShifts();
|
||||
|
||||
try {
|
||||
setPublishing(true);
|
||||
|
||||
// DEBUG: Überprüfe die Eingabedaten
|
||||
console.log('🔍 INPUT DATA FOR SCHEDULING:', {
|
||||
shiftPlan: {
|
||||
id: shiftPlan.id,
|
||||
name: shiftPlan.name,
|
||||
shifts: shiftPlan.shifts?.length,
|
||||
timeSlots: shiftPlan.timeSlots?.length
|
||||
},
|
||||
employees: employees.length,
|
||||
availabilities: availabilities.length,
|
||||
employeeDetails: employees.map(emp => ({
|
||||
id: emp.id,
|
||||
name: emp.name,
|
||||
role: emp.role,
|
||||
employeeType: emp.employeeType,
|
||||
canWorkAlone: emp.canWorkAlone
|
||||
}))
|
||||
});
|
||||
|
||||
// FORCE COMPLETE REFRESH - don't rely on cached state
|
||||
const [refreshedEmployees, refreshedAvailabilities] = await Promise.all([
|
||||
// Reload employees fresh
|
||||
employeeService.getEmployees().then(emps => emps.filter(emp => emp.isActive)),
|
||||
// Reload availabilities fresh
|
||||
refreshAllAvailabilities()
|
||||
]);
|
||||
|
||||
console.log('🔄 USING FRESH DATA:');
|
||||
console.log('- Employees:', refreshedEmployees.length);
|
||||
console.log('- Availabilities:', refreshedAvailabilities.length);
|
||||
|
||||
// DEBUG: Verify we have new data
|
||||
debugSchedulingInput(refreshedEmployees, refreshedAvailabilities);
|
||||
|
||||
// ADD THIS: Define constraints object
|
||||
const constraints = {
|
||||
enforceNoTraineeAlone: true,
|
||||
enforceExperiencedWithChef: true,
|
||||
maxRepairAttempts: 50,
|
||||
targetEmployeesPerShift: 2
|
||||
};
|
||||
|
||||
// Use the freshly loaded data, not the state
|
||||
const result = await ShiftAssignmentService.assignShifts(
|
||||
shiftPlan,
|
||||
employees,
|
||||
availabilities,
|
||||
{
|
||||
enforceExperiencedWithChef: true,
|
||||
enforceNoTraineeAlone: true,
|
||||
maxRepairAttempts: 50
|
||||
}
|
||||
refreshedEmployees, // Use fresh array, not state
|
||||
refreshedAvailabilities, // Use fresh array, not state
|
||||
constraints // Now this variable is defined
|
||||
);
|
||||
|
||||
// DEBUG: Detaillierte Analyse des Results
|
||||
console.log('🔍 DETAILED ASSIGNMENT RESULT:', {
|
||||
totalAssignments: Object.keys(result.assignments).length,
|
||||
assignments: result.assignments,
|
||||
violations: result.violations,
|
||||
hasResolutionReport: !!result.resolutionReport,
|
||||
assignmentDetails: Object.entries(result.assignments).map(([shiftId, empIds]) => ({
|
||||
shiftId,
|
||||
employeeCount: empIds.length,
|
||||
employees: empIds
|
||||
}))
|
||||
});
|
||||
// DEBUG: Überprüfe die tatsächlichen Violations
|
||||
console.log('🔍 VIOLATIONS ANALYSIS:', {
|
||||
allViolations: result.violations,
|
||||
criticalViolations: result.violations.filter(v =>
|
||||
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
|
||||
),
|
||||
warningViolations: result.violations.filter(v =>
|
||||
v.includes('WARNING:') || v.includes('⚠️')
|
||||
),
|
||||
infoViolations: result.violations.filter(v =>
|
||||
v.includes('INFO:')
|
||||
),
|
||||
criticalCount: result.violations.filter(v =>
|
||||
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
|
||||
).length,
|
||||
canPublish: result.violations.filter(v =>
|
||||
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
|
||||
).length === 0
|
||||
});
|
||||
|
||||
setAssignmentResult(result);
|
||||
setShowAssignmentPreview(true);
|
||||
|
||||
// Zeige Reparatur-Bericht in der Konsole
|
||||
if (result.resolutionReport) {
|
||||
console.log('🔧 Reparatur-Bericht:');
|
||||
result.resolutionReport.forEach(line => console.log(line));
|
||||
}
|
||||
|
||||
// Entscheidung basierend auf tatsächlichen kritischen Violations
|
||||
const criticalCount = result.violations.filter(v =>
|
||||
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
|
||||
).length;
|
||||
|
||||
if (criticalCount === 0) {
|
||||
showNotification({
|
||||
type: 'success',
|
||||
title: 'Erfolg',
|
||||
message: 'Alle kritischen Probleme wurden behoben! Der Schichtplan kann veröffentlicht werden.'
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
type: 'error',
|
||||
title: 'Kritische Probleme',
|
||||
message: `${criticalCount} kritische Probleme müssen behoben werden`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during assignment:', error);
|
||||
showNotification({
|
||||
@@ -396,6 +443,57 @@ const ShiftPlanView: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const debugSchedulingInput = (employees: Employee[], availabilities: EmployeeAvailability[]) => {
|
||||
console.log('🔍 DEBUG SCHEDULING INPUT:');
|
||||
console.log('==========================');
|
||||
|
||||
// Check if we have the latest data
|
||||
console.log('📊 Employee Count:', employees.length);
|
||||
console.log('📊 Availability Count:', availabilities.length);
|
||||
|
||||
// Log each employee's availability
|
||||
employees.forEach(emp => {
|
||||
const empAvailabilities = availabilities.filter(avail => avail.employeeId === emp.id);
|
||||
console.log(`👤 ${emp.name} (${emp.role}, ${emp.employeeType}): ${empAvailabilities.length} availabilities`);
|
||||
|
||||
if (empAvailabilities.length > 0) {
|
||||
empAvailabilities.forEach(avail => {
|
||||
console.log(` - Day ${avail.dayOfWeek}, TimeSlot ${avail.timeSlotId}: Level ${avail.preferenceLevel}`);
|
||||
});
|
||||
} else {
|
||||
console.log(` ❌ NO AVAILABILITIES SET!`);
|
||||
}
|
||||
});
|
||||
|
||||
// REMOVED: The problematic code that tries to access shiftPlan.employees
|
||||
// We don't have old employee data stored in shiftPlan
|
||||
|
||||
console.log('🔄 All employees are considered "changed" since we loaded fresh data');
|
||||
};
|
||||
|
||||
const forceRefreshData = async () => {
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
const [plan, employeesData, shiftsData] = await Promise.all([
|
||||
shiftPlanService.getShiftPlan(id),
|
||||
employeeService.getEmployees(),
|
||||
shiftAssignmentService.getScheduledShiftsForPlan(id)
|
||||
]);
|
||||
|
||||
setShiftPlan(plan);
|
||||
setEmployees(employeesData.filter(emp => emp.isActive));
|
||||
setScheduledShifts(shiftsData);
|
||||
|
||||
// Force refresh availabilities
|
||||
await refreshAllAvailabilities();
|
||||
|
||||
console.log('✅ All data force-refreshed');
|
||||
} catch (error) {
|
||||
console.error('Error force-refreshing data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePublish = async () => {
|
||||
if (!shiftPlan || !assignmentResult) return;
|
||||
|
||||
@@ -418,15 +516,18 @@ const ShiftPlanView: React.FC = () => {
|
||||
const updatePromises = updatedShifts.map(async (scheduledShift) => {
|
||||
const assignedEmployees = assignmentResult.assignments[scheduledShift.id] || [];
|
||||
|
||||
console.log(`📝 Updating shift ${scheduledShift.id} with`, assignedEmployees, 'employees');
|
||||
//console.log(`📝 Updating shift ${scheduledShift.id} with`, assignedEmployees, 'employees');
|
||||
|
||||
try {
|
||||
// Update the shift with assigned employees
|
||||
const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
||||
await shiftAssignmentService.updateScheduledShift(scheduledShift.id, {
|
||||
assignedEmployees
|
||||
});
|
||||
|
||||
console.log(`✅ Successfully updated shift ${scheduledShift.id}`);
|
||||
if (scheduledShifts.some(s => s.id === scheduledShift.id)) {
|
||||
console.log(`✅ Successfully updated scheduled shift ${scheduledShift.id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to update shift ${scheduledShift.id}:`, error);
|
||||
throw error;
|
||||
@@ -450,18 +551,6 @@ const ShiftPlanView: React.FC = () => {
|
||||
setShiftPlan(reloadedPlan);
|
||||
setScheduledShifts(reloadedShifts);
|
||||
|
||||
// Debug: Überprüfe die aktualisierten Daten
|
||||
console.log('🔍 After publish - Reloaded data:', {
|
||||
planStatus: reloadedPlan.status,
|
||||
scheduledShiftsCount: reloadedShifts.length,
|
||||
shiftsWithAssignments: reloadedShifts.filter(s => s.assignedEmployees && s.assignedEmployees.length > 0).length,
|
||||
allAssignments: reloadedShifts.map(s => ({
|
||||
id: s.id,
|
||||
date: s.date,
|
||||
assigned: s.assignedEmployees
|
||||
}))
|
||||
});
|
||||
|
||||
showNotification({
|
||||
type: 'success',
|
||||
title: 'Erfolg',
|
||||
@@ -488,53 +577,78 @@ const ShiftPlanView: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRevertToDraft = async () => {
|
||||
if (!shiftPlan || !id) return;
|
||||
|
||||
if (!window.confirm('Möchten Sie diesen Schichtplan wirklich zurück in den Entwurfsstatus setzen? Alle Zuweisungen werden entfernt.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const refreshAllAvailabilities = async (): Promise<EmployeeAvailability[]> => {
|
||||
try {
|
||||
setReverting(true);
|
||||
console.log('🔄 Force refreshing ALL availabilities with error handling...');
|
||||
|
||||
// 1. Zuerst zurücksetzen
|
||||
const updatedPlan = await shiftPlanService.revertToDraft(id);
|
||||
|
||||
// 2. Dann ALLE Daten neu laden
|
||||
await loadShiftPlanData();
|
||||
|
||||
// 3. Assignment-Result zurücksetzen
|
||||
setAssignmentResult(null);
|
||||
|
||||
// 4. Preview schließen falls geöffnet
|
||||
setShowAssignmentPreview(false);
|
||||
|
||||
showNotification({
|
||||
type: 'success',
|
||||
title: 'Erfolg',
|
||||
message: 'Schichtplan wurde erfolgreich zurück in den Entwurfsstatus gesetzt. Alle Daten wurden neu geladen.'
|
||||
});
|
||||
|
||||
//const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
||||
console.log('Scheduled shifts after revert:', {
|
||||
hasScheduledShifts: !! scheduledShifts,
|
||||
count: scheduledShifts.length || 0,
|
||||
firstFew: scheduledShifts?.slice(0, 3)
|
||||
});
|
||||
if (!id) {
|
||||
console.error('❌ No plan ID available');
|
||||
return [];
|
||||
}
|
||||
|
||||
const availabilityPromises = employees
|
||||
.filter(emp => emp.isActive)
|
||||
.map(async (emp) => {
|
||||
try {
|
||||
return await employeeService.getAvailabilities(emp.id);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to load availabilities for ${emp.name}:`, error);
|
||||
return []; // Return empty array instead of failing entire operation
|
||||
}
|
||||
});
|
||||
|
||||
const allAvailabilities = await Promise.all(availabilityPromises);
|
||||
const flattenedAvailabilities = allAvailabilities.flat();
|
||||
|
||||
// More robust filtering
|
||||
const planAvailabilities = flattenedAvailabilities.filter(
|
||||
availability => availability && availability.planId === id
|
||||
);
|
||||
|
||||
console.log(`✅ Successfully refreshed ${planAvailabilities.length} availabilities for plan ${id}`);
|
||||
|
||||
// IMMEDIATELY update state
|
||||
setAvailabilities(planAvailabilities);
|
||||
|
||||
return planAvailabilities;
|
||||
} catch (error) {
|
||||
console.error('Error reverting plan to draft:', error);
|
||||
showNotification({
|
||||
type: 'error',
|
||||
title: 'Fehler',
|
||||
message: 'Schichtplan konnte nicht zurückgesetzt werden.'
|
||||
});
|
||||
} finally {
|
||||
setReverting(false);
|
||||
console.error('❌ Critical error refreshing availabilities:', error);
|
||||
// DON'T return old data - throw error or return empty array
|
||||
throw new Error('Failed to refresh availabilities: ' + error);
|
||||
}
|
||||
};
|
||||
|
||||
const validateSchedulingData = (): boolean => {
|
||||
console.log('🔍 Validating scheduling data...');
|
||||
|
||||
const totalEmployees = employees.length;
|
||||
const employeesWithAvailabilities = new Set(
|
||||
availabilities.map(avail => avail.employeeId)
|
||||
).size;
|
||||
|
||||
const availabilityStatus = {
|
||||
totalEmployees,
|
||||
employeesWithAvailabilities,
|
||||
coverage: Math.round((employeesWithAvailabilities / totalEmployees) * 100)
|
||||
};
|
||||
|
||||
console.log('📊 Availability Coverage:', availabilityStatus);
|
||||
|
||||
// Check if we have ALL employee availabilities
|
||||
if (employeesWithAvailabilities < totalEmployees) {
|
||||
const missingEmployees = employees.filter(emp =>
|
||||
!availabilities.some(avail => avail.employeeId === emp.id)
|
||||
);
|
||||
|
||||
console.warn('⚠️ Missing availabilities for employees:',
|
||||
missingEmployees.map(emp => emp.name));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const canPublish = () => {
|
||||
if (!shiftPlan || shiftPlan.status === 'published') return false;
|
||||
|
||||
@@ -560,40 +674,49 @@ const ShiftPlanView: React.FC = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const debugCurrentState = () => {
|
||||
console.log('🔍 CURRENT STATE DEBUG:', {
|
||||
shiftPlan: shiftPlan ? {
|
||||
id: shiftPlan.id,
|
||||
name: shiftPlan.name,
|
||||
status: shiftPlan.status
|
||||
} : null,
|
||||
scheduledShifts: {
|
||||
count: scheduledShifts.length,
|
||||
withAssignments: scheduledShifts.filter(s => s.assignedEmployees && s.assignedEmployees.length > 0).length,
|
||||
details: scheduledShifts.map(s => ({
|
||||
id: s.id,
|
||||
date: s.date,
|
||||
timeSlotId: s.timeSlotId,
|
||||
assignedEmployees: s.assignedEmployees
|
||||
}))
|
||||
},
|
||||
employees: employees.length
|
||||
const reloadAvailabilities = async () => {
|
||||
try {
|
||||
console.log('🔄 Lade Verfügbarkeiten neu...');
|
||||
|
||||
// Load availabilities for all employees
|
||||
const availabilityPromises = employees
|
||||
.filter(emp => emp.isActive)
|
||||
.map(emp => employeeService.getAvailabilities(emp.id));
|
||||
|
||||
const allAvailabilities = await Promise.all(availabilityPromises);
|
||||
const flattenedAvailabilities = allAvailabilities.flat();
|
||||
|
||||
// Filter availabilities to only include those for the current shift plan
|
||||
const planAvailabilities = flattenedAvailabilities.filter(
|
||||
availability => availability.planId === id
|
||||
);
|
||||
|
||||
setAvailabilities(planAvailabilities);
|
||||
console.log('✅ Verfügbarkeiten neu geladen:', planAvailabilities.length);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Neuladen der Verfügbarkeiten:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Füge diese Funktion zu den verfügbaren Aktionen hinzu
|
||||
const handleReloadData = async () => {
|
||||
await loadShiftPlanData();
|
||||
showNotification({
|
||||
type: 'success',
|
||||
title: 'Erfolg',
|
||||
message: 'Daten wurden neu geladen.'
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// Render timetable using the same structure as AvailabilityManager
|
||||
const renderTimetable = () => {
|
||||
debugAssignments();
|
||||
debugCurrentState();
|
||||
const { days, allTimeSlots, timeSlotsByDay } = getTimetableData();
|
||||
const { days, allTimeSlots, timeSlotsByDay } = getExtendedTimetableData();
|
||||
if (!shiftPlan?.id) {
|
||||
console.warn("Shift plan ID is missing");
|
||||
return null;
|
||||
}
|
||||
|
||||
//const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
||||
|
||||
|
||||
if (days.length === 0 || allTimeSlots.length === 0) {
|
||||
return (
|
||||
<div style={{
|
||||
@@ -692,7 +815,6 @@ const ShiftPlanView: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Get assigned employees for this shift
|
||||
let assignedEmployees: string[] = [];
|
||||
let displayText = '';
|
||||
|
||||
@@ -701,11 +823,17 @@ const ShiftPlanView: React.FC = () => {
|
||||
const scheduledShift = scheduledShifts.find(scheduled => {
|
||||
const scheduledDayOfWeek = getDayOfWeek(scheduled.date);
|
||||
return scheduledDayOfWeek === weekday.id &&
|
||||
scheduled.timeSlotId === timeSlot.id;
|
||||
scheduled.timeSlotId === timeSlot.id;
|
||||
});
|
||||
|
||||
if (scheduledShift) {
|
||||
assignedEmployees = scheduledShift.assignedEmployees || [];
|
||||
|
||||
// DEBUG: Log if we're still seeing old data
|
||||
if (assignedEmployees.length > 0) {
|
||||
console.warn(`⚠️ Found non-empty assignments for ${weekday.name} ${timeSlot.name}:`, assignedEmployees);
|
||||
}
|
||||
|
||||
displayText = assignedEmployees.map(empId => {
|
||||
const employee = employees.find(emp => emp.id === empId);
|
||||
return employee ? employee.name : 'Unbekannt';
|
||||
@@ -716,7 +844,7 @@ const ShiftPlanView: React.FC = () => {
|
||||
const scheduledShift = scheduledShifts.find(scheduled => {
|
||||
const scheduledDayOfWeek = getDayOfWeek(scheduled.date);
|
||||
return scheduledDayOfWeek === weekday.id &&
|
||||
scheduled.timeSlotId === timeSlot.id;
|
||||
scheduled.timeSlotId === timeSlot.id;
|
||||
});
|
||||
|
||||
if (scheduledShift && assignmentResult.assignments[scheduledShift.id]) {
|
||||
@@ -728,7 +856,7 @@ const ShiftPlanView: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// If no assignments yet, show required count
|
||||
// If no assignments yet, show empty or required count
|
||||
if (!displayText) {
|
||||
const shiftsForSlot = shiftPlan?.shifts?.filter(shift =>
|
||||
shift.dayOfWeek === weekday.id &&
|
||||
@@ -738,7 +866,13 @@ const ShiftPlanView: React.FC = () => {
|
||||
const totalRequired = shiftsForSlot.reduce((sum, shift) =>
|
||||
sum + shift.requiredEmployees, 0);
|
||||
|
||||
// Show "0/2" instead of just "0" to indicate it's empty
|
||||
displayText = `0/${totalRequired}`;
|
||||
|
||||
// Optional: Show empty state more clearly
|
||||
if (totalRequired === 0) {
|
||||
displayText = '-';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -766,7 +900,7 @@ const ShiftPlanView: React.FC = () => {
|
||||
if (loading) return <div>Lade Schichtplan...</div>;
|
||||
if (!shiftPlan) return <div>Schichtplan nicht gefunden</div>;
|
||||
|
||||
const { days, allTimeSlots } = getTimetableData();
|
||||
const { days, allTimeSlots } = getExtendedTimetableData();
|
||||
const availabilityStatus = getAvailabilityStatus();
|
||||
|
||||
|
||||
@@ -799,25 +933,25 @@ const ShiftPlanView: React.FC = () => {
|
||||
{shiftPlan.status === 'published' ? 'Veröffentlicht' : 'Entwurf'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
|
||||
{shiftPlan.status === 'published' && hasRole(['admin', 'instandhalter']) && (
|
||||
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
|
||||
{shiftPlan.status === 'published' && hasRole(['admin', 'instandhalter']) && (
|
||||
<button
|
||||
onClick={handleRevertToDraft}
|
||||
disabled={reverting}
|
||||
onClick={handleRecreateAssignments}
|
||||
disabled={recreating}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
backgroundColor: '#e74c3c',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
cursor: recreating ? 'not-allowed' : 'pointer',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
{reverting ? 'Zurücksetzen...' : 'Zu Entwurf zurücksetzen'}
|
||||
{recreating ? 'Lösche Zuweisungen...' : 'Zuweisungen neu berechnen'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => navigate('/shift-plans')}
|
||||
style={{
|
||||
|
||||
Reference in New Issue
Block a user