Files
lunarfront-app/packages/admin/src/routes/_authenticated.tsx
Ryan Moon 01cff80f2b Add repair list filters, template management page, and backend filter support
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.
2026-03-29 10:13:38 -05:00

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>
)
}