Files
edueasy/app/api/dashboard/summary/route.ts
T
renato97 411f235f75 feat: fase 5 — botones volver, seguridad APIs, infraestructura
- Botón 'Volver' en todas las sesiones de ejercicios (ExerciseSession)
- Botón 'Cambiar perfil' en las 3 home pages (Isabella/Francesca/Sebastián)
- Páginas inline de Isabella migradas a ExerciseSession (elimina código duplicado)
- APIs con try/catch: curriculum/next, dashboard/summary, fsrs/next, children
- Validación childId en curriculum/next (404 si no existe)
- /api/health endpoint para Docker healthcheck
- .env.example, .dockerignore, README.md creados
- docker-compose.yml con healthcheck en app service
- Test E2E: health endpoint + childId validation
- 62/62 tests pasan, TS exit 0
2026-07-26 02:00:27 -03:00

165 lines
4.8 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server"
import { prisma } from "@/lib/db"
export async function GET(request: NextRequest) {
try {
const childId = request.nextUrl.searchParams.get("childId")
if (!childId) {
return NextResponse.json({ error: "childId required" }, { status: 400 })
}
const today = new Date()
today.setHours(0, 0, 0, 0)
const [totalSessions, todaySessions, milestones, fsrsCards, lastSession, weeklySessions, milestoneRecords, curriculumProgress, errorPatterns, allAttempts] =
await Promise.all([
prisma.sessionLog.count({ where: { childId } }),
prisma.sessionLog.count({
where: { childId, startedAt: { gte: today } },
}),
prisma.milestone.count({ where: { childId } }),
prisma.fsrsCard.findMany({ where: { childId } }),
prisma.sessionLog.findFirst({
where: { childId },
orderBy: { startedAt: "desc" },
include: { skillAttempts: true },
}),
prisma.sessionLog.findMany({
where: { childId, startedAt: { gte: new Date(Date.now() - 7 * 86400000) } },
orderBy: { startedAt: "asc" },
select: { startedAt: true, durationSec: true },
}),
prisma.milestone.findMany({
where: { childId },
orderBy: { reachedAt: "desc" },
select: { skillCode: true, reachedAt: true },
}),
prisma.curriculumProgress.findMany({
where: { childId },
orderBy: { stage: "asc" },
}),
prisma.skillAttempt.findMany({
where: {
errorType: { not: null },
sessionLog: { childId },
},
select: { errorType: true },
}),
prisma.skillAttempt.findMany({
where: {
correct: false,
exerciseId: { not: null },
sessionLog: { childId },
},
select: { exerciseId: true, errorType: true },
}),
])
const todayMinutes = todaySessions > 0
? await prisma.sessionLog.aggregate({
where: { childId, startedAt: { gte: today } },
_sum: { durationSec: true },
}).then((r) => Math.round((r._sum.durationSec ?? 0) / 60))
: 0
const totalSkills = fsrsCards.length
const masteredSkills = fsrsCards.filter((c) => c.reps >= 5 && c.stability > 30).length
const masteryPercent = totalSkills > 0 ? Math.round((masteredSkills / totalSkills) * 100) : 0
const weeklyData = weeklySessions.map((s) => ({
date: s.startedAt.toISOString().slice(0, 10),
minutes: Math.round((s.durationSec ?? 0) / 60),
}))
const skillBreakdown = fsrsCards.map((c) => ({
skillCode: c.skillCode,
stability: c.stability,
difficulty: c.difficulty,
reps: c.reps,
due: c.dueAt.toISOString(),
}))
const streak = await calculateStreak(childId)
const totalErrors = allAttempts.length
const errorTypeCount = errorPatterns.reduce<Record<string, number>>((acc, e) => {
const key = e.errorType || "sin-clasificar"
acc[key] = (acc[key] || 0) + 1
return acc
}, {})
const stageProgress = curriculumProgress.map((cp) => ({
topic: cp.topic,
stage: cp.stage,
completedAt: cp.completedAt?.toISOString() || null,
}))
return NextResponse.json({
totalSessions,
todaySessions,
todayMinutes,
masteryPercent,
masteredSkills,
totalSkills,
milestones,
streak,
weeklyData,
skillBreakdown,
milestoneData: milestoneRecords,
stageProgress,
errorTypeCount,
totalErrors,
lastSession: lastSession
? {
startedAt: lastSession.startedAt.toISOString(),
durationSec: lastSession.durationSec,
skillsPracticed: lastSession.skillsPracticed,
correctCount: lastSession.skillAttempts.filter((a) => a.correct).length,
totalAttempts: lastSession.skillAttempts.length,
}
: null,
})
} catch (error) {
console.error("[dashboard/summary] error:", error)
return NextResponse.json({ error: "Internal error" }, { status: 500 })
}
}
async function calculateStreak(childId: string): Promise<number> {
const logs = await prisma.sessionLog.findMany({
where: { childId },
orderBy: { startedAt: "desc" },
select: { startedAt: true },
})
if (logs.length === 0) return 0
const days = [
...new Set(logs.map((l) => l.startedAt.toISOString().slice(0, 10))),
].sort()
.reverse()
let streak = 1
const today = new Date().toISOString().slice(0, 10)
if (days[0] !== today && days[0] !== getYesterday()) return 0
for (let i = 1; i < days.length; i++) {
const prev = new Date(days[i - 1])
const curr = new Date(days[i])
const diff = (prev.getTime() - curr.getTime()) / 86400000
if (Math.round(diff) === 1) {
streak++
} else {
break
}
}
return streak
}
function getYesterday(): string {
const d = new Date()
d.setDate(d.getDate() - 1)
return d.toISOString().slice(0, 10)
}