import { NextRequest, NextResponse } from "next/server"; import { auth } from "@clerk/nextjs/server"; import Database from "better-sqlite3"; import log from "@/lib/logger"; const db = new Database("./data/fitai.db"); interface WeeklyStats { week: string; // ISO week start date checkIns: number; goalsCompleted: number; avgProgress: number; } interface GoalStats { total: number; active: number; completed: number; avgProgress: number; byType: Record; } interface AttendanceStats { totalCheckIns: number; currentStreak: number; longestStreak: number; thisWeek: number; thisMonth: number; recentCheckIns: Array<{ id: string; checkInTime: string; // ISO string checkOutTime: string | null; type: string; duration?: number; // in minutes }>; } interface DashboardStatistics { goals: GoalStats; attendance: AttendanceStats; weeklyTrend: WeeklyStats[]; } // GET - Fetch dashboard statistics for authenticated user export async function GET(request: NextRequest) { try { // Log the request details for debugging log.debug("Statistics endpoint called", { url: request.url, hasAuthHeader: !!request.headers.get("authorization"), authHeaderPreview: request.headers.get("authorization")?.substring(0, 30), }); const { userId: authenticatedUserId } = await auth(); log.debug("Auth result", { authenticatedUserId, isAuthenticated: !!authenticatedUserId, }); if (!authenticatedUserId) { log.error("Statistics GET - authentication failed", { hasAuthHeader: !!request.headers.get("authorization"), url: request.url, }); return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } // Get target user ID from query params, default to authenticated user const { searchParams } = new URL(request.url); const userId = searchParams.get("userId") || authenticatedUserId; // Users can only access their own statistics unless they're staff // (we could add role checking here if needed) log.debug("Fetching statistics for user", { authenticatedUserId, targetUserId: userId, }); // Get goal statistics const goals = db .prepare( `SELECT goal_type as goalType, status, progress, target_value as targetValue, current_value as currentValue FROM fitness_goals WHERE user_id = ?`, ) .all(userId) as Array<{ goalType: string; status: string; progress: number; targetValue: number | null; currentValue: number | null; }>; const activeGoals = goals.filter((g) => g.status === "active"); const completedGoals = goals.filter((g) => g.status === "completed"); const goalsByType: Record = {}; goals.forEach((goal) => { goalsByType[goal.goalType] = (goalsByType[goal.goalType] || 0) + 1; }); const avgProgress = activeGoals.length > 0 ? activeGoals.reduce((sum, g) => sum + (g.progress || 0), 0) / activeGoals.length : 0; const goalStats: GoalStats = { total: goals.length, active: activeGoals.length, completed: completedGoals.length, avgProgress: Math.round(avgProgress), byType: goalsByType, }; // Get attendance statistics const attendanceRecords = db .prepare( `SELECT id, check_in_time as checkInTime, check_out_time as checkOutTime, type FROM attendance WHERE user_id = ? ORDER BY check_in_time DESC`, ) .all(userId) as Array<{ id: string; checkInTime: string | number; // Can be ISO string or Unix timestamp checkOutTime: string | number | null; type: string; }>; const totalCheckIns = attendanceRecords.length; // Get recent check-ins (last 10) const recentCheckIns = attendanceRecords.slice(0, 10).map((record) => { let duration: number | undefined; // Convert Unix timestamps to milliseconds for calculation const checkInMs = typeof record.checkInTime === "number" ? record.checkInTime * 1000 : new Date(record.checkInTime).getTime(); if (record.checkOutTime) { const checkOutMs = typeof record.checkOutTime === "number" ? record.checkOutTime * 1000 : new Date(record.checkOutTime).getTime(); duration = Math.round((checkOutMs - checkInMs) / (1000 * 60)); // minutes } // Return with ISO string timestamps for consistency return { id: record.id, checkInTime: new Date(checkInMs).toISOString(), checkOutTime: record.checkOutTime ? new Date( typeof record.checkOutTime === "number" ? record.checkOutTime * 1000 : record.checkOutTime, ).toISOString() : null, type: record.type, duration, }; }); // Calculate this week's check-ins const now = new Date(); const startOfWeek = new Date(now); startOfWeek.setDate(now.getDate() - now.getDay()); // Sunday startOfWeek.setHours(0, 0, 0, 0); const thisWeekCheckIns = attendanceRecords.filter((r) => { // checkInTime is Unix timestamp in seconds, convert to milliseconds const checkInDate = typeof r.checkInTime === "number" ? new Date(r.checkInTime * 1000) : new Date(r.checkInTime); return checkInDate >= startOfWeek; }).length; // Calculate this month's check-ins const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); const thisMonthCheckIns = attendanceRecords.filter((r) => { // checkInTime is Unix timestamp in seconds, convert to milliseconds const checkInDate = typeof r.checkInTime === "number" ? new Date(r.checkInTime * 1000) : new Date(r.checkInTime); return checkInDate >= startOfMonth; }).length; // Calculate current streak (consecutive days with check-ins) let currentStreak = 0; let longestStreak = 0; let tempStreak = 0; let lastDate: Date | null = null; const sortedRecords = [...attendanceRecords].sort((a, b) => { const dateA = typeof a.checkInTime === "number" ? new Date(a.checkInTime * 1000) : new Date(a.checkInTime); const dateB = typeof b.checkInTime === "number" ? new Date(b.checkInTime * 1000) : new Date(b.checkInTime); return dateB.getTime() - dateA.getTime(); }); const uniqueDays = new Set(); sortedRecords.forEach((record) => { const date = typeof record.checkInTime === "number" ? new Date(record.checkInTime * 1000) : new Date(record.checkInTime); const dateStr = date.toISOString().split("T")[0]; uniqueDays.add(dateStr); }); const sortedDays = Array.from(uniqueDays).sort().reverse(); for (let i = 0; i < sortedDays.length; i++) { const currentDate = new Date(sortedDays[i]); if (lastDate === null) { tempStreak = 1; currentStreak = 1; } else { const dayDiff = Math.floor( (lastDate.getTime() - currentDate.getTime()) / (1000 * 60 * 60 * 24), ); if (dayDiff === 1) { tempStreak++; if (i === 0 || currentStreak > 0) { currentStreak = tempStreak; } } else { if (currentStreak === 0) { currentStreak = 0; } tempStreak = 1; } } longestStreak = Math.max(longestStreak, tempStreak); lastDate = currentDate; } // Check if streak is still active (last check-in was today or yesterday) if (sortedDays.length > 0) { const lastCheckIn = new Date(sortedDays[0]); const today = new Date(); today.setHours(0, 0, 0, 0); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); lastCheckIn.setHours(0, 0, 0, 0); if ( lastCheckIn.getTime() !== today.getTime() && lastCheckIn.getTime() !== yesterday.getTime() ) { currentStreak = 0; } } const attendanceStats: AttendanceStats = { totalCheckIns, currentStreak, longestStreak, thisWeek: thisWeekCheckIns, thisMonth: thisMonthCheckIns, recentCheckIns, }; // Calculate weekly trend (last 8 weeks) const weeklyTrend: WeeklyStats[] = []; for (let i = 7; i >= 0; i--) { const weekStart = new Date(now); weekStart.setDate(now.getDate() - now.getDay() - i * 7); weekStart.setHours(0, 0, 0, 0); const weekEnd = new Date(weekStart); weekEnd.setDate(weekStart.getDate() + 7); const weekCheckIns = attendanceRecords.filter((r) => { // checkInTime is Unix timestamp in seconds, convert to milliseconds const checkInDate = typeof r.checkInTime === "number" ? new Date(r.checkInTime * 1000) : new Date(r.checkInTime); return checkInDate >= weekStart && checkInDate < weekEnd; }).length; log.debug("Weekly calculation", { weekIndex: i, weekStart: weekStart.toISOString(), weekEnd: weekEnd.toISOString(), weekCheckIns, sampleRecord: attendanceRecords[0] ? { checkInTime: attendanceRecords[0].checkInTime, converted: typeof attendanceRecords[0].checkInTime === "number" ? new Date( attendanceRecords[0].checkInTime * 1000, ).toISOString() : attendanceRecords[0].checkInTime, } : null, }); const weekGoalsCompleted = db .prepare( `SELECT COUNT(*) as count FROM fitness_goals WHERE user_id = ? AND status = 'completed' AND completed_date >= ? AND completed_date < ?`, ) .get(userId, weekStart.toISOString(), weekEnd.toISOString()) as { count: number; }; const weekActiveGoals = db .prepare( `SELECT progress FROM fitness_goals WHERE user_id = ? AND status = 'active' AND created_at < ?`, ) .all(userId, weekEnd.toISOString()) as Array<{ progress: number }>; const weekAvgProgress = weekActiveGoals.length > 0 ? weekActiveGoals.reduce((sum, g) => sum + (g.progress || 0), 0) / weekActiveGoals.length : 0; weeklyTrend.push({ week: weekStart.toISOString().split("T")[0], checkIns: weekCheckIns, goalsCompleted: weekGoalsCompleted.count, avgProgress: Math.round(weekAvgProgress), }); } const statistics: DashboardStatistics = { goals: goalStats, attendance: attendanceStats, weeklyTrend, }; log.debug("Statistics calculated successfully", { userId, totalGoals: goalStats.total, totalCheckIns, weeklyTrendLength: weeklyTrend.length, weeklyTrendSample: weeklyTrend[0], }); // Return statistics in standardized format that matches mobile app expectations return NextResponse.json({ success: true, data: { statistics: { userId, goals: { totalGoals: goalStats.total, activeGoals: goalStats.active, completedGoals: goalStats.completed, averageProgress: goalStats.avgProgress, goalsByType: Object.entries(goalStats.byType).map( ([goalType, count]) => ({ goalType, count, }), ), }, attendance: { totalCheckIns: attendanceStats.totalCheckIns, currentStreak: attendanceStats.currentStreak, longestStreak: attendanceStats.longestStreak, checkInsThisWeek: attendanceStats.thisWeek, checkInsThisMonth: attendanceStats.thisMonth, recentCheckIns: attendanceStats.recentCheckIns, }, weeklyTrend: weeklyTrend.map((week) => ({ weekLabel: week.week, checkIns: week.checkIns, goalsCompleted: week.goalsCompleted, averageProgress: week.avgProgress, })), }, }, meta: { timestamp: new Date().toISOString(), requestId: `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, }, }); } catch (error) { log.error("Failed to fetch statistics", error); return NextResponse.json( { success: false, error: { message: "Failed to fetch statistics" }, meta: { timestamp: new Date().toISOString(), requestId: `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, }, }, { status: 500 }, ); } }