158 lines
5.5 KiB
React
158 lines
5.5 KiB
React
import { useEffect, useState } from 'react'
|
|
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
|
import { useAuth } from '../context/AuthContext'
|
|
import {
|
|
IconTicket,
|
|
IconUsers,
|
|
IconTargetArrow,
|
|
IconReportMoney,
|
|
IconScale,
|
|
IconActivityHeartbeat,
|
|
IconFolders,
|
|
IconLayoutGrid,
|
|
IconCalendar,
|
|
IconSettings,
|
|
IconLogout,
|
|
IconLayoutSidebarLeftCollapse,
|
|
IconLayoutSidebarLeftExpand,
|
|
IconMessages,
|
|
IconReceiptRefund,
|
|
IconDiscount,
|
|
IconMailCog,
|
|
} from '@tabler/icons-react'
|
|
import { serverpulsApi } from '../lib/serverpulsApi'
|
|
import { startsCollapsed } from '../lib/sidebar'
|
|
import { buildNavLinks } from '../lib/navLinks'
|
|
import { useSidebarCounts } from '../hooks/useSidebarCounts'
|
|
|
|
const ICONS = {
|
|
tickets: <IconTicket />,
|
|
planboard: <IconCalendar />,
|
|
projects: <IconFolders />,
|
|
services: <IconLayoutGrid />,
|
|
customers: <IconUsers />,
|
|
chat: <IconMessages />,
|
|
gutschriften: <IconReceiptRefund />,
|
|
angebote: <IconDiscount />,
|
|
speicher: <IconMailCog />,
|
|
leads: <IconTargetArrow />,
|
|
finance: <IconReportMoney />,
|
|
legal: <IconScale />,
|
|
serverpuls: <IconActivityHeartbeat />,
|
|
admin: <IconSettings />,
|
|
}
|
|
|
|
export default function Sidebar() {
|
|
const { user, logout, isAdmin } = useAuth()
|
|
const navigate = useNavigate()
|
|
const location = useLocation()
|
|
const [collapsed, setCollapsed] = useState(() => startsCollapsed())
|
|
const [pulseLevel, setPulseLevel] = useState('ok')
|
|
const counts = useSidebarCounts()
|
|
|
|
// Serverpuls-Punkt: nur Admins, beim Laden und alle 5 Minuten; Fehler bleiben still
|
|
useEffect(() => {
|
|
if (!isAdmin) { setPulseLevel('ok'); return undefined }
|
|
let dead = false
|
|
const load = () => {
|
|
serverpulsApi.overview()
|
|
.then((o) => { if (!dead) setPulseLevel(o?.level || 'ok') })
|
|
.catch(() => { if (!dead) setPulseLevel('ok') })
|
|
}
|
|
load()
|
|
const iv = setInterval(load, 5 * 60 * 1000)
|
|
return () => { dead = true; clearInterval(iv) }
|
|
}, [isAdmin])
|
|
|
|
const handleLogout = async () => {
|
|
await logout()
|
|
navigate('/login')
|
|
}
|
|
|
|
// Eintraege und Unterpunkte (Kunden -> Chat) aus lib/navLinks.js, Icons ueber key
|
|
const links = buildNavLinks({ isAdmin, pulseLevel, counts })
|
|
|
|
const isActive = (href) =>
|
|
location.pathname === href || location.pathname.startsWith(href + '/')
|
|
// Finanzen bleibt auch auf der Rueckkehrseite der Bankfreigabe (/finanzen/...) markiert
|
|
const isLinkActive = (link) => isActive(link.href) || (link.also ? isActive(link.also) : false)
|
|
|
|
return (
|
|
<aside className={`side ${collapsed ? 'collapsed' : ''}`}>
|
|
<div className="side-head">
|
|
<Link to="/" className="side-logo" title="Webklar">W</Link>
|
|
<div className="side-brand">
|
|
Webklar
|
|
<small>Ticket-Plattform</small>
|
|
</div>
|
|
</div>
|
|
|
|
<nav className="side-nav">
|
|
<div className="side-group">
|
|
{links.map((link) => link.paused ? (
|
|
<span
|
|
key={link.href}
|
|
className="side-link side-link-paused"
|
|
title={`${link.label} (pausiert)`}
|
|
aria-disabled="true"
|
|
>
|
|
{ICONS[link.key]}
|
|
<span className="side-link-label">{link.label}</span>
|
|
</span>
|
|
) : (
|
|
<Link
|
|
key={link.href}
|
|
to={link.href}
|
|
className={`side-link ${isLinkActive(link) ? 'active' : ''}`}
|
|
title={link.label}
|
|
>
|
|
{ICONS[link.key]}
|
|
<span className="side-link-label">{link.label}</span>
|
|
{link.dot && link.dot !== 'ok' && (
|
|
<span
|
|
className={`side-dot side-dot-${link.dot}`}
|
|
data-testid="sp-nav-dot"
|
|
title={link.dot === 'critical' ? 'Critical-Warnung aktiv' : 'Warnung aktiv'}
|
|
/>
|
|
)}
|
|
</Link>
|
|
)).flatMap((item, i) => [
|
|
item,
|
|
...(links[i].children || []).map((child) => (
|
|
<Link
|
|
key={child.href}
|
|
to={child.href}
|
|
className={`side-link side-sublink ${isActive(child.href) ? 'active' : ''}`}
|
|
title={child.count > 0 ? `${child.label} (${child.count} ${child.key === 'chat' ? 'ungelesen' : 'offen'})` : child.label}
|
|
>
|
|
{ICONS[child.key]}
|
|
<span className="side-link-label">{child.label}</span>
|
|
{child.count > 0 && <span className="side-count">{child.count}</span>}
|
|
</Link>
|
|
)),
|
|
])}
|
|
</div>
|
|
</nav>
|
|
|
|
<div className="side-foot">
|
|
{user && (
|
|
<div className="side-link" title={user.name || user.email} style={{ cursor: 'default' }}>
|
|
<div className="side-logo" style={{ width: 28, height: 28, fontSize: 12, background: 'linear-gradient(135deg,#667eea,#764ba2)' }}>
|
|
{(user.name || user.email || '?').charAt(0).toUpperCase()}
|
|
</div>
|
|
<span className="side-link-label text-ellipsis">{user.name || user.email}</span>
|
|
</div>
|
|
)}
|
|
<button className="side-toggle" onClick={handleLogout} title="Logout">
|
|
<IconLogout size={20} />
|
|
<span className="side-link-label">Logout</span>
|
|
</button>
|
|
<button className="side-toggle" onClick={() => setCollapsed((c) => !c)} title="Menue ein-/ausklappen">
|
|
{collapsed ? <IconLayoutSidebarLeftExpand size={20} /> : <IconLayoutSidebarLeftCollapse size={20} />}
|
|
<span className="side-link-label">Einklappen</span>
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
)
|
|
}
|