"use server"; import { db } from "@/lib/prisma"; import { requireTeacher } from "@/lib/auth/requireTeacher"; import { z } from "zod"; import { revalidatePath } from "next/cache"; import { ContentStatus, Prisma, ProficiencyLevel } from "@prisma/client"; // --- VALIDATION SCHEMAS (Zod) --- const createCourseSchema = z.object({ title: z.string().min(3, "El título debe tener al menos 3 caracteres"), }); // --- ACTIONS --- /** * 1. LIST COURSES * Used by: app/(protected)/teacher/page.tsx (Dashboard) */ export async function getTeacherCourses() { const user = await requireTeacher(); if (!user) { return { success: false, error: "No autorizado" }; } try { const courses = await db.course.findMany({ where: { authorId: user.id, }, orderBy: { updatedAt: "desc", }, select: { id: true, title: true, slug: true, price: true, status: true, level: true, _count: { select: { modules: true, enrollments: true, } } } }); return { success: true, data: courses }; } catch (error) { console.error("Error fetching courses:", error); return { success: false, error: "Error al cargar los cursos" }; } } /** * 2. CREATE COURSE (Draft) */ export async function createCourse(formData: FormData) { const user = await requireTeacher(); if (!user) { return { success: false, error: "No autorizado" }; } // 1. Extract & Validate const rawTitle = formData.get("title"); const validated = createCourseSchema.safeParse({ title: rawTitle }); if (!validated.success) { return { success: false, error: validated.error.flatten().fieldErrors.title?.[0] || "Error en el título" }; } const title = validated.data.title; // 2. Generate basic slug const slug = `${title.toLowerCase().replace(/ /g, "-").replace(/[^\w-]/g, "")}-${Math.floor(Math.random() * 10000)}`; try { // 3. Create DB Record const course = await db.course.create({ data: { title: title, slug: slug, authorId: user.id, description: "Descripción pendiente...", status: "DRAFT", }, }); // 4. Revalidate & Return revalidatePath("/teacher/courses"); return { success: true, data: course }; } catch (error) { console.error("Create course error:", error); return { success: false, error: "No se pudo crear el curso." }; } } /** * 3. DELETE COURSE */ export async function deleteCourse(courseId: string) { const user = await requireTeacher(); if (!user) return { success: false, error: "No autorizado" }; try { const course = await db.course.findUnique({ where: { id: courseId }, select: { authorId: true } }); if (!course || course.authorId !== user.id) { return { success: false, error: "No tienes permiso para eliminar este curso." }; } await db.course.delete({ where: { id: courseId }, }); revalidatePath("/teacher/courses"); return { success: true, data: "Curso eliminado" }; } catch { return { success: false, error: "Error al eliminar el curso" }; } } export async function updateCourse(courseId: string, courseSlug: string, formData: FormData) { const user = await requireTeacher(); if (!user) return { success: false, error: "Unauthorized" }; try { const title = formData.get("title") as string; const summary = formData.get("summary") as string; // Direct cast is okay if you trust the select/input values const level = formData.get("level") as ProficiencyLevel; const status = formData.get("status") as ContentStatus; const price = parseFloat(formData.get("price") as string) || 0; await db.course.update({ where: { id: courseId, authorId: user.id }, data: { title, description: summary, level, status, price, }, }); // Revalidate both the list and the editor (edit route uses slug, not id) revalidatePath("/teacher/courses"); revalidatePath(`/teacher/courses/${courseSlug}/edit`, "page"); return { success: true }; } catch { return { success: false, error: "Failed to update" }; } } export async function updateLesson(lessonId: string, data: { title?: string; description?: Prisma.InputJsonValue | null; videoUrl?: string; isPublished?: boolean; // optional: for later }) { const user = await requireTeacher(); if (!user) return { success: false, error: "Unauthorized" }; try { const updateData: Prisma.LessonUpdateInput = { updatedAt: new Date() }; if (data.title !== undefined) updateData.title = data.title; if (data.description !== undefined) updateData.description = data.description === null ? Prisma.JsonNull : data.description; if (data.videoUrl !== undefined) updateData.videoUrl = data.videoUrl; await db.lesson.update({ where: { id: lessonId }, data: updateData, }); // 2. Revalidate to show changes immediately revalidatePath("/teacher/courses"); return { success: true }; } catch (error) { console.error("Update Lesson Error:", error); return { success: false, error: "Failed to update lesson" }; } } export async function createModule(courseId: string) { const user = await requireTeacher(); if (!user) return { success: false, error: "Unauthorized" }; try { // Find the highest orderIndex so we can put the new one at the end const lastModule = await db.module.findFirst({ where: { courseId }, orderBy: { orderIndex: "desc" }, }); const newOrder = lastModule ? lastModule.orderIndex + 1 : 0; await db.module.create({ data: { courseId, title: "Nuevo Módulo", // Default title orderIndex: newOrder, }, }); revalidatePath(`/teacher/courses/${courseId}/edit`, "page"); return { success: true }; } catch (error) { console.error("Create Module Error:", error); return { success: false, error: "Failed to create module" }; } } // 2. CREATE LESSON export async function createLesson(moduleId: string) { const user = await requireTeacher(); if (!user) return { success: false, error: "Unauthorized" }; try { // Find the highest orderIndex for lessons in this module const lastLesson = await db.lesson.findFirst({ where: { moduleId }, orderBy: { orderIndex: "desc" }, }); const newOrder = lastLesson ? lastLesson.orderIndex + 1 : 0; // Create the lesson with default values const lesson = await db.lesson.create({ data: { moduleId, title: "Nueva Lección", orderIndex: newOrder, estimatedDuration: 0, version: 1, // The type field is required in your schema (based on previous context) // If you have a 'type' enum, set a default like 'VIDEO' or 'TEXT' // type: "VIDEO", }, }); revalidatePath(`/teacher/courses/${moduleId}/edit`, "page"); return { success: true, lessonId: lesson.id }; } catch (error) { console.error("Create Lesson Error:", error); return { success: false, error: "Failed to create lesson" }; } } /** * DELETE MODULE * Also deletes all lessons inside it due to Prisma cascade or manual cleanup */ export async function deleteModule(moduleId: string) { const user = await requireTeacher(); if (!user) return { success: false, error: "Unauthorized" }; try { await db.module.delete({ where: { id: moduleId, course: { authorId: user.id } // Security: ownership check }, }); revalidatePath("/teacher/courses"); return { success: true }; } catch { return { success: false, error: "No se pudo eliminar el módulo" }; } } /** * DELETE LESSON */ export async function deleteLesson(lessonId: string) { const user = await requireTeacher(); if (!user) return { success: false, error: "Unauthorized" }; try { await db.lesson.delete({ where: { id: lessonId, module: { course: { authorId: user.id } } // Deep ownership check }, }); revalidatePath("/teacher/courses"); return { success: true }; } catch { return { success: false, error: "No se pudo eliminar la lección" }; } } export async function reorderModules(moduleId: string, direction: 'up' | 'down') { const user = await requireTeacher(); if (!user) return { success: false, error: "Unauthorized" }; const currentModule = await db.module.findUnique({ where: { id: moduleId }, include: { course: true } }); if (!currentModule || currentModule.course.authorId !== user.id) { return { success: false, error: "Module not found" }; } const siblingModule = await db.module.findFirst({ where: { courseId: currentModule.courseId, orderIndex: direction === 'up' ? { lt: currentModule.orderIndex } : { gt: currentModule.orderIndex } }, orderBy: { orderIndex: direction === 'up' ? 'desc' : 'asc' } }); if (!siblingModule) return { success: true }; // Already at the top/bottom // Swap orderIndex values await db.$transaction([ db.module.update({ where: { id: currentModule.id }, data: { orderIndex: siblingModule.orderIndex } }), db.module.update({ where: { id: siblingModule.id }, data: { orderIndex: currentModule.orderIndex } }) ]); revalidatePath(`/teacher/courses/${currentModule.course.slug}/edit`, "page"); return { success: true }; } /** * Reorder lessons within the same module (swap orderIndex with sibling) */ export async function reorderLessons(lessonId: string, direction: "up" | "down") { const user = await requireTeacher(); if (!user) return { success: false, error: "Unauthorized" }; const currentLesson = await db.lesson.findUnique({ where: { id: lessonId }, include: { module: { include: { course: true } } }, }); if (!currentLesson || currentLesson.module.course.authorId !== user.id) { return { success: false, error: "Lesson not found" }; } const siblingLesson = await db.lesson.findFirst({ where: { moduleId: currentLesson.moduleId, orderIndex: direction === "up" ? { lt: currentLesson.orderIndex } : { gt: currentLesson.orderIndex }, }, orderBy: { orderIndex: direction === "up" ? "desc" : "asc" }, }); if (!siblingLesson) return { success: true }; // Already at top/bottom await db.$transaction([ db.lesson.update({ where: { id: currentLesson.id }, data: { orderIndex: siblingLesson.orderIndex }, }), db.lesson.update({ where: { id: siblingLesson.id }, data: { orderIndex: currentLesson.orderIndex }, }), ]); revalidatePath(`/teacher/courses/${currentLesson.module.course.slug}/edit`, "page"); return { success: true }; }