"use server"; import { db } from "@/db"; import { unlink } from "fs/promises"; import path from "path"; import { questions } from "@/db/schema"; import type { QuestionType } from "@/db/schema"; import { eq } from "drizzle-orm"; import { revalidatePath } from "next/cache"; import { requireAdmin } from "@/lib/auth"; import { sanitizeHtml } from "@/lib/sanitize-html"; import { sse } from "@/lib/sse"; export async function createQuestion( formId: string, data: { type: QuestionType; label: string; required: boolean; displayOrder: number; options?: string[]; } ) { await requireAdmin(); await db.insert(questions).values({ formId, type: data.type, label: sanitizeHtml(data.label), required: data.required, displayOrder: data.displayOrder, options: data.options ?? [], allowOther: false, }); void sse.forms.pub("update", { formId, entity: "question", action: "created", }); revalidatePath("/form"); revalidatePath("/admin/form"); } export async function updateQuestion( id: string, formId: string, data: Partial<{ label: string; type: QuestionType; required: boolean; displayOrder: number; options: string[]; imageUrl: string | null; allowOther: boolean; }> ) { await requireAdmin(); if (data.imageUrl !== undefined) { const q = await db.query.questions.findFirst({ where: (q, { eq }) => eq(q.id, id), }); if (q?.imageUrl && q.imageUrl !== data.imageUrl) { await deleteImageFile(q.imageUrl); } } const nextData = { ...data, label: data.label === undefined ? undefined : sanitizeHtml(data.label), }; await db.update(questions).set(nextData).where(eq(questions.id, id)); void sse.forms.pub("update", { formId, entity: "question", action: "updated", }); revalidatePath("/form"); revalidatePath("/admin/form"); } export async function bulkUpdateQuestions( formId: string, updates: { id: string; label: string; type: QuestionType; required: boolean; displayOrder: number; options: string[]; allowOther: boolean; }[] ) { await requireAdmin(); // Use Promise.all to update all questions concurrently await Promise.all( updates.map((u) => db .update(questions) .set({ label: sanitizeHtml(u.label), type: u.type, required: u.required, displayOrder: u.displayOrder, options: u.options ?? [], allowOther: u.allowOther, }) .where(eq(questions.id, u.id)) ) ); void sse.forms.pub("update", { formId, entity: "question", action: "updated", }); revalidatePath("/form"); revalidatePath("/admin/form"); } async function deleteImageFile(imageUrl: string | null) { if (!imageUrl) return; try { const filename = imageUrl.split("/").pop(); if (filename) { const filepath = path.join(process.cwd(), "public", "form", filename); await unlink(filepath); } } catch (err) { console.error("Failed to delete image file", err); } } export async function deleteQuestion(id: string, formId: string) { await requireAdmin(); const q = await db.query.questions.findFirst({ where: (q, { eq }) => eq(q.id, id), }); if (q?.imageUrl) { await deleteImageFile(q.imageUrl); } await db.delete(questions).where(eq(questions.id, id)); void sse.forms.pub("update", { formId, entity: "question", action: "deleted", }); revalidatePath("/form"); revalidatePath(`/admin/form/${formId}`); } export async function reorderQuestions( formId: string, orderedIds: string[] ) { await requireAdmin(); await Promise.all( orderedIds.map((id, index) => db .update(questions) .set({ displayOrder: index }) .where(eq(questions.id, id)) ) ); void sse.forms.pub("update", { formId, entity: "question", action: "updated", }); revalidatePath("/form"); revalidatePath(`/admin/form/${formId}`); }