Repairs list now has a filter panel with status (defaults to active only), condition, batch/individual toggle, and date range filters for intake and promised dates. Added Batch column to the repairs table. Backend list endpoint accepts filter query params for status, condition, dates, and batch membership. Template management page (admin only) with CRUD for common repair services (rehair, string change, etc.) with instrument type, size, and default pricing. Sidebar updated with Repair Templates link gated on repairs.admin permission.
131 lines
5.1 KiB
TypeScript
131 lines
5.1 KiB
TypeScript
import { createFileRoute, Outlet, Link, redirect, useRouter } from '@tanstack/react-router'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { useEffect } from 'react'
|
|
import { useAuthStore } from '@/stores/auth.store'
|
|
import { myPermissionsOptions } from '@/api/rbac'
|
|
import { Avatar } from '@/components/shared/avatar-upload'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Users, UserRound, HelpCircle, Shield, UserCog, LogOut, User, Wrench, Package, ClipboardList } from 'lucide-react'
|
|
|
|
export const Route = createFileRoute('/_authenticated')({
|
|
beforeLoad: () => {
|
|
const { token } = useAuthStore.getState()
|
|
if (!token) {
|
|
throw redirect({ to: '/login' })
|
|
}
|
|
},
|
|
component: AuthenticatedLayout,
|
|
})
|
|
|
|
function NavLink({ to, icon, label }: { to: string; icon: React.ReactNode; label: string }) {
|
|
return (
|
|
<Link
|
|
to={to as '/accounts'}
|
|
search={{} as any}
|
|
className="flex items-center gap-2 px-3 py-2 rounded-md text-sm text-sidebar-foreground hover:bg-sidebar-accent"
|
|
activeProps={{ className: 'flex items-center gap-2 px-3 py-2 rounded-md text-sm bg-sidebar-accent text-sidebar-accent-foreground' }}
|
|
>
|
|
{icon}
|
|
{label}
|
|
</Link>
|
|
)
|
|
}
|
|
|
|
function AuthenticatedLayout() {
|
|
const router = useRouter()
|
|
const user = useAuthStore((s) => s.user)
|
|
const logout = useAuthStore((s) => s.logout)
|
|
const hasPermission = useAuthStore((s) => s.hasPermission)
|
|
const setPermissions = useAuthStore((s) => s.setPermissions)
|
|
const permissionsLoaded = useAuthStore((s) => s.permissionsLoaded)
|
|
|
|
// Fetch permissions on mount
|
|
const { data: permData } = useQuery({
|
|
...myPermissionsOptions(),
|
|
enabled: !!useAuthStore.getState().token,
|
|
})
|
|
|
|
useEffect(() => {
|
|
if (permData?.permissions) {
|
|
setPermissions(permData.permissions)
|
|
}
|
|
}, [permData, setPermissions])
|
|
|
|
function handleLogout() {
|
|
logout()
|
|
router.navigate({ to: '/login', replace: true })
|
|
}
|
|
|
|
const canViewAccounts = !permissionsLoaded || hasPermission('accounts.view')
|
|
const canViewRepairs = !permissionsLoaded || hasPermission('repairs.view')
|
|
const canViewUsers = !permissionsLoaded || hasPermission('users.view')
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background text-foreground">
|
|
<div className="flex">
|
|
<nav className="w-56 border-r border-border bg-sidebar min-h-screen flex flex-col">
|
|
<div className="p-4">
|
|
<h2 className="text-lg font-semibold text-sidebar-foreground">Forte</h2>
|
|
</div>
|
|
|
|
{/* Sidebar links use `as any` on search because TanStack Router
|
|
requires the full validated search shape, but these links just
|
|
navigate to the page with default params. */}
|
|
<div className="flex-1 px-3 space-y-1">
|
|
{canViewAccounts && (
|
|
<>
|
|
<NavLink to="/accounts" icon={<Users className="h-4 w-4" />} label="Accounts" />
|
|
<NavLink to="/members" icon={<UserRound className="h-4 w-4" />} label="Members" />
|
|
</>
|
|
)}
|
|
{canViewRepairs && (
|
|
<>
|
|
<NavLink to="/repairs" icon={<Wrench className="h-4 w-4" />} label="Repairs" />
|
|
<NavLink to="/repair-batches" icon={<Package className="h-4 w-4" />} label="Repair Batches" />
|
|
{hasPermission('repairs.admin') && (
|
|
<NavLink to="/repairs/templates" icon={<ClipboardList className="h-4 w-4" />} label="Repair Templates" />
|
|
)}
|
|
</>
|
|
)}
|
|
{canViewUsers && (
|
|
<div className="mt-4 mb-1 px-3">
|
|
<span className="text-xs font-semibold text-sidebar-foreground/50 uppercase tracking-wide">Admin</span>
|
|
</div>
|
|
)}
|
|
{canViewUsers && (
|
|
<>
|
|
<NavLink to="/users" icon={<UserCog className="h-4 w-4" />} label="Users" />
|
|
<NavLink to="/roles" icon={<Shield className="h-4 w-4" />} label="Roles" />
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<div className="p-3 border-t border-sidebar-border space-y-1">
|
|
<NavLink to="/help" icon={<HelpCircle className="h-4 w-4" />} label="Help" />
|
|
<Link
|
|
to="/profile"
|
|
className="flex items-center gap-2 px-3 py-2 rounded-md text-sm text-sidebar-foreground hover:bg-sidebar-accent w-full"
|
|
activeProps={{ className: 'flex items-center gap-2 px-3 py-2 rounded-md text-sm bg-sidebar-accent text-sidebar-accent-foreground w-full' }}
|
|
>
|
|
{user?.id ? <Avatar entityType="user" entityId={user.id} size="sm" /> : <User className="h-4 w-4" />}
|
|
<span className="truncate">{user?.firstName} {user?.lastName}</span>
|
|
</Link>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="w-full justify-start gap-2 text-sm text-sidebar-foreground/70 hover:text-sidebar-foreground"
|
|
onClick={handleLogout}
|
|
>
|
|
<LogOut className="h-4 w-4" />
|
|
Sign out
|
|
</Button>
|
|
</div>
|
|
</nav>
|
|
<main className="flex-1 p-6">
|
|
<Outlet />
|
|
</main>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|