removed unnecessary scheduledshift files in backend and put in shiftPlan

This commit is contained in:
2025-10-14 22:47:08 +02:00
parent f45d0391c1
commit d60a6d9fae
7 changed files with 740 additions and 366 deletions

View File

@@ -1,45 +1,274 @@
// frontend/src/pages/Dashboard/Dashboard.tsx
// frontend/src/pages/Dashboard/Dashboard.tsx - Updated calculations
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
import { shiftPlanService } from '../../services/shiftPlanService';
import { employeeService } from '../../services/employeeService';
import { ShiftPlan } from '../../models/ShiftPlan';
import { Employee } from '../../models/Employee';
// Mock Data für die Demo
const mockData = {
currentShiftPlan: {
id: '1',
name: 'November Schichtplan 2024',
period: '01.11.2024 - 30.11.2024',
status: 'Aktiv',
shiftsCovered: 85,
totalShifts: 120
},
upcomingShifts: [
{ id: '1', date: 'Heute', time: '08:00 - 16:00', type: 'Frühschicht', assigned: true },
{ id: '2', date: 'Morgen', time: '14:00 - 22:00', type: 'Spätschicht', assigned: true },
{ id: '3', date: '15.11.2024', time: '08:00 - 16:00', type: 'Frühschicht', assigned: false }
],
interface DashboardData {
currentShiftPlan: ShiftPlan | null;
upcomingShifts: Array<{
id: string;
date: string;
time: string;
type: string;
assigned: boolean;
planName: string;
}>;
teamStats: {
totalEmployees: 24,
availableToday: 18,
onVacation: 3,
sickLeave: 2
},
recentActivities: [
{ id: '1', action: 'Schichtplan veröffentlicht', user: 'Max Mustermann', time: 'vor 2 Stunden' },
{ id: '2', action: 'Mitarbeiter hinzugefügt', user: 'Sarah Admin', time: 'vor 4 Stunden' },
{ id: '3', action: 'Verfügbarkeit geändert', user: 'Tom Bauer', time: 'vor 1 Tag' }
]
};
totalEmployees: number;
availableToday: number;
onVacation: number;
sickLeave: number;
};
recentPlans: ShiftPlan[];
}
const Dashboard: React.FC = () => {
const { user, hasRole } = useAuth();
const [loading, setLoading] = useState(true);
const [data, setData] = useState<DashboardData>({
currentShiftPlan: null,
upcomingShifts: [],
teamStats: {
totalEmployees: 0,
availableToday: 0,
onVacation: 0,
sickLeave: 0
},
recentPlans: []
});
useEffect(() => {
// Simuliere Daten laden
setTimeout(() => setLoading(false), 1000);
loadDashboardData();
}, []);
const loadDashboardData = async () => {
try {
setLoading(true);
console.log('🔄 Loading dashboard data...');
const [shiftPlans, employees] = await Promise.all([
shiftPlanService.getShiftPlans(),
employeeService.getEmployees()
]);
console.log('📊 Loaded data:', {
plans: shiftPlans.length,
employees: employees.length,
plansWithShifts: shiftPlans.filter(p => p.scheduledShifts && p.scheduledShifts.length > 0).length
});
// Debug: Log plan details
shiftPlans.forEach(plan => {
console.log(`Plan: ${plan.name}`, {
status: plan.status,
startDate: plan.startDate,
endDate: plan.endDate,
scheduledShifts: plan.scheduledShifts?.length || 0,
isTemplate: plan.isTemplate
});
});
// Find current shift plan (published and current date within range)
const today = new Date().toISOString().split('T')[0];
const currentPlan = findCurrentShiftPlan(shiftPlans, today);
// Get user's upcoming shifts
const userShifts = await loadUserUpcomingShifts(shiftPlans, today);
// Calculate team stats
const activeEmployees = employees.filter(emp => emp.isActive);
const teamStats = calculateTeamStats(activeEmployees);
// Get recent plans (non-templates, sorted by creation date)
const recentPlans = getRecentPlans(shiftPlans);
setData({
currentShiftPlan: currentPlan,
upcomingShifts: userShifts,
teamStats,
recentPlans
});
console.log('✅ Dashboard data loaded:', {
currentPlan: currentPlan?.name,
userShifts: userShifts.length,
teamStats,
recentPlans: recentPlans.length
});
} catch (error) {
console.error('❌ Error loading dashboard data:', error);
} finally {
setLoading(false);
}
};
const findCurrentShiftPlan = (plans: ShiftPlan[], today: string): ShiftPlan | null => {
// First, try to find a published plan where today is within the date range
const activePlan = plans.find(plan =>
plan.status === 'published' &&
!plan.isTemplate &&
plan.startDate &&
plan.endDate &&
plan.startDate <= today &&
plan.endDate >= today
);
if (activePlan) {
console.log('✅ Found active plan:', activePlan.name);
return activePlan;
}
// If no active plan found, try to find the most recent published plan
const publishedPlans = plans
.filter(plan => plan.status === 'published' && !plan.isTemplate)
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
console.log('📅 Published plans available:', publishedPlans.map(p => p.name));
return publishedPlans[0] || null;
};
const loadUserUpcomingShifts = async (shiftPlans: ShiftPlan[], today: string): Promise<DashboardData['upcomingShifts']> => {
if (!user) return [];
try {
const userShifts: DashboardData['upcomingShifts'] = [];
// Check each plan for user assignments
for (const plan of shiftPlans) {
if (plan.status !== 'published' || !plan.scheduledShifts || plan.scheduledShifts.length === 0) {
continue;
}
console.log(`🔍 Checking plan ${plan.name} for user shifts:`, plan.scheduledShifts.length);
for (const scheduledShift of plan.scheduledShifts) {
// Ensure assignedEmployees is an array
const assignedEmployees = Array.isArray(scheduledShift.assignedEmployees)
? scheduledShift.assignedEmployees
: [];
if (scheduledShift.date >= today && assignedEmployees.includes(user.id)) {
const timeSlot = plan.timeSlots.find(ts => ts.id === scheduledShift.timeSlotId);
userShifts.push({
id: scheduledShift.id,
date: formatShiftDate(scheduledShift.date),
time: timeSlot ? `${timeSlot.startTime} - ${timeSlot.endTime}` : 'Unbekannt',
type: timeSlot?.name || 'Unbekannt',
assigned: true,
planName: plan.name
});
}
}
}
// Sort by date and limit to 5 upcoming shifts
return userShifts
.sort((a, b) => {
// Convert formatted dates back to Date objects for sorting
const dateA = a.date === 'Heute' ? today : a.date === 'Morgen' ?
new Date(new Date().setDate(new Date().getDate() + 1)).toISOString().split('T')[0] : a.date;
const dateB = b.date === 'Heute' ? today : b.date === 'Morgen' ?
new Date(new Date().setDate(new Date().getDate() + 1)).toISOString().split('T')[0] : b.date;
return new Date(dateA).getTime() - new Date(dateB).getTime();
})
.slice(0, 5);
} catch (error) {
console.error('Error loading user shifts:', error);
return [];
}
};
const calculateTeamStats = (employees: Employee[]) => {
const totalEmployees = employees.length;
// For now, we'll use simpler calculations
// In a real app, you'd check actual availability from the database
const availableToday = Math.max(1, Math.floor(totalEmployees * 0.7)); // 70% available as estimate
return {
totalEmployees,
availableToday,
onVacation: 0, // Would need vacation tracking
sickLeave: 0 // Would need sick leave tracking
};
};
const getRecentPlans = (plans: ShiftPlan[]): ShiftPlan[] => {
return plans
.filter(plan => !plan.isTemplate)
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
.slice(0, 3);
};
const formatShiftDate = (dateString: string): string => {
const today = new Date().toISOString().split('T')[0];
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const tomorrowString = tomorrow.toISOString().split('T')[0];
if (dateString === today) {
return 'Heute';
} else if (dateString === tomorrowString) {
return 'Morgen';
} else {
return new Date(dateString).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
}
};
const formatPlanPeriod = (plan: ShiftPlan): string => {
if (!plan.startDate || !plan.endDate) return 'Kein Zeitraum definiert';
const start = new Date(plan.startDate).toLocaleDateString('de-DE');
const end = new Date(plan.endDate).toLocaleDateString('de-DE');
return `${start} - ${end}`;
};
const calculatePlanProgress = (plan: ShiftPlan): { covered: number; total: number; percentage: number } => {
if (!plan.scheduledShifts || plan.scheduledShifts.length === 0) {
console.log(`📊 Plan ${plan.name} has no scheduled shifts`);
return { covered: 0, total: 0, percentage: 0 };
}
const totalShifts = plan.scheduledShifts.length;
const coveredShifts = plan.scheduledShifts.filter(shift => {
const assigned = Array.isArray(shift.assignedEmployees) ? shift.assignedEmployees : [];
return assigned.length > 0;
}).length;
const percentage = totalShifts > 0 ? Math.round((coveredShifts / totalShifts) * 100) : 0;
console.log(`📊 Plan ${plan.name} progress:`, {
totalShifts,
coveredShifts,
percentage
});
return {
covered: coveredShifts,
total: totalShifts,
percentage
};
};
// Add refresh functionality
const handleRefresh = () => {
loadDashboardData();
};
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '40px' }}>
@@ -48,6 +277,50 @@ const Dashboard: React.FC = () => {
);
}
const regenerateScheduledShifts = async (planId: string) => {
await shiftPlanService.regenerateScheduledShifts(planId);
loadDashboardData();
};
const PlanDebugInfo = () => {
if (!data.currentShiftPlan) return null;
return (
<div style={{
backgroundColor: '#fff3cd',
padding: '15px',
borderRadius: '8px',
marginBottom: '20px',
border: '1px solid #ffeaa7',
fontSize: '14px'
}}>
<h4>🔍 Plan Debug Information:</h4>
<div><strong>Plan ID:</strong> {data.currentShiftPlan.id}</div>
<div><strong>Status:</strong> {data.currentShiftPlan.status}</div>
<div><strong>Is Template:</strong> {data.currentShiftPlan.isTemplate ? 'Yes' : 'No'}</div>
<div><strong>Start Date:</strong> {data.currentShiftPlan.startDate}</div>
<div><strong>End Date:</strong> {data.currentShiftPlan.endDate}</div>
<div><strong>Shifts Defined:</strong> {data.currentShiftPlan.shifts?.length || 0}</div>
<div><strong>Time Slots:</strong> {data.currentShiftPlan.timeSlots?.length || 0}</div>
<div><strong>Scheduled Shifts:</strong> {data.currentShiftPlan.scheduledShifts?.length || 0}</div>
{data.currentShiftPlan.shifts && data.currentShiftPlan.shifts.length > 0 && (
<div style={{ marginTop: '10px' }}>
<strong>Defined Shifts:</strong>
{data.currentShiftPlan.shifts.slice(0, 3).map(shift => (
<div key={shift.id} style={{ marginLeft: '10px', fontSize: '12px' }}>
Day {shift.dayOfWeek} - TimeSlot: {shift.timeSlotId} - Required: {shift.requiredEmployees}
</div>
))}
{data.currentShiftPlan.shifts.length > 3 && <div>... and {data.currentShiftPlan.shifts.length - 3} more</div>}
</div>
)}
</div>
);
};
const progress = data.currentShiftPlan ? calculatePlanProgress(data.currentShiftPlan) : { covered: 0, total: 0, percentage: 0 };
return (
<div>
{/* Willkommens-Bereich */}
@@ -56,21 +329,41 @@ const Dashboard: React.FC = () => {
padding: '25px',
borderRadius: '8px',
marginBottom: '30px',
border: '1px solid #b6d7e8'
border: '1px solid #b6d7e8',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<h1 style={{ margin: '0 0 10px 0', color: '#2c3e50' }}>
Willkommen zurück, {user?.name}! 👋
</h1>
<p style={{ margin: 0, color: '#546e7a', fontSize: '16px' }}>
{new Date().toLocaleDateString('de-DE', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</p>
<div>
<h1 style={{ margin: '0 0 10px 0', color: '#2c3e50' }}>
Willkommen zurück, {user?.name}! 👋
</h1>
<p style={{ margin: 0, color: '#546e7a', fontSize: '16px' }}>
{new Date().toLocaleDateString('de-DE', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</p>
</div>
<button
onClick={handleRefresh}
style={{
padding: '8px 16px',
backgroundColor: '#3498db',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
🔄 Aktualisieren
</button>
</div>
<PlanDebugInfo />
{/* Quick Actions - Nur für Admins/Instandhalter */}
{hasRole(['admin', 'instandhalter']) && (
<div style={{ marginBottom: '30px' }}>
@@ -159,46 +452,78 @@ const Dashboard: React.FC = () => {
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
<h3 style={{ margin: '0 0 15px 0', color: '#2c3e50' }}>📊 Aktueller Schichtplan</h3>
<div style={{ marginBottom: '15px' }}>
<div style={{ fontWeight: 'bold', fontSize: '18px' }}>
{mockData.currentShiftPlan.name}
</div>
<div style={{ color: '#666', fontSize: '14px' }}>
{mockData.currentShiftPlan.period}
</div>
</div>
<div style={{ marginBottom: '15px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '5px' }}>
<span>Fortschritt:</span>
<span>{mockData.currentShiftPlan.shiftsCovered}/{mockData.currentShiftPlan.totalShifts} Schichten</span>
</div>
<div style={{
width: '100%',
backgroundColor: '#ecf0f1',
borderRadius: '10px',
overflow: 'hidden'
}}>
{data.currentShiftPlan ? (
<>
<div style={{ marginBottom: '15px' }}>
<div style={{ fontWeight: 'bold', fontSize: '18px' }}>
{data.currentShiftPlan.name}
</div>
<div style={{ color: '#666', fontSize: '14px' }}>
{formatPlanPeriod(data.currentShiftPlan)}
</div>
</div>
<div style={{ marginBottom: '15px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '5px' }}>
<span>Fortschritt:</span>
<span>
{progress.covered}/{progress.total} Schichten ({progress.percentage}%)
</span>
</div>
<div style={{
width: '100%',
backgroundColor: '#ecf0f1',
borderRadius: '10px',
overflow: 'hidden'
}}>
<div style={{
width: `${progress.percentage}%`,
backgroundColor: progress.percentage > 0 ? '#3498db' : '#95a5a6',
height: '8px',
borderRadius: '10px',
transition: 'width 0.3s ease'
}} />
</div>
{progress.total === 0 && (
<div style={{ fontSize: '12px', color: '#e74c3c', marginTop: '5px' }}>
Keine Schichten im Plan definiert
</div>
)}
</div>
<div style={{
width: `${(mockData.currentShiftPlan.shiftsCovered / mockData.currentShiftPlan.totalShifts) * 100}%`,
backgroundColor: '#3498db',
height: '8px',
borderRadius: '10px'
}} />
display: 'inline-block',
backgroundColor: data.currentShiftPlan.status === 'published' ? '#2ecc71' : '#f39c12',
color: 'white',
padding: '4px 12px',
borderRadius: '20px',
fontSize: '12px',
fontWeight: 'bold'
}}>
{data.currentShiftPlan.status === 'published' ? 'Aktiv' : 'Entwurf'}
</div>
</>
) : (
<div style={{ textAlign: 'center', padding: '20px', color: '#666' }}>
<div style={{ fontSize: '48px', marginBottom: '10px' }}>📅</div>
<div>Kein aktiver Schichtplan</div>
{hasRole(['admin', 'instandhalter']) && (
<Link to="/shift-plans/new">
<button style={{
marginTop: '10px',
padding: '8px 16px',
backgroundColor: '#3498db',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}>
Ersten Plan erstellen
</button>
</Link>
)}
</div>
</div>
<div style={{
display: 'inline-block',
backgroundColor: mockData.currentShiftPlan.status === 'Aktiv' ? '#2ecc71' : '#f39c12',
color: 'white',
padding: '4px 12px',
borderRadius: '20px',
fontSize: '12px',
fontWeight: 'bold'
}}>
{mockData.currentShiftPlan.status}
</div>
)}
</div>
{/* Team-Statistiken */}
@@ -214,25 +539,25 @@ const Dashboard: React.FC = () => {
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>Gesamt Mitarbeiter:</span>
<span style={{ fontWeight: 'bold', fontSize: '18px' }}>
{mockData.teamStats.totalEmployees}
{data.teamStats.totalEmployees}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>Verfügbar heute:</span>
<span style={{ fontWeight: 'bold', color: '#2ecc71' }}>
{mockData.teamStats.availableToday}
{data.teamStats.availableToday}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>Im Urlaub:</span>
<span style={{ fontWeight: 'bold', color: '#f39c12' }}>
{mockData.teamStats.onVacation}
{data.teamStats.onVacation}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>Krankgeschrieben:</span>
<span style={{ fontWeight: 'bold', color: '#e74c3c' }}>
{mockData.teamStats.sickLeave}
{data.teamStats.sickLeave}
</span>
</div>
</div>
@@ -255,39 +580,47 @@ const Dashboard: React.FC = () => {
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
<h3 style={{ margin: '0 0 15px 0', color: '#2c3e50' }}> Meine nächsten Schichten</h3>
<div style={{ display: 'grid', gap: '10px' }}>
{mockData.upcomingShifts.map(shift => (
<div key={shift.id} style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '12px',
backgroundColor: '#f8f9fa',
borderRadius: '6px',
border: shift.assigned ? '1px solid #d4edda' : '1px solid #fff3cd'
}}>
<div>
<div style={{ fontWeight: 'bold' }}>{shift.date}</div>
<div style={{ fontSize: '14px', color: '#666' }}>{shift.time}</div>
<div style={{ fontSize: '12px', color: '#999' }}>{shift.type}</div>
</div>
<div style={{
padding: '4px 8px',
backgroundColor: shift.assigned ? '#d4edda' : '#fff3cd',
color: shift.assigned ? '#155724' : '#856404',
borderRadius: '4px',
fontSize: '12px',
fontWeight: 'bold'
{data.upcomingShifts.length > 0 ? (
<div style={{ display: 'grid', gap: '10px' }}>
{data.upcomingShifts.map(shift => (
<div key={shift.id} style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '12px',
backgroundColor: '#f8f9fa',
borderRadius: '6px',
border: '1px solid #d4edda'
}}>
{shift.assigned ? 'Zugewiesen' : 'Noch offen'}
<div>
<div style={{ fontWeight: 'bold' }}>{shift.date}</div>
<div style={{ fontSize: '14px', color: '#666' }}>{shift.time}</div>
<div style={{ fontSize: '12px', color: '#999' }}>{shift.type}</div>
<div style={{ fontSize: '11px', color: '#666' }}>{shift.planName}</div>
</div>
<div style={{
padding: '4px 8px',
backgroundColor: '#d4edda',
color: '#155724',
borderRadius: '4px',
fontSize: '12px',
fontWeight: 'bold'
}}>
Zugewiesen
</div>
</div>
</div>
))}
</div>
))}
</div>
) : (
<div style={{ textAlign: 'center', padding: '20px', color: '#666' }}>
<div style={{ fontSize: '48px', marginBottom: '10px' }}></div>
<div>Keine anstehenden Schichten</div>
</div>
)}
</div>
)}
{/* Letzte Aktivitäten (für Admins/Instandhalter) */}
{/* Letzte Schichtpläne (für Admins/Instandhalter) */}
{hasRole(['admin', 'instandhalter']) && (
<div style={{
backgroundColor: 'white',
@@ -296,27 +629,63 @@ const Dashboard: React.FC = () => {
border: '1px solid #e0e0e0',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
<h3 style={{ margin: '0 0 15px 0', color: '#2c3e50' }}>📝 Letzte Aktivitäten</h3>
<div style={{ display: 'grid', gap: '12px' }}>
{mockData.recentActivities.map(activity => (
<div key={activity.id} style={{
padding: '12px',
backgroundColor: '#f8f9fa',
borderRadius: '6px',
borderLeft: '4px solid #3498db'
}}>
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>
{activity.action}
<h3 style={{ margin: '0 0 15px 0', color: '#2c3e50' }}>📝 Letzte Schichtpläne</h3>
{data.recentPlans.length > 0 ? (
<div style={{ display: 'grid', gap: '12px' }}>
{data.recentPlans.map(plan => (
<div key={plan.id} style={{
padding: '12px',
backgroundColor: '#f8f9fa',
borderRadius: '6px',
borderLeft: `4px solid ${
plan.status === 'published' ? '#2ecc71' :
plan.status === 'draft' ? '#f39c12' : '#95a5a6'
}`
}}>
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>
{plan.name}
</div>
<div style={{ fontSize: '14px', color: '#666' }}>
{formatPlanPeriod(plan)}
</div>
<div style={{
fontSize: '12px',
color: '#999',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: '4px'
}}>
<span>
Status: {plan.status === 'published' ? 'Veröffentlicht' :
plan.status === 'draft' ? 'Entwurf' : 'Archiviert'}
</span>
<Link to={`/shift-plans/${plan.id}`} style={{ color: '#3498db', textDecoration: 'none' }}>
Anzeigen
</Link>
</div>
</div>
<div style={{ fontSize: '14px', color: '#666' }}>
von {activity.user}
</div>
<div style={{ fontSize: '12px', color: '#999' }}>
{activity.time}
</div>
</div>
))}
</div>
))}
</div>
) : (
<div style={{ textAlign: 'center', padding: '20px', color: '#666' }}>
<div style={{ fontSize: '48px', marginBottom: '10px' }}>📋</div>
<div>Noch keine Schichtpläne erstellt</div>
<Link to="/shift-plans/new">
<button style={{
marginTop: '10px',
padding: '8px 16px',
backgroundColor: '#3498db',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}>
Ersten Plan erstellen
</button>
</Link>
</div>
)}
</div>
)}