import { NextResponse } from 'next/server' import { auth } from '@/auth' import prisma from '@/lib/prisma' import { isCardMastered } from '@/lib/flashcards/sm2' export async function GET() { try { const session = await auth() if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const userId = session.user.id const now = new Date() const oneYearAgo = new Date() oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1) // Récupérer les reviews de la dernière année + toutes les cartes const [reviews, allCards] = await Promise.all([ prisma.flashcardReview.findMany({ where: { card: { deck: { userId } }, reviewedAt: { gte: oneYearAgo }, }, select: { reviewedAt: true, grade: true }, orderBy: { reviewedAt: 'asc' }, }), prisma.flashcard.findMany({ where: { deck: { userId } }, select: { id: true, interval: true, easinessFactor: true, front: true, deck: { select: { id: true, name: true } } }, }), ]) // --- Heatmap (90 derniers jours) --- const heatmapMap = new Map() for (const r of reviews) { const day = r.reviewedAt.toISOString().slice(0, 10) heatmapMap.set(day, (heatmapMap.get(day) || 0) + 1) } const heatmap = Array.from(heatmapMap.entries()) .map(([date, count]) => ({ date, count })) .sort((a, b) => a.date.localeCompare(b.date)) .slice(-90) // --- Taux de rétention global (cartes maîtrisées / total) --- const totalCards = allCards.length // Maîtrisée = interval >= 7 jours (cohérent avec deck-queries) const masteredCount = allCards.filter((c) => c.interval >= 7).length const retentionRate = totalCards > 0 ? Math.round((masteredCount / totalCards) * 100) : 0 // --- Rétention par semaine : vrai taux de succès (grade >= 3) sur les reviews réelles --- const weekMs = 7 * 24 * 60 * 60 * 1000 const retentionByWeek: { week: string; rate: number; total: number }[] = [] for (let i = 7; i >= 0; i--) { const weekEnd = new Date(now.getTime() - i * weekMs) const weekStart = new Date(weekEnd.getTime() - weekMs) const weekReviews = reviews.filter((r) => { const t = r.reviewedAt.getTime() return t >= weekStart.getTime() && t < weekEnd.getTime() }) const total = weekReviews.length const successful = weekReviews.filter((r) => r.grade >= 3).length // Si aucune review cette semaine, on laisse null pour distinguer "pas de données" vs "0%" const rate = total > 0 ? Math.round((successful / total) * 100) : -1 retentionByWeek.push({ week: weekStart.toISOString().slice(0, 10), rate, total, }) } // --- Streak (jours consécutifs avec au moins 1 review) --- const reviewDays = new Set(reviews.map((r) => r.reviewedAt.toISOString().slice(0, 10))) let streak = 0 for (let d = 0; d < 365; d++) { const day = new Date(now) day.setDate(day.getDate() - d) const key = day.toISOString().slice(0, 10) if (reviewDays.has(key)) { streak++ } else { // On tolère le jour actuel s'il n'a pas encore eu de review (on commence à hier) if (d === 0) continue break } } // --- Cartes difficiles (facteur d'aisance le plus bas) --- const difficultCards = [...allCards] .sort((a, b) => a.easinessFactor - b.easinessFactor) .slice(0, 10) .map((c) => ({ id: c.id, front: c.front.slice(0, 200), easinessFactor: c.easinessFactor, deckId: c.deck.id, deckName: c.deck.name, })) return NextResponse.json({ heatmap, retentionRate, retentionByWeek, difficultCards, totalReviews: reviews.length, streak, totalCards, masteredCount, }) } catch (error) { console.error('[flashcards/stats]', error) return NextResponse.json({ error: 'Failed to load stats' }, { status: 500 }) } }