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]) => ( +
+
+ + {key} + +
+ ))} +
+
+ + {/* Two-Column: Top Flows & Top Engineers */} +
+ {/* Top Flows */} +
+

+ Top Flows +

+ {top_flows.length === 0 ? ( +

No flow data for this period.

+ ) : ( +
+ + + + + + + + + + + {top_flows.map((flow) => ( + + + + + + + ))} + +
+ Name + + Sessions + + Completion + + Median +
+ {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.

+ ) : ( +
+ + + + + + + + + + + {top_engineers.map((eng) => ( + + + + + + + ))} + +
+ Name + + Sessions + + Completion + + Median +
+ {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',