import "server-only"; import { Prisma } from "@prisma/client"; import { prisma } from "@/lib/prisma"; export type DiagnosticModuleProgress = { key: string; name: string; completion: number; answeredQuestions: number; totalQuestions: number; status: "Completado" | "En curso" | "Pendiente"; resumeQuestionIndex: number; }; type DiagnosticQuestion = { id: string; prompt: string; helpText: string | null; sortOrder: number; options: { id: string; label: string; sortOrder: number; }[]; selectedAnswerOptionId: string | null; evidence: { notes: string; links: string[]; } | null; }; function parseResponseEvidence(rawValue: unknown): { notes: string; links: string[] } | null { if (!rawValue || typeof rawValue !== "object" || Array.isArray(rawValue)) { return null; } const value = rawValue as Record; const notes = typeof value.notes === "string" ? value.notes.trim() : ""; const links = Array.isArray(value.links) ? value.links .filter((entry): entry is string => typeof entry === "string") .map((entry) => entry.trim()) .filter((entry) => entry.length > 0) : []; if (!notes && links.length === 0) { return null; } return { notes, links, }; } function isLegacyResponseEvidenceSchemaError(error: unknown) { return error instanceof Prisma.PrismaClientKnownRequestError && (error.code === "P2021" || error.code === "P2022"); } export async function getDiagnosticOverview(userId: string) { const modules = await prisma.diagnosticModule.findMany({ orderBy: [{ sortOrder: "asc" }, { name: "asc" }], include: { questions: { orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }], select: { id: true, }, }, }, }); const allQuestionIds = modules.flatMap((module) => module.questions.map((question) => question.id)); const responses = allQuestionIds.length ? await prisma.response.findMany({ where: { userId, questionId: { in: allQuestionIds, }, }, select: { questionId: true, }, }) : []; const answeredQuestionIdSet = new Set(responses.map((response) => response.questionId)); const modulesWithProgress: DiagnosticModuleProgress[] = modules.map((module) => { const questionIds = module.questions.map((question) => question.id); const totalQuestions = questionIds.length; const answeredQuestions = questionIds.reduce((total, questionId) => { return total + (answeredQuestionIdSet.has(questionId) ? 1 : 0); }, 0); const completion = totalQuestions > 0 ? Math.round((answeredQuestions / totalQuestions) * 100) : 0; let status: DiagnosticModuleProgress["status"] = "Pendiente"; if (completion >= 100 && totalQuestions > 0) { status = "Completado"; } else if (completion > 0) { status = "En curso"; } const firstUnansweredIndex = questionIds.findIndex((questionId) => !answeredQuestionIdSet.has(questionId)); const resumeQuestionIndex = firstUnansweredIndex >= 0 ? firstUnansweredIndex + 1 : Math.max(totalQuestions, 1); return { key: module.key, name: module.name, completion, answeredQuestions, totalQuestions, status, resumeQuestionIndex, }; }); const totalQuestions = modulesWithProgress.reduce((total, module) => total + module.totalQuestions, 0); const answeredQuestions = modulesWithProgress.reduce((total, module) => total + module.answeredQuestions, 0); const overallCompletion = totalQuestions > 0 ? Math.round((answeredQuestions / totalQuestions) * 100) : 0; const activeResumeModule = modulesWithProgress.find((module) => module.status === "En curso") ?? modulesWithProgress.find((module) => module.status === "Pendiente") ?? modulesWithProgress[0] ?? null; const resumeHref = activeResumeModule ? `/diagnostic/${activeResumeModule.key}?q=${activeResumeModule.resumeQuestionIndex}` : null; return { modules: modulesWithProgress, stats: { modules: modulesWithProgress.length, completedModules: modulesWithProgress.filter((module) => module.status === "Completado").length, overallCompletion, answeredQuestions, totalQuestions, }, resumeHref, }; } export async function getDiagnosticModuleQuestions(userId: string, moduleKey: string) { const moduleRecord = await prisma.diagnosticModule.findUnique({ where: { key: moduleKey }, select: { id: true, key: true, name: true, description: true, questions: { orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }], select: { id: true, prompt: true, helpText: true, sortOrder: true, answerOptions: { orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }], select: { id: true, label: true, sortOrder: true, }, }, }, }, }, }); if (!moduleRecord) { return null; } const questionIds = moduleRecord.questions.map((question) => question.id); let responses: { questionId: string; answerOptionId: string; evidence?: unknown; }[] = []; if (questionIds.length) { try { responses = await prisma.response.findMany({ where: { userId, questionId: { in: questionIds, }, }, select: { questionId: true, answerOptionId: true, evidence: true, }, }); } catch (error) { if (!isLegacyResponseEvidenceSchemaError(error)) { throw error; } responses = await prisma.response.findMany({ where: { userId, questionId: { in: questionIds, }, }, select: { questionId: true, answerOptionId: true, }, }); } } const answerByQuestionId = new Map(responses.map((response) => [response.questionId, response.answerOptionId])); const evidenceByQuestionId = new Map(responses.map((response) => [response.questionId, parseResponseEvidence(response.evidence)])); const questions: DiagnosticQuestion[] = moduleRecord.questions.map((question) => ({ id: question.id, prompt: question.prompt, helpText: question.helpText, sortOrder: question.sortOrder, options: question.answerOptions.map((option) => ({ id: option.id, label: option.label, sortOrder: option.sortOrder, })), selectedAnswerOptionId: answerByQuestionId.get(question.id) ?? null, evidence: evidenceByQuestionId.get(question.id) ?? null, })); const answeredCount = questions.reduce((total, question) => total + (question.selectedAnswerOptionId ? 1 : 0), 0); const totalQuestions = questions.length; const completion = totalQuestions > 0 ? Math.round((answeredCount / totalQuestions) * 100) : 0; const firstUnansweredQuestionIndex = questions.findIndex((question) => !question.selectedAnswerOptionId); return { module: { key: moduleRecord.key, name: moduleRecord.name, description: moduleRecord.description, }, questions, progress: { answeredCount, totalQuestions, completion, defaultQuestionIndex: firstUnansweredQuestionIndex >= 0 ? firstUnansweredQuestionIndex + 1 : Math.max(totalQuestions, 1), }, }; }