diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 630b191..a02422c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -32,6 +32,7 @@ import { SpecialAssignmentsPage } from './pages/SpecialAssignmentsPage' import { HoursPayoutPage } from './pages/HoursPayoutPage' import { TenantsPage } from './pages/TenantsPage' import { ResellerCompaniesPage } from './pages/ResellerCompaniesPage' +import { SettingsPage } from './pages/SettingsPage' export default function App() { return ( @@ -53,6 +54,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index d1a6a3f..680884f 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -2,44 +2,13 @@ import { Link, useLocation, useNavigate } from 'react-router-dom' import { useAuth } from '../context/AuthContext' import { useState, useRef, useEffect } from 'react' import { api } from '../api/client' - -interface NavItem { - path: string - label: string - roles?: string[] -} +import { PRIMARY_NAV, MANAGEMENT_NAV, visibleFor, hasAnySettings } from './navConfig' interface KioskHealthDevice { status: 'pending' | 'approved' | 'revoked' heartbeat_status: 'online' | 'stale' | 'offline' } -const MAIN_NAV: NavItem[] = [ - { path: '/dashboard', label: 'Dashboard' }, - { path: '/time', label: 'Zeiterfassung' }, - { path: '/absences', label: 'Abwesenheiten' }, - { path: '/calendar', label: 'Kalender' }, - { path: '/reports', label: 'Berichte', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN', 'HR', 'MANAGER'] }, - { path: '/hr/special-assignments', label: 'Sondervertretungen', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN', 'HR', 'MANAGER'] }, - { path: '/hr/payouts', label: 'Stunden-Auszahlung', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN', 'HR'] }, - { path: '/users', label: 'Mitarbeiter', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN', 'HR'] }, - { path: '/admin/tenants', label: 'Mandanten', roles: ['SUPER_ADMIN'] }, -] - -const SETTINGS_NAV: NavItem[] = [ - { path: '/work-schedules', label: 'Arbeitspläne', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN'] }, - { path: '/settings/absence-types', label: 'Abwesenheitstypen', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN'] }, - { path: '/settings/company', label: 'Firma', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN'] }, - { path: '/settings/kiosk', label: 'Kiosk-Geräte', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN'] }, - { path: '/settings/qr-stamp', label: 'QR-Stempeln', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN'] }, - { path: '/users/import', label: 'Mitarbeiter-Import', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN'] }, - { path: '/settings/import', label: 'Daten importieren', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN', 'HR'] }, - { path: '/settings/caldav', label: 'CalDAV' }, - { path: '/settings/ldap', label: 'LDAP', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN'] }, - { path: '/settings/smtp', label: 'SMTP', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN'] }, - { path: '/settings/audit-log', label: 'Audit-Log', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN'] }, -] - const ROLE_LABELS: Record = { SUPER_ADMIN: 'Super Admin', COMPANY_ADMIN: 'Administrator', @@ -111,23 +80,26 @@ export function Layout({ children, userRole, userName }: { }) { const { logout } = useAuth() const { pathname } = useLocation() - const [settingsOpen, setSettingsOpen] = useState(false) - const settingsRef = useRef(null) + const [mgmtOpen, setMgmtOpen] = useState(false) + const mgmtRef = useRef(null) - const visibleMain = MAIN_NAV.filter(n => !n.roles || n.roles.includes(userRole)) - const visibleSettings = SETTINGS_NAV.filter(n => !n.roles || n.roles.includes(userRole)) - const isSettingsActive = visibleSettings.some(n => pathname === n.path) + const visiblePrimary = visibleFor(PRIMARY_NAV, userRole) + const visibleMgmt = visibleFor(MANAGEMENT_NAV, userRole) + const isMgmtActive = visibleMgmt.some(n => pathname === n.path) + const settingsActive = pathname.startsWith('/settings') || + ['/work-schedules', '/users/import'].includes(pathname) + const showSettings = hasAnySettings(userRole) useEffect(() => { function handleClick(e: MouseEvent) { - if (settingsRef.current && !settingsRef.current.contains(e.target as Node)) - setSettingsOpen(false) + if (mgmtRef.current && !mgmtRef.current.contains(e.target as Node)) + setMgmtOpen(false) } - if (settingsOpen) document.addEventListener('mousedown', handleClick) + if (mgmtOpen) document.addEventListener('mousedown', handleClick) return () => document.removeEventListener('mousedown', handleClick) - }, [settingsOpen]) + }, [mgmtOpen]) - useEffect(() => { setSettingsOpen(false) }, [pathname]) + useEffect(() => { setMgmtOpen(false) }, [pathname]) const initials = userName ? userName.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase() @@ -148,7 +120,7 @@ export function Layout({ children, userRole, userName }: { {/* Hauptnavigation */} {/* Rechte Seite: Health-Badge + Einstellungen + User + Abmelden */} @@ -169,49 +179,23 @@ export function Layout({ children, userRole, userName }: { {/* Kiosk Health-Badge */} - {/* Zahnrad-Dropdown */} - {visibleSettings.length > 0 && ( -
- - - {settingsOpen && ( -
-

Einstellungen

- {visibleSettings.map((n, i) => { - const divider = i > 0 && n.path === '/settings/caldav' - return ( -
- {divider &&
} - - {n.label} - -
- ) - })} -
- )} -
+ {/* Zahnrad → Einstellungs-Seite */} + {showSettings && ( + + + + + + )} {/* User-Avatar → Profil */} diff --git a/frontend/src/components/navConfig.ts b/frontend/src/components/navConfig.ts new file mode 100644 index 0000000..944d214 --- /dev/null +++ b/frontend/src/components/navConfig.ts @@ -0,0 +1,86 @@ +// Zentrale Navigations-Konfiguration für Layout (Topbar/Verwaltung) und +// die Einstellungs-Seite (/settings). Rollen-Gating bleibt überall identisch: +// roles === undefined → für alle Rollen sichtbar; sonst nur gelistete Rollen. + +export interface NavItem { + path: string + label: string + roles?: string[] + /** Kurzbeschreibung – nur in den /settings-Kacheln genutzt */ + description?: string +} + +const ADMIN = ['COMPANY_ADMIN', 'SUPER_ADMIN'] +const HR_PLUS = ['COMPANY_ADMIN', 'SUPER_ADMIN', 'HR', 'MANAGER'] +const HR_ADMIN = ['COMPANY_ADMIN', 'SUPER_ADMIN', 'HR'] + +// Täglich genutzt – bleibt flach in der Topbar +export const PRIMARY_NAV: NavItem[] = [ + { path: '/dashboard', label: 'Dashboard' }, + { path: '/time', label: 'Zeiterfassung' }, + { path: '/absences', label: 'Abwesenheiten' }, + { path: '/calendar', label: 'Kalender' }, +] + +// HR/Admin-Funktionen – gebündelt im "Verwaltung"-Dropdown +export const MANAGEMENT_NAV: NavItem[] = [ + { path: '/reports', label: 'Berichte', roles: HR_PLUS }, + { path: '/users', label: 'Mitarbeiter', roles: HR_ADMIN }, + { path: '/hr/special-assignments', label: 'Sondervertretungen', roles: HR_PLUS }, + { path: '/hr/payouts', label: 'Stunden-Auszahlung', roles: HR_ADMIN }, + { path: '/admin/tenants', label: 'Mandanten', roles: ['SUPER_ADMIN'] }, +] + +export interface SettingsGroup { + title: string + items: NavItem[] +} + +// Einstellungs-Seite (/settings): nach Kategorien gruppierte Kacheln +export const SETTINGS_GROUPS: SettingsGroup[] = [ + { + title: 'Stammdaten', + items: [ + { path: '/work-schedules', label: 'Arbeitspläne', roles: ADMIN, description: 'Wochenarbeitszeiten & Teilzeitmodelle' }, + { path: '/settings/absence-types', label: 'Abwesenheitstypen', roles: ADMIN, description: 'Urlaub, Krankheit & weitere Typen' }, + { path: '/settings/company', label: 'Firma', roles: ADMIN, description: 'Firmendaten, Urlaubsregeln & Optionen' }, + ], + }, + { + title: 'Geräte', + items: [ + { path: '/settings/kiosk', label: 'Kiosk-Geräte', roles: ADMIN, description: 'Terminals freigeben & überwachen' }, + { path: '/settings/qr-stamp', label: 'QR-Stempeln', roles: ADMIN, description: 'Firmenweiten QR-Code verwalten' }, + ], + }, + { + title: 'Import', + items: [ + { path: '/users/import', label: 'Mitarbeiter-Import', roles: ADMIN, description: 'Mitarbeiter per CSV anlegen' }, + { path: '/settings/import', label: 'Daten importieren', roles: HR_ADMIN, description: 'Zeit- & Abwesenheitsdaten importieren' }, + ], + }, + { + title: 'Integrationen', + items: [ + { path: '/settings/caldav', label: 'CalDAV', description: 'Kalender-Sync (Nextcloud o. ä.)' }, + { path: '/settings/ldap', label: 'LDAP', roles: ADMIN, description: 'Verzeichnis-Sync der Mitarbeiter' }, + { path: '/settings/smtp', label: 'SMTP', roles: ADMIN, description: 'E-Mail-Versand konfigurieren' }, + ], + }, + { + title: 'System', + items: [ + { path: '/settings/audit-log', label: 'Audit-Log', roles: ADMIN, description: 'Sicherheits- & Änderungsprotokoll' }, + ], + }, +] + +export function visibleFor(items: T[], role: string): T[] { + return items.filter(n => !n.roles || n.roles.includes(role)) +} + +/** Hat die Rolle Zugriff auf mindestens einen Einstellungs-Eintrag? */ +export function hasAnySettings(role: string): boolean { + return SETTINGS_GROUPS.some(g => visibleFor(g.items, role).length > 0) +} diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx new file mode 100644 index 0000000..d0345b6 --- /dev/null +++ b/frontend/src/pages/SettingsPage.tsx @@ -0,0 +1,61 @@ +import { useEffect, useState } from 'react' +import { Link } from 'react-router-dom' +import { api } from '../api/client' +import { Layout } from '../components/Layout' +import { SETTINGS_GROUPS, visibleFor } from '../components/navConfig' + +interface UserOut { + role: string + first_name: string + last_name: string +} + +export function SettingsPage() { + const [me, setMe] = useState(null) + + useEffect(() => { + api.get('/auth/me').then(setMe).catch(() => {}) + }, []) + + const role = me?.role ?? '' + const groups = SETTINGS_GROUPS + .map(g => ({ ...g, items: visibleFor(g.items, role) })) + .filter(g => g.items.length > 0) + + return ( + +
+

Einstellungen

+

Konfiguration & Verwaltung von TimeMaster

+
+ + {groups.length === 0 ? ( +

Keine Einstellungen für deine Rolle verfügbar.

+ ) : ( +
+ {groups.map(group => ( +
+

+ {group.title} +

+
+ {group.items.map(item => ( + +

{item.label}

+ {item.description && ( +

{item.description}

+ )} + + ))} +
+
+ ))} +
+ )} +
+ ) +}