d3ro-voice/apps/admin/src/components/admin-sidebar.tsx
Yun Chan 5aa268970a feat(admin): read live back-office data in every console view
Models, pipelines, users, subscriptions, usage, audit log, ads, and releases
still rendered placeholder or duplicated implementations from the earlier
admin split. They now read the live back-office API, share one sidebar and
console theme, and the license issuer dialog uses the rotated signing key.
2026-09-16 23:24:35 +09:00

457 lines
16 KiB
TypeScript

'use client'
// apps/admin/src/components/admin-sidebar.tsx
// D3RO Voice Admin CRM — "Midnight Glass v2" Floating Navigation Island
import { usePathname, useRouter } from 'next/navigation'
import { Box, Typography } from '@mui/material'
import { C, FONT_SANS, FONT_MONO } from '@/lib/console-theme'
import { getSupabaseBrowserClient } from '@/lib/supabase-browser'
interface NavGroup {
title: string
items: Array<{
key: string
path: string
label: string
icon: React.ReactElement
}>
}
const NAV_GROUPS: NavGroup[] = [
{
title: 'Core Platform',
items: [
{
key: 'overview',
path: '/',
label: 'Dashboard Overview',
icon: (
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
),
},
{
key: 'pipelines',
path: '/pipelines',
label: 'AI & Voice Pipelines',
icon: (
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 100-6 3 3 0 000 6z" />
</svg>
),
},
{
key: 'models',
path: '/models',
label: 'Service Models',
icon: (
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
),
},
{
key: 'releases',
path: '/releases',
label: 'Release & Downloads',
icon: (
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
),
},
],
},
{
title: 'Customer & Revenue',
items: [
{
key: 'users',
path: '/users',
label: 'User Directory',
icon: (
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
),
},
{
key: 'subscriptions',
path: '/subscriptions',
label: 'Subscriptions & ARR',
icon: (
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
</svg>
),
},
{
key: 'ads',
path: '/ads',
label: 'Ad Monetization',
icon: (
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
),
},
{
key: 'support',
path: '/support',
label: 'Customer Support (CA)',
icon: (
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192l-3.536 3.536M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
),
},
],
},
{
title: 'Intelligence & Security',
items: [
{
key: 'usage',
path: '/usage',
label: 'Usage & Token Costs',
icon: (
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
),
},
{
key: 'audit-log',
path: '/audit-log',
label: 'Security Audit Log',
icon: (
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
),
},
],
},
]
export function AdminSidebar({ identity }: {
identity: { email: string; role: 'manager' | 'admin' | 'super_admin' }
}): React.ReactElement {
const pathname = usePathname()
const router = useRouter()
const handleLogout = async (): Promise<void> => {
try {
await fetch('/api/auth/logout', { method: 'POST' })
} catch {
// ignore
}
try {
const supabase = getSupabaseBrowserClient()
await supabase.auth.signOut().catch(() => {})
} catch {
// ignore
}
router.replace('/login')
router.refresh()
}
const isActive = (path: string): boolean =>
path === '/' ? pathname === '/' : pathname.startsWith(path)
return (
<Box
sx={{
width: { xs: 260, md: 290 },
height: '100%',
display: 'flex',
flexDirection: 'column',
bgcolor: 'var(--d3-bg-card-soft)',
backdropFilter: 'blur(28px)',
border: `1px solid ${C.border}`,
borderRadius: '22px',
flexShrink: 0,
position: 'relative',
overflow: 'hidden',
boxShadow: '0 24px 60px var(--d3-scrim), inset 0 1px 0 var(--d3-glass-hairlineStrong)',
}}
>
{/* Top Ambient Sheen */}
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 120,
background: 'radial-gradient(ellipse 80% 60% at 50% -10%, var(--d3-accent-glow) 0%, transparent 80%)',
pointerEvents: 'none',
}}
/>
{/* Brand Header */}
<Box
sx={{
px: 3,
pt: 3.5,
pb: 2.5,
borderBottom: `1px solid ${C.border}`,
position: 'relative',
zIndex: 1,
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
{/* Logo Mark */}
<Box
sx={{
width: 34,
height: 34,
borderRadius: '10px',
background: 'linear-gradient(135deg, var(--d3-tag-cyan) 0%, var(--d3-accent-main) 50%, var(--d3-tag-purple) 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 0 20px var(--d3-accent-glow)',
}}
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#ffffff" strokeWidth="2.2">
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" />
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
<line x1="12" y1="19" x2="12" y2="22" />
</svg>
</Box>
<Box>
<Typography
sx={{
fontFamily: FONT_SANS,
fontSize: '15px',
fontWeight: 500,
letterSpacing: '-0.02em',
color: C.bright,
lineHeight: 1.2,
}}
>
D3RO Voice
</Typography>
<Typography
sx={{
fontFamily: FONT_MONO,
fontSize: '10px',
fontWeight: 600,
color: C.cyan,
letterSpacing: '0.04em',
}}
>
INTELLIGENCE CRM
</Typography>
</Box>
</Box>
{/* Verified local session indicator */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.75,
px: 1,
py: 0.35,
borderRadius: '999px',
bgcolor: 'var(--d3-status-success)',
border: '1px solid var(--d3-status-success)',
}}
>
<Box
sx={{
width: 6,
height: 6,
borderRadius: '50%',
bgcolor: 'var(--d3-status-success)',
boxShadow: '0 0 8px var(--d3-status-success)',
animation: 'pulse-ring 2s infinite',
}}
/>
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '9px', fontWeight: 600, color: 'var(--d3-status-success)' }}>
SESSION
</Typography>
</Box>
</Box>
</Box>
{/* Nav Scroll Area */}
<Box
component="nav"
sx={{
flex: 1,
overflowY: 'auto',
px: 2,
py: 2,
display: 'flex',
flexDirection: 'column',
gap: 2.5,
position: 'relative',
zIndex: 1,
}}
>
{NAV_GROUPS.map((group) => (
<Box key={group.title}>
<Typography
sx={{
px: 1.5,
mb: 0.75,
fontFamily: FONT_SANS,
fontSize: '11px',
fontWeight: 600,
color: C.dim,
textTransform: 'uppercase',
letterSpacing: '0.08em',
}}
>
{group.title}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
{group.items.map((item) => {
const active = isActive(item.path)
return (
<Box
key={item.key}
onClick={() => router.push(item.path)}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: 1.75,
py: 1.1,
borderRadius: '12px',
cursor: 'pointer',
position: 'relative',
border: `1px solid ${active ? 'var(--d3-accent-glow)' : 'transparent'}`,
bgcolor: active ? 'var(--d3-accent-glow)' : 'transparent',
color: active ? C.bright : C.text,
boxShadow: active ? '0 0 20px var(--d3-accent-glow), inset 0 1px 0 var(--d3-overlay-strong)' : 'none',
transition: 'all 0.18s cubic-bezier(0.16, 1, 0.3, 1)',
'&:hover': {
bgcolor: active ? 'var(--d3-accent-glow)' : 'var(--d3-bg-raised-soft)',
borderColor: active ? 'var(--d3-accent-light)' : C.border,
color: C.bright,
transform: 'translateX(3px)',
},
'&:active': {
transform: 'scale(0.98)',
},
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
color: active ? C.accentLight : C.dim,
transition: 'color 0.15s ease',
}}
>
{item.icon}
</Box>
<Typography
sx={{
fontFamily: FONT_SANS,
fontSize: '13px',
fontWeight: active ? 600 : 500,
letterSpacing: '-0.01em',
}}
>
{item.label}
</Typography>
</Box>
</Box>
)
})}
</Box>
</Box>
))}
</Box>
{/* User Footer */}
<Box
sx={{
p: 2,
bgcolor: 'var(--d3-bg-inset-soft)',
borderTop: `1px solid ${C.border}`,
position: 'relative',
zIndex: 1,
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
p: 1.25,
borderRadius: '12px',
border: `1px solid ${C.borderHl}`,
bgcolor: 'var(--d3-bg-card-soft)',
transition: 'border-color 0.15s ease',
'&:hover': {
borderColor: C.accentLight,
},
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.25 }}>
<Box
sx={{
width: 32,
height: 32,
borderRadius: '8px',
background: 'linear-gradient(135deg, var(--d3-accent-dark) 0%, var(--d3-accent-main) 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 500,
fontSize: '13px',
color: 'var(--d3-text-inverse)',
boxShadow: '0 0 12px var(--d3-accent-glow)',
}}
>
{identity.email.slice(0, 1).toUpperCase()}
</Box>
<Box>
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '12px', fontWeight: 600, color: C.bright, lineHeight: 1.2 }}>
{identity.email}
</Typography>
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '9px', fontWeight: 600, color: C.purple400, letterSpacing: '0.04em' }}>
{identity.role.toUpperCase()}
</Typography>
</Box>
</Box>
<Box
onClick={() => void handleLogout()}
title="Sign Out"
sx={{
p: 0.75,
borderRadius: '8px',
color: C.dim,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.15s ease',
'&:hover': {
color: C.red400,
bgcolor: 'var(--d3-status-danger)',
},
}}
>
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
</Box>
</Box>
</Box>
</Box>
)
}