feat: Komplett-Redesign + Ausbau zur Plattform

- Ruhiges Dark-Designsystem (Tokens, Karten, Badges/Pills, KPIs) in global.css,
  animierter PixelBlast-Hintergrund entfernt (kleineres Bundle)
- Aufgeraeumter App-Rahmen: feste Sidebar mit sichtbaren Labels, gruppierter
  Navigation, aktivem Zustand und Collapse
- Wiederverwendbare UI-Bausteine unter src/components/ui/
- Ticketliste als scanbare Karten (TicketCard) statt rowSpan-Tabelle
- Neue Bereiche: Kunden (CRM-360), Finanzen-Cockpit, Dokumente-Browser
- Dashboard als echtes Cockpit (Ticket-/Finanz-/Dokument-Kennzahlen)
- Alle Seiten auf einheitlichen PageHeader/Container/Tokens umgestellt

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Webklar DeployandCursor committed 2026-06-17 09:41:33 +00:00
1 parent 8f6927fbb0
commit d20a4de117
28 files changed
+1639 -840

No files matched your search

+88 -140
View File
@@ -1,174 +1,122 @@
import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
import { motion } from 'motion/react'
import {
IconTicket,
IconDeviceDesktop,
IconChartBar,
IconCalendar,
IconFolders,
IconFileText,
IconBook,
import {
IconTicket,
IconUsers,
IconReportMoney,
IconFiles,
IconFolders,
IconCalendar,
IconLayoutDashboard,
IconReport,
IconDeviceDesktop,
IconSettings,
IconLogout
IconBook,
IconLogout,
IconLayoutSidebarLeftCollapse,
IconLayoutSidebarLeftExpand,
} from '@tabler/icons-react'
import { Sidebar, SidebarBody, SidebarLink } from './ModernSidebar'
export default function Navbar() {
export default function Sidebar() {
const { user, logout, isAdmin } = useAuth()
const navigate = useNavigate()
const [open, setOpen] = useState(false)
const location = useLocation()
const [collapsed, setCollapsed] = useState(false)
const handleLogout = async () => {
await logout()
navigate('/login')
}
const links = [
const groups = [
{
label: 'Tickets',
href: '/tickets',
icon: <IconTicket className="icon" />,
label: 'Betrieb',
links: [
{ label: 'Tickets', href: '/tickets', icon: <IconTicket /> },
{ label: 'Planboard', href: '/planboard', icon: <IconCalendar /> },
{ label: 'Projekte', href: '/projects', icon: <IconFolders /> },
],
},
{
label: 'Assets',
href: '/assets',
icon: <IconDeviceDesktop className="icon" />,
label: 'CRM',
links: [{ label: 'Kunden', href: '/customers', icon: <IconUsers /> }],
},
{
label: 'Dashboard',
href: '/dashboard',
icon: <IconChartBar className="icon" />,
label: 'Finanzen',
links: [{ label: 'Finanzen', href: '/finance', icon: <IconReportMoney /> }],
},
{
label: 'Planboard',
href: '/planboard',
icon: <IconCalendar className="icon" />,
label: 'Dokumente',
links: [{ label: 'Dokumente', href: '/documents', icon: <IconFiles /> }],
},
{
label: 'Projects',
href: '/projects',
icon: <IconFolders className="icon" />,
label: 'Auswertung',
links: [
{ label: 'Dashboard', href: '/dashboard', icon: <IconLayoutDashboard /> },
{ label: 'Reports', href: '/reports', icon: <IconReport /> },
],
},
{
label: 'Reports',
href: '/reports',
icon: <IconFileText className="icon" />,
},
{
label: 'Docs',
href: '/docs',
icon: <IconBook className="icon" />,
label: 'System',
links: [
{ label: 'Assets', href: '/assets', icon: <IconDeviceDesktop /> },
...(isAdmin ? [{ label: 'Admin', href: '/admin', icon: <IconSettings /> }] : []),
{ label: 'Docs', href: '/docs', icon: <IconBook /> },
],
},
]
// Admin link nur wenn isAdmin
if (isAdmin) {
links.push({
label: 'Admin',
href: '/admin',
icon: <IconSettings className="icon" />,
})
}
const isActive = (href) =>
location.pathname === href || location.pathname.startsWith(href + '/')
return (
<Sidebar open={open} setOpen={setOpen}>
<SidebarBody className="justify-between gap-10">
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflowX: 'hidden', overflowY: 'auto' }}>
<Logo />
<div style={{ marginTop: '2rem', display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
{links.map((link, idx) => (
<SidebarLink key={idx} link={link} />
<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">
{groups.map((group) => (
<div className="side-group" key={group.label}>
<div className="side-group-label">{group.label}</div>
{group.links.map((link) => (
<Link
key={link.href}
to={link.href}
className={`side-link ${isActive(link.href) ? 'active' : ''}`}
title={link.label}
>
{link.icon}
<span className="side-link-label">{link.label}</span>
</Link>
))}
</div>
</div>
<div>
{user ? (
<>
<SidebarLink
link={{
label: user.name || user.email,
href: '#',
icon: (
<div style={{
height: '28px',
width: '28px',
borderRadius: '50%',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontSize: '12px',
fontWeight: 'bold',
flexShrink: 0
}}>
{(user.name || user.email).charAt(0).toUpperCase()}
</div>
),
}}
/>
<div onClick={handleLogout} style={{ cursor: 'pointer', marginTop: '0.5rem' }}>
<SidebarLink
link={{
label: 'Logout',
href: '#',
icon: <IconLogout className="icon" />,
}}
/>
</div>
</>
) : (
<SidebarLink
link={{
label: 'Login',
href: '/login',
icon: <IconLogout className="icon" />,
}}
/>
)}
</div>
</SidebarBody>
</Sidebar>
)
}
))}
</nav>
const Logo = () => {
return (
<Link
to="/"
style={{
position: 'relative',
zIndex: 20,
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.25rem 0',
fontSize: '0.875rem',
fontWeight: 'normal',
color: 'var(--text-color, #333)',
textDecoration: 'none'
}}
>
<div style={{
height: '20px',
width: '24px',
flexShrink: 0,
borderRadius: '4px 2px 4px 2px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
}} />
<motion.span
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
style={{
fontWeight: 500,
whiteSpace: 'pre',
color: 'var(--text-color, #333)'
}}
>
Webklar
</motion.span>
</Link>
<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>
)
}