feat: initial commit
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
import { Outlet, NavLink, useNavigate, Link } from 'react-router-dom'
|
||||
import { LayoutDashboard, Package, Settings, LogOut, FlaskConical, Activity, Library, Plus, SlidersHorizontal } 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>
|
||||
)}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user