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 (
);
}
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 (
);
}
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) => (
))}
);
}
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) => (
))}
);
}
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
{/* 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,
});