Files
HartOMat/frontend/src/components/layout/Layout.tsx
T
Hartmut 82bf46725b feat(B3): add tenant management UI (CRUD + tenant selector)
- frontend/src/api/tenants.ts: Tenant CRUD API client (getTenants, getTenant, createTenant, updateTenant, deleteTenant)
- frontend/src/pages/Tenants.tsx: Admin page with table, create/edit modals, delete confirm, and cross-tenant selector persisted in localStorage
- App.tsx: /tenants route (AdminRoute-guarded)
- Layout.tsx: Tenants sidebar link (admin-only, Building2 icon)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 16:13:26 +01:00

170 lines
6.4 KiB
TypeScript

import { Outlet, NavLink, useNavigate, Link } from 'react-router-dom'
import { LayoutDashboard, Package, Settings, LogOut, FlaskConical, Activity, Library, Plus, SlidersHorizontal, Building2 } from 'lucide-react'
import { useAuthStore } from '../../store/auth'
import { clsx } from 'clsx'
import { useQuery } from '@tanstack/react-query'
import { getWorkerActivity } from '../../api/worker'
import { listOrders } from '../../api/orders'
import NotificationCenter from './NotificationCenter'
const nav = [
{ to: '/', icon: LayoutDashboard, label: 'Dashboard', end: true },
{ to: '/orders', icon: Package, label: 'Orders' },
{ to: '/products', icon: Library, label: 'Products' },
{ to: '/materials', icon: FlaskConical, label: 'Materials' },
{ to: '/activity', icon: Activity, label: 'Activity' },
{ to: '/preferences', icon: SlidersHorizontal, label: 'Preferences' },
]
export default function Layout() {
const { user, logout } = useAuthStore()
const navigate = useNavigate()
const { data: activity } = useQuery({
queryKey: ['worker-activity'],
queryFn: getWorkerActivity,
refetchInterval: 8000,
staleTime: 4000,
})
const { data: draftOrders } = useQuery({
queryKey: ['orders', 'draft-count'],
queryFn: () => listOrders({ status: 'draft' }),
staleTime: 10_000,
refetchInterval: 30_000,
})
const draftCount = draftOrders?.length ?? 0
function handleLogout() {
logout()
navigate('/login')
}
return (
<div className="flex h-screen overflow-hidden bg-surface-alt">
{/* Sidebar */}
<aside className="w-60 flex-shrink-0 bg-surface border-r border-border-default flex flex-col">
<div className="p-5 border-b border-border-default">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-accent rounded flex items-center justify-center">
<span className="text-accent-text text-sm font-bold">S</span>
</div>
<div className="flex-1">
<p className="font-semibold text-content text-sm">Schaeffler</p>
<p className="text-xs text-content-muted">Automat</p>
</div>
<NotificationCenter />
</div>
</div>
<nav className="flex-1 p-3 space-y-1">
{/* New Order — primary CTA at the top */}
<Link
to="/orders/new"
className="flex items-center gap-2 px-3 py-2.5 mb-3 rounded-md text-sm font-semibold bg-accent text-accent-text hover:bg-accent-hover transition-colors shadow-sm"
>
<Plus size={18} />
New Order
</Link>
{nav.map(({ to, icon: Icon, label, end }) => {
const isActivity = to === '/activity'
const isOrders = to === '/orders'
const showSpinner = isActivity && ((activity?.active_count ?? 0) + (activity?.render_active_count ?? 0)) > 0
const showFailed = isActivity && !showSpinner && ((activity?.failed_count ?? 0) + (activity?.render_failed_count ?? 0)) > 0
const showDraftBadge = isOrders && draftCount > 0
return (
<NavLink
key={to}
to={to}
end={end}
className={({ isActive }) =>
clsx(
'flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors',
isActive
? 'bg-accent-light text-accent'
: 'text-content-secondary hover:bg-surface-hover',
)
}
>
<Icon size={18} />
{label}
{showDraftBadge && (
<span className="ml-auto text-xs px-1.5 py-0.5 rounded-full bg-surface-muted text-content-muted font-semibold leading-none">
{draftCount}
</span>
)}
{showSpinner && (
<span className="ml-auto w-2 h-2 rounded-full bg-blue-500 animate-pulse" title="Processing…" />
)}
{showFailed && (
<span className="ml-auto w-2 h-2 rounded-full bg-red-500" title="Failed tasks" />
)}
</NavLink>
)
})}
{(user?.role === 'admin' || user?.role === 'project_manager') && (
<NavLink
to="/admin"
className={({ isActive }) =>
clsx(
'flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors',
isActive
? 'bg-accent-light text-accent'
: 'text-content-secondary hover:bg-surface-hover',
)
}
>
<Settings size={18} />
Admin
</NavLink>
)}
{user?.role === 'admin' && (
<NavLink
to="/tenants"
className={({ isActive }) =>
clsx(
'flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors',
isActive
? 'bg-accent-light text-accent'
: 'text-content-secondary hover:bg-surface-hover',
)
}
>
<Building2 size={18} />
Tenants
</NavLink>
)}
</nav>
<div className="p-3 border-t border-border-default space-y-1">
<div className="flex items-center gap-3 px-3 py-2">
<div className="w-7 h-7 bg-accent rounded-full flex items-center justify-center shrink-0">
<span className="text-accent-text text-xs font-bold">{user?.full_name?.[0] ?? 'U'}</span>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-content truncate">{user?.full_name}</p>
<p className="text-xs text-content-muted truncate">
{user?.role === 'project_manager' ? 'Project Manager' : user?.role}
</p>
</div>
</div>
<button
onClick={handleLogout}
className="flex items-center gap-2 w-full px-3 py-2 text-sm text-content-secondary hover:bg-surface-hover rounded-md transition-colors"
>
<LogOut size={16} />
Sign out
</button>
</div>
</aside>
{/* Main content */}
<main className="flex-1 overflow-auto">
<Outlet />
</main>
</div>
)
}