import { useState, useMemo } from "react"; import { createRoute } from "@tanstack/react-router"; import { Route as appRoute } from "../_app"; import { useApiQuery } from "@/lib/api"; import { useApiDomain } from "@/lib/stores/use-active-domain-store"; import { useRealtime } from "@/hooks/use-realtime"; import { Download, Calendar } from "lucide-react"; import { Button } from "@/components/ui/button"; import { LoadingState, ErrorState } from "@/components/state"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { cn } from "@/lib/utils"; import type { DailyAnalytics, HabitAnalytics, ProjectAnalytics } from "@/lib/types"; import { format, subDays } from "date-fns"; // Simple SVG-based charts (no recharts dependency needed for basic charts) function LineChart({ data, xKey, yKey, color = "#3b82f6", height = 120 }: { data: any[]; xKey: string; yKey: string; color?: string; height?: number }) { if (!data.length) return

No data

; const maxVal = Math.max(...data.map((d) => d[yKey]), 1); const width = Math.max(data.length * 30, 200); const points = data.map((d, i) => { const x = (i / (data.length - 1 || 1)) * (width - 40) + 20; const y = height - 20 - ((d[yKey] / maxVal) * (height - 40)); return `${x},${y}`; }).join(" "); return ( {data.map((d, i) => { const x = (i / (data.length - 1 || 1)) * (width - 40) + 20; const y = height - 20 - ((d[yKey] / maxVal) * (height - 40)); return ; })} ); } function BarChart({ data, xKey, yKey, yKey2, color = "#3b82f6", color2 = "#f97316", height = 120 }: { data: any[]; xKey: string; yKey: string; yKey2?: string; color?: string; color2?: string; height?: number }) { if (!data.length) return

No data

; const valOf = (d: any, key?: string) => (key ? ((d[key] as number) ?? 0) : 0); const maxVal = Math.max(...data.map((d) => Math.max(valOf(d, yKey), valOf(d, yKey2))), 1); const series = yKey2 ? 2 : 1; const barWidth = Math.max(20, Math.min(40, (300 / data.length) / series)); const width = Math.max(data.length * (barWidth * series + 4) + 40, 200); return ( {data.map((d, i) => { const barH = (valOf(d, yKey) / maxVal) * (height - 30); const x = i * (barWidth * series + 4) + 20; const y = height - 20 - barH; return ; })} {yKey2 && data.map((d, i) => { const barH = (valOf(d, yKey2) / maxVal) * (height - 30); const x = i * (barWidth * series + 4) + 20 + barWidth; const y = height - 20 - barH; return ; })} ); } function HorizontalBar({ data, xKey, yKey, height = 100 }: { data: any[]; xKey: string; yKey: string; height?: number }) { if (!data.length) return

No data

; const maxVal = Math.max(...data.map((d) => d[yKey]), 1); return (
{data.map((d, i) => (
{d[xKey]}
{d[yKey]}
))}
); } function PieChartSimple({ data, labelKey, valueKey, size = 120 }: { data: any[]; labelKey: string; valueKey: string; size?: number }) { if (!data.length) return

No data

; const total = data.reduce((s, d) => s + d[valueKey], 0) || 1; const colors = ["#3b82f6", "#22c55e", "#f97316", "#a855f7", "#e11d48", "#14b8a6"]; let cumulative = 0; const slices = data.map((d, i) => { const pct = d[valueKey] / total; const startAngle = cumulative * 360; cumulative += pct; const endAngle = cumulative * 360; const startRad = (startAngle - 90) * Math.PI / 180; const endRad = (endAngle - 90) * Math.PI / 180; const r = size / 2 - 4; const cx = size / 2; const cy = size / 2; const x1 = cx + r * Math.cos(startRad); const y1 = cy + r * Math.sin(startRad); const x2 = cx + r * Math.cos(endRad); const y2 = cy + r * Math.sin(endRad); const largeArc = pct > 0.5 ? 1 : 0; return { path: `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${largeArc} 1 ${x2} ${y2} Z`, color: colors[i % colors.length], label: d[labelKey], pct: Math.round(pct * 100) }; }); return (
{slices.map((s, i) => )}
{slices.map((s, i) => (
{s.label} ({s.pct}%)
))}
); } function CalendarHeatmap({ data, days = 30 }: { data: any[]; days?: number }) { const today = new Date(); const dateMap = new Map(data.map((d) => [d.date?.slice(0, 10), d.count || 0])); const cells = []; for (let i = days - 1; i >= 0; i--) { const d = subDays(today, i); const key = format(d, "yyyy-MM-dd"); const count = dateMap.get(key) || 0; const intensity = count > 0 ? Math.min(count / 5, 1) : 0; const color = intensity > 0.75 ? "bg-green-600" : intensity > 0.5 ? "bg-green-500" : intensity > 0.25 ? "bg-green-400" : intensity > 0 ? "bg-green-200" : "bg-muted"; cells.push(
); } return
{cells}
; } // ─── Analytics Page ────────────────────────────────────────────────────── function AnalyticsPage() { const [range, setRange] = useState("30"); const activeDomainId = useApiDomain(); const domainSuffix = activeDomainId ? "&domain=" + activeDomainId : ""; useRealtime({ enabled: true }); const { data: habitData, isLoading: habitsLoading, error: habitsError, refetch: refetchHabits } = useApiQuery(["analytics-habits", activeDomainId, range], "/analytics/habits?range=" + range + domainSuffix); const { data: projectData, isLoading: projectsLoading, error: projectsError, refetch: refetchProjects } = useApiQuery(["analytics-projects", activeDomainId, range], "/analytics/projects?range=" + range + domainSuffix); const { data: dailyData, isLoading: dailyLoading, error: dailyError, refetch: refetchDaily } = useApiQuery(["analytics-daily", activeDomainId, range], "/analytics/daily?range=" + range + domainSuffix); const analyticsLoading = habitsLoading || projectsLoading || dailyLoading; const analyticsError = habitsError || projectsError || dailyError; const refetchAnalytics = () => { refetchHabits(); refetchProjects(); refetchDaily(); }; const dailyItems = dailyData?.items || []; const habitRateData = useMemo(() => { return [ { name: "Completed", value: habitData?.totalLogs || 0 }, { name: "Missed", value: Math.max(0, (habitData?.totalHabits || 1) * parseInt(range) - (habitData?.totalLogs || 0)) }, ]; }, [habitData, range]); const downloadCSV = (filename: string, rows: string[][]) => { const csv = rows.map((r) => r.map((c) => '"' + c.replace(/"/g, '""') + '"').join(",")).join("\n"); const blob = new Blob([csv], { type: "text/csv" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); }; return (

Analytics

{analyticsLoading ? ( ) : analyticsError ? ( ) : (
{/* Tasks completed per day */} Tasks Completed {/* Tasks created vs completed */} Created vs Completed
Created
Completed
{/* Habit completion rate */} Habit Completion {/* Project progress */} Project Progress {projectData?.projects.length ? (

{projectData.totalProjects} project{projectData.totalProjects === 1 ? "" : "s"} · progress is % of tasks done

({ name: p.name, progress: Math.round(p.progress * 100) }))} xKey="name" yKey="progress" height={Math.max(100, projectData.projects.length * 26)} />
) : (

No projects yet

)}
{/* Tasks by project (pie) */} Tasks by Project {projectData?.projects.length ? ( ({ name: p.name, value: p.totalTasks }))} labelKey="name" valueKey="value" /> ) : (

No projects yet

)}
{/* Productivity heatmap */} Productivity Heatmap ({ date: d.date, count: d.completed }))} days={parseInt(range)} />
)}
); } export const Route = createRoute({ getParentRoute: () => appRoute, path: "/analytics", component: AnalyticsPage, });