From b86040dc04307a763c43345dba2562d6afbf8320 Mon Sep 17 00:00:00 2001 From: donpat1to Date: Thu, 16 Oct 2025 20:41:51 +0200 Subject: [PATCH] updated more simple modern layout --- frontend/src/App.tsx | 20 +- frontend/src/components/Layout/Footer.tsx | 93 ++-- frontend/src/components/Layout/Layout.tsx | 10 +- frontend/src/components/Layout/Navigation.tsx | 219 ++++++---- .../src/components/PillNav/PillNav.module.css | 88 ++++ frontend/src/components/PillNav/PillNav.tsx | 181 ++++++++ frontend/src/components/PillNav/index.ts | 3 + frontend/src/design/DesignSystem.tsx | 396 ++++++++++++++++++ frontend/src/design/index.ts | 4 + frontend/src/pages/Dashboard/Dashboard.tsx | 58 +-- frontend/src/pages/Settings/Settings.tsx | 56 ++- .../src/pages/ShiftPlans/ShiftPlanView.tsx | 215 ++++++++-- .../src/services/shiftAssignmentService.ts | 50 ++- 13 files changed, 1167 insertions(+), 226 deletions(-) create mode 100644 frontend/src/components/PillNav/PillNav.module.css create mode 100644 frontend/src/components/PillNav/PillNav.tsx create mode 100644 frontend/src/components/PillNav/index.ts create mode 100644 frontend/src/design/DesignSystem.tsx create mode 100644 frontend/src/design/index.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index db3cc92..98510cc 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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,14 +133,17 @@ const AppContent: React.FC = () => { function App() { return ( - - - - - - - - + + + + + + + + + + + ); } diff --git a/frontend/src/components/Layout/Footer.tsx b/frontend/src/components/Layout/Footer.tsx index 043d17b..e988d81 100644 --- a/frontend/src/components/Layout/Footer.tsx +++ b/frontend/src/components/Layout/Footer.tsx @@ -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 = () => {

Schichtenplaner

-

+

Professionelle Schichtplanung für Ihr Team. - Effiziente Personalplanung für optimale Abläufe. + Effiziente Personalplanung für optimale Abläube.

@@ -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 diff --git a/frontend/src/components/Layout/Layout.tsx b/frontend/src/components/Layout/Layout.tsx index b1eb7fc..9287fd2 100644 --- a/frontend/src/components/Layout/Layout.tsx +++ b/frontend/src/components/Layout/Layout.tsx @@ -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 = ({ 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', }, }; diff --git a/frontend/src/components/Layout/Navigation.tsx b/frontend/src/components/Layout/Navigation.tsx index 36c42fa..5acd217 100644 --- a/frontend/src/components/Layout/Navigation.tsx +++ b/frontend/src/components/Layout/Navigation.tsx @@ -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 (
+ {/* Logo - Links */}
-

🔄 Schichtenplaner

+

Schichtenplaner

- {/* Desktop Navigation */} - + {/* PillNav - Zentriert */} +
+
+ +
+
- {/* User Menu */} + {/* User Menu - Rechts */}
- {user?.name} ({user?.role}) + {user?.name} ({user?.role}) @@ -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 = () => { ))}
- {user?.name} ({user?.role}) +
+ {user?.name} + ({user?.role}) +
diff --git a/frontend/src/components/PillNav/PillNav.module.css b/frontend/src/components/PillNav/PillNav.module.css new file mode 100644 index 0000000..f543422 --- /dev/null +++ b/frontend/src/components/PillNav/PillNav.module.css @@ -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); +} \ No newline at end of file diff --git a/frontend/src/components/PillNav/PillNav.tsx b/frontend/src/components/PillNav/PillNav.tsx new file mode 100644 index 0000000..0a6825a --- /dev/null +++ b/frontend/src/components/PillNav/PillNav.tsx @@ -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 = ({ + items, + activeId, + onChange, + className = '', + variant = 'solid' +}) => { + const pillRefs = useRef>([]); + + 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 ( +
+ {items.map((item, index) => { + const isActive = item.id === activeId; + const pillStyle = { + ...baseStyles.pill, + ...getVariantStyles(isActive), + }; + + return ( + + ); + })} +
+ ); +}; + +export default PillNav; \ No newline at end of file diff --git a/frontend/src/components/PillNav/index.ts b/frontend/src/components/PillNav/index.ts new file mode 100644 index 0000000..15bd0e7 --- /dev/null +++ b/frontend/src/components/PillNav/index.ts @@ -0,0 +1,3 @@ +// frontend/src/components/PillNav/index.ts +export { default } from './PillNav'; +export type { PillNavProps, PillNavItem } from './PillNav'; \ No newline at end of file diff --git a/frontend/src/design/DesignSystem.tsx b/frontend/src/design/DesignSystem.tsx new file mode 100644 index 0000000..e8c57a6 --- /dev/null +++ b/frontend/src/design/DesignSystem.tsx @@ -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(undefined); + +// Design System Provider +interface DesignSystemProviderProps { + children: ReactNode; +} + +export const DesignSystemProvider: React.FC = ({ 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 ( + + {children} + + ); +}; + +// 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 = ({ + 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 ( +
+ {children} +
+ ); +}; + +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 = ({ + 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 ( + + {children} + + ); +}; + +// 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 ; +}; + +export default designTokens; \ No newline at end of file diff --git a/frontend/src/design/index.ts b/frontend/src/design/index.ts new file mode 100644 index 0000000..bb8663b --- /dev/null +++ b/frontend/src/design/index.ts @@ -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'; \ No newline at end of file diff --git a/frontend/src/pages/Dashboard/Dashboard.tsx b/frontend/src/pages/Dashboard/Dashboard.tsx index 48c34cc..40088e0 100644 --- a/frontend/src/pages/Dashboard/Dashboard.tsx +++ b/frontend/src/pages/Dashboard/Dashboard.tsx @@ -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: [] }); @@ -205,16 +205,17 @@ 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 = () => { })}

-
@@ -557,27 +545,27 @@ const Dashboard: React.FC = () => {

👥 Team-Übersicht

- Gesamt Mitarbeiter: + Mitarbeiter: {data.teamStats.totalEmployees}
- Verfügbar heute: + Chef: - {data.teamStats.availableToday} + {data.teamStats.manager}
- Im Urlaub: + Erfahrene: - {data.teamStats.onVacation} + {data.teamStats.experienced}
- Krankgeschrieben: + Neue: - {data.teamStats.sickLeave} + {data.teamStats.trainee}
@@ -649,7 +637,7 @@ const Dashboard: React.FC = () => { border: '1px solid #e0e0e0', boxShadow: '0 2px 4px rgba(0,0,0,0.1)' }}> -

📝 Letzte Schichtpläne

+

📝 Schichtpläne

{data.recentPlans.length > 0 ? (
{data.recentPlans.map(plan => ( diff --git a/frontend/src/pages/Settings/Settings.tsx b/frontend/src/pages/Settings/Settings.tsx index e470e5f..1e2aadd 100644 --- a/frontend/src/pages/Settings/Settings.tsx +++ b/frontend/src/pages/Settings/Settings.tsx @@ -313,28 +313,42 @@ const Settings: React.FC = () => {
+ - {/* Editable name */} -
- - -
+
+ +
{ const [assignmentResult, setAssignmentResult] = useState(null); const [loading, setLoading] = useState(true); const [publishing, setPublishing] = useState(false); + const [scheduledShifts, setScheduledShifts] = useState([]); 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,8 +91,70 @@ 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 = () => { + const getTimetableData = () => { if (!shiftPlan || !shiftPlan.shifts || !shiftPlan.timeSlots) { return { days: [], timeSlotsByDay: {}, allTimeSlots: [] }; } @@ -199,11 +266,53 @@ const ShiftPlanView: React.FC = () => { }); };*/ + 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,26 +441,43 @@ 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) { console.error('❌ Error publishing shift plan:', error); - + let message = 'Unbekannter Fehler'; if (error instanceof Error) { message = error.message; - } else if (typeof error === 'string') { - message = error; } - + showNotification({ type: 'error', title: 'Fehler', @@ -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) { @@ -534,10 +695,8 @@ const ShiftPlanView: React.FC = () => { // Get assigned employees for this shift 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; diff --git a/frontend/src/services/shiftAssignmentService.ts b/frontend/src/services/shiftAssignmentService.ts index f420826..4844d34 100644 --- a/frontend/src/services/shiftAssignmentService.ts +++ b/frontend/src/services/shiftAssignmentService.ts @@ -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 { - 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(