diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx
index 8345b000..cd00f5b2 100644
--- a/frontend/src/components/layout/Sidebar.tsx
+++ b/frontend/src/components/layout/Sidebar.tsx
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
-import { LayoutGrid, Box, PenLine, Clock, FileText, Bookmark, Users, Settings, PanelLeftClose, PanelLeftOpen } from 'lucide-react'
+import { LayoutGrid, Box, PenLine, Clock, FileText, Bookmark, BarChart3, Users, Settings, PanelLeftClose, PanelLeftOpen } from 'lucide-react'
import { cn } from '@/lib/utils'
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
import { CategoryList } from '@/components/sidebar/CategoryList'
@@ -123,6 +123,7 @@ export function Sidebar() {
+
>
) : (
@@ -150,6 +151,7 @@ export function Sidebar() {
+
diff --git a/frontend/src/pages/MyAnalyticsPage.tsx b/frontend/src/pages/MyAnalyticsPage.tsx
new file mode 100644
index 00000000..f58b8e4b
--- /dev/null
+++ b/frontend/src/pages/MyAnalyticsPage.tsx
@@ -0,0 +1,3 @@
+export default function MyAnalyticsPage() {
+ return My Analytics — coming soon
+}
diff --git a/frontend/src/pages/TeamAnalyticsPage.tsx b/frontend/src/pages/TeamAnalyticsPage.tsx
new file mode 100644
index 00000000..3df0ff73
--- /dev/null
+++ b/frontend/src/pages/TeamAnalyticsPage.tsx
@@ -0,0 +1,366 @@
+import { useState, useEffect } from 'react'
+import { Link } from 'react-router-dom'
+import { BarChart3, Loader2, Users, Target, Clock, TrendingUp, ShieldX } from 'lucide-react'
+import {
+ AreaChart,
+ Area,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ ResponsiveContainer,
+} from 'recharts'
+import { analyticsApi } from '@/api'
+import { usePermissions } from '@/hooks/usePermissions'
+import type { TeamAnalyticsResponse, AnalyticsPeriod } from '@/types'
+
+const CHART_COLORS = {
+ resolved: '#34d399',
+ escalated: '#f87171',
+ workaround: '#fbbf24',
+ unresolved: '#94a3b8',
+}
+
+const PERIOD_OPTIONS: { value: AnalyticsPeriod; label: string }[] = [
+ { value: '7d', label: 'Last 7 days' },
+ { value: '30d', label: 'Last 30 days' },
+ { value: '90d', label: 'Last 90 days' },
+]
+
+export default function TeamAnalyticsPage() {
+ const { isAccountOwner, isSuperAdmin } = usePermissions()
+ const [period, setPeriod] = useState('30d')
+ const [data, setData] = useState(null)
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ if (!isAccountOwner && !isSuperAdmin) return
+
+ setLoading(true)
+ analyticsApi
+ .getTeamAnalytics(period)
+ .then(setData)
+ .catch(console.error)
+ .finally(() => setLoading(false))
+ }, [period, isAccountOwner, isSuperAdmin])
+
+ // Permission guard
+ if (!isAccountOwner && !isSuperAdmin) {
+ return (
+
+
+
Access Denied
+
+ Team Analytics is only available to account owners and administrators.
+ You can view your personal stats instead.
+
+
+
+ View My Stats
+
+
+ )
+ }
+
+ if (loading) {
+ return (
+
+
+
+ )
+ }
+
+ if (!data) {
+ return (
+
+
Failed to load analytics data.
+
+ )
+ }
+
+ const { summary, time_series, top_flows, top_engineers } = data
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
Team Analytics
+
+
+
+
+ My Stats
+
+
+
+
+
+ {/* Stat Cards */}
+
+
+
+
+
+
+
+ {/* Area Chart — Sessions over Time */}
+
+
+ Sessions Over Time
+
+
+
+
+ {
+ const d = new Date(value)
+ return `${d.getMonth() + 1}/${d.getDate()}`
+ }}
+ />
+
+ {
+ const d = new Date(String(value))
+ return d.toLocaleDateString(undefined, {
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ })
+ }}
+ />
+
+
+
+
+
+
+
+ {/* Chart legend */}
+
+ {Object.entries(CHART_COLORS).map(([key, color]) => (
+
+ ))}
+
+
+
+ {/* Two-Column: Top Flows & Top Engineers */}
+
+ {/* Top Flows */}
+
+
+ Top Flows
+
+ {top_flows.length === 0 ? (
+
No flow data for this period.
+ ) : (
+
+
+
+
+ |
+ Name
+ |
+
+ Sessions
+ |
+
+ Completion
+ |
+
+ Median
+ |
+
+
+
+ {top_flows.map((flow) => (
+
+ |
+ {flow.name}
+ |
+
+ {flow.sessions}
+ |
+
+ {(flow.completion_rate * 100).toFixed(1)}%
+ |
+
+ {flow.median_duration_minutes} min
+ |
+
+ ))}
+
+
+
+ )}
+
+
+ {/* Top Engineers */}
+
+
+ Top Engineers
+
+ {top_engineers.length === 0 ? (
+
No engineer data for this period.
+ ) : (
+
+
+
+
+ |
+ Name
+ |
+
+ Sessions
+ |
+
+ Completion
+ |
+
+ Median
+ |
+
+
+
+ {top_engineers.map((eng) => (
+
+ |
+ {eng.name}
+ |
+
+ {eng.sessions}
+ |
+
+ {(eng.completion_rate * 100).toFixed(1)}%
+ |
+
+ {eng.median_duration_minutes} min
+ |
+
+ ))}
+
+
+
+ )}
+
+
+
+ )
+}
+
+function StatCard({
+ icon: Icon,
+ label,
+ value,
+}: {
+ icon: React.ComponentType<{ size?: number; className?: string }>
+ label: string
+ value: string
+}) {
+ return (
+
+
+
+ {label}
+
+
{value}
+
+ )
+}
diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx
index 9d6b3232..a19098e6 100644
--- a/frontend/src/router.tsx
+++ b/frontend/src/router.tsx
@@ -27,6 +27,8 @@ const ProceduralNavigationPage = lazy(() => import('@/pages/ProceduralNavigation
const SessionHistoryPage = lazy(() => import('@/pages/SessionHistoryPage'))
const SessionDetailPage = lazy(() => import('@/pages/SessionDetailPage'))
const MySharesPage = lazy(() => import('@/pages/MySharesPage'))
+const TeamAnalyticsPage = lazy(() => import('@/pages/TeamAnalyticsPage'))
+const MyAnalyticsPage = lazy(() => import('@/pages/MyAnalyticsPage'))
const AccountSettingsPage = lazy(() => import('@/pages/AccountSettingsPage'))
// Admin pages
const AdminLayout = lazy(() => import('@/components/admin/AdminLayout'))
@@ -198,6 +200,22 @@ export const router = createBrowserRouter([
),
},
+ {
+ path: 'analytics',
+ element: (
+ }>
+
+
+ ),
+ },
+ {
+ path: 'analytics/me',
+ element: (
+ }>
+
+
+ ),
+ },
// Admin routes
{
path: 'admin',