feat(ui): Navigation neu strukturiert (flache Topbar + Verwaltung-Menü + /settings)
Topbar nur noch täglich genutzte Punkte (Dashboard/Zeit/Abwesenheiten/Kalender); HR/Admin-Funktionen im neuen "Verwaltung"-Dropdown. Zahnrad führt auf eigene /settings-Seite mit Kategorie-Kacheln statt 11er-Flachliste. Zentrale navConfig.ts für PRIMARY/MANAGEMENT/SETTINGS_GROUPS; Rollen-Gating unverändert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,7 @@ import { SpecialAssignmentsPage } from './pages/SpecialAssignmentsPage'
|
|||||||
import { HoursPayoutPage } from './pages/HoursPayoutPage'
|
import { HoursPayoutPage } from './pages/HoursPayoutPage'
|
||||||
import { TenantsPage } from './pages/TenantsPage'
|
import { TenantsPage } from './pages/TenantsPage'
|
||||||
import { ResellerCompaniesPage } from './pages/ResellerCompaniesPage'
|
import { ResellerCompaniesPage } from './pages/ResellerCompaniesPage'
|
||||||
|
import { SettingsPage } from './pages/SettingsPage'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
@@ -53,6 +54,7 @@ export default function App() {
|
|||||||
<Route path='/absences' element={<AbsencesPage />} />
|
<Route path='/absences' element={<AbsencesPage />} />
|
||||||
<Route path='/users' element={<UsersPage />} />
|
<Route path='/users' element={<UsersPage />} />
|
||||||
<Route path='/users/import' element={<UserImportPage />} />
|
<Route path='/users/import' element={<UserImportPage />} />
|
||||||
|
<Route path='/settings' element={<SettingsPage />} />
|
||||||
<Route path='/settings/ldap' element={<LdapSettingsPage />} />
|
<Route path='/settings/ldap' element={<LdapSettingsPage />} />
|
||||||
<Route path='/settings/smtp' element={<SmtpSettingsPage />} />
|
<Route path='/settings/smtp' element={<SmtpSettingsPage />} />
|
||||||
<Route path='/work-schedules' element={<WorkSchedulePage />} />
|
<Route path='/work-schedules' element={<WorkSchedulePage />} />
|
||||||
|
|||||||
@@ -2,44 +2,13 @@ import { Link, useLocation, useNavigate } from 'react-router-dom'
|
|||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
import { useState, useRef, useEffect } from 'react'
|
import { useState, useRef, useEffect } from 'react'
|
||||||
import { api } from '../api/client'
|
import { api } from '../api/client'
|
||||||
|
import { PRIMARY_NAV, MANAGEMENT_NAV, visibleFor, hasAnySettings } from './navConfig'
|
||||||
interface NavItem {
|
|
||||||
path: string
|
|
||||||
label: string
|
|
||||||
roles?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface KioskHealthDevice {
|
interface KioskHealthDevice {
|
||||||
status: 'pending' | 'approved' | 'revoked'
|
status: 'pending' | 'approved' | 'revoked'
|
||||||
heartbeat_status: 'online' | 'stale' | 'offline'
|
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<string, string> = {
|
const ROLE_LABELS: Record<string, string> = {
|
||||||
SUPER_ADMIN: 'Super Admin',
|
SUPER_ADMIN: 'Super Admin',
|
||||||
COMPANY_ADMIN: 'Administrator',
|
COMPANY_ADMIN: 'Administrator',
|
||||||
@@ -111,23 +80,26 @@ export function Layout({ children, userRole, userName }: {
|
|||||||
}) {
|
}) {
|
||||||
const { logout } = useAuth()
|
const { logout } = useAuth()
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
const [mgmtOpen, setMgmtOpen] = useState(false)
|
||||||
const settingsRef = useRef<HTMLDivElement>(null)
|
const mgmtRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const visibleMain = MAIN_NAV.filter(n => !n.roles || n.roles.includes(userRole))
|
const visiblePrimary = visibleFor(PRIMARY_NAV, userRole)
|
||||||
const visibleSettings = SETTINGS_NAV.filter(n => !n.roles || n.roles.includes(userRole))
|
const visibleMgmt = visibleFor(MANAGEMENT_NAV, userRole)
|
||||||
const isSettingsActive = visibleSettings.some(n => pathname === n.path)
|
const isMgmtActive = visibleMgmt.some(n => pathname === n.path)
|
||||||
|
const settingsActive = pathname.startsWith('/settings') ||
|
||||||
|
['/work-schedules', '/users/import'].includes(pathname)
|
||||||
|
const showSettings = hasAnySettings(userRole)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleClick(e: MouseEvent) {
|
function handleClick(e: MouseEvent) {
|
||||||
if (settingsRef.current && !settingsRef.current.contains(e.target as Node))
|
if (mgmtRef.current && !mgmtRef.current.contains(e.target as Node))
|
||||||
setSettingsOpen(false)
|
setMgmtOpen(false)
|
||||||
}
|
}
|
||||||
if (settingsOpen) document.addEventListener('mousedown', handleClick)
|
if (mgmtOpen) document.addEventListener('mousedown', handleClick)
|
||||||
return () => document.removeEventListener('mousedown', handleClick)
|
return () => document.removeEventListener('mousedown', handleClick)
|
||||||
}, [settingsOpen])
|
}, [mgmtOpen])
|
||||||
|
|
||||||
useEffect(() => { setSettingsOpen(false) }, [pathname])
|
useEffect(() => { setMgmtOpen(false) }, [pathname])
|
||||||
|
|
||||||
const initials = userName
|
const initials = userName
|
||||||
? userName.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase()
|
? userName.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase()
|
||||||
@@ -148,7 +120,7 @@ export function Layout({ children, userRole, userName }: {
|
|||||||
|
|
||||||
{/* Hauptnavigation */}
|
{/* Hauptnavigation */}
|
||||||
<nav className='flex items-center gap-0.5 flex-1 overflow-x-auto'>
|
<nav className='flex items-center gap-0.5 flex-1 overflow-x-auto'>
|
||||||
{visibleMain.map(n => (
|
{visiblePrimary.map(n => (
|
||||||
<Link
|
<Link
|
||||||
key={n.path}
|
key={n.path}
|
||||||
to={n.path}
|
to={n.path}
|
||||||
@@ -161,6 +133,44 @@ export function Layout({ children, userRole, userName }: {
|
|||||||
{n.label}
|
{n.label}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* Verwaltung-Dropdown (HR/Admin) */}
|
||||||
|
{visibleMgmt.length > 0 && (
|
||||||
|
<div ref={mgmtRef} className='relative'>
|
||||||
|
<button
|
||||||
|
onClick={() => setMgmtOpen(o => !o)}
|
||||||
|
className={`flex items-center gap-1 px-3 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap ${
|
||||||
|
isMgmtActive || mgmtOpen
|
||||||
|
? 'bg-blue-50 text-blue-700'
|
||||||
|
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Verwaltung
|
||||||
|
<svg className={`w-3.5 h-3.5 transition-transform ${mgmtOpen ? 'rotate-180' : ''}`}
|
||||||
|
fill='none' viewBox='0 0 24 24' stroke='currentColor'>
|
||||||
|
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M19 9l-7 7-7-7' />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{mgmtOpen && (
|
||||||
|
<div className='absolute left-0 top-full mt-1.5 w-56 bg-white rounded-xl shadow-lg border border-gray-200 py-1.5 z-50'>
|
||||||
|
{visibleMgmt.map(n => (
|
||||||
|
<Link
|
||||||
|
key={n.path}
|
||||||
|
to={n.path}
|
||||||
|
className={`flex items-center px-4 py-2 text-sm transition-colors ${
|
||||||
|
pathname === n.path
|
||||||
|
? 'bg-blue-50 text-blue-700 font-medium'
|
||||||
|
: 'text-gray-700 hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{n.label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Rechte Seite: Health-Badge + Einstellungen + User + Abmelden */}
|
{/* Rechte Seite: Health-Badge + Einstellungen + User + Abmelden */}
|
||||||
@@ -169,49 +179,23 @@ export function Layout({ children, userRole, userName }: {
|
|||||||
{/* Kiosk Health-Badge */}
|
{/* Kiosk Health-Badge */}
|
||||||
<KioskHealthBadge userRole={userRole} />
|
<KioskHealthBadge userRole={userRole} />
|
||||||
|
|
||||||
{/* Zahnrad-Dropdown */}
|
{/* Zahnrad → Einstellungs-Seite */}
|
||||||
{visibleSettings.length > 0 && (
|
{showSettings && (
|
||||||
<div ref={settingsRef} className='relative'>
|
<Link
|
||||||
<button
|
to='/settings'
|
||||||
onClick={() => setSettingsOpen(o => !o)}
|
title='Einstellungen'
|
||||||
title='Einstellungen'
|
className={`p-2 rounded-lg transition-colors ${
|
||||||
className={`p-2 rounded-lg transition-colors ${
|
settingsActive
|
||||||
isSettingsActive || settingsOpen
|
? 'bg-blue-50 text-blue-700'
|
||||||
? 'bg-blue-50 text-blue-700'
|
: 'text-gray-500 hover:bg-gray-100 hover:text-gray-700'
|
||||||
: 'text-gray-500 hover:bg-gray-100 hover:text-gray-700'
|
}`}
|
||||||
}`}
|
>
|
||||||
>
|
<svg className='w-5 h-5' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
|
||||||
<svg className='w-5 h-5' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
|
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={1.75}
|
||||||
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={1.75}
|
d='M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z' />
|
||||||
d='M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z' />
|
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={1.75} d='M15 12a3 3 0 11-6 0 3 3 0 016 0z' />
|
||||||
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={1.75} d='M15 12a3 3 0 11-6 0 3 3 0 016 0z' />
|
</svg>
|
||||||
</svg>
|
</Link>
|
||||||
</button>
|
|
||||||
|
|
||||||
{settingsOpen && (
|
|
||||||
<div className='absolute right-0 top-full mt-1.5 w-52 bg-white rounded-xl shadow-lg border border-gray-200 py-1.5 z-50'>
|
|
||||||
<p className='px-4 py-1.5 text-xs font-semibold text-gray-400 uppercase tracking-wide'>Einstellungen</p>
|
|
||||||
{visibleSettings.map((n, i) => {
|
|
||||||
const divider = i > 0 && n.path === '/settings/caldav'
|
|
||||||
return (
|
|
||||||
<div key={n.path}>
|
|
||||||
{divider && <div className='my-1 border-t border-gray-100' />}
|
|
||||||
<Link
|
|
||||||
to={n.path}
|
|
||||||
className={`flex items-center px-4 py-2 text-sm transition-colors ${
|
|
||||||
pathname === n.path
|
|
||||||
? 'bg-blue-50 text-blue-700 font-medium'
|
|
||||||
: 'text-gray-700 hover:bg-gray-50'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{n.label}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* User-Avatar → Profil */}
|
{/* User-Avatar → Profil */}
|
||||||
|
|||||||
@@ -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<T extends NavItem>(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)
|
||||||
|
}
|
||||||
@@ -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<UserOut | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api.get<UserOut>('/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 (
|
||||||
|
<Layout userRole={role} userName={me ? `${me.first_name} ${me.last_name}` : ''}>
|
||||||
|
<div className='mb-6'>
|
||||||
|
<h1 className='text-2xl font-bold text-gray-800'>Einstellungen</h1>
|
||||||
|
<p className='text-sm text-gray-500 mt-1'>Konfiguration & Verwaltung von TimeMaster</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{groups.length === 0 ? (
|
||||||
|
<p className='text-gray-500'>Keine Einstellungen für deine Rolle verfügbar.</p>
|
||||||
|
) : (
|
||||||
|
<div className='space-y-8'>
|
||||||
|
{groups.map(group => (
|
||||||
|
<section key={group.title}>
|
||||||
|
<h2 className='text-xs font-semibold text-gray-400 uppercase tracking-wide mb-3'>
|
||||||
|
{group.title}
|
||||||
|
</h2>
|
||||||
|
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3'>
|
||||||
|
{group.items.map(item => (
|
||||||
|
<Link
|
||||||
|
key={item.path}
|
||||||
|
to={item.path}
|
||||||
|
className='group bg-white rounded-xl border border-gray-200 p-4 hover:border-blue-300 hover:shadow-sm transition-all'
|
||||||
|
>
|
||||||
|
<p className='font-medium text-gray-800 group-hover:text-blue-700'>{item.label}</p>
|
||||||
|
{item.description && (
|
||||||
|
<p className='text-sm text-gray-500 mt-1 leading-snug'>{item.description}</p>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user