Files
offlineacademy/app/api/progress/route.ts
T
2026-06-26 02:51:08 +00:00

320 lines
9.3 KiB
TypeScript
Executable File

import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { z } from 'zod'
const progressSchema = z.object({
lessonId: z.string(),
courseId: z.string(),
moduleId: z.string(),
position: z.number().min(0),
completed: z.boolean().default(false),
})
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const parsed = progressSchema.safeParse(body)
if (!parsed.success) {
return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 400 })
}
const { lessonId, courseId, moduleId, position, completed } = parsed.data
const progress = await prisma.progress.upsert({
where: {
userId_lessonId: {
userId: 'local-user',
lessonId,
},
},
update: {
position,
completed,
lastWatched: new Date(),
courseId,
moduleId,
},
create: {
userId: 'local-user',
lessonId,
courseId,
moduleId,
position,
completed,
lastWatched: new Date(),
},
})
// Also maintain a course-level summary row so course pages can resume correctly.
// This is stored separately from lesson progress using null lessonId/moduleId.
const totalLessons = await prisma.lesson.count({
where: { module: { courseId } },
})
const completedLessons = await prisma.lesson.count({
where: {
module: { courseId },
progress: { some: { userId: 'local-user', completed: true } },
},
})
if (totalLessons > 0) {
const courseCompleted = completedLessons === totalLessons
const courseProgressData = {
userId: 'local-user',
courseId,
lessonId: null,
moduleId: null,
position,
completed: courseCompleted,
lastWatched: new Date(),
}
const existingCourseProgress = await prisma.progress.findFirst({
where: {
userId: 'local-user',
courseId,
lessonId: null,
moduleId: null,
},
})
if (existingCourseProgress) {
await prisma.progress.update({
where: { id: existingCourseProgress.id },
data: courseProgressData,
})
} else {
await prisma.progress.create({
data: courseProgressData,
})
}
}
return NextResponse.json({ success: true, progress })
} catch (error) {
console.error('Progress save error:', error)
return NextResponse.json({ error: 'Failed to save progress' }, { status: 500 })
}
}
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const lessonId = searchParams.get('lessonId')
const courseId = searchParams.get('courseId')
const type = searchParams.get('type') // 'continue' or 'completed'
if (type === 'continue') {
// Get in-progress lessons
const progressRecords = await prisma.progress.findMany({
where: {
userId: 'local-user',
lessonId: { not: null },
completed: false,
lesson: { module: { course: { hidden: false } } },
},
orderBy: { lastWatched: 'desc' },
take: 20,
include: {
lesson: {
include: {
module: {
include: {
course: true,
},
},
},
},
},
})
const items = progressRecords
.filter(p => p.lesson)
.map(p => ({
progress: {
...p,
lastWatched: p.lastWatched.toISOString(),
},
lesson: p.lesson!,
course: p.lesson!.module.course,
module: p.lesson!.module,
}))
return NextResponse.json({ items })
}
if (type === 'completed') {
// Get completed courses
const completedProgress = await prisma.progress.findMany({
where: {
userId: 'local-user',
lessonId: null,
moduleId: null,
completed: true,
course: { hidden: false },
},
include: {
course: true,
},
})
const items = completedProgress.map(p => p.course)
return NextResponse.json({ items })
}
if (type === 'analytics') {
// Get analytics data for dashboard
const allProgress = await prisma.progress.findMany({
where: {
userId: 'local-user',
course: { hidden: false },
},
})
const completedLessons = allProgress.filter(p => p.lessonId && p.completed)
const inProgressLessons = allProgress.filter(p => p.lessonId && !p.completed && p.position > 0)
const notStartedCount = await prisma.lesson.count({
where: {
module: { course: { hidden: false } },
progress: { none: { userId: 'local-user' } },
},
})
const totalCourses = await prisma.course.count({ where: { hidden: false } })
const completedCourses = await prisma.progress.count({
where: {
userId: 'local-user',
lessonId: null,
moduleId: null,
completed: true,
course: { hidden: false },
},
})
const inProgressCourses = await prisma.progress.count({
where: {
userId: 'local-user',
lessonId: null,
moduleId: null,
completed: false,
course: { hidden: false },
},
})
// Calculate total watched time (in seconds)
const totalWatchedSeconds = allProgress.reduce((sum, p) => sum + (p.position || 0), 0)
// Get weekly activity (last 7 days)
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
const recentProgress = allProgress.filter(p => new Date(p.lastWatched) >= sevenDaysAgo)
const weeklyWatchedSeconds = recentProgress.reduce((sum, p) => sum + (p.position || 0), 0)
const weeklyCompleted = recentProgress.filter(p => p.lessonId && p.completed).length
// Lessons by type
const lessonsByType = await prisma.lesson.groupBy({
by: ['type'],
_count: true,
where: { module: { course: { hidden: false } } },
})
// Completed lessons by type
const completedByType: Record<string, number> = {}
for (const p of completedLessons) {
if (!p.lessonId) continue
const lesson = await prisma.lesson.findUnique({ where: { id: p.lessonId }, select: { type: true } })
if (lesson) {
completedByType[lesson.type] = (completedByType[lesson.type] || 0) + 1
}
}
return NextResponse.json({
totalCourses,
completedCourses,
inProgressCourses,
totalLessons: completedLessons.length + inProgressLessons.length + notStartedCount,
completedLessons: completedLessons.length,
inProgressLessons: inProgressLessons.length,
notStartedLessons: notStartedCount,
totalWatchedHours: Math.round(totalWatchedSeconds / 3600 * 10) / 10,
totalWatchedMinutes: Math.round(totalWatchedSeconds / 60),
weeklyWatchedHours: Math.round(weeklyWatchedSeconds / 3600 * 10) / 10,
weeklyWatchedMinutes: Math.round(weeklyWatchedSeconds / 60),
weeklyCompletedLessons: weeklyCompleted,
lessonsByType,
completedByType,
completionRate: totalCourses > 0 ? Math.round((completedCourses / totalCourses) * 100) : 0,
lessonCompletionRate: (completedLessons.length + inProgressLessons.length + notStartedCount) > 0
? Math.round((completedLessons.length / (completedLessons.length + inProgressLessons.length + notStartedCount)) * 100)
: 0,
})
}
if (lessonId) {
const progress = await prisma.progress.findUnique({
where: {
userId_lessonId: {
userId: 'local-user',
lessonId,
},
},
})
if (progress) {
return NextResponse.json({
progress: {
...progress,
lastWatched: progress.lastWatched.toISOString()
}
})
}
return NextResponse.json({ progress: null })
}
if (courseId) {
const progress = await prisma.progress.findFirst({
where: {
userId: 'local-user',
courseId,
lessonId: null,
moduleId: null,
course: { hidden: false },
},
})
if (progress) {
return NextResponse.json({
progress: {
...progress,
lastWatched: progress.lastWatched.toISOString(),
},
})
}
const latestLessonProgress = await prisma.progress.findFirst({
where: {
userId: 'local-user',
courseId,
lessonId: { not: null },
course: { hidden: false },
},
orderBy: { lastWatched: 'desc' },
})
if (latestLessonProgress) {
return NextResponse.json({
progress: {
...latestLessonProgress,
lastWatched: latestLessonProgress.lastWatched.toISOString(),
},
})
}
return NextResponse.json({ progress: null })
}
return NextResponse.json({ error: 'lessonId, courseId or type required' }, { status: 400 })
} catch (error) {
console.error('Progress fetch error:', error)
return NextResponse.json({ error: 'Failed to fetch progress' }, { status: 500 })
}
}