@@ -1,5 +1,5 @@
// frontend/src/pages/ShiftPlans/ShiftPlanView.tsx
import React , { useState , useEffect } from 'react' ;
import React , { useState , useEffect , useRef } from 'react' ;
import { useParams , useNavigate } from 'react-router-dom' ;
import { useAuth } from '../../contexts/AuthContext' ;
import { shiftPlanService } from '../../services/shiftPlanService' ;
@@ -56,6 +56,9 @@ const ShiftPlanView: React.FC = () => {
const [ showAssignmentPreview , setShowAssignmentPreview ] = useState ( false ) ;
const [ recreating , setRecreating ] = useState ( false ) ;
const [ exporting , setExporting ] = useState ( false ) ;
const [ exportType , setExportType ] = useState < 'pdf' | 'excel' | null > ( null ) ;
const [ dropdownWidth , setDropdownWidth ] = useState ( 0 ) ;
const dropdownRef = useRef < HTMLDivElement > ( null ) ;
useEffect ( ( ) = > {
loadShiftPlanData ( ) ;
@@ -121,6 +124,12 @@ const ShiftPlanView: React.FC = () => {
}
} , [ availabilities ] ) ;
useEffect ( ( ) = > {
if ( dropdownRef . current ) {
setDropdownWidth ( dropdownRef . current . offsetWidth ) ;
}
} , [ exportType ] ) ;
// Create a data structure that maps days to their shifts with time slot info - SAME AS AVAILABILITYMANAGER
const getTimetableData = ( ) = > {
if ( ! shiftPlan || ! shiftPlan . shifts || ! shiftPlan . timeSlots ) {
@@ -242,60 +251,33 @@ const ShiftPlanView: React.FC = () => {
} ;
} ;
const handleExportExcel = async ( ) = > {
if ( ! shiftPlan ) return ;
const handleExport = async ( ) = > {
if ( ! shiftPlan || ! exportType ) return ;
try {
setExporting ( true ) ;
// Call the export service
const blob = await shiftPlanService . exportShiftPlanToExcel ( shiftPlan . id ) ;
// Use file-saver to download the file
saveAs ( blob , ` Schichtplan_ ${ shiftPlan . name } _ ${ new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] } .xlsx ` ) ;
let blob : Blob ;
if ( exportType === 'excel' ) {
blob = await shiftPlanService . exportShiftPlanToExcel ( shiftPlan . id ) ;
saveAs ( blob , ` Schichtplan_ ${ shiftPlan . name } _ ${ new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] } .xlsx ` ) ;
} else {
blob = await shiftPlanService . exportShiftPlanToPDF ( shiftPlan . id ) ;
saveAs ( blob , ` Schichtplan_ ${ shiftPlan . name } _ ${ new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] } .pdf ` ) ;
}
showNotification ( {
type : 'success' ,
title : 'Export erfolgreich' ,
message : ' Der Schichtplan wurde als Excel-Datei exportiert.'
message : ` Der Schichtplan wurde als ${ exportType === 'excel' ? 'Excel' : 'PDF' } exportiert.`
} ) ;
} catch ( error ) {
console . error ( ' Error exporting to Excel:' , error ) ;
console . error ( ` Error exporting to ${ exportType } : ` , error ) ;
showNotification ( {
type : 'error' ,
title : 'Export fehlgeschlagen' ,
message : 'Der Excel -Export konnte nicht durchgeführt werden.'
} ) ;
} finally {
setExporting ( false ) ;
}
} ;
const handleExportPDF = async ( ) = > {
if ( ! shiftPlan ) return ;
try {
setExporting ( true ) ;
// Call the PDF export service
const blob = await shiftPlanService . exportShiftPlanToPDF ( shiftPlan . id ) ;
// Use file-saver to download the file
saveAs ( blob , ` Schichtplan_ ${ shiftPlan . name } _ ${ new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] } .pdf ` ) ;
showNotification ( {
type : 'success' ,
title : 'Export erfolgreich' ,
message : 'Der Schichtplan wurde als PDF exportiert.'
} ) ;
} catch ( error ) {
console . error ( 'Error exporting to PDF:' , error ) ;
showNotification ( {
type : 'error' ,
title : 'Export fehlgeschlagen' ,
message : 'Der PDF-Export konnte nicht durchgeführt werden.'
message : ` Der ${ exportType === 'excel' ? 'Excel' : 'PDF' } -Export konnte nicht durchgeführt werden.`
} ) ;
} finally {
setExporting ( false ) ;
@@ -679,7 +661,7 @@ const ShiftPlanView: React.FC = () => {
const matchingScheduledShifts = scheduledShifts . filter ( scheduled = > {
const dayOfWeek = getDayOfWeek ( scheduled . date ) ;
return dayOfWeek === shiftPattern . dayOfWeek &&
scheduled . timeSlotId === shiftPattern . timeSlotId ;
scheduled . timeSlotId === shiftPattern . timeSlotId ;
} ) ;
console . log ( ` 📅 Shift Pattern: ${ shiftPattern . id } ` ) ;
@@ -896,9 +878,6 @@ const ShiftPlanView: React.FC = () => {
< div style = { { fontSize : '14px' , color : '#666' } } >
{ formatTime ( timeSlot . startTime ) } - { formatTime ( timeSlot . endTime ) }
< / div >
< div style = { { fontSize : '11px' , color : '#999' , marginTop : '4px' } } >
ID : { timeSlot . id . substring ( 0 , 8 ) } . . .
< / div >
< / td >
{ days . map ( weekday = > {
const shift = timeSlot . shiftsByDay [ weekday . id ] ;
@@ -922,63 +901,113 @@ const ShiftPlanView: React.FC = () => {
const isValidShift = shift . timeSlotId === timeSlot . id && shift . dayOfWeek === weekday . id ;
let assignedEmployees : string [ ] = [ ] ;
let displayText = '' ;
let displayContent : React.ReactNode = null ;
// Helper function to create employee boxes
const createEmployeeBoxes = ( employeeIds : string [ ] ) = > {
return employeeIds . map ( empId = > {
const employee = employees . find ( emp = > emp . id === empId ) ;
if ( ! employee ) return null ;
// Determine background color based on employee role
let backgroundColor = '#642ab5' ; // Default: non-trainee personnel (purple)
if ( employee . isTrainee ) {
backgroundColor = '#cda8f0' ; // Trainee
} else if ( employee . employeeType === 'manager' ) {
backgroundColor = '#CC0000' ; // Manager
}
return (
< div
key = { empId }
style = { {
backgroundColor ,
color : 'white' ,
padding : '4px 8px' ,
borderRadius : '4px' ,
marginBottom : '2px' ,
fontSize : '12px' ,
textAlign : 'center' ,
whiteSpace : 'nowrap' ,
overflow : 'hidden' ,
textOverflow : 'ellipsis'
} }
title = { ` ${ employee . firstname } ${ employee . lastname } ${ employee . isTrainee ? ' (Trainee)' : '' } ` }
>
{ employee . firstname } { employee . lastname }
< / div >
) ;
} ) . filter ( Boolean ) ;
} ;
// Helper function to get fallback content
const getFallbackContent = ( ) = > {
const shiftsForSlot = shiftPlan ? . shifts ? . filter ( s = >
s . dayOfWeek === weekday . id &&
s . timeSlotId === timeSlot . id
) || [ ] ;
const totalRequired = shiftsForSlot . reduce ( ( sum , s ) = > sum + s . requiredEmployees , 0 ) ;
return totalRequired === 0 ? '-' : ` 0/ ${ totalRequired } ` ;
} ;
if ( shiftPlan ? . status === 'published' ) {
// For published plans, use actual assignments from scheduled shifts
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
// 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 = assigned Employees. map ( empId = > {
const employee = employees . find ( emp = > emp . id === empId ) ;
return employee ? ` ${ employee . firstname } ${ employee . lastname } ` : 'Unbekannt' ;
} ) . join ( ', ' ) ;
const employeeBoxes = create EmployeeBoxe s( assignedEmployees ) ;
displayContent = employeeBoxe s . length > 0 ? (
< div style = { { display : 'flex' , flexDirection : 'column' , gap : '2px' } } >
{ employeeBoxes }
< / div >
) : (
< div style = { { color : '#666' , fontStyle : 'italic' } } >
{ getFallbackContent ( ) }
< / div >
) ;
}
} else if ( assignmentResult ) {
// For draft with preview, use assignment result
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 = getAssignmentsForScheduledShift ( scheduledShift ) ;
displayText = assigned Employees. map ( empId = > {
const employee = employees . find ( emp = > emp . id === empId ) ;
return employee ? ` ${ employee . firstname } ${ employee . lastname } ` : 'Unbekannt' ;
} ) . join ( ', ' ) ;
const employeeBoxes = create EmployeeBoxe s( assignedEmployees ) ;
displayContent = employeeBoxe s . length > 0 ? (
< div style = { { display : 'flex' , flexDirection : 'column' , gap : '2px' } } >
{ employeeBoxes }
< / div >
) : (
< div style = { { color : '#666' , fontStyle : 'italic' } } >
{ getFallbackContent ( ) }
< / div >
) ;
}
}
// If no assignments yet, show empty or required count
if ( ! displayTex t ) {
const shiftsForSlot = shiftPlan ? . shifts ? . filter ( s = >
s . dayOfWeek === weekday . id &&
s . timeSlotId === timeSlot . id
) || [ ] ;
const totalRequired = shiftsForSlot . reduce ( ( sum , s ) = >
sum + s . 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 = '-' ;
}
// If no display content set yet, use fallback
if ( ! displayConten t ) {
displayContent = (
< div style = { { color : '#666' , fontStyle : 'italic' } } >
{ getFallbackContent ( ) }
< / div >
) ;
}
return (
@@ -1007,13 +1036,13 @@ const ShiftPlanView: React.FC = () => {
alignItems : 'center' ,
justifyContent : 'center'
} }
title = { ` Shift Validierung: timeSlotId= ${ shift . timeSlotId } , dayOfWeek= ${ shift . dayOfWeek } ` }
title = { ` Shift Validierung: timeSlotId= ${ shift . timeSlotId } , dayOfWeek= ${ shift . dayOfWeek } ` }
>
⚠ ️
< / div >
) }
{ displayTex t }
{ displayConten t }
{ /* Shift debug info - SAME AS AVAILABILITYMANAGER */ }
< div style = { {
@@ -1023,8 +1052,6 @@ const ShiftPlanView: React.FC = () => {
textAlign : 'left' ,
fontFamily : 'monospace'
} } >
< div > Shift : { shift . id . substring ( 0 , 6 ) } . . . < / div >
< div > Day : { shift . dayOfWeek } < / div >
{ ! isValidShift && (
< div style = { { color : '#e74c3c' , fontWeight : 'bold' } } >
VALIDATION ERROR
@@ -1039,7 +1066,6 @@ const ShiftPlanView: React.FC = () => {
< / tbody >
< / table >
< / div >
< / div >
) ;
} ;
@@ -1080,50 +1106,8 @@ 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' , 'maintenance' ] ) && (
< >
< button
onClick = { handleExportExcel }
disabled = { exporting }
style = { {
padding : '10px 20px' ,
backgroundColor : '#27ae60' ,
color : 'white' ,
border : 'none' ,
borderRadius : '4px' ,
cursor : exporting ? 'not-allowed' : 'pointer' ,
fontWeight : 'bold' ,
display : 'flex' ,
alignItems : 'center' ,
gap : '8px'
} }
>
{ exporting ? '🔄' : '📊' } { exporting ? 'Exportiert...' : 'Excel Export' }
< / button >
< button
onClick = { handleExportPDF }
disabled = { exporting }
style = { {
padding : '10px 20px' ,
backgroundColor : '#e74c3c' ,
color : 'white' ,
border : 'none' ,
borderRadius : '4px' ,
cursor : exporting ? 'not-allowed' : 'pointer' ,
fontWeight : 'bold' ,
display : 'flex' ,
alignItems : 'center' ,
gap : '8px'
} }
>
{ exporting ? '🔄' : '📄' } { exporting ? 'Exportiert...' : 'PDF Export' }
< / button >
< / >
) }
{ /* Your existing "Zuweisungen neu berechnen" button */ }
< div style = { { display : 'flex' , gap : '10px' , alignItems : 'center' } } >
{ /* "Zuweisungen neu berechnen" button */ }
{ shiftPlan . status === 'published' && hasRole ( [ 'admin' , 'maintenance' ] ) && (
< button
onClick = { handleRecreateAssignments }
@@ -1405,7 +1389,7 @@ const ShiftPlanView: React.FC = () => {
Abbrechen
< / button >
{ /* KORRIGIERTER BUTTON MIT TYPESCRIPT-FIX */ }
{ /* BUTTON zum publishen */ }
< button
onClick = { handlePublish }
disabled = { publishing || ! canPublishAssignment ( ) }
@@ -1448,6 +1432,65 @@ const ShiftPlanView: React.FC = () => {
{ renderTimetable ( ) }
{ shiftPlan . status === 'published' && hasRole ( [ 'admin' , 'maintenance' ] ) && (
< div style = { {
display : 'flex' ,
alignItems : 'center' ,
position : 'relative' ,
marginLeft : '10px'
} } >
{ /* Export Dropdown */ }
< div
ref = { dropdownRef }
style = { {
transform : exportType ? ` translateX(- ${ dropdownWidth } px) ` : 'translateX(0)' ,
transition : 'transform 0.3s ease-in-out' ,
position : exportType ? 'absolute' : 'relative' ,
right : exportType ? ` - ${ dropdownWidth } px ` : '0'
} }
>
< select
value = { exportType || '' }
onChange = { ( e ) = > setExportType ( e . target . value as 'pdf' | 'excel' | null ) }
style = { {
padding : '10px 20px' ,
backgroundColor : 'white' ,
border : '1px solid #ddd' ,
borderRadius : '4px' ,
cursor : 'pointer' ,
minWidth : '120px'
} }
>
< option value = "" > Export < / option >
< option value = "pdf" > PDF < / option >
< option value = "excel" > Excel < / option >
< / select >
< / div >
{ /* Export Button */ }
{ exportType && (
< button
onClick = { handleExport }
disabled = { exporting }
style = { {
padding : '10px 20px' ,
backgroundColor : '#51258f' ,
color : 'white' ,
border : 'none' ,
borderRadius : '4px' ,
cursor : exporting ? 'not-allowed' : 'pointer' ,
fontWeight : 'bold' ,
marginLeft : '10px' ,
opacity : exporting ? 0.7 : 1 ,
transition : 'opacity 0.2s ease'
} }
>
{ exporting ? '🔄 Exportiert...' : 'EXPORT' }
< / button >
) }
< / div >
) }
{ /* Summary */ }
{ days . length > 0 && (
< div style = { {
@@ -1462,8 +1505,8 @@ const ShiftPlanView: React.FC = () => {
shiftPlan . status === 'published'
? 'Angezeigt werden die aktuell zugewiesenen Mitarbeiter'
: assignmentResult
? 'Angezeigt werden die vorgeschlagenen Mitarbeiter für eine exemplarische Woche'
: 'Angezeigt wird "zugewiesene/benötigte Mitarbeiter" pro Schicht und Wochentag'
? 'Angezeigt werden die vorgeschlagenen Mitarbeiter für eine exemplarische Woche'
: 'Angezeigt wird "zugewiesene/benötigte Mitarbeiter" pro Schicht und Wochentag'
}
< / div >
) }