Files
carry-your-live/carry-your-live/src/hooks/useStats.tsx
T

201 lines
5.4 KiB
TypeScript

import { useEffect, useState } from 'react';
import { useDatabase } from './useDatabase';
const DAY_MS = 24 * 60 * 60 * 1000;
const WEEK_MS = 7 * DAY_MS;
const MONTH_MS = 30 * DAY_MS;
export interface DailyCompletion {
label: string;
count: number;
}
export interface CategoryStat {
id: string;
name: string;
color: string;
count: number;
}
export interface PriorityStat {
priority: string;
label: string;
count: number;
}
export interface TaskStats {
loading: boolean;
totalCompleted: number;
completedLast7: number;
completedLast30: number;
activeCount: number;
overdueCount: number;
completionRate: number;
daily: DailyCompletion[];
byCategory: CategoryStat[];
byPriority: PriorityStat[];
currentStreak: number;
}
export const PRIORITY_LABELS: Record<string, string> = {
high: 'High',
medium: 'Medium',
low: 'Low',
none: 'No priority',
};
function startOfDay(ts: number): number {
const d = new Date(ts);
d.setHours(0, 0, 0, 0);
return d.getTime();
}
function dayLabel(dayStart: number): string {
const d = new Date(dayStart);
const today = new Date();
today.setHours(0, 0, 0, 0);
const diff = Math.round((today.getTime() - d.getTime()) / DAY_MS);
if (diff === 0) return 'Today';
if (diff === 1) return 'Yest';
return d.toLocaleDateString(undefined, { weekday: 'short' });
}
const EMPTY_STATS: TaskStats = {
loading: true,
totalCompleted: 0,
completedLast7: 0,
completedLast30: 0,
activeCount: 0,
overdueCount: 0,
completionRate: 0,
daily: [],
byCategory: [],
byPriority: [],
currentStreak: 0,
};
export function useStats(): TaskStats {
const { collections, isReady } = useDatabase();
const [stats, setStats] = useState<TaskStats>(EMPTY_STATS);
useEffect(() => {
if (!isReady) return;
let mounted = true;
const compute = () => {
Promise.all([collections.tasks.query().fetch(), collections.categories.query().fetch()])
.then(([allTasks, categories]) => {
const now = Date.now();
const todayStart = startOfDay(now);
const weekStart = todayStart - WEEK_MS;
const monthStart = todayStart - MONTH_MS;
const categoryCounts = new Map<string, number>();
const priorityCounts = new Map<string, number>();
const dayBucket = new Map<number, number>();
let totalCompleted = 0;
let completedLast7 = 0;
let completedLast30 = 0;
let activeCount = 0;
let overdueCount = 0;
for (const t of allTasks) {
if (t.completed) {
if (t.completedAt == null) continue;
const ts = t.completedAt as number;
totalCompleted++;
categoryCounts.set(t.categoryId, (categoryCounts.get(t.categoryId) ?? 0) + 1);
priorityCounts.set(t.priority, (priorityCounts.get(t.priority) ?? 0) + 1);
if (ts >= weekStart) completedLast7++;
if (ts >= monthStart) completedLast30++;
const day = startOfDay(ts);
dayBucket.set(day, (dayBucket.get(day) ?? 0) + 1);
} else {
activeCount++;
if (t.dueDate > 0 && t.dueDate < now) overdueCount++;
}
}
const daily: DailyCompletion[] = [];
for (let i = 6; i >= 0; i--) {
const dayStart = todayStart - i * DAY_MS;
daily.push({
label: dayLabel(dayStart),
count: dayBucket.get(dayStart) ?? 0,
});
}
const categoryMap = new Map(categories.map(c => [c.id, c]));
const byCategory: CategoryStat[] = Array.from(categoryMap.values())
.map(c => ({
id: c.id,
name: c.name,
color: c.color,
count: categoryCounts.get(c.id) ?? 0,
}))
.filter(c => c.count > 0)
.sort((a, b) => b.count - a.count);
const priorityOrder = ['high', 'medium', 'low', 'none'];
const byPriority: PriorityStat[] = priorityOrder
.map(p => ({
priority: p,
label: PRIORITY_LABELS[p],
count: priorityCounts.get(p) ?? 0,
}))
.filter(p => p.count > 0);
let streak = 0;
const todayCount = dayBucket.get(todayStart) ?? 0;
let cursor = todayCount > 0 ? todayStart : todayStart - DAY_MS;
while ((dayBucket.get(cursor) ?? 0) > 0 && streak < 365) {
streak++;
cursor -= DAY_MS;
}
if (mounted) {
setStats({
loading: false,
totalCompleted,
completedLast7,
completedLast30,
activeCount,
overdueCount,
completionRate:
totalCompleted > 0
? Math.round((totalCompleted / (totalCompleted + activeCount)) * 100)
: 0,
daily,
byCategory,
byPriority,
currentStreak: streak,
});
}
})
.catch(() => {
if (mounted) setStats(s => ({ ...s, loading: false }));
});
};
compute();
const subscription = collections.tasks
.query()
.observe()
.subscribe({
next: () => {
compute();
},
error: () => {},
});
return () => {
mounted = false;
subscription.unsubscribe();
};
}, [collections, isReady]);
return stats;
}