updated more simple modern layout

This commit is contained in:
2025-10-16 20:41:51 +02:00
parent 7b2256c0ed
commit b86040dc04
13 changed files with 1167 additions and 226 deletions

View File

@@ -5,6 +5,7 @@ import { AuthProvider, useAuth } from './contexts/AuthContext';
import { NotificationProvider } from './contexts/NotificationContext';
import NotificationContainer from './components/Notification/NotificationContainer';
import Layout from './components/Layout/Layout';
import { DesignSystemProvider, GlobalStyles } from './design/DesignSystem';
import Login from './pages/Auth/Login';
import Dashboard from './pages/Dashboard/Dashboard';
import ShiftPlanList from './pages/ShiftPlans/ShiftPlanList';
@@ -132,6 +133,8 @@ const AppContent: React.FC = () => {
function App() {
return (
<DesignSystemProvider>
<GlobalStyles />
<NotificationProvider>
<AuthProvider>
<Router>
@@ -140,6 +143,7 @@ function App() {
</Router>
</AuthProvider>
</NotificationProvider>
</DesignSystemProvider>
);
}

View File

@@ -1,46 +1,51 @@
// frontend/src/components/Layout/Footer.tsx
// frontend/src/components/Layout/Footer.tsx - ELEGANT WHITE DESIGN
import React from 'react';
const Footer: React.FC = () => {
const styles = {
footer: {
background: '#2c3e50',
background: 'linear-gradient(135deg, #1a1325 0%, #24163a 100%)',
color: 'white',
marginTop: 'auto',
borderTop: '1px solid rgba(251, 250, 246, 0.1)',
},
footerContent: {
maxWidth: '1200px',
margin: '0 auto',
padding: '2rem 20px',
padding: '3rem 2rem 2rem',
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
gap: '2rem',
gap: '3rem',
},
footerSection: {
display: 'flex',
flexDirection: 'column' as const,
},
footerSectionH3: {
marginBottom: '1rem',
color: '#ecf0f1',
fontSize: '1.2rem',
marginBottom: '1.5rem',
color: '#FBFAF6',
fontSize: '1.1rem',
fontWeight: 600,
},
footerSectionH4: {
marginBottom: '1rem',
color: '#ecf0f1',
fontSize: '1.1rem',
color: '#FBFAF6',
fontSize: '1rem',
fontWeight: 600,
},
footerLink: {
color: '#bdc3c7',
color: 'rgba(251, 250, 246, 0.7)',
textDecoration: 'none',
marginBottom: '0.5rem',
transition: 'color 0.3s ease',
marginBottom: '0.75rem',
transition: 'all 0.2s ease',
fontSize: '0.9rem',
},
footerBottom: {
borderTop: '1px solid #34495e',
padding: '1rem 20px',
borderTop: '1px solid rgba(251, 250, 246, 0.1)',
padding: '1.5rem 2rem',
textAlign: 'center' as const,
color: '#95a5a6',
color: 'rgba(251, 250, 246, 0.6)',
fontSize: '0.9rem',
},
};
@@ -49,9 +54,9 @@ const Footer: React.FC = () => {
<div style={styles.footerContent}>
<div style={styles.footerSection}>
<h3 style={styles.footerSectionH3}>Schichtenplaner</h3>
<p style={{color: '#bdc3c7', margin: 0}}>
<p style={{color: 'rgba(251, 250, 246, 0.7)', margin: 0, lineHeight: 1.6}}>
Professionelle Schichtplanung für Ihr Team.
Effiziente Personalplanung für optimale Abläufe.
Effiziente Personalplanung für optimale Abläube.
</p>
</div>
@@ -61,10 +66,12 @@ const Footer: React.FC = () => {
href="/help"
style={styles.footerLink}
onMouseEnter={(e) => {
e.currentTarget.style.color = '#3498db';
e.currentTarget.style.color = '#FBFAF6';
e.currentTarget.style.transform = 'translateX(4px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = '#bdc3c7';
e.currentTarget.style.color = 'rgba(251, 250, 246, 0.7)';
e.currentTarget.style.transform = 'translateX(0)';
}}
>
Hilfe & Anleitungen
@@ -73,10 +80,12 @@ const Footer: React.FC = () => {
href="/contact"
style={styles.footerLink}
onMouseEnter={(e) => {
e.currentTarget.style.color = '#3498db';
e.currentTarget.style.color = '#FBFAF6';
e.currentTarget.style.transform = 'translateX(4px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = '#bdc3c7';
e.currentTarget.style.color = 'rgba(251, 250, 246, 0.7)';
e.currentTarget.style.transform = 'translateX(0)';
}}
>
Kontakt & Support
@@ -85,10 +94,12 @@ const Footer: React.FC = () => {
href="/faq"
style={styles.footerLink}
onMouseEnter={(e) => {
e.currentTarget.style.color = '#3498db';
e.currentTarget.style.color = '#FBFAF6';
e.currentTarget.style.transform = 'translateX(4px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = '#bdc3c7';
e.currentTarget.style.color = 'rgba(251, 250, 246, 0.7)';
e.currentTarget.style.transform = 'translateX(0)';
}}
>
Häufige Fragen
@@ -101,10 +112,12 @@ const Footer: React.FC = () => {
href="/privacy"
style={styles.footerLink}
onMouseEnter={(e) => {
e.currentTarget.style.color = '#3498db';
e.currentTarget.style.color = '#FBFAF6';
e.currentTarget.style.transform = 'translateX(4px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = '#bdc3c7';
e.currentTarget.style.color = 'rgba(251, 250, 246, 0.7)';
e.currentTarget.style.transform = 'translateX(0)';
}}
>
Datenschutzerklärung
@@ -113,10 +126,12 @@ const Footer: React.FC = () => {
href="/imprint"
style={styles.footerLink}
onMouseEnter={(e) => {
e.currentTarget.style.color = '#3498db';
e.currentTarget.style.color = '#FBFAF6';
e.currentTarget.style.transform = 'translateX(4px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = '#bdc3c7';
e.currentTarget.style.color = 'rgba(251, 250, 246, 0.7)';
e.currentTarget.style.transform = 'translateX(0)';
}}
>
Impressum
@@ -125,10 +140,12 @@ const Footer: React.FC = () => {
href="/terms"
style={styles.footerLink}
onMouseEnter={(e) => {
e.currentTarget.style.color = '#3498db';
e.currentTarget.style.color = '#FBFAF6';
e.currentTarget.style.transform = 'translateX(4px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = '#bdc3c7';
e.currentTarget.style.color = 'rgba(251, 250, 246, 0.7)';
e.currentTarget.style.transform = 'translateX(0)';
}}
>
Allgemeine Geschäftsbedingungen
@@ -141,10 +158,12 @@ const Footer: React.FC = () => {
href="/about"
style={styles.footerLink}
onMouseEnter={(e) => {
e.currentTarget.style.color = '#3498db';
e.currentTarget.style.color = '#FBFAF6';
e.currentTarget.style.transform = 'translateX(4px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = '#bdc3c7';
e.currentTarget.style.color = 'rgba(251, 250, 246, 0.7)';
e.currentTarget.style.transform = 'translateX(0)';
}}
>
Über uns
@@ -153,10 +172,12 @@ const Footer: React.FC = () => {
href="/features"
style={styles.footerLink}
onMouseEnter={(e) => {
e.currentTarget.style.color = '#3498db';
e.currentTarget.style.color = '#FBFAF6';
e.currentTarget.style.transform = 'translateX(4px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = '#bdc3c7';
e.currentTarget.style.color = 'rgba(251, 250, 246, 0.7)';
e.currentTarget.style.transform = 'translateX(0)';
}}
>
Funktionen
@@ -165,10 +186,12 @@ const Footer: React.FC = () => {
href="/pricing"
style={styles.footerLink}
onMouseEnter={(e) => {
e.currentTarget.style.color = '#3498db';
e.currentTarget.style.color = '#FBFAF6';
e.currentTarget.style.transform = 'translateX(4px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = '#bdc3c7';
e.currentTarget.style.color = 'rgba(251, 250, 246, 0.7)';
e.currentTarget.style.transform = 'translateX(0)';
}}
>
Preise

View File

@@ -1,4 +1,4 @@
// frontend/src/components/Layout/Layout.tsx - KORRIGIERT
// frontend/src/components/Layout/Layout.tsx - ELEGANT WHITE DESIGN
import React from 'react';
import Navigation from './Navigation';
import Footer from './Footer';
@@ -13,16 +13,20 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
minHeight: '100vh',
display: 'flex',
flexDirection: 'column' as const,
background: '#FBFAF6',
fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
lineHeight: 1.6,
color: '#161718',
},
mainContent: {
flex: 1,
backgroundColor: '#f8f9fa',
minHeight: 'calc(100vh - 140px)',
paddingTop: '80px',
},
contentContainer: {
maxWidth: '1200px',
margin: '0 auto',
padding: '2rem 20px',
padding: '3rem 2rem',
},
};

View File

@@ -1,10 +1,24 @@
// frontend/src/components/Layout/Navigation.tsx
import React, { useState } from 'react';
// frontend/src/components/Layout/Navigation.tsx - ELEGANT WHITE DESIGN
import React, { useState, useEffect } from 'react';
import { useAuth } from '../../contexts/AuthContext';
import PillNav from '../PillNav/PillNav';
const Navigation: React.FC = () => {
const { user, logout, hasRole } = useAuth();
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [activePath, setActivePath] = useState('/');
const [isScrolled, setIsScrolled] = useState(false);
useEffect(() => {
setActivePath(window.location.pathname);
const handleScroll = () => {
setIsScrolled(window.scrollY > 10);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const handleLogout = () => {
logout();
@@ -16,160 +30,194 @@ const Navigation: React.FC = () => {
};
const navigationItems = [
{ path: '/', label: '📊 Dashboard', roles: ['admin', 'instandhalter', 'user'] },
{ path: '/shift-plans', label: '📅 Schichtpläne', roles: ['admin', 'instandhalter', 'user'] },
{ path: '/employees', label: '👥 Mitarbeiter', roles: ['admin', 'instandhalter'] },
{ path: '/help', label: 'Hilfe & Support', roles: ['admin', 'instandhalter', 'user'] },
{ path: '/settings', label: '⚙️ Einstellungen', roles: ['admin', 'instandhalter', 'user'] },
{ path: '/', label: 'Dashboard', roles: ['admin', 'instandhalter', 'user'] },
{ path: '/shift-plans', label: 'Schichtpläne', roles: ['admin', 'instandhalter', 'user'] },
{ path: '/employees', label: 'Mitarbeiter', roles: ['admin', 'instandhalter'] },
{ path: '/help', label: 'Hilfe', roles: ['admin', 'instandhalter', 'user'] },
{ path: '/settings', label: 'Einstellungen', roles: ['admin', 'instandhalter', 'user'] },
];
const filteredNavigation = navigationItems.filter(item =>
hasRole(item.roles)
);
const handlePillChange = (path: string) => {
setActivePath(path);
window.location.href = path;
};
const pillNavItems = filteredNavigation.map(item => ({
id: item.path,
label: item.label
}));
const styles = {
header: {
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
position: 'sticky' as const,
background: isScrolled
? 'rgba(251, 250, 246, 0.95)'
: '#FBFAF6',
backdropFilter: isScrolled ? 'blur(10px)' : 'none',
borderBottom: isScrolled
? '1px solid rgba(22, 23, 24, 0.08)'
: '1px solid transparent',
color: '#161718',
position: 'fixed' as const,
top: 0,
left: 0,
right: 0,
zIndex: 1000,
transition: 'all 0.3s ease-in-out',
boxShadow: isScrolled
? '0 2px 20px rgba(22, 23, 24, 0.06)'
: 'none',
},
headerContent: {
maxWidth: '1200px',
margin: '0 auto',
padding: '0 20px',
padding: '0 2rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
height: '70px',
transition: 'all 0.3s ease',
},
logo: {
flex: 1,
display: 'flex',
justifyContent: 'flex-start',
},
logoH1: {
margin: 0,
fontSize: '1.5rem',
fontWeight: 700,
color: '#161718',
letterSpacing: '-0.02em',
},
desktopNav: {
pillNavWrapper: {
display: 'flex',
gap: '2rem',
justifyContent: 'center',
alignItems: 'center',
flex: 2,
minWidth: 0,
},
navLink: {
color: 'white',
textDecoration: 'none',
padding: '0.5rem 1rem',
borderRadius: '6px',
transition: 'all 0.3s ease',
fontWeight: 500,
pillNavContainer: {
display: 'flex',
justifyContent: 'center',
maxWidth: '600px',
width: '100%',
margin: '0 auto',
},
userMenu: {
flex: 1,
display: 'flex',
alignItems: 'center',
gap: '1rem',
marginLeft: '2rem',
justifyContent: 'flex-end',
gap: '1.5rem',
},
userInfo: {
fontWeight: 500,
color: '#666',
fontSize: '0.9rem',
textAlign: 'right' as const,
},
logoutBtn: {
background: 'rgba(255, 255, 255, 0.1)',
color: 'white',
border: '1px solid rgba(255, 255, 255, 0.3)',
padding: '0.5rem 1rem',
borderRadius: '6px',
background: 'transparent',
color: '#161718',
border: '1.5px solid #51258f',
padding: '0.5rem 1.25rem',
borderRadius: '8px',
cursor: 'pointer',
transition: 'all 0.3s ease',
transition: 'all 0.2s ease-in-out',
fontWeight: 500,
fontSize: '0.9rem',
whiteSpace: 'nowrap' as const,
},
mobileMenuBtn: {
display: 'none',
background: 'none',
border: 'none',
color: 'white',
color: '#161718',
fontSize: '1.5rem',
cursor: 'pointer',
padding: '0.5rem',
borderRadius: '4px',
transition: 'background-color 0.2s ease',
},
mobileNav: {
display: isMobileMenuOpen ? 'flex' : 'none',
flexDirection: 'column' as const,
background: 'white',
padding: '1rem',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
background: '#FBFAF6',
padding: '1rem 0',
borderTop: '1px solid rgba(22, 23, 24, 0.1)',
boxShadow: '0 4px 20px rgba(22, 23, 24, 0.08)',
},
mobileNavLink: {
color: '#333',
color: '#161718',
textDecoration: 'none',
padding: '1rem',
borderBottom: '1px solid #eee',
transition: 'background-color 0.3s ease',
padding: '1rem 2rem',
borderBottom: '1px solid rgba(22, 23, 24, 0.05)',
transition: 'all 0.2s ease',
fontWeight: 500,
},
mobileUserInfo: {
padding: '1rem',
borderTop: '1px solid #eee',
marginTop: '1rem',
color: '#333',
padding: '1.5rem 2rem',
borderTop: '1px solid rgba(22, 23, 24, 0.1)',
marginTop: '0.5rem',
color: '#666',
},
mobileLogoutBtn: {
background: '#667eea',
background: '#51258f',
color: 'white',
border: 'none',
padding: '0.5rem 1rem',
borderRadius: '6px',
padding: '0.75rem 1.5rem',
borderRadius: '8px',
cursor: 'pointer',
marginTop: '0.5rem',
marginTop: '1rem',
width: '100%',
fontWeight: 500,
transition: 'all 0.2s ease',
},
};
return (
<header style={styles.header}>
<div style={styles.headerContent}>
{/* Logo - Links */}
<div style={styles.logo}>
<h1 style={styles.logoH1}>🔄 Schichtenplaner</h1>
<h1 style={styles.logoH1}>Schichtenplaner</h1>
</div>
{/* Desktop Navigation */}
<nav style={styles.desktopNav}>
{filteredNavigation.map((item) => (
<a
key={item.path}
href={item.path}
style={styles.navLink}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.1)';
e.currentTarget.style.transform = 'translateY(-1px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'none';
e.currentTarget.style.transform = 'translateY(0)';
}}
onClick={(e) => {
e.preventDefault();
window.location.href = item.path;
}}
>
{item.label}
</a>
))}
</nav>
{/* PillNav - Zentriert */}
<div style={styles.pillNavWrapper}>
<div style={styles.pillNavContainer}>
<PillNav
items={pillNavItems}
activeId={activePath}
onChange={handlePillChange}
variant="solid"
/>
</div>
</div>
{/* User Menu */}
{/* User Menu - Rechts */}
<div style={styles.userMenu}>
<span style={styles.userInfo}>
{user?.name} ({user?.role})
{user?.name} <span style={{color: '#999'}}>({user?.role})</span>
</span>
<button
onClick={handleLogout}
style={styles.logoutBtn}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.2)';
e.currentTarget.style.background = '#51258f';
e.currentTarget.style.color = 'white';
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.borderColor = '#51258f';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.1)';
e.currentTarget.style.background = 'transparent';
e.currentTarget.style.color = '#161718';
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.borderColor = '#51258f';
}}
>
Abmelden
@@ -180,6 +228,12 @@ const Navigation: React.FC = () => {
<button
style={styles.mobileMenuBtn}
onClick={toggleMobileMenu}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = 'rgba(81, 37, 143, 0.08)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent';
}}
>
</button>
@@ -194,10 +248,12 @@ const Navigation: React.FC = () => {
href={item.path}
style={styles.mobileNavLink}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#f5f5f5';
e.currentTarget.style.backgroundColor = 'rgba(81, 37, 143, 0.08)';
e.currentTarget.style.color = '#51258f';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent';
e.currentTarget.style.color = '#161718';
}}
onClick={(e) => {
e.preventDefault();
@@ -209,10 +265,21 @@ const Navigation: React.FC = () => {
</a>
))}
<div style={styles.mobileUserInfo}>
<span>{user?.name} ({user?.role})</span>
<div style={{marginBottom: '0.5rem'}}>
<span style={{fontWeight: 500}}>{user?.name}</span>
<span style={{color: '#999', marginLeft: '0.5rem'}}>({user?.role})</span>
</div>
<button
onClick={handleLogout}
style={styles.mobileLogoutBtn}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#642ab5';
e.currentTarget.style.transform = 'translateY(-1px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = '#51258f';
e.currentTarget.style.transform = 'translateY(0)';
}}
>
Abmelden
</button>

View File

@@ -0,0 +1,88 @@
/* frontend/src/components/PillNav/PillNav.module.css */
.pillNavContainer {
display: flex;
gap: 8px;
overflow-x: auto;
padding: 4px;
scrollbar-width: none;
-ms-overflow-style: none;
}
.pillNavContainer::-webkit-scrollbar {
display: none;
}
.pill {
padding: 8px 16px;
border-radius: 9999px;
border: 1px solid;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease-in-out;
white-space: nowrap;
outline: none;
}
.pill:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
/* Solid Variant */
.pillSolid {
background-color: transparent;
color: #6b7280;
border-color: #d1d5db;
}
.pillSolidActive {
background-color: #2563eb;
color: white;
border-color: #2563eb;
}
.pillSolid:hover:not(.pillSolidActive) {
background-color: #f3f4f6;
color: #374151;
border-color: #9ca3af;
transform: translateY(-1px);
}
/* Outline Variant */
.pillOutline {
background-color: transparent;
color: #6b7280;
border-color: #d1d5db;
}
.pillOutlineActive {
color: #2563eb;
border-color: #2563eb;
font-weight: 600;
}
.pillOutline:hover:not(.pillOutlineActive) {
background-color: #f3f4f6;
color: #374151;
border-color: #9ca3af;
transform: translateY(-1px);
}
/* Ghost Variant */
.pillGhost {
background-color: transparent;
color: #6b7280;
border-color: transparent;
}
.pillGhostActive {
background-color: #f3f4f6;
color: #111827;
}
.pillGhost:hover:not(.pillGhostActive) {
background-color: #f9fafb;
color: #374151;
transform: translateY(-1px);
}

View File

@@ -0,0 +1,181 @@
// frontend/src/components/PillNav/PillNav.tsx - ELEGANT WHITE DESIGN
import React, { useEffect, useRef } from 'react';
export interface PillNavItem {
id: string;
label: string;
}
export interface PillNavProps {
items: PillNavItem[];
activeId: string;
onChange: (id: string) => void;
className?: string;
variant?: 'solid' | 'outline' | 'ghost';
}
const PillNav: React.FC<PillNavProps> = ({
items,
activeId,
onChange,
className = '',
variant = 'solid'
}) => {
const pillRefs = useRef<Array<HTMLButtonElement | null>>([]);
const baseStyles = {
container: {
display: 'flex',
gap: '4px',
overflowX: 'auto' as const,
padding: '4px',
scrollbarWidth: 'none' as const,
msOverflowStyle: 'none' as const,
background: 'rgba(22, 23, 24, 0.02)',
borderRadius: '12px',
border: '1px solid rgba(22, 23, 24, 0.06)',
},
pill: {
padding: '10px 20px',
borderRadius: '8px',
border: 'none',
fontSize: '14px',
fontWeight: 500,
cursor: 'pointer',
transition: 'all 0.2s ease-in-out',
whiteSpace: 'nowrap' as const,
outline: 'none',
flexShrink: 0,
}
};
const getVariantStyles = (isActive: boolean) => {
const variants = {
solid: {
active: {
backgroundColor: '#51258f',
color: '#FBFAF6',
boxShadow: '0 2px 8px rgba(81, 37, 143, 0.2)',
},
inactive: {
backgroundColor: 'transparent',
color: '#666',
}
},
outline: {
active: {
backgroundColor: '#51258f',
color: '#FBFAF6',
boxShadow: '0 2px 8px rgba(81, 37, 143, 0.2)',
},
inactive: {
backgroundColor: 'transparent',
color: '#666',
border: '1px solid rgba(22, 23, 24, 0.2)',
}
},
ghost: {
active: {
backgroundColor: 'rgba(81, 37, 143, 0.1)',
color: '#51258f',
fontWeight: 600,
},
inactive: {
backgroundColor: 'transparent',
color: '#666',
}
}
};
return variants[variant][isActive ? 'active' : 'inactive'];
};
const handleKeyDown = (event: React.KeyboardEvent, index: number) => {
switch (event.key) {
case 'ArrowLeft': {
event.preventDefault();
const prevIndex = (index - 1 + items.length) % items.length;
onChange(items[prevIndex].id);
pillRefs.current[prevIndex]?.focus();
break;
}
case 'ArrowRight': {
event.preventDefault();
const nextIndex = (index + 1) % items.length;
onChange(items[nextIndex].id);
pillRefs.current[nextIndex]?.focus();
break;
}
case 'Home': {
event.preventDefault();
onChange(items[0].id);
pillRefs.current[0]?.focus();
break;
}
case 'End': {
event.preventDefault();
onChange(items[items.length - 1].id);
pillRefs.current[items.length - 1]?.focus();
break;
}
}
};
// Initialize refs array
useEffect(() => {
pillRefs.current = pillRefs.current.slice(0, items.length);
}, [items.length]);
const containerStyle = {
...baseStyles.container,
};
return (
<div
role="tablist"
aria-label="Navigation tabs"
style={containerStyle}
className={className}
>
{items.map((item, index) => {
const isActive = item.id === activeId;
const pillStyle = {
...baseStyles.pill,
...getVariantStyles(isActive),
};
return (
<button
key={item.id}
ref={el => {
pillRefs.current[index] = el;
}}
role="tab"
aria-selected={isActive}
aria-controls={`tabpanel-${item.id}`}
tabIndex={isActive ? 0 : -1}
style={pillStyle}
onClick={() => onChange(item.id)}
onKeyDown={(e) => handleKeyDown(e, index)}
onMouseEnter={(e) => {
if (!isActive) {
e.currentTarget.style.backgroundColor = 'rgba(81, 37, 143, 0.08)';
e.currentTarget.style.color = '#51258f';
e.currentTarget.style.transform = 'translateY(-1px)';
}
}}
onMouseLeave={(e) => {
if (!isActive) {
Object.assign(e.currentTarget.style, pillStyle);
}
}}
>
{item.label}
</button>
);
})}
</div>
);
};
export default PillNav;

View File

@@ -0,0 +1,3 @@
// frontend/src/components/PillNav/index.ts
export { default } from './PillNav';
export type { PillNavProps, PillNavItem } from './PillNav';

View File

@@ -0,0 +1,396 @@
// frontend/src/design/DesignSystem.tsx
import React, { createContext, useContext, ReactNode } from 'react';
// Design Tokens
export const designTokens = {
colors: {
// Primary Colors
white: '#FBFAF6',
black: '#161718',
// Purple Gradients
purple: {
1: '#1a1325',
2: '#24163a',
3: '#301c4d',
4: '#3e2069',
5: '#51258f',
6: '#642ab5',
7: '#854eca',
8: '#ab7ae0',
9: '#cda8f0',
10: '#ebd7fa',
},
// Semantic Colors
primary: '#51258f',
secondary: '#642ab5',
accent: '#854eca',
background: '#FBFAF6',
text: {
primary: '#161718',
secondary: '#666666',
light: '#999999',
inverted: '#FBFAF6',
},
border: {
light: 'rgba(22, 23, 24, 0.1)',
medium: 'rgba(22, 23, 24, 0.2)',
dark: 'rgba(22, 23, 24, 0.3)',
},
state: {
hover: 'rgba(81, 37, 143, 0.08)',
active: 'rgba(81, 37, 143, 0.12)',
focus: 'rgba(81, 37, 143, 0.16)',
}
},
typography: {
fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
fontWeights: {
light: 300,
normal: 400,
medium: 500,
semibold: 600,
bold: 700,
},
fontSizes: {
xs: '0.75rem', // 12px
sm: '0.875rem', // 14px
base: '1rem', // 16px
lg: '1.125rem', // 18px
xl: '1.25rem', // 20px
'2xl': '1.5rem', // 24px
'3xl': '1.875rem', // 30px
'4xl': '2.25rem', // 36px
},
lineHeights: {
tight: 1.25,
normal: 1.5,
relaxed: 1.75,
},
letterSpacing: {
tight: '-0.02em',
normal: '0',
wide: '0.02em',
},
},
spacing: {
0: '0',
1: '0.25rem', // 4px
2: '0.5rem', // 8px
3: '0.75rem', // 12px
4: '1rem', // 16px
5: '1.25rem', // 20px
6: '1.5rem', // 24px
8: '2rem', // 32px
10: '2.5rem', // 40px
12: '3rem', // 48px
16: '4rem', // 64px
20: '5rem', // 80px
},
borderRadius: {
none: '0',
sm: '0.25rem', // 4px
base: '0.5rem', // 8px
md: '0.75rem', // 12px
lg: '1rem', // 16px
xl: '1.5rem', // 24px
full: '9999px',
},
shadows: {
sm: '0 1px 2px 0 rgba(22, 23, 24, 0.05)',
base: '0 1px 3px 0 rgba(22, 23, 24, 0.1), 0 1px 2px 0 rgba(22, 23, 24, 0.06)',
md: '0 4px 6px -1px rgba(22, 23, 24, 0.1), 0 2px 4px -1px rgba(22, 23, 24, 0.06)',
lg: '0 10px 15px -3px rgba(22, 23, 24, 0.1), 0 4px 6px -2px rgba(22, 23, 24, 0.05)',
xl: '0 20px 25px -5px rgba(22, 23, 24, 0.1), 0 10px 10px -5px rgba(22, 23, 24, 0.04)',
},
transitions: {
default: 'all 0.2s ease-in-out',
slow: 'all 0.3s ease-in-out',
fast: 'all 0.15s ease-in-out',
},
breakpoints: {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
'2xl': '1536px',
},
} as const;
// Context for Design System
interface DesignSystemContextType {
tokens: typeof designTokens;
getColor: (path: string) => string;
getSpacing: (size: keyof typeof designTokens.spacing) => string;
}
const DesignSystemContext = createContext<DesignSystemContextType | undefined>(undefined);
// Design System Provider
interface DesignSystemProviderProps {
children: ReactNode;
}
export const DesignSystemProvider: React.FC<DesignSystemProviderProps> = ({ children }) => {
const getColor = (path: string): string => {
const parts = path.split('.');
let current: any = designTokens.colors;
for (const part of parts) {
if (current[part] === undefined) {
console.warn(`Color path "${path}" not found in design tokens`);
return designTokens.colors.primary;
}
current = current[part];
}
return current;
};
const getSpacing = (size: keyof typeof designTokens.spacing): string => {
return designTokens.spacing[size];
};
const value: DesignSystemContextType = {
tokens: designTokens,
getColor,
getSpacing,
};
return (
<DesignSystemContext.Provider value={value}>
{children}
</DesignSystemContext.Provider>
);
};
// Hook to use Design System
export const useDesignSystem = (): DesignSystemContextType => {
const context = useContext(DesignSystemContext);
if (context === undefined) {
throw new Error('useDesignSystem must be used within a DesignSystemProvider');
}
return context;
};
// Utility Components
export interface BoxProps {
children?: ReactNode;
className?: string;
style?: React.CSSProperties;
p?: keyof typeof designTokens.spacing;
px?: keyof typeof designTokens.spacing;
py?: keyof typeof designTokens.spacing;
m?: keyof typeof designTokens.spacing;
mx?: keyof typeof designTokens.spacing;
my?: keyof typeof designTokens.spacing;
bg?: string;
color?: string;
borderRadius?: keyof typeof designTokens.borderRadius;
}
export const Box: React.FC<BoxProps> = ({
children,
className,
style,
p,
px,
py,
m,
mx,
my,
bg,
color,
borderRadius,
...props
}) => {
const { tokens, getColor } = useDesignSystem();
const boxStyle: React.CSSProperties = {
padding: p && tokens.spacing[p],
paddingLeft: px && tokens.spacing[px],
paddingRight: px && tokens.spacing[px],
paddingTop: py && tokens.spacing[py],
paddingBottom: py && tokens.spacing[py],
margin: m && tokens.spacing[m],
marginLeft: mx && tokens.spacing[mx],
marginRight: mx && tokens.spacing[mx],
marginTop: my && tokens.spacing[my],
marginBottom: my && tokens.spacing[my],
backgroundColor: bg && getColor(bg),
color: color && getColor(color),
borderRadius: borderRadius && tokens.borderRadius[borderRadius],
fontFamily: tokens.typography.fontFamily,
...style,
};
return (
<div className={className} style={boxStyle} {...props}>
{children}
</div>
);
};
export interface TextProps {
children: ReactNode;
className?: string;
style?: React.CSSProperties;
variant?: 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
weight?: keyof typeof designTokens.typography.fontWeights;
color?: string;
align?: 'left' | 'center' | 'right' | 'justify';
lineHeight?: keyof typeof designTokens.typography.lineHeights;
letterSpacing?: keyof typeof designTokens.typography.letterSpacing;
}
export const Text: React.FC<TextProps> = ({
children,
className,
style,
variant = 'base',
weight = 'normal',
color = 'text.primary',
align = 'left',
lineHeight = 'normal',
letterSpacing = 'normal',
...props
}) => {
const { tokens, getColor } = useDesignSystem();
const textStyle: React.CSSProperties = {
fontSize: tokens.typography.fontSizes[variant],
fontWeight: tokens.typography.fontWeights[weight],
color: getColor(color),
textAlign: align,
lineHeight: tokens.typography.lineHeights[lineHeight],
letterSpacing: tokens.typography.letterSpacing[letterSpacing],
fontFamily: tokens.typography.fontFamily,
...style,
};
return (
<span className={className} style={textStyle} {...props}>
{children}
</span>
);
};
// Global Styles Component
export const GlobalStyles: React.FC = () => {
const { tokens } = useDesignSystem();
const globalStyles = `
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-family: ${tokens.typography.fontFamily};
font-size: 16px;
line-height: ${tokens.typography.lineHeights.normal};
color: ${tokens.colors.text.primary};
background-color: ${tokens.colors.background};
}
body {
font-family: ${tokens.typography.fontFamily};
font-weight: ${tokens.typography.fontWeights.normal};
line-height: ${tokens.typography.lineHeights.normal};
color: ${tokens.colors.text.primary};
background-color: ${tokens.colors.background};
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3, h4, h5, h6 {
font-family: ${tokens.typography.fontFamily};
font-weight: ${tokens.typography.fontWeights.bold};
line-height: ${tokens.typography.lineHeights.tight};
color: ${tokens.colors.text.primary};
}
h1 {
font-size: ${tokens.typography.fontSizes['4xl']};
letter-spacing: ${tokens.typography.letterSpacing.tight};
}
h2 {
font-size: ${tokens.typography.fontSizes['3xl']};
letter-spacing: ${tokens.typography.letterSpacing.tight};
}
h3 {
font-size: ${tokens.typography.fontSizes['2xl']};
}
h4 {
font-size: ${tokens.typography.fontSizes.xl};
}
h5 {
font-size: ${tokens.typography.fontSizes.lg};
}
h6 {
font-size: ${tokens.typography.fontSizes.base};
}
p {
font-size: ${tokens.typography.fontSizes.base};
line-height: ${tokens.typography.lineHeights.relaxed};
color: ${tokens.colors.text.primary};
}
a {
color: ${tokens.colors.primary};
text-decoration: none;
transition: ${tokens.transitions.default};
}
a:hover {
color: ${tokens.colors.secondary};
}
button {
font-family: ${tokens.typography.fontFamily};
transition: ${tokens.transitions.default};
}
input, textarea, select {
font-family: ${tokens.typography.fontFamily};
}
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: ${tokens.colors.background};
}
::-webkit-scrollbar-thumb {
background: ${tokens.colors.border.medium};
border-radius: ${tokens.borderRadius.full};
}
::-webkit-scrollbar-thumb:hover {
background: ${tokens.colors.border.dark};
}
`;
return <style>{globalStyles}</style>;
};
export default designTokens;

View File

@@ -0,0 +1,4 @@
// frontend/src/design/index.ts
export { designTokens } from './DesignSystem';
export { DesignSystemProvider, useDesignSystem, GlobalStyles, Box, Text } from './DesignSystem';
export type { BoxProps, TextProps } from './DesignSystem';

View File

@@ -20,9 +20,9 @@ interface DashboardData {
}>;
teamStats: {
totalEmployees: number;
availableToday: number;
onVacation: number;
sickLeave: number;
manager: number;
trainee: number;
experienced: number;
};
recentPlans: ShiftPlan[];
}
@@ -36,9 +36,9 @@ const Dashboard: React.FC = () => {
upcomingShifts: [],
teamStats: {
totalEmployees: 0,
availableToday: 0,
onVacation: 0,
sickLeave: 0
manager: 0,
trainee: 0,
experienced: 0
},
recentPlans: []
});
@@ -206,15 +206,16 @@ const Dashboard: React.FC = () => {
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
// Count by type
const managerCount = employees.filter(e => e.employeeType === 'manager').length;
const traineeCount = employees.filter(e => e.employeeType === 'trainee').length;
const experiencedCount = employees.filter(e => e.employeeType === 'experienced').length;
return {
totalEmployees,
availableToday,
onVacation: 0, // Would need vacation tracking
sickLeave: 0 // Would need sick leave tracking
manager: managerCount,
trainee: traineeCount,
experienced: experiencedCount,
};
};
@@ -367,19 +368,6 @@ const Dashboard: React.FC = () => {
})}
</p>
</div>
<button
onClick={handleRefresh}
style={{
padding: '8px 16px',
backgroundColor: '#3498db',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
🔄 Aktualisieren
</button>
</div>
<PlanDebugInfo />
@@ -557,27 +545,27 @@ const Dashboard: React.FC = () => {
<h3 style={{ margin: '0 0 15px 0', color: '#2c3e50' }}>👥 Team-Übersicht</h3>
<div style={{ display: 'grid', gap: '12px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>Gesamt Mitarbeiter:</span>
<span>Mitarbeiter:</span>
<span style={{ fontWeight: 'bold', fontSize: '18px' }}>
{data.teamStats.totalEmployees}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>Verfügbar heute:</span>
<span>Chef:</span>
<span style={{ fontWeight: 'bold', color: '#2ecc71' }}>
{data.teamStats.availableToday}
{data.teamStats.manager}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>Im Urlaub:</span>
<span>Erfahrene:</span>
<span style={{ fontWeight: 'bold', color: '#f39c12' }}>
{data.teamStats.onVacation}
{data.teamStats.experienced}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>Krankgeschrieben:</span>
<span>Neue:</span>
<span style={{ fontWeight: 'bold', color: '#e74c3c' }}>
{data.teamStats.sickLeave}
{data.teamStats.trainee}
</span>
</div>
</div>
@@ -649,7 +637,7 @@ 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 Schichtpläne</h3>
<h3 style={{ margin: '0 0 15px 0', color: '#2c3e50' }}>📝 Schichtpläne</h3>
{data.recentPlans.length > 0 ? (
<div style={{ display: 'grid', gap: '12px' }}>
{data.recentPlans.map(plan => (

View File

@@ -313,10 +313,25 @@ const Settings: React.FC = () => {
</div>
</div>
</div>
</div>
{/* Editable name */}
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
<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
@@ -326,16 +341,15 @@ const Settings: React.FC = () => {
onChange={handleProfileChange}
required
style={{
width: '97%',
width: '97.5%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px'
fontSize: '16px',
}}
placeholder="Ihr vollständiger Name"
/>
</div>
</div>
<div style={{
display: 'flex',

View File

@@ -6,7 +6,7 @@ import { shiftPlanService } from '../../services/shiftPlanService';
import { employeeService } from '../../services/employeeService';
import { shiftAssignmentService, ShiftAssignmentService } from '../../services/shiftAssignmentService';
import { AssignmentResult } from '../../services/scheduling';
import { ShiftPlan, TimeSlot } from '../../models/ShiftPlan';
import { ShiftPlan, TimeSlot, ScheduledShift } from '../../models/ShiftPlan';
import { Employee, EmployeeAvailability } from '../../models/Employee';
import { useNotification } from '../../contexts/NotificationContext';
import { formatDate, formatTime } from '../../utils/foramatters';
@@ -39,11 +39,13 @@ const ShiftPlanView: React.FC = () => {
const [assignmentResult, setAssignmentResult] = useState<AssignmentResult | null>(null);
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);
useEffect(() => {
loadShiftPlanData();
debugScheduledShifts();
}, [id]);
const loadShiftPlanData = async () => {
@@ -51,13 +53,15 @@ const ShiftPlanView: React.FC = () => {
try {
setLoading(true);
const [plan, employeesData] = await Promise.all([
const [plan, employeesData, shiftsData] = await Promise.all([
shiftPlanService.getShiftPlan(id),
employeeService.getEmployees()
employeeService.getEmployees(),
shiftAssignmentService.getScheduledShiftsForPlan(id) // Load shifts here
]);
setShiftPlan(plan);
setEmployees(employeesData.filter(emp => emp.isActive));
setScheduledShifts(shiftsData);
// Load availabilities for all employees
const availabilityPromises = employeesData
@@ -73,6 +77,7 @@ const ShiftPlanView: React.FC = () => {
);
setAvailabilities(planAvailabilities);
debugAvailabilities();
} catch (error) {
console.error('Error loading shift plan data:', error);
@@ -86,6 +91,68 @@ const ShiftPlanView: React.FC = () => {
}
};
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 () => {
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
}))
});
// 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');
}
} catch (error) {
console.error('❌ Error loading scheduled shifts:', error);
}
};
// Extract plan-specific shifts using the same logic as AvailabilityManager
const getTimetableData = () => {
if (!shiftPlan || !shiftPlan.shifts || !shiftPlan.timeSlots) {
@@ -199,11 +266,53 @@ const ShiftPlanView: React.FC = () => {
});
};*/
const handlePreviewAssignment = async () => {
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
}))
});
const result = await ShiftAssignmentService.assignShifts(
shiftPlan,
employees,
@@ -215,6 +324,18 @@ const ShiftPlanView: React.FC = () => {
}
);
// 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,
@@ -283,24 +404,24 @@ const ShiftPlanView: React.FC = () => {
console.log('🔄 Starting to publish assignments...');
const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
// ✅ KORREKTUR: Verwende die neu geladenen Shifts
const updatedShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
// Debug: Check if scheduled shifts exist
if (!scheduledShifts || scheduledShifts.length === 0) {
if (!updatedShifts || updatedShifts.length === 0) {
throw new Error('No scheduled shifts found in the plan');
}
// Update scheduled shifts with assignments
const updatePromises = scheduledShifts.map(async (scheduledShift) => {
console.log(`📊 Found ${updatedShifts.length} scheduled shifts to update`);
// ✅ KORREKTUR: Verwende updatedShifts statt scheduledShifts
const updatePromises = updatedShifts.map(async (scheduledShift) => {
const assignedEmployees = assignmentResult.assignments[scheduledShift.id] || [];
console.log(`📝 Updating shift ${scheduledShift.id} with`, assignedEmployees.length, 'employees');
console.log(`📝 Updating shift ${scheduledShift.id} with`, assignedEmployees, 'employees');
try {
// First, verify the shift exists
await shiftAssignmentService.getScheduledShift(scheduledShift.id);
// Then update it
// Update the shift with assigned employees
await shiftAssignmentService.updateScheduledShift(scheduledShift.id, {
assignedEmployees
});
@@ -320,14 +441,33 @@ const ShiftPlanView: React.FC = () => {
status: 'published'
});
// ✅ KORREKTUR: Explizit alle Daten neu laden und State aktualisieren
const [reloadedPlan, reloadedShifts] = await Promise.all([
shiftPlanService.getShiftPlan(shiftPlan.id),
shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id)
]);
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',
message: 'Schichtplan wurde erfolgreich veröffentlicht!'
});
// Reload the plan to reflect changes
loadShiftPlanData();
setShowAssignmentPreview(false);
} catch (error) {
@@ -336,8 +476,6 @@ const ShiftPlanView: React.FC = () => {
let message = 'Unbekannter Fehler';
if (error instanceof Error) {
message = error.message;
} else if (typeof error === 'string') {
message = error;
}
showNotification({
@@ -378,7 +516,7 @@ const ShiftPlanView: React.FC = () => {
message: 'Schichtplan wurde erfolgreich zurück in den Entwurfsstatus gesetzt. Alle Daten wurden neu geladen.'
});
const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
//const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
console.log('Scheduled shifts after revert:', {
hasScheduledShifts: !! scheduledShifts,
count: scheduledShifts.length || 0,
@@ -422,15 +560,38 @@ 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
});
};
// Render timetable using the same structure as AvailabilityManager
const renderTimetable = async () => {
const renderTimetable = () => {
debugAssignments();
debugCurrentState();
const { days, allTimeSlots, timeSlotsByDay } = getTimetableData();
if (!shiftPlan?.id) {
console.warn("Shift plan ID is missing");
return []; // safely exit
return null;
}
const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
//const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
if (days.length === 0 || allTimeSlots.length === 0) {
@@ -535,9 +696,7 @@ const ShiftPlanView: React.FC = () => {
let assignedEmployees: string[] = [];
let displayText = '';
if (shiftPlan?.status === 'published' && scheduledShifts) {
if (shiftPlan?.status === 'published') {
// For published plans, use actual assignments from scheduled shifts
const scheduledShift = scheduledShifts.find(scheduled => {
const scheduledDayOfWeek = getDayOfWeek(scheduled.date);
@@ -554,7 +713,7 @@ const ShiftPlanView: React.FC = () => {
}
} else if (assignmentResult) {
// For draft with preview, use assignment result
const scheduledShift = scheduledShifts?.find(scheduled => {
const scheduledShift = scheduledShifts.find(scheduled => {
const scheduledDayOfWeek = getDayOfWeek(scheduled.date);
return scheduledDayOfWeek === weekday.id &&
scheduled.timeSlotId === timeSlot.id;

View File

@@ -113,14 +113,27 @@ export class ShiftAssignmentService {
throw new Error(`Failed to fetch scheduled shifts: ${response.status}`);
}
return await response.json();
const shifts = await response.json();
// DEBUG: Check the structure of returned shifts
console.log('🔍 SCHEDULED SHIFTS STRUCTURE:', shifts.slice(0, 3));
// Fix: Ensure timeSlotId is properly mapped
const fixedShifts = shifts.map((shift: any) => ({
...shift,
timeSlotId: shift.timeSlotId || shift.time_slot_id, // Handle both naming conventions
requiredEmployees: shift.requiredEmployees || shift.required_employees || 2, // Default fallback
assignedEmployees: shift.assignedEmployees || shift.assigned_employees || []
}));
console.log('✅ Fixed scheduled shifts:', fixedShifts.length);
return fixedShifts;
} catch (error) {
console.error('Error fetching scheduled shifts for plan:', error);
throw error;
}
}
static async assignShifts(
shiftPlan: ShiftPlan,
employees: Employee[],
@@ -401,28 +414,25 @@ export class ShiftAssignmentService {
// ========== EXISTING HELPER METHODS ==========
static async getDefinedShifts(shiftPlan: ShiftPlan): Promise<ScheduledShift[]> {
let scheduledShifts: ScheduledShift[] = [];
try {
scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
console.log('📋 Loaded scheduled shifts:', scheduledShifts.length);
if (!shiftPlan.shifts || shiftPlan.shifts.length === 0) {
console.warn('⚠️ No shifts defined in shift plan');
return scheduledShifts;
}
// Use first week for weekly pattern (7 days)
const firstWeekShifts = this.getFirstWeekShifts(scheduledShifts);
console.log('📅 Using first week shifts for pattern:', firstWeekShifts.length);
return firstWeekShifts;
} catch (err) {
console.error("Failed to load scheduled shifts:", err);
console.error("Failed to load scheduled shifts:", err);
return [];
}
if (scheduledShifts.length) return [];
const definedShiftPatterns = new Set(
shiftPlan.shifts.map(shift =>
`${shift.dayOfWeek}-${shift.timeSlotId}`
)
);
const definedShifts = scheduledShifts.filter(scheduledShift => {
const dayOfWeek = this.getDayOfWeek(scheduledShift.date);
const pattern = `${dayOfWeek}-${scheduledShift.timeSlotId}`;
return definedShiftPatterns.has(pattern);
});
return definedShifts;
}
private static countAvailableEmployees(