Files
kandev/apps/web/app/stats/stats-utils.ts
T
2026-08-10 22:37:07 +01:00

101 lines
3.5 KiB
TypeScript

import type { StatsRange } from "@/lib/api/domains/stats-api";
import type { StatsResponse } from "@/lib/types/http";
export type RangeKey = StatsRange;
export const RANGE_KEYS = ["week", "month", "all"] as const satisfies readonly RangeKey[];
export const DEFAULT_RANGE: RangeKey = "month";
export function isRangeKey(value: string | null | undefined): value is RangeKey {
return value === "week" || value === "month" || value === "all";
}
export function getRangeLabel(range: RangeKey): string {
switch (range) {
case "week":
return "Last Week";
case "month":
return "Last Month";
case "all":
return "All Time";
}
}
export function formatDuration(ms: number): string {
if (ms === 0) return "-";
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) return `${hours}h ${minutes % 60}m`;
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
return `${seconds}s`;
}
export function getSubtitle(global: StatsResponse["global"] | null, hasError: boolean): string {
if (global) {
return `${global.total_tasks} tasks · ${global.total_sessions} sessions · ${formatDuration(global.total_duration_ms)}`;
}
return hasError ? "Failed to load stats" : "Loading stats…";
}
export type StatsState = {
stats: StatsResponse | null;
error: string | null;
};
export type StatsAction =
| { type: "fetch" }
| { type: "success"; stats: StatsResponse }
| { type: "failure"; error: string };
export function statsReducer(state: StatsState, action: StatsAction): StatsState {
switch (action.type) {
case "fetch":
return { stats: null, error: null };
case "success":
return { stats: action.stats, error: null };
case "failure":
return { stats: null, error: action.error };
}
}
export type PanelState =
| { kind: "loading" }
| { kind: "error"; message: string }
| { kind: "ready"; stats: StatsResponse };
export function toPanelState(stats: StatsResponse | null, error: string | null): PanelState {
if (error) return { kind: "error", message: error };
if (stats) return { kind: "ready", stats };
return { kind: "loading" };
}
export function buildStatsSummary(
resolvedStats: StatsResponse,
rangeLabel: string,
completedInRange: number,
): string {
const { global, repository_stats, git_stats } = resolvedStats;
const completion =
global.total_tasks > 0
? `${Math.round((global.completed_tasks / global.total_tasks) * 100)}%`
: "-";
const topRepo = repository_stats
.filter((r) => r.total_tasks > 0)
.sort((a, b) => b.total_tasks - a.total_tasks)[0];
const topRepoLabel = topRepo ? `${topRepo.repository_name} (${topRepo.total_tasks} tasks)` : "-";
const hasGitStats =
git_stats && (git_stats.total_commits > 0 || git_stats.total_files_changed > 0);
const gitLine = hasGitStats
? `${git_stats.total_commits} commits, +${git_stats.total_insertions.toLocaleString()}/-${git_stats.total_deletions.toLocaleString()}`
: "no git activity";
return [
`*Kandev Stats - ${rangeLabel}*`,
`- Tasks: ${global.total_tasks} total (${global.completed_tasks} done, ${global.in_progress_tasks} in progress) · ${completion} completion`,
`- Completed (${rangeLabel}): ${completedInRange}`,
`- Time: ${formatDuration(global.total_duration_ms)} total · ${formatDuration(global.avg_duration_ms_per_task)} avg/task`,
`- Repos: ${repository_stats.length} tracked · Top repo: ${topRepoLabel}`,
`- Git: ${gitLine}`,
].join("\n");
}