feat(l1): drafts + tickets pages + coverage banner + seat counter widget

L1DraftsPage is a Phase 1 placeholder (AI drafts arrive in Phase 2).
L1TicketsPage replaces the stub with a status-filterable internal-tickets
queue. L1CoverageBanner renders inside L1RouteGuard so every /l1/* page
shows it for engineer-coverers (hidden for native L1). SeatCounterWidget
+ /api/seats.ts surface engineer + L1 seat usage from the /accounts/me/
seats endpoint (T9).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 14:28:27 -04:00
parent d3fd9143d7
commit 1acc780359
7 changed files with 140 additions and 7 deletions

View File

@@ -0,0 +1,33 @@
import { useEffect, useState } from 'react'
import { seatsApi, type SeatUsage } from '@/api/seats'
interface RowProps { label: string; check: SeatUsage['engineer'] }
function SeatRow({ label, check }: RowProps) {
const overLimit = check.limit !== null && check.current > check.limit
const limitText = check.limit === null ? '∞' : check.limit
return (
<div className={overLimit ? 'text-warning' : ''}>
<p className="text-xs uppercase tracking-wider text-muted-foreground mb-1">{label}</p>
<p className="text-lg font-mono">{check.current} / {limitText}</p>
{overLimit && <p className="text-xs">Over limit (grandfathered)</p>}
</div>
)
}
export function SeatCounterWidget() {
const [usage, setUsage] = useState<SeatUsage | null>(null)
useEffect(() => {
seatsApi.getUsage().then(setUsage).catch(() => setUsage(null))
}, [])
if (!usage) return null
return (
<div className="rounded-lg border border-default bg-card p-4 grid grid-cols-2 gap-4">
<SeatRow label="Engineer seats" check={usage.engineer} />
<SeatRow label="L1 seats" check={usage.l1_tech} />
</div>
)
}

View File

@@ -0,0 +1,23 @@
import { useNavigate } from 'react-router-dom'
import { usePermissions } from '@/hooks/usePermissions'
export function L1CoverageBanner() {
const perms = usePermissions()
const navigate = useNavigate()
// Show only for engineer-coverers / owners-stepping-in. Native L1 doesn't see it.
if (perms.isL1Tech) return null
if (!perms.canCoverL1) return null
return (
<div className="bg-info/10 text-info text-sm px-4 py-1.5 flex items-center justify-between border-b border-info/20">
<span>You're covering L1. Actions logged as coverage.</span>
<button
onClick={() => navigate('/')}
className="text-info hover:underline underline-offset-2"
>
Switch back
</button>
</div>
)
}

View File

@@ -1,10 +1,18 @@
import { Navigate } from 'react-router-dom'
import { usePermissions } from '@/hooks/usePermissions'
import { L1CoverageBanner } from '@/components/l1/L1CoverageBanner'
export function L1RouteGuard({ children }: { children: React.ReactNode }) {
const { canUseL1Surface } = usePermissions()
if (!canUseL1Surface) {
return <Navigate to="/" replace />
}
return <>{children}</>
return (
<div className="flex flex-col h-full">
<L1CoverageBanner />
<div className="flex-1 min-h-0 flex flex-col">
{children}
</div>
</div>
)
}