diff --git a/frontend/src/pages/MyAnalyticsPage.tsx b/frontend/src/pages/MyAnalyticsPage.tsx index f58b8e4b..3670d601 100644 --- a/frontend/src/pages/MyAnalyticsPage.tsx +++ b/frontend/src/pages/MyAnalyticsPage.tsx @@ -1,3 +1,348 @@ -export default function MyAnalyticsPage() { - return
My Analytics — coming soon
+import { useState, useEffect } from 'react' +import { Link } from 'react-router-dom' +import { BarChart3, Loader2, Target, Clock, TrendingUp, CheckCircle } from 'lucide-react' +import { + AreaChart, + Area, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from 'recharts' +import { analyticsApi } from '@/api' +import { usePermissions } from '@/hooks/usePermissions' +import type { PersonalAnalyticsResponse, AnalyticsPeriod } from '@/types' + +const OUTCOME_COLORS: Record = { + 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 MyAnalyticsPage() { + const { isAccountOwner, isSuperAdmin } = usePermissions() + const [period, setPeriod] = useState('30d') + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + setLoading(true) + analyticsApi + .getPersonalAnalytics(period) + .then(setData) + .catch(console.error) + .finally(() => setLoading(false)) + }, [period]) + + if (loading) { + return ( +
+ +
+ ) + } + + if (!data) { + return ( +
+

Failed to load analytics data.

+
+ ) + } + + const { summary, time_series, top_flows } = data + const outcomeBreakdown = summary.outcome_breakdown + + return ( +
+ {/* Header */} +
+
+ + + +

My Analytics

+
+ +
+ {(isAccountOwner || isSuperAdmin) && ( + + Team Analytics + + )} + +
+
+ + {/* Stat Cards */} +
+ + + + +
+ + {/* Area Chart — Sessions over Time */} +
+

+ My 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(OUTCOME_COLORS).map(([key, color]) => ( +
+
+ + {key} + +
+ ))} +
+
+ + {/* Two-Column: Top Flows & Outcome Distribution */} +
+ {/* My Top Flows */} +
+

+ My 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 +
+
+ )} +
+ + {/* Outcome Distribution */} +
+

+ Outcome Distribution +

+ {summary.total_sessions === 0 ? ( +

No session data for this period.

+ ) : ( +
+ {( + Object.entries(outcomeBreakdown) as [string, number][] + ).map(([outcome, count]) => { + const total = Object.values(outcomeBreakdown).reduce( + (sum, v) => sum + v, + 0 + ) + const pct = total > 0 ? (count / total) * 100 : 0 + + return ( +
+
+
+
+ + {outcome} + +
+ + {count} ({pct.toFixed(1)}%) + +
+
+
+
+
+ ) + })} +
+ )} +
+
+
+ ) +} + +function StatCard({ + icon: Icon, + label, + value, +}: { + icon: React.ComponentType<{ size?: number; className?: string }> + label: string + value: string +}) { + return ( +
+
+ + {label} +
+

{value}

+
+ ) }