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:
33
frontend/src/components/admin/SeatCounterWidget.tsx
Normal file
33
frontend/src/components/admin/SeatCounterWidget.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
23
frontend/src/components/l1/L1CoverageBanner.tsx
Normal file
23
frontend/src/components/l1/L1CoverageBanner.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user