import { useState, useMemo } from "react";
import { createRoute } from "@tanstack/react-router";
import { Route as appRoute } from "../_app";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { api, useApiQuery, useApiMutation } from "@/lib/api";
import { Download, Calendar, TrendingUp, BarChart3, PieChart, Activity, Grid3X3 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import type { ProductivityData, HabitAnalytics, ProjectAnalytics } from "@/lib/types";
import { format, subDays, parseISO, startOfMonth, eachDayOfInterval } 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, 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 barWidth = Math.max(20, Math.min(40, (300 / data.length)));
const width = Math.max(data.length * (barWidth + 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 { data: prodData } = useApiQuery(["analytics-productivity", range], "/analytics/productivity?range=" + range);
const { data: habitData } = useApiQuery(["analytics-habits", range], "/analytics/habits?range=" + range);
const { data: projectData } = useApiQuery(["analytics-projects", range], "/analytics/projects?range=" + range);
// Generate mock daily data for charts (real API returns aggregated, we simulate daily breakdown)
const dailyData = useMemo(() => {
const days = parseInt(range);
return Array.from({ length: days }, (_, i) => {
const d = subDays(new Date(), days - 1 - i);
return {
date: format(d, "yyyy-MM-dd"),
completed: Math.floor(Math.random() * 5),
created: Math.floor(Math.random() * 8) + 1,
};
});
}, [range]);
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
{/* Tasks completed per day */}
Tasks Completed
{/* Tasks created vs completed */}
Created vs Completed
{/* Habit completion rate */}
Habit Completion
{/* Project progress */}
Project Progress
{projectData?.taskCompletionRate || 0}%
Rate
{projectData?.totalTasks || 0}
Total
{projectData?.completedTasks || 0}
Done
{/* Time spent per domain (pie) */}
Time per Domain
{/* Productivity heatmap */}
Productivity Heatmap
({ date: d.date, count: d.completed }))} days={parseInt(range)} />
);
}
export const Route = createRoute({
getParentRoute: () => appRoute,
path: "/analytics",
component: AnalyticsPage,
});