Files
ACVE/app/(protected)/teacher/actions.ts
2026-02-17 00:07:00 +00:00

386 lines
12 KiB
TypeScript

"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 };
}