"use server"; import { prisma } from "@/lib/prisma"; import { auth } from "@/lib/auth"; import { headers } from "next/headers"; import { revalidatePath } from "next/cache"; import { sendHomeworkSubmittedEmail, sendCommentNotificationEmail } from "@/lib/email"; import { getSettings, parseNotificationEmails, asBool } from "@/lib/settings"; // ── Lesson Progress ─────────────────────────────────────────────────────────── export async function toggleLessonProgress(lessonId: string, slug: string) { const session = await auth.api.getSession({ headers: await headers() }); if (!session) throw new Error("Unauthorized"); const existing = await prisma.lessonProgress.findUnique({ where: { userId_lessonId: { userId: session.user.id, lessonId } }, }); if (existing) { await prisma.lessonProgress.delete({ where: { userId_lessonId: { userId: session.user.id, lessonId } }, }); } else { await prisma.lessonProgress.create({ data: { userId: session.user.id, lessonId }, }); } revalidatePath(`/courses/${slug}/lessons/${lessonId}`); revalidatePath(`/courses/${slug}`); revalidatePath("/dashboard"); } // ── Homework ────────────────────────────────────────────────────────────────── interface HomeworkFile { name: string; url: string; size: number; } export async function submitHomework( homeworkId: string, slug: string, lessonId: string, text: string, files: HomeworkFile[], audioUrl?: string | null ) { const session = await auth.api.getSession({ headers: await headers() }); if (!session) throw new Error("Unauthorized"); const existing = await prisma.homeworkSubmission.findFirst({ where: { homeworkId, userId: session.user.id }, include: { feedbacks: true }, }); if ((existing?.feedbacks && existing.feedbacks.length > 0) || existing?.status === "APPROVED") { throw new Error("Работа уже проверена"); } let submissionId: string; if (existing) { const updated = await prisma.homeworkSubmission.update({ where: { id: existing.id }, data: { text, files: files as object[], audioUrl: audioUrl ?? null, submittedAt: new Date() }, }); submissionId = updated.id; } else { const created = await prisma.homeworkSubmission.create({ data: { homeworkId, userId: session.user.id, text, files: files as object[], audioUrl: audioUrl ?? null }, }); submissionId = created.id; const [lesson, settings] = await Promise.all([ prisma.homework.findUnique({ where: { id: homeworkId }, include: { lesson: { select: { title: true } } }, }), getSettings(), ]); if (lesson && asBool(settings.notifyOnHomework)) { const configuredEmails = parseNotificationEmails(settings.notificationEmails); const recipients = configuredEmails.length > 0 ? configuredEmails.map((email) => ({ email, name: "" })) : await prisma.user.findMany({ where: { role: { in: ["admin", "curator"] } }, select: { email: true, name: true }, }); await Promise.all( recipients.map((r) => sendHomeworkSubmittedEmail(r.email, r.name, session.user.name, lesson.lesson.title, submissionId) ) ); } } // Отметить урок пройденным по факту сдачи ДЗ (идемпотентно) await prisma.lessonProgress.upsert({ where: { userId_lessonId: { userId: session.user.id, lessonId } }, create: { userId: session.user.id, lessonId }, update: {}, }); revalidatePath(`/courses/${slug}/lessons/${lessonId}`); revalidatePath(`/courses/${slug}`); revalidatePath("/dashboard"); } // ── Quiz ────────────────────────────────────────────────────────────────────── export async function submitQuizAttempt( quizId: string, lessonId: string, slug: string, answers: Record ) { const session = await auth.api.getSession({ headers: await headers() }); if (!session) throw new Error("Unauthorized"); const existing = await prisma.quizAttempt.findFirst({ where: { quizId, userId: session.user.id }, }); if (existing) return; await prisma.$transaction(async (tx) => { await tx.quizAttempt.create({ data: { userId: session.user.id, quizId, score: 0, answers: answers as object, }, }); await tx.lessonProgress.upsert({ where: { userId_lessonId: { userId: session.user.id, lessonId } }, create: { userId: session.user.id, lessonId }, update: {}, }); }); revalidatePath(`/courses/${slug}/lessons/${lessonId}`); revalidatePath(`/courses/${slug}`); revalidatePath("/dashboard"); } // ── Comments ────────────────────────────────────────────────────────────────── export async function addComment(lessonId: string, slug: string, text: string, parentId?: string) { const session = await auth.api.getSession({ headers: await headers() }); if (!session) throw new Error("Unauthorized"); const trimmed = text.trim(); if (!trimmed || trimmed.length > 2000) throw new Error("Invalid text"); const lesson = await prisma.lesson.findUnique({ where: { id: lessonId }, select: { title: true, module: { select: { course: { select: { id: true, slug: true } } } } }, }); if (!lesson) throw new Error("Lesson not found"); const isAdmin = session.user.role === "admin"; const isCurator = session.user.role === "curator"; if (!isAdmin && !isCurator) { const enrollment = await prisma.courseEnrollment.findUnique({ where: { userId_courseId: { userId: session.user.id, courseId: lesson.module.course.id, }, }, }); if (!enrollment) throw new Error("Not enrolled"); if (enrollment.expiresAt && enrollment.expiresAt < new Date()) throw new Error("Access expired"); } if (parentId) { if (!isAdmin && !isCurator) throw new Error("Forbidden"); const parent = await prisma.lessonComment.findUnique({ where: { id: parentId } }); if (!parent || parent.lessonId !== lessonId) throw new Error("Invalid parent"); } await prisma.lessonComment.create({ data: { lessonId, userId: session.user.id, text: trimmed, ...(parentId ? { parentId } : {}) }, }); // Уведомление админам/кураторам о новом комментарии студента (не о своих ответах) if (!isAdmin && !isCurator) { const settings = await getSettings(); if (asBool(settings.notifyOnComment)) { const configured = parseNotificationEmails(settings.notificationEmails); const recipients = configured.length > 0 ? configured.map((email) => ({ email, name: "" })) : await prisma.user.findMany({ where: { role: { in: ["admin", "curator"] } }, select: { email: true, name: true }, }); const lessonUrl = `${process.env.BETTER_AUTH_URL ?? "https://school.second-brain.ru"}/courses/${slug}/lessons/${lessonId}`; await Promise.all( recipients.map((r) => sendCommentNotificationEmail(r.email, r.name, session.user.name, lesson.title, trimmed, lessonUrl) ) ); } } revalidatePath(`/courses/${slug}/lessons/${lessonId}`); } export async function deleteComment(commentId: string, lessonId: string, slug: string) { const session = await auth.api.getSession({ headers: await headers() }); if (!session) throw new Error("Unauthorized"); const comment = await prisma.lessonComment.findUnique({ where: { id: commentId } }); if (!comment) throw new Error("Not found"); const canDelete = comment.userId === session.user.id || session.user.role === "curator" || session.user.role === "admin"; if (!canDelete) throw new Error("Forbidden"); await prisma.lessonComment.update({ where: { id: commentId }, data: { deleted: true }, }); revalidatePath(`/courses/${slug}/lessons/${lessonId}`); }