import { query } from './_generated/server.js'; import { requireOwner } from './lib.js'; import { dashboardRangeValidator } from './validators.js'; const rangeDays = { '7d': 7, '30d': 30, '90d': 90, '1y': 365 } as const; function dayKey(date: Date) { return date.toISOString().slice(0, 10); } function daysAgo(days: number) { const date = new Date(); date.setHours(0, 0, 0, 0); date.setDate(date.getDate() - days); return dayKey(date); } export const summary = query({ args: { range: dashboardRangeValidator }, handler: async (ctx, args) => { const owner = await requireOwner(ctx); const selectedDays = rangeDays[args.range]; const entries = await ctx.db .query('entries') .withIndex('by_owner_and_entryDate', (q) => q.eq('owner', owner)) .order('desc') .take(Math.min(800, selectedDays * 3)); const activeEntries = entries.filter((entry) => !entry.archived); const rangeStart = daysAgo(selectedDays - 1); const last7Start = daysAgo(6); const last30Start = daysAgo(29); const rangedEntries = activeEntries.filter((entry) => entry.entryDate >= rangeStart); const last7 = activeEntries.filter((entry) => entry.entryDate >= last7Start); const last30 = activeEntries.filter((entry) => entry.entryDate >= last30Start); const moodCounts: Record = {}; const dailyCounts = new Map(); const tagCounts: Record = {}; for (const entry of rangedEntries) { moodCounts[entry.mood] = (moodCounts[entry.mood] ?? 0) + 1; dailyCounts.set(entry.entryDate, (dailyCounts.get(entry.entryDate) ?? 0) + 1); for (const tagName of entry.tagNames) { tagCounts[tagName] = (tagCounts[tagName] ?? 0) + 1; } } let currentStreak = 0; for (let offset = 0; offset < 365; offset += 1) { const key = daysAgo(offset); if (!activeEntries.some((entry) => entry.entryDate === key)) break; currentStreak += 1; } const dailySeries = Array.from({ length: selectedDays }, (_, index) => { const date = daysAgo(selectedDays - 1 - index); return { date, count: dailyCounts.get(date) ?? 0 }; }); return { totalEntries: activeEntries.length, entriesThisWeek: last7.length, entriesThisMonth: last30.length, entriesInRange: rangedEntries.length, range: args.range, currentStreak, averageWords: activeEntries.length === 0 ? 0 : Math.round( activeEntries.reduce( (sum, entry) => sum + entry.plainText.split(/\s+/).filter(Boolean).length, 0 ) / activeEntries.length ), moodCounts, dailySeries, topTags: Object.entries(tagCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 8) .map(([name, count]) => ({ name, count })), insight: last7.length >= 5 ? 'You have a strong writing rhythm this week.' : last7.length >= 2 ? 'A few reflections this week are already building momentum.' : 'A short entry today is enough to restart the thread.' }; } });