feat: Puente Mayúscula-Minúscula + TTS SpeechSynthesis + E2E fixes
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { prisma } from "@/lib/db"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user