mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-12-01 06:55:45 +01:00
updated more simple modern layout
This commit is contained in:
@@ -5,6 +5,7 @@ import { AuthProvider, useAuth } from './contexts/AuthContext';
|
|||||||
import { NotificationProvider } from './contexts/NotificationContext';
|
import { NotificationProvider } from './contexts/NotificationContext';
|
||||||
import NotificationContainer from './components/Notification/NotificationContainer';
|
import NotificationContainer from './components/Notification/NotificationContainer';
|
||||||
import Layout from './components/Layout/Layout';
|
import Layout from './components/Layout/Layout';
|
||||||
|
import { DesignSystemProvider, GlobalStyles } from './design/DesignSystem';
|
||||||
import Login from './pages/Auth/Login';
|
import Login from './pages/Auth/Login';
|
||||||
import Dashboard from './pages/Dashboard/Dashboard';
|
import Dashboard from './pages/Dashboard/Dashboard';
|
||||||
import ShiftPlanList from './pages/ShiftPlans/ShiftPlanList';
|
import ShiftPlanList from './pages/ShiftPlans/ShiftPlanList';
|
||||||
@@ -132,6 +133,8 @@ const AppContent: React.FC = () => {
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
|
<DesignSystemProvider>
|
||||||
|
<GlobalStyles />
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<Router>
|
<Router>
|
||||||
@@ -140,6 +143,7 @@ function App() {
|
|||||||
</Router>
|
</Router>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
|
</DesignSystemProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,46 +1,51 @@
|
|||||||
// frontend/src/components/Layout/Footer.tsx
|
// frontend/src/components/Layout/Footer.tsx - ELEGANT WHITE DESIGN
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const Footer: React.FC = () => {
|
const Footer: React.FC = () => {
|
||||||
const styles = {
|
const styles = {
|
||||||
footer: {
|
footer: {
|
||||||
background: '#2c3e50',
|
background: 'linear-gradient(135deg, #1a1325 0%, #24163a 100%)',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
marginTop: 'auto',
|
marginTop: 'auto',
|
||||||
|
borderTop: '1px solid rgba(251, 250, 246, 0.1)',
|
||||||
},
|
},
|
||||||
footerContent: {
|
footerContent: {
|
||||||
maxWidth: '1200px',
|
maxWidth: '1200px',
|
||||||
margin: '0 auto',
|
margin: '0 auto',
|
||||||
padding: '2rem 20px',
|
padding: '3rem 2rem 2rem',
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
|
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
|
||||||
gap: '2rem',
|
gap: '3rem',
|
||||||
},
|
},
|
||||||
footerSection: {
|
footerSection: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column' as const,
|
flexDirection: 'column' as const,
|
||||||
},
|
},
|
||||||
footerSectionH3: {
|
footerSectionH3: {
|
||||||
marginBottom: '1rem',
|
marginBottom: '1.5rem',
|
||||||
color: '#ecf0f1',
|
color: '#FBFAF6',
|
||||||
fontSize: '1.2rem',
|
fontSize: '1.1rem',
|
||||||
|
fontWeight: 600,
|
||||||
},
|
},
|
||||||
footerSectionH4: {
|
footerSectionH4: {
|
||||||
marginBottom: '1rem',
|
marginBottom: '1rem',
|
||||||
color: '#ecf0f1',
|
color: '#FBFAF6',
|
||||||
fontSize: '1.1rem',
|
fontSize: '1rem',
|
||||||
|
fontWeight: 600,
|
||||||
},
|
},
|
||||||
footerLink: {
|
footerLink: {
|
||||||
color: '#bdc3c7',
|
color: 'rgba(251, 250, 246, 0.7)',
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
marginBottom: '0.5rem',
|
marginBottom: '0.75rem',
|
||||||
transition: 'color 0.3s ease',
|
transition: 'all 0.2s ease',
|
||||||
|
fontSize: '0.9rem',
|
||||||
},
|
},
|
||||||
footerBottom: {
|
footerBottom: {
|
||||||
borderTop: '1px solid #34495e',
|
borderTop: '1px solid rgba(251, 250, 246, 0.1)',
|
||||||
padding: '1rem 20px',
|
padding: '1.5rem 2rem',
|
||||||
textAlign: 'center' as const,
|
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.footerContent}>
|
||||||
<div style={styles.footerSection}>
|
<div style={styles.footerSection}>
|
||||||
<h3 style={styles.footerSectionH3}>Schichtenplaner</h3>
|
<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.
|
Professionelle Schichtplanung für Ihr Team.
|
||||||
Effiziente Personalplanung für optimale Abläufe.
|
Effiziente Personalplanung für optimale Abläube.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -61,10 +66,12 @@ const Footer: React.FC = () => {
|
|||||||
href="/help"
|
href="/help"
|
||||||
style={styles.footerLink}
|
style={styles.footerLink}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.color = '#3498db';
|
e.currentTarget.style.color = '#FBFAF6';
|
||||||
|
e.currentTarget.style.transform = 'translateX(4px)';
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
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
|
Hilfe & Anleitungen
|
||||||
@@ -73,10 +80,12 @@ const Footer: React.FC = () => {
|
|||||||
href="/contact"
|
href="/contact"
|
||||||
style={styles.footerLink}
|
style={styles.footerLink}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.color = '#3498db';
|
e.currentTarget.style.color = '#FBFAF6';
|
||||||
|
e.currentTarget.style.transform = 'translateX(4px)';
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
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
|
Kontakt & Support
|
||||||
@@ -85,10 +94,12 @@ const Footer: React.FC = () => {
|
|||||||
href="/faq"
|
href="/faq"
|
||||||
style={styles.footerLink}
|
style={styles.footerLink}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.color = '#3498db';
|
e.currentTarget.style.color = '#FBFAF6';
|
||||||
|
e.currentTarget.style.transform = 'translateX(4px)';
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
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
|
Häufige Fragen
|
||||||
@@ -101,10 +112,12 @@ const Footer: React.FC = () => {
|
|||||||
href="/privacy"
|
href="/privacy"
|
||||||
style={styles.footerLink}
|
style={styles.footerLink}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.color = '#3498db';
|
e.currentTarget.style.color = '#FBFAF6';
|
||||||
|
e.currentTarget.style.transform = 'translateX(4px)';
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
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
|
Datenschutzerklärung
|
||||||
@@ -113,10 +126,12 @@ const Footer: React.FC = () => {
|
|||||||
href="/imprint"
|
href="/imprint"
|
||||||
style={styles.footerLink}
|
style={styles.footerLink}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.color = '#3498db';
|
e.currentTarget.style.color = '#FBFAF6';
|
||||||
|
e.currentTarget.style.transform = 'translateX(4px)';
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.color = '#bdc3c7';
|
e.currentTarget.style.color = 'rgba(251, 250, 246, 0.7)';
|
||||||
|
e.currentTarget.style.transform = 'translateX(0)';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Impressum
|
Impressum
|
||||||
@@ -125,10 +140,12 @@ const Footer: React.FC = () => {
|
|||||||
href="/terms"
|
href="/terms"
|
||||||
style={styles.footerLink}
|
style={styles.footerLink}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.color = '#3498db';
|
e.currentTarget.style.color = '#FBFAF6';
|
||||||
|
e.currentTarget.style.transform = 'translateX(4px)';
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
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
|
Allgemeine Geschäftsbedingungen
|
||||||
@@ -141,10 +158,12 @@ const Footer: React.FC = () => {
|
|||||||
href="/about"
|
href="/about"
|
||||||
style={styles.footerLink}
|
style={styles.footerLink}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.color = '#3498db';
|
e.currentTarget.style.color = '#FBFAF6';
|
||||||
|
e.currentTarget.style.transform = 'translateX(4px)';
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
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
|
Über uns
|
||||||
@@ -153,10 +172,12 @@ const Footer: React.FC = () => {
|
|||||||
href="/features"
|
href="/features"
|
||||||
style={styles.footerLink}
|
style={styles.footerLink}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.color = '#3498db';
|
e.currentTarget.style.color = '#FBFAF6';
|
||||||
|
e.currentTarget.style.transform = 'translateX(4px)';
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.color = '#bdc3c7';
|
e.currentTarget.style.color = 'rgba(251, 250, 246, 0.7)';
|
||||||
|
e.currentTarget.style.transform = 'translateX(0)';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Funktionen
|
Funktionen
|
||||||
@@ -165,10 +186,12 @@ const Footer: React.FC = () => {
|
|||||||
href="/pricing"
|
href="/pricing"
|
||||||
style={styles.footerLink}
|
style={styles.footerLink}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.color = '#3498db';
|
e.currentTarget.style.color = '#FBFAF6';
|
||||||
|
e.currentTarget.style.transform = 'translateX(4px)';
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.color = '#bdc3c7';
|
e.currentTarget.style.color = 'rgba(251, 250, 246, 0.7)';
|
||||||
|
e.currentTarget.style.transform = 'translateX(0)';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Preise
|
Preise
|
||||||
|
|||||||
@@ -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 React from 'react';
|
||||||
import Navigation from './Navigation';
|
import Navigation from './Navigation';
|
||||||
import Footer from './Footer';
|
import Footer from './Footer';
|
||||||
@@ -13,16 +13,20 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
minHeight: '100vh',
|
minHeight: '100vh',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column' as const,
|
flexDirection: 'column' as const,
|
||||||
|
background: '#FBFAF6',
|
||||||
|
fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
||||||
|
lineHeight: 1.6,
|
||||||
|
color: '#161718',
|
||||||
},
|
},
|
||||||
mainContent: {
|
mainContent: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#f8f9fa',
|
|
||||||
minHeight: 'calc(100vh - 140px)',
|
minHeight: 'calc(100vh - 140px)',
|
||||||
|
paddingTop: '80px',
|
||||||
},
|
},
|
||||||
contentContainer: {
|
contentContainer: {
|
||||||
maxWidth: '1200px',
|
maxWidth: '1200px',
|
||||||
margin: '0 auto',
|
margin: '0 auto',
|
||||||
padding: '2rem 20px',
|
padding: '3rem 2rem',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
// frontend/src/components/Layout/Navigation.tsx
|
// frontend/src/components/Layout/Navigation.tsx - ELEGANT WHITE DESIGN
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
|
import PillNav from '../PillNav/PillNav';
|
||||||
|
|
||||||
const Navigation: React.FC = () => {
|
const Navigation: React.FC = () => {
|
||||||
const { user, logout, hasRole } = useAuth();
|
const { user, logout, hasRole } = useAuth();
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
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 = () => {
|
const handleLogout = () => {
|
||||||
logout();
|
logout();
|
||||||
@@ -16,160 +30,194 @@ const Navigation: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const navigationItems = [
|
const navigationItems = [
|
||||||
{ path: '/', label: '📊 Dashboard', roles: ['admin', 'instandhalter', 'user'] },
|
{ path: '/', label: 'Dashboard', roles: ['admin', 'instandhalter', 'user'] },
|
||||||
{ path: '/shift-plans', label: '📅 Schichtpläne', roles: ['admin', 'instandhalter', 'user'] },
|
{ path: '/shift-plans', label: 'Schichtpläne', roles: ['admin', 'instandhalter', 'user'] },
|
||||||
{ path: '/employees', label: '👥 Mitarbeiter', roles: ['admin', 'instandhalter'] },
|
{ path: '/employees', label: 'Mitarbeiter', roles: ['admin', 'instandhalter'] },
|
||||||
{ path: '/help', label: '❓ Hilfe & Support', roles: ['admin', 'instandhalter', 'user'] },
|
{ path: '/help', label: 'Hilfe', roles: ['admin', 'instandhalter', 'user'] },
|
||||||
{ path: '/settings', label: '⚙️ Einstellungen', roles: ['admin', 'instandhalter', 'user'] },
|
{ path: '/settings', label: 'Einstellungen', roles: ['admin', 'instandhalter', 'user'] },
|
||||||
];
|
];
|
||||||
|
|
||||||
const filteredNavigation = navigationItems.filter(item =>
|
const filteredNavigation = navigationItems.filter(item =>
|
||||||
hasRole(item.roles)
|
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 = {
|
const styles = {
|
||||||
header: {
|
header: {
|
||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
background: isScrolled
|
||||||
color: 'white',
|
? 'rgba(251, 250, 246, 0.95)'
|
||||||
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
|
: '#FBFAF6',
|
||||||
position: 'sticky' as const,
|
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,
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
|
transition: 'all 0.3s ease-in-out',
|
||||||
|
boxShadow: isScrolled
|
||||||
|
? '0 2px 20px rgba(22, 23, 24, 0.06)'
|
||||||
|
: 'none',
|
||||||
},
|
},
|
||||||
headerContent: {
|
headerContent: {
|
||||||
maxWidth: '1200px',
|
maxWidth: '1200px',
|
||||||
margin: '0 auto',
|
margin: '0 auto',
|
||||||
padding: '0 20px',
|
padding: '0 2rem',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
height: '70px',
|
height: '70px',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
},
|
},
|
||||||
logo: {
|
logo: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
},
|
},
|
||||||
logoH1: {
|
logoH1: {
|
||||||
margin: 0,
|
margin: 0,
|
||||||
fontSize: '1.5rem',
|
fontSize: '1.5rem',
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
|
color: '#161718',
|
||||||
|
letterSpacing: '-0.02em',
|
||||||
},
|
},
|
||||||
desktopNav: {
|
pillNavWrapper: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: '2rem',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
flex: 2,
|
||||||
|
minWidth: 0,
|
||||||
},
|
},
|
||||||
navLink: {
|
pillNavContainer: {
|
||||||
color: 'white',
|
display: 'flex',
|
||||||
textDecoration: 'none',
|
justifyContent: 'center',
|
||||||
padding: '0.5rem 1rem',
|
maxWidth: '600px',
|
||||||
borderRadius: '6px',
|
width: '100%',
|
||||||
transition: 'all 0.3s ease',
|
margin: '0 auto',
|
||||||
fontWeight: 500,
|
|
||||||
},
|
},
|
||||||
userMenu: {
|
userMenu: {
|
||||||
|
flex: 1,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '1rem',
|
justifyContent: 'flex-end',
|
||||||
marginLeft: '2rem',
|
gap: '1.5rem',
|
||||||
},
|
},
|
||||||
userInfo: {
|
userInfo: {
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
|
color: '#666',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
textAlign: 'right' as const,
|
||||||
},
|
},
|
||||||
logoutBtn: {
|
logoutBtn: {
|
||||||
background: 'rgba(255, 255, 255, 0.1)',
|
background: 'transparent',
|
||||||
color: 'white',
|
color: '#161718',
|
||||||
border: '1px solid rgba(255, 255, 255, 0.3)',
|
border: '1.5px solid #51258f',
|
||||||
padding: '0.5rem 1rem',
|
padding: '0.5rem 1.25rem',
|
||||||
borderRadius: '6px',
|
borderRadius: '8px',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'all 0.3s ease',
|
transition: 'all 0.2s ease-in-out',
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
whiteSpace: 'nowrap' as const,
|
||||||
},
|
},
|
||||||
mobileMenuBtn: {
|
mobileMenuBtn: {
|
||||||
display: 'none',
|
display: 'none',
|
||||||
background: 'none',
|
background: 'none',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
color: 'white',
|
color: '#161718',
|
||||||
fontSize: '1.5rem',
|
fontSize: '1.5rem',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
padding: '0.5rem',
|
padding: '0.5rem',
|
||||||
|
borderRadius: '4px',
|
||||||
|
transition: 'background-color 0.2s ease',
|
||||||
},
|
},
|
||||||
mobileNav: {
|
mobileNav: {
|
||||||
display: isMobileMenuOpen ? 'flex' : 'none',
|
display: isMobileMenuOpen ? 'flex' : 'none',
|
||||||
flexDirection: 'column' as const,
|
flexDirection: 'column' as const,
|
||||||
background: 'white',
|
background: '#FBFAF6',
|
||||||
padding: '1rem',
|
padding: '1rem 0',
|
||||||
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
|
borderTop: '1px solid rgba(22, 23, 24, 0.1)',
|
||||||
|
boxShadow: '0 4px 20px rgba(22, 23, 24, 0.08)',
|
||||||
},
|
},
|
||||||
mobileNavLink: {
|
mobileNavLink: {
|
||||||
color: '#333',
|
color: '#161718',
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
padding: '1rem',
|
padding: '1rem 2rem',
|
||||||
borderBottom: '1px solid #eee',
|
borderBottom: '1px solid rgba(22, 23, 24, 0.05)',
|
||||||
transition: 'background-color 0.3s ease',
|
transition: 'all 0.2s ease',
|
||||||
|
fontWeight: 500,
|
||||||
},
|
},
|
||||||
mobileUserInfo: {
|
mobileUserInfo: {
|
||||||
padding: '1rem',
|
padding: '1.5rem 2rem',
|
||||||
borderTop: '1px solid #eee',
|
borderTop: '1px solid rgba(22, 23, 24, 0.1)',
|
||||||
marginTop: '1rem',
|
marginTop: '0.5rem',
|
||||||
color: '#333',
|
color: '#666',
|
||||||
},
|
},
|
||||||
mobileLogoutBtn: {
|
mobileLogoutBtn: {
|
||||||
background: '#667eea',
|
background: '#51258f',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
padding: '0.5rem 1rem',
|
padding: '0.75rem 1.5rem',
|
||||||
borderRadius: '6px',
|
borderRadius: '8px',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
marginTop: '0.5rem',
|
marginTop: '1rem',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
fontWeight: 500,
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header style={styles.header}>
|
<header style={styles.header}>
|
||||||
<div style={styles.headerContent}>
|
<div style={styles.headerContent}>
|
||||||
|
{/* Logo - Links */}
|
||||||
<div style={styles.logo}>
|
<div style={styles.logo}>
|
||||||
<h1 style={styles.logoH1}>🔄 Schichtenplaner</h1>
|
<h1 style={styles.logoH1}>Schichtenplaner</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop Navigation */}
|
{/* PillNav - Zentriert */}
|
||||||
<nav style={styles.desktopNav}>
|
<div style={styles.pillNavWrapper}>
|
||||||
{filteredNavigation.map((item) => (
|
<div style={styles.pillNavContainer}>
|
||||||
<a
|
<PillNav
|
||||||
key={item.path}
|
items={pillNavItems}
|
||||||
href={item.path}
|
activeId={activePath}
|
||||||
style={styles.navLink}
|
onChange={handlePillChange}
|
||||||
onMouseEnter={(e) => {
|
variant="solid"
|
||||||
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.1)';
|
/>
|
||||||
e.currentTarget.style.transform = 'translateY(-1px)';
|
</div>
|
||||||
}}
|
</div>
|
||||||
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>
|
|
||||||
|
|
||||||
{/* User Menu */}
|
{/* User Menu - Rechts */}
|
||||||
<div style={styles.userMenu}>
|
<div style={styles.userMenu}>
|
||||||
<span style={styles.userInfo}>
|
<span style={styles.userInfo}>
|
||||||
{user?.name} ({user?.role})
|
{user?.name} <span style={{color: '#999'}}>({user?.role})</span>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
style={styles.logoutBtn}
|
style={styles.logoutBtn}
|
||||||
onMouseEnter={(e) => {
|
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) => {
|
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
|
Abmelden
|
||||||
@@ -180,6 +228,12 @@ const Navigation: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
style={styles.mobileMenuBtn}
|
style={styles.mobileMenuBtn}
|
||||||
onClick={toggleMobileMenu}
|
onClick={toggleMobileMenu}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = 'rgba(81, 37, 143, 0.08)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = 'transparent';
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
☰
|
☰
|
||||||
</button>
|
</button>
|
||||||
@@ -194,10 +248,12 @@ const Navigation: React.FC = () => {
|
|||||||
href={item.path}
|
href={item.path}
|
||||||
style={styles.mobileNavLink}
|
style={styles.mobileNavLink}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.backgroundColor = '#f5f5f5';
|
e.currentTarget.style.backgroundColor = 'rgba(81, 37, 143, 0.08)';
|
||||||
|
e.currentTarget.style.color = '#51258f';
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.backgroundColor = 'transparent';
|
e.currentTarget.style.backgroundColor = 'transparent';
|
||||||
|
e.currentTarget.style.color = '#161718';
|
||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -209,10 +265,21 @@ const Navigation: React.FC = () => {
|
|||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
<div style={styles.mobileUserInfo}>
|
<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
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
style={styles.mobileLogoutBtn}
|
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
|
Abmelden
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
88
frontend/src/components/PillNav/PillNav.module.css
Normal file
88
frontend/src/components/PillNav/PillNav.module.css
Normal 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);
|
||||||
|
}
|
||||||
181
frontend/src/components/PillNav/PillNav.tsx
Normal file
181
frontend/src/components/PillNav/PillNav.tsx
Normal 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;
|
||||||
3
frontend/src/components/PillNav/index.ts
Normal file
3
frontend/src/components/PillNav/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// frontend/src/components/PillNav/index.ts
|
||||||
|
export { default } from './PillNav';
|
||||||
|
export type { PillNavProps, PillNavItem } from './PillNav';
|
||||||
396
frontend/src/design/DesignSystem.tsx
Normal file
396
frontend/src/design/DesignSystem.tsx
Normal 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;
|
||||||
4
frontend/src/design/index.ts
Normal file
4
frontend/src/design/index.ts
Normal 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';
|
||||||
@@ -20,9 +20,9 @@ interface DashboardData {
|
|||||||
}>;
|
}>;
|
||||||
teamStats: {
|
teamStats: {
|
||||||
totalEmployees: number;
|
totalEmployees: number;
|
||||||
availableToday: number;
|
manager: number;
|
||||||
onVacation: number;
|
trainee: number;
|
||||||
sickLeave: number;
|
experienced: number;
|
||||||
};
|
};
|
||||||
recentPlans: ShiftPlan[];
|
recentPlans: ShiftPlan[];
|
||||||
}
|
}
|
||||||
@@ -36,9 +36,9 @@ const Dashboard: React.FC = () => {
|
|||||||
upcomingShifts: [],
|
upcomingShifts: [],
|
||||||
teamStats: {
|
teamStats: {
|
||||||
totalEmployees: 0,
|
totalEmployees: 0,
|
||||||
availableToday: 0,
|
manager: 0,
|
||||||
onVacation: 0,
|
trainee: 0,
|
||||||
sickLeave: 0
|
experienced: 0
|
||||||
},
|
},
|
||||||
recentPlans: []
|
recentPlans: []
|
||||||
});
|
});
|
||||||
@@ -206,15 +206,16 @@ const Dashboard: React.FC = () => {
|
|||||||
const calculateTeamStats = (employees: Employee[]) => {
|
const calculateTeamStats = (employees: Employee[]) => {
|
||||||
const totalEmployees = employees.length;
|
const totalEmployees = employees.length;
|
||||||
|
|
||||||
// For now, we'll use simpler calculations
|
// Count by type
|
||||||
// In a real app, you'd check actual availability from the database
|
const managerCount = employees.filter(e => e.employeeType === 'manager').length;
|
||||||
const availableToday = Math.max(1, Math.floor(totalEmployees * 0.7)); // 70% available as estimate
|
const traineeCount = employees.filter(e => e.employeeType === 'trainee').length;
|
||||||
|
const experiencedCount = employees.filter(e => e.employeeType === 'experienced').length;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalEmployees,
|
totalEmployees,
|
||||||
availableToday,
|
manager: managerCount,
|
||||||
onVacation: 0, // Would need vacation tracking
|
trainee: traineeCount,
|
||||||
sickLeave: 0 // Would need sick leave tracking
|
experienced: experiencedCount,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -367,19 +368,6 @@ const Dashboard: React.FC = () => {
|
|||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
onClick={handleRefresh}
|
|
||||||
style={{
|
|
||||||
padding: '8px 16px',
|
|
||||||
backgroundColor: '#3498db',
|
|
||||||
color: 'white',
|
|
||||||
border: 'none',
|
|
||||||
borderRadius: '4px',
|
|
||||||
cursor: 'pointer'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
🔄 Aktualisieren
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PlanDebugInfo />
|
<PlanDebugInfo />
|
||||||
@@ -557,27 +545,27 @@ const Dashboard: React.FC = () => {
|
|||||||
<h3 style={{ margin: '0 0 15px 0', color: '#2c3e50' }}>👥 Team-Übersicht</h3>
|
<h3 style={{ margin: '0 0 15px 0', color: '#2c3e50' }}>👥 Team-Übersicht</h3>
|
||||||
<div style={{ display: 'grid', gap: '12px' }}>
|
<div style={{ display: 'grid', gap: '12px' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<span>Gesamt Mitarbeiter:</span>
|
<span>Mitarbeiter:</span>
|
||||||
<span style={{ fontWeight: 'bold', fontSize: '18px' }}>
|
<span style={{ fontWeight: 'bold', fontSize: '18px' }}>
|
||||||
{data.teamStats.totalEmployees}
|
{data.teamStats.totalEmployees}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<span>Verfügbar heute:</span>
|
<span>Chef:</span>
|
||||||
<span style={{ fontWeight: 'bold', color: '#2ecc71' }}>
|
<span style={{ fontWeight: 'bold', color: '#2ecc71' }}>
|
||||||
{data.teamStats.availableToday}
|
{data.teamStats.manager}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<span>Im Urlaub:</span>
|
<span>Erfahrene:</span>
|
||||||
<span style={{ fontWeight: 'bold', color: '#f39c12' }}>
|
<span style={{ fontWeight: 'bold', color: '#f39c12' }}>
|
||||||
{data.teamStats.onVacation}
|
{data.teamStats.experienced}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<span>Krankgeschrieben:</span>
|
<span>Neue:</span>
|
||||||
<span style={{ fontWeight: 'bold', color: '#e74c3c' }}>
|
<span style={{ fontWeight: 'bold', color: '#e74c3c' }}>
|
||||||
{data.teamStats.sickLeave}
|
{data.teamStats.trainee}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -649,7 +637,7 @@ const Dashboard: React.FC = () => {
|
|||||||
border: '1px solid #e0e0e0',
|
border: '1px solid #e0e0e0',
|
||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
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 ? (
|
{data.recentPlans.length > 0 ? (
|
||||||
<div style={{ display: 'grid', gap: '12px' }}>
|
<div style={{ display: 'grid', gap: '12px' }}>
|
||||||
{data.recentPlans.map(plan => (
|
{data.recentPlans.map(plan => (
|
||||||
|
|||||||
@@ -313,10 +313,25 @@ const Settings: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Editable name */}
|
<div
|
||||||
<div>
|
style={{
|
||||||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
|
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 *
|
Vollständiger Name *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -326,16 +341,15 @@ const Settings: React.FC = () => {
|
|||||||
onChange={handleProfileChange}
|
onChange={handleProfileChange}
|
||||||
required
|
required
|
||||||
style={{
|
style={{
|
||||||
width: '97%',
|
width: '97.5%',
|
||||||
padding: '10px',
|
padding: '10px',
|
||||||
border: '1px solid #ddd',
|
border: '1px solid #ddd',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
fontSize: '16px'
|
fontSize: '16px',
|
||||||
}}
|
}}
|
||||||
placeholder="Ihr vollständiger Name"
|
placeholder="Ihr vollständiger Name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { shiftPlanService } from '../../services/shiftPlanService';
|
|||||||
import { employeeService } from '../../services/employeeService';
|
import { employeeService } from '../../services/employeeService';
|
||||||
import { shiftAssignmentService, ShiftAssignmentService } from '../../services/shiftAssignmentService';
|
import { shiftAssignmentService, ShiftAssignmentService } from '../../services/shiftAssignmentService';
|
||||||
import { AssignmentResult } from '../../services/scheduling';
|
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 { Employee, EmployeeAvailability } from '../../models/Employee';
|
||||||
import { useNotification } from '../../contexts/NotificationContext';
|
import { useNotification } from '../../contexts/NotificationContext';
|
||||||
import { formatDate, formatTime } from '../../utils/foramatters';
|
import { formatDate, formatTime } from '../../utils/foramatters';
|
||||||
@@ -39,11 +39,13 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
const [assignmentResult, setAssignmentResult] = useState<AssignmentResult | null>(null);
|
const [assignmentResult, setAssignmentResult] = useState<AssignmentResult | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [publishing, setPublishing] = useState(false);
|
const [publishing, setPublishing] = useState(false);
|
||||||
|
const [scheduledShifts, setScheduledShifts] = useState<ScheduledShift[]>([]);
|
||||||
const [reverting, setReverting] = useState(false);
|
const [reverting, setReverting] = useState(false);
|
||||||
const [showAssignmentPreview, setShowAssignmentPreview] = useState(false);
|
const [showAssignmentPreview, setShowAssignmentPreview] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadShiftPlanData();
|
loadShiftPlanData();
|
||||||
|
debugScheduledShifts();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
const loadShiftPlanData = async () => {
|
const loadShiftPlanData = async () => {
|
||||||
@@ -51,13 +53,15 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const [plan, employeesData] = await Promise.all([
|
const [plan, employeesData, shiftsData] = await Promise.all([
|
||||||
shiftPlanService.getShiftPlan(id),
|
shiftPlanService.getShiftPlan(id),
|
||||||
employeeService.getEmployees()
|
employeeService.getEmployees(),
|
||||||
|
shiftAssignmentService.getScheduledShiftsForPlan(id) // Load shifts here
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setShiftPlan(plan);
|
setShiftPlan(plan);
|
||||||
setEmployees(employeesData.filter(emp => emp.isActive));
|
setEmployees(employeesData.filter(emp => emp.isActive));
|
||||||
|
setScheduledShifts(shiftsData);
|
||||||
|
|
||||||
// Load availabilities for all employees
|
// Load availabilities for all employees
|
||||||
const availabilityPromises = employeesData
|
const availabilityPromises = employeesData
|
||||||
@@ -73,6 +77,7 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
setAvailabilities(planAvailabilities);
|
setAvailabilities(planAvailabilities);
|
||||||
|
debugAvailabilities();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading shift plan data:', 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
|
// Extract plan-specific shifts using the same logic as AvailabilityManager
|
||||||
const getTimetableData = () => {
|
const getTimetableData = () => {
|
||||||
if (!shiftPlan || !shiftPlan.shifts || !shiftPlan.timeSlots) {
|
if (!shiftPlan || !shiftPlan.shifts || !shiftPlan.timeSlots) {
|
||||||
@@ -199,11 +266,53 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
});
|
});
|
||||||
};*/
|
};*/
|
||||||
|
|
||||||
const handlePreviewAssignment = async () => {
|
const debugAssignments = async () => {
|
||||||
if (!shiftPlan) return;
|
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 {
|
try {
|
||||||
setPublishing(true);
|
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(
|
const result = await ShiftAssignmentService.assignShifts(
|
||||||
shiftPlan,
|
shiftPlan,
|
||||||
employees,
|
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
|
// DEBUG: Überprüfe die tatsächlichen Violations
|
||||||
console.log('🔍 VIOLATIONS ANALYSIS:', {
|
console.log('🔍 VIOLATIONS ANALYSIS:', {
|
||||||
allViolations: result.violations,
|
allViolations: result.violations,
|
||||||
@@ -283,24 +404,24 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
|
|
||||||
console.log('🔄 Starting to publish assignments...');
|
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
|
// 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');
|
throw new Error('No scheduled shifts found in the plan');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update scheduled shifts with assignments
|
console.log(`📊 Found ${updatedShifts.length} scheduled shifts to update`);
|
||||||
const updatePromises = scheduledShifts.map(async (scheduledShift) => {
|
|
||||||
|
// ✅ KORREKTUR: Verwende updatedShifts statt scheduledShifts
|
||||||
|
const updatePromises = updatedShifts.map(async (scheduledShift) => {
|
||||||
const assignedEmployees = assignmentResult.assignments[scheduledShift.id] || [];
|
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 {
|
try {
|
||||||
// First, verify the shift exists
|
// Update the shift with assigned employees
|
||||||
await shiftAssignmentService.getScheduledShift(scheduledShift.id);
|
|
||||||
|
|
||||||
// Then update it
|
|
||||||
await shiftAssignmentService.updateScheduledShift(scheduledShift.id, {
|
await shiftAssignmentService.updateScheduledShift(scheduledShift.id, {
|
||||||
assignedEmployees
|
assignedEmployees
|
||||||
});
|
});
|
||||||
@@ -320,14 +441,33 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
status: 'published'
|
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({
|
showNotification({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: 'Erfolg',
|
title: 'Erfolg',
|
||||||
message: 'Schichtplan wurde erfolgreich veröffentlicht!'
|
message: 'Schichtplan wurde erfolgreich veröffentlicht!'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reload the plan to reflect changes
|
|
||||||
loadShiftPlanData();
|
|
||||||
setShowAssignmentPreview(false);
|
setShowAssignmentPreview(false);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -336,8 +476,6 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
let message = 'Unbekannter Fehler';
|
let message = 'Unbekannter Fehler';
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
message = error.message;
|
message = error.message;
|
||||||
} else if (typeof error === 'string') {
|
|
||||||
message = error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showNotification({
|
showNotification({
|
||||||
@@ -378,7 +516,7 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
message: 'Schichtplan wurde erfolgreich zurück in den Entwurfsstatus gesetzt. Alle Daten wurden neu geladen.'
|
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:', {
|
console.log('Scheduled shifts after revert:', {
|
||||||
hasScheduledShifts: !! scheduledShifts,
|
hasScheduledShifts: !! scheduledShifts,
|
||||||
count: scheduledShifts.length || 0,
|
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
|
// Render timetable using the same structure as AvailabilityManager
|
||||||
const renderTimetable = async () => {
|
const renderTimetable = () => {
|
||||||
|
debugAssignments();
|
||||||
|
debugCurrentState();
|
||||||
const { days, allTimeSlots, timeSlotsByDay } = getTimetableData();
|
const { days, allTimeSlots, timeSlotsByDay } = getTimetableData();
|
||||||
if (!shiftPlan?.id) {
|
if (!shiftPlan?.id) {
|
||||||
console.warn("Shift plan ID is missing");
|
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) {
|
if (days.length === 0 || allTimeSlots.length === 0) {
|
||||||
@@ -535,9 +696,7 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
let assignedEmployees: string[] = [];
|
let assignedEmployees: string[] = [];
|
||||||
let displayText = '';
|
let displayText = '';
|
||||||
|
|
||||||
|
if (shiftPlan?.status === 'published') {
|
||||||
|
|
||||||
if (shiftPlan?.status === 'published' && scheduledShifts) {
|
|
||||||
// For published plans, use actual assignments from scheduled shifts
|
// For published plans, use actual assignments from scheduled shifts
|
||||||
const scheduledShift = scheduledShifts.find(scheduled => {
|
const scheduledShift = scheduledShifts.find(scheduled => {
|
||||||
const scheduledDayOfWeek = getDayOfWeek(scheduled.date);
|
const scheduledDayOfWeek = getDayOfWeek(scheduled.date);
|
||||||
@@ -554,7 +713,7 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
}
|
}
|
||||||
} else if (assignmentResult) {
|
} else if (assignmentResult) {
|
||||||
// For draft with preview, use assignment result
|
// For draft with preview, use assignment result
|
||||||
const scheduledShift = scheduledShifts?.find(scheduled => {
|
const scheduledShift = scheduledShifts.find(scheduled => {
|
||||||
const scheduledDayOfWeek = getDayOfWeek(scheduled.date);
|
const scheduledDayOfWeek = getDayOfWeek(scheduled.date);
|
||||||
return scheduledDayOfWeek === weekday.id &&
|
return scheduledDayOfWeek === weekday.id &&
|
||||||
scheduled.timeSlotId === timeSlot.id;
|
scheduled.timeSlotId === timeSlot.id;
|
||||||
|
|||||||
@@ -113,14 +113,27 @@ export class ShiftAssignmentService {
|
|||||||
throw new Error(`Failed to fetch scheduled shifts: ${response.status}`);
|
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) {
|
} catch (error) {
|
||||||
console.error('Error fetching scheduled shifts for plan:', error);
|
console.error('Error fetching scheduled shifts for plan:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static async assignShifts(
|
static async assignShifts(
|
||||||
shiftPlan: ShiftPlan,
|
shiftPlan: ShiftPlan,
|
||||||
employees: Employee[],
|
employees: Employee[],
|
||||||
@@ -401,28 +414,25 @@ export class ShiftAssignmentService {
|
|||||||
// ========== EXISTING HELPER METHODS ==========
|
// ========== EXISTING HELPER METHODS ==========
|
||||||
|
|
||||||
static async getDefinedShifts(shiftPlan: ShiftPlan): Promise<ScheduledShift[]> {
|
static async getDefinedShifts(shiftPlan: ShiftPlan): Promise<ScheduledShift[]> {
|
||||||
let scheduledShifts: ScheduledShift[] = [];
|
|
||||||
try {
|
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) {
|
} catch (err) {
|
||||||
console.error("Failed to load scheduled shifts:", err);
|
console.error("❌ Failed to load scheduled shifts:", err);
|
||||||
return [];
|
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(
|
private static countAvailableEmployees(
|
||||||
|
|||||||
Reference in New Issue
Block a user