import { and, eq, ilike, isNull, inArray, or, sql, is } from "drizzle-orm"; import { Hono } from "hono"; import { z } from "zod"; import db from "../../drizzle"; import { answers } from "../../drizzle/schema/answers"; import { options } from "../../drizzle/schema/options"; import { questions } from "../../drizzle/schema/questions"; import { subAspects } from "../../drizzle/schema/subAspects"; import { aspects } from "../../drizzle/schema/aspects"; import { assessments } from "../../drizzle/schema/assessments"; import HonoEnv from "../../types/HonoEnv"; import requestValidator from "../../utils/requestValidator"; import authInfo from "../../middlewares/authInfo"; import checkPermission from "../../middlewares/checkPermission"; import path from "path"; import fs from 'fs'; import { notFound } from "../../errors/DashboardError"; import { answerRevisions } from "../../drizzle/schema/answerRevisions"; export const answerFormSchema = z.object({ optionId: z.string().min(1), assessmentId: z.string().min(1), isFlagged: z.boolean().optional().default(false), filename: z.string().optional(), validationInformation: z.string().min(1), }); // optionFormSchema: untuk /submitOption export const optionFormSchema = z.object({ optionId: z.string().min(1), assessmentId: z.string().min(1), questionId: z.string().min(1), isFlagged: z.boolean().optional().default(false), filename: z.string().optional(), }); // newOptionFormSchema: untuk /updateOption export const newOptionFormSchema = z.object({ newOptionId: z.string().min(1), assessmentId: z.string().min(1), questionId: z.string().min(1), }); // validationFormSchema: untuk /submitValidation export const validationFormSchema = z.object({ assessmentId: z.string().min(1), questionId: z.string().min(1), validationInformation: z.string().min(1, "Validation information is required"), }); // newValidationFormSchema: untuk /updateValidation export const newValidationFormSchema = z.object({ assessmentId: z.string().min(1), questionId: z.string().min(1), newValidationInformation: z.string().min(1, "Validation information is required"), }); // validationFormSchema: untuk /submitValidation export const flagFormSchema = z.object({ assessmentId: z.string().min(1), questionId: z.string().min(1), isFlagged: z.boolean().optional().default(false), }); export const answerUpdateSchema = answerFormSchema.partial(); // Helper untuk menyimpan file async function saveFile(filePath: string, fileBuffer: Buffer): Promise { await fs.promises.writeFile(filePath, fileBuffer); } // Cari answer berdasarkan assessmentId dan questionId async function findAnswerId(assessmentId: string, questionId: string): Promise { const result = await db .select({ answerId: answers.id }) .from(answers) .leftJoin(options, eq(answers.optionId, options.id)) .where( and( eq(answers.assessmentId, assessmentId), eq(options.questionId, questionId) ) ) .limit(1); return result.length > 0 ? result[0].answerId : null; } // Update filename di tabel answers async function updateFilename(answerId: string, filename: string): Promise { // Dapatkan tanggal dan waktu saat ini const currentDate = new Date(); await db .update(answers) .set({ filename, updatedAt: currentDate, }) .where(eq(answers.id, answerId)); } const assessmentsRoute = new Hono() .use(authInfo) // Get all aspects .get( "/aspect", checkPermission("assessments.readAspect"), requestValidator( "query", z.object({ includeTrashed: z .string() .optional() .transform((v) => v?.toLowerCase() === "true"), withMetadata: z .string() .optional() .transform((v) => v?.toLowerCase() === "true"), page: z.coerce.number().int().min(0).default(0), limit: z.coerce.number().int().min(1).max(1000).default(10), q: z.string().default(""), }) ), async (c) => { const { includeTrashed, page, limit, q } = c.req.valid("query"); const totalCountQuery = includeTrashed ? sql`(SELECT count(DISTINCT ${aspects.id}) FROM ${aspects})` : sql`(SELECT count(DISTINCT ${aspects.id}) FROM ${aspects} WHERE ${aspects.deletedAt} IS NULL)`; const aspectIdsQuery = await db .select({ id: aspects.id, }) .from(aspects) .where( and( includeTrashed ? undefined : isNull(aspects.deletedAt), q ? or(ilike(aspects.name, q), eq(aspects.id, q)) : undefined ) ) .offset(page * limit) .limit(limit); const aspectIds = aspectIdsQuery.map(a => a.id); if (aspectIds.length === 0) { return c.json({ data: [], _metadata: { currentPage: page, totalPages: 0, totalItems: 0, perPage: limit, }, }); } // Main query to get aspects, sub-aspects, and number of questions const result = await db .select({ id: aspects.id, name: aspects.name, createdAt: aspects.createdAt, updatedAt: aspects.updatedAt, ...(includeTrashed ? { deletedAt: aspects.deletedAt } : {}), subAspectId: subAspects.id, subAspectName: subAspects.name, // Increase the number of questions related to sub aspects questionCount: sql`( SELECT count(*) FROM ${questions} WHERE ${questions.subAspectId} = ${subAspects.id} )`.as('questionCount'), fullCount: totalCountQuery, }) .from(aspects) .leftJoin(subAspects, eq(subAspects.aspectId, aspects.id)) .where(inArray(aspects.id, aspectIds)); // Grouping sub aspects by aspect ID const groupedResult = result.reduce((acc, curr) => { const aspectId = curr.id; if (!acc[aspectId]) { acc[aspectId] = { id: curr.id, name: curr.name, createdAt: curr.createdAt ? new Date(curr.createdAt).toISOString() : null, updatedAt: curr.updatedAt ? new Date(curr.updatedAt).toISOString() : null, subAspects: curr.subAspectName ? [{ id: curr.subAspectId!, name: curr.subAspectName, questionCount: curr.questionCount }] : [], }; } else { if (curr.subAspectName) { const exists = acc[aspectId].subAspects.some(sub => sub.id === curr.subAspectId); if (!exists) { acc[aspectId].subAspects.push({ id: curr.subAspectId!, name: curr.subAspectName, questionCount: curr.questionCount, }); } } } return acc; }, {} as Record); const groupedArray = Object.values(groupedResult); return c.json({ data: groupedArray, _metadata: { currentPage: page, totalPages: Math.ceil((Number(result[0]?.fullCount) ?? 0) / limit), totalItems: Number(result[0]?.fullCount) ?? 0, perPage: limit, }, }); } ) // Get data for current Assessment Score from submitted options By Assessment Id .get( "/getCurrentAssessmentScore", checkPermission("assessments.readAssessmentScore"), requestValidator( "query", z.object({ assessmentId: z.string(), }) ), async (c) => { const { assessmentId } = c.req.valid("query"); // Query to sum the scores of selected options for the current assessment const result = await db .select({ totalScore: sql`SUM(${options.score})`, }) .from(answers) .leftJoin(options, eq(answers.optionId, options.id)) .where(eq(answers.assessmentId, assessmentId)) .execute(); return c.json({ assessmentId, totalScore: result[0]?.totalScore ?? 0, // Return 0 if no answers are found }); } ) // Get all Questions and Options that relate to Sub Aspects and Aspects .get( "/getAllQuestions", checkPermission("assessments.readAllQuestions"), async (c) => { // Definisikan tipe untuk hasil query dan izinkan nilai null type QuestionWithOptions = { aspectsId: string | null; aspectsName: string | null; subAspectId: string | null; subAspectName: string | null; questionId: string | null; questionText: string | null; optionId: string; optionText: string; needFile: boolean | null; optionScore: number; fullCount?: number; }; const totalCountQuery = sql`(SELECT count(*) FROM ${options} LEFT JOIN ${questions} ON ${options.questionId} = ${questions.id} LEFT JOIN ${subAspects} ON ${questions.subAspectId} = ${subAspects.id} LEFT JOIN ${aspects} ON ${subAspects.aspectId} = ${aspects.id} WHERE ${questions.deletedAt} IS NULL )`; // Sesuaikan tipe hasil query const result: QuestionWithOptions[] = await db .select({ aspectsId: aspects.id, aspectsName: aspects.name, subAspectId: subAspects.id, subAspectName: subAspects.name, questionId: questions.id, questionText: questions.question, optionId: options.id, optionText: options.text, needFile: questions.needFile, optionScore: options.score, fullCount: totalCountQuery, }) .from(options) .leftJoin(questions, eq(options.questionId, questions.id)) .leftJoin(subAspects, eq(questions.subAspectId, subAspects.id)) .leftJoin(aspects, eq(subAspects.aspectId, aspects.id)) .where(sql`${questions.deletedAt} IS NULL`); // Definisikan tipe untuk hasil pengelompokan type GroupedQuestion = { questionId: string | null; questionText: string | null; needFile: boolean | null; aspectsId: string | null; aspectsName: string | null; subAspectId: string | null; subAspectName: string | null; options: { optionId: string; optionText: string; optionScore: number; }[]; }; // Mengelompokkan berdasarkan questionId const groupedResult: GroupedQuestion[] = result.reduce((acc, current) => { const { questionId, questionText, needFile, aspectsId, aspectsName, subAspectId, subAspectName, optionId, optionText, optionScore } = current; // Cek apakah questionId sudah ada dalam accumulator const existingQuestion = acc.find(q => q.questionId === questionId); if (existingQuestion) { // Tambahkan opsi baru ke array options dari pertanyaan yang ada existingQuestion.options.push({ optionId, optionText, optionScore }); } else { // Jika pertanyaan belum ada, tambahkan objek baru acc.push({ questionId, questionText, needFile, aspectsId, aspectsName, subAspectId, subAspectName, options: [ { optionId, optionText, optionScore } ] }); } return acc; }, [] as GroupedQuestion[]); // Pastikan tipe untuk accumulator didefinisikan return c.json({ data: groupedResult, }); } ) // Get all Answers Data by Assessment Id .get( "/getAnswers", checkPermission("assessments.readAnswers"), requestValidator( "query", z.object({ assessmentId: z.string(), // Require assessmentId as a query parameter }) ), async (c) => { const { assessmentId } = c.req.valid("query"); // Query to count total answers for the specific assessmentId const totalCountQuery = sql`(SELECT count(*) FROM ${answers} WHERE ${answers.assessmentId} = ${assessmentId})`; // Query to retrieve answers for the specific assessmentId const result = await db .select({ id: answers.id, assessmentId: answers.assessmentId, questionId: options.questionId, optionId: answers.optionId, isFlagged: answers.isFlagged, filename: answers.filename, validationInformation: answers.validationInformation, fullCount: totalCountQuery, }) .from(answers) .leftJoin(options, eq(answers.optionId, options.id)) .where( and( eq(answers.assessmentId, assessmentId), // Filter by assessmentId ) ) return c.json({ data: result.map((d) => ({ ...d, fullCount: undefined })), }); } ) .get( "/getAllAnswers/:assessmentId", // Use :assessmentId in the URL path checkPermission("assessments.readAnswers"), async (c) => { const assessmentId = c.req.param("assessmentId"); // Retrieve assessmentId from the URL // Query to retrieve answers for the specific assessmentId, including the associated questionId const result = await db .select({ id: answers.id, assessmentId: answers.assessmentId, questionId: options.questionId, // Get the questionId from the options table optionId: answers.optionId, isFlagged: answers.isFlagged, filename: answers.filename, validationInformation: answers.validationInformation, }) .from(answers) .leftJoin(options, eq(answers.optionId, options.id)) // Join with the options table .where(eq(answers.assessmentId, assessmentId)); // Filter by assessmentId return c.json({ data: result, }); } ) // Toggles the isFlagged field between true and false // .patch( // "/:questionId/toggleFlag", // checkPermission("assessments.toggleFlag"), // async (c) => { // const questionId = c.req.param("questionId"); // // Join answers and options to retrieve answer based on questionId // const currentAnswer = await db // .select({ // isFlagged: answers.isFlagged, // answerId: answers.id, // }) // .from(answers) // .innerJoin(options, eq(answers.optionId, options.id)) // .where(eq(options.questionId, questionId)) // .limit(1); // if (!currentAnswer.length) { // throw notFound({ // message: "Answer not found", // }); // } // // Toggle the isFlagged value // const newIsFlaggedValue = !currentAnswer[0].isFlagged; // // Update the answer with the toggled value // const updatedAnswer = await db // .update(answers) // .set({ // isFlagged: newIsFlaggedValue, // }) // .where(eq(answers.id, currentAnswer[0].answerId)) // .returning(); // if (!updatedAnswer.length) { // throw notFound({ // message: "Failed to update answer", // }); // } // return c.json( // { // message: "Answer flag toggled successfully", // answer: updatedAnswer[0], // }, // 200 // ); // } // ) // Toggles the isFlagged field between true and false .patch( "/toggleFlag", checkPermission("assessments.submitOption"), requestValidator("json", flagFormSchema), async (c) => { const flagData = c.req.valid("json"); // Update jawaban yang ada berdasarkan assessmentId dan questionId const answer = await db .update(answers) .set({ isFlagged: flagData.isFlagged, // Ubah ke pilihan baru }) .where( and( eq(answers.assessmentId, flagData.assessmentId), eq(answers.questionId, flagData.questionId) ) ) .returning(); return c.json( { message: "Flag changed successfully", answer: answer[0], }, 200 ); } ) // Get data answers from table answers by optionId and assessmentId .post( "/checkDataAnswer", checkPermission("assessments.checkAnswer"), async (c) => { const { optionId, assessmentId } = await c.req.json(); const result = await db .select() .from(answers) .where( and(eq(answers.optionId, optionId), eq(answers.assessmentId, assessmentId)) ) .execute(); const existingAnswer = result[0]; let response; if (existingAnswer) { response = { exists: true, answerId: existingAnswer.id }; } else { response = { exists: false }; } return c.json(response); } ) // Upload filename to the table answers and save the file on the local storage .post( "/uploadFile", checkPermission("assessments.uploadFile"), async (c) => { const contentType = c.req.header('content-type'); if (!contentType || !contentType.includes('multipart/form-data')) { return c.json({ message: "Invalid Content-Type" }, 400); } const boundary = contentType.split('boundary=')[1]; if (!boundary) { return c.json({ message: "Boundary not found" }, 400); } const body = await c.req.arrayBuffer(); const bodyString = Buffer.from(body).toString(); const parts = bodyString.split(`--${boundary}`); let fileUrl: string | null = null; for (const part of parts) { if (part.includes('Content-Disposition: form-data;')) { const match = /filename="(.+?)"/.exec(part); if (match) { const fileName = match[1]; const fileContentStart = part.indexOf('\r\n\r\n') + 4; const fileContentEnd = part.lastIndexOf('\r\n'); const fileBuffer = Buffer.from(part.slice(fileContentStart, fileContentEnd), 'binary'); const filePath = path.join('files', `${Date.now()}-${fileName}`); await saveFile(filePath, fileBuffer); const assessmentId = c.req.query('assessmentId'); const questionId = c.req.query('questionId'); if (!assessmentId || !questionId) { return c.json({ message: "assessmentId and questionId are required" }, 400); } const answerId = await findAnswerId(assessmentId, questionId); if (!answerId) { return c.json({ message: 'Answer not found' }, 404); } await updateFilename(answerId, path.basename(filePath)); fileUrl = `/files/${path.basename(filePath)}`; } } } if (!fileUrl) { return c.json({ message: 'No file uploaded' }, 400); } return c.json({ success: true, imageUrl: fileUrl }); } ) // Submit option to table answers from use-form in frontend .post( "/submitAnswer", checkPermission("assessments.submitAnswer"), requestValidator("json", answerFormSchema), async (c) => { const answerData = c.req.valid("json"); const answer = await db .insert(answers) .values({ optionId: answerData.optionId, assessmentId: answerData.assessmentId, isFlagged: answerData.isFlagged, filename: answerData.filename, validationInformation: answerData.validationInformation, }) .returning(); return c.json( { message: "Answer created successfully", answer: answer[0], }, 201 ); } ) // .post( // "/submitOption", // checkPermission("assessments.submitOption"), // requestValidator("json", optionFormSchema), // async (c) => { // const optionData = c.req.valid("json"); // // Cek apakah jawaban sudah ada berdasarkan assessmentId dan questionId // const existingAnswer = await db // .select() // .from(answers) // .leftJoin(options, eq(answers.optionId, options.id)) // .leftJoin(questions, eq(options.questionId, questions.id)) // .where( // sql`answers."assessmentId" = ${optionData.assessmentId} // AND questions.id = ${optionData.questionId}` // ) // .limit(1); // let answer; // if (existingAnswer.length > 0) { // // Update jika jawaban sudah ada // answer = await db // .update(answers) // .set({ // optionId: optionData.optionId, // Ubah ke pilihan baru // }) // .where( // sql`answers."assessmentId" = ${optionData.assessmentId} // AND answers."optionId" IN ( // SELECT id FROM options WHERE "questionId" = ${optionData.questionId} // )` // Mendapatkan optionId berdasarkan questionId // ) // .returning(); // } else { // // Insert jika belum ada jawaban // answer = await db // .insert(answers) // .values({ // optionId: optionData.optionId, // assessmentId: optionData.assessmentId, // isFlagged: optionData.isFlagged ?? false, // filename: optionData.filename ?? null, // validationInformation: "", // Placeholder untuk not-null constraint // }) // .returning(); // } // return c.json( // { // message: "Option submitted successfully", // answer: answer[0], // }, // 201 // ); // } // ) .post( "/submitOption", checkPermission("assessments.submitOption"), requestValidator("json", optionFormSchema), async (c) => { const optionData = c.req.valid("json"); // Update jawaban yang ada berdasarkan assessmentId dan questionId const answer = await db .update(answers) .set({ optionId: optionData.optionId, // Ubah ke pilihan baru }) .where( and( eq(answers.assessmentId, optionData.assessmentId), eq(answers.questionId, optionData.questionId) ) ) .returning(); return c.json( { message: "Option submitted successfully", answer: answer[0], }, 200 ); } ) .post( "/submitValidation", checkPermission("assessments.submitValidation"), requestValidator("json", validationFormSchema), async (c) => { const validationData = c.req.valid("json"); // Cek apakah jawaban ada berdasarkan assessmentId dan questionId const existingAnswer = await db .select() .from(answers) .leftJoin(options, eq(answers.optionId, options.id)) .leftJoin(questions, eq(options.questionId, questions.id)) .where( sql`answers."assessmentId" = ${validationData.assessmentId} AND questions.id = ${validationData.questionId}` ) .limit(1); if (existingAnswer.length === 0) { return c.json( { message: "No existing answer found for the given assessmentId and questionId.", }, 404 ); } // Dapatkan tanggal dan waktu saat ini const currentDate = new Date(); // Update dengan melakukan JOIN yang sama const updatedAnswer = await db .update(answers) .set({ validationInformation: validationData.validationInformation, updatedAt: currentDate, }) .where( sql`answers."assessmentId" = ${validationData.assessmentId} AND answers."optionId" IN ( SELECT id FROM options WHERE "questionId" = ${validationData.questionId} )` ) .returning(); return c.json( { message: "Validation information updated successfully", answer: updatedAnswer[0], }, 200 ); } ) .patch( "/submitAssessment/:id", checkPermission("assessments.submitAssessment"), async (c) => { const assessmentId = c.req.param("id"); const status = "belum diverifikasi"; const assessment = await db .select() .from(assessments) .where(and(eq(assessments.id, assessmentId),)); if (!assessment[0]) { throw notFound({ message: "Assessment not found.", }); } await db .update(assessments) .set({ status, }) .where(eq(assessments.id, assessmentId)); return c.json({ message: "Status assessment berhasil diperbarui.", }); } ) // Update answer in table answers if answer changes .patch( "/:id/updateAnswer", checkPermission("assessments.updateAnswer"), requestValidator("json", answerUpdateSchema), async (c) => { const answerId = c.req.param("id"); const answerData = c.req.valid("json"); const updatedAnswer = await db .update(answers) .set({ optionId: answerData.optionId, }) .where(eq(answers.id, answerId)) .returning(); if (!updatedAnswer.length) { throw notFound({ message: "Answer not found or update failed" }) } return c.json({ message: "Answer updated successfully", answer: updatedAnswer[0], }); } ) // Get data for One Sub Aspect average score By Sub Aspect Id and Assessment Id .get( '/average-score/sub-aspects/:subAspectId/assessments/:assessmentId', checkPermission("assessments.readAssessmentScore"), async (c) => { const { subAspectId, assessmentId } = c.req.param(); const averageScore = await db .select({ subAspectName: subAspects.name, average: sql`AVG(options.score)` }) .from(answers) .innerJoin(options, eq(answers.optionId, options.id)) .innerJoin(questions, eq(options.questionId, questions.id)) .innerJoin(subAspects, eq(questions.subAspectId, subAspects.id)) .innerJoin(assessments, eq(answers.assessmentId, assessments.id)) .where( sql`sub_aspects.id = ${subAspectId} AND assessments.id = ${assessmentId}` ) .groupBy(subAspects.id); return c.json({ subAspectId, subAspectName: averageScore[0].subAspectName, assessmentId, averageScore: averageScore.length > 0 ? averageScore[0].average : 0 }); } ) // Get data for All Sub Aspects average score By Assessment Id .get( '/average-score/sub-aspects/assessments/:assessmentId', checkPermission("assessments.readAssessmentScore"), async (c) => { const { assessmentId } = c.req.param(); const averageScores = await db .select({ aspectId: subAspects.aspectId, subAspectId: subAspects.id, subAspectName: subAspects.name, average: sql`AVG(options.score)` }) .from(answers) .innerJoin(options, eq(answers.optionId, options.id)) .innerJoin(questions, eq(options.questionId, questions.id)) .innerJoin(subAspects, eq(questions.subAspectId, subAspects.id)) .innerJoin(assessments, eq(answers.assessmentId, assessments.id)) .where(eq(assessments.id, assessmentId)) .groupBy(subAspects.id); return c.json({ assessmentId, subAspects: averageScores.map(score => ({ subAspectId: score.subAspectId, subAspectName: score.subAspectName, averageScore: score.average, aspectId: score.aspectId })) }); } ) // Get data for One Aspect average score By Aspect Id and Assessment Id .get( "/average-score/aspects/:aspectId/assessments/:assessmentId", checkPermission("assessments.readAssessmentScore"), async (c) => { const { aspectId, assessmentId } = c.req.param(); const averageScore = await db .select({ aspectName: aspects.name, average: sql`AVG(options.score)` }) .from(answers) .innerJoin(options, eq(answers.optionId, options.id)) .innerJoin(questions, eq(options.questionId, questions.id)) .innerJoin(subAspects, eq(questions.subAspectId, subAspects.id)) .innerJoin(aspects, eq(subAspects.aspectId, aspects.id)) .innerJoin(assessments, eq(answers.assessmentId, assessments.id)) .where( sql`aspects.id = ${aspectId} AND assessments.id = ${assessmentId}` ) .groupBy(aspects.id); return c.json({ aspectId, aspectName: averageScore[0].aspectName, assessmentId, averageScore: averageScore.length > 0 ? averageScore[0].average : 0 }); } ) // Get data for Aspects average score and all related Sub Aspects average score By Assessment Id .get( '/average-score/aspects/assessments/:assessmentId', checkPermission("assessments.readAssessmentScore"), async (c) => { const { assessmentId } = c.req.param(); // Query untuk mendapatkan average score per aspect const aspectScores = await db .select({ aspectId: aspects.id, aspectName: aspects.name, averageScore: sql`AVG(options.score)`, }) .from(answers) .innerJoin(options, eq(answers.optionId, options.id)) .innerJoin(questions, eq(options.questionId, questions.id)) .innerJoin(subAspects, eq(questions.subAspectId, subAspects.id)) .innerJoin(aspects, eq(subAspects.aspectId, aspects.id)) .innerJoin(assessments, eq(answers.assessmentId, assessments.id)) .where(eq(assessments.id, assessmentId)) .groupBy(aspects.id); // Query untuk mendapatkan average score per sub-aspect const subAspectScores = await db .select({ aspectId: subAspects.aspectId, subAspectId: subAspects.id, subAspectName: subAspects.name, averageScore: sql`AVG(options.score)`, }) .from(answers) .innerJoin(options, eq(answers.optionId, options.id)) .innerJoin(questions, eq(options.questionId, questions.id)) .innerJoin(subAspects, eq(questions.subAspectId, subAspects.id)) .innerJoin(assessments, eq(answers.assessmentId, assessments.id)) .where(eq(assessments.id, assessmentId)) .groupBy(subAspects.id); // Menggabungkan sub-aspects ke dalam masing-masing aspect const aspectsWithSubAspects = aspectScores.map((aspect) => ({ aspectId: aspect.aspectId, aspectName: aspect.aspectName, averageScore: aspect.averageScore, subAspects: subAspectScores .filter((sub) => sub.aspectId === aspect.aspectId) .map((sub) => ({ subAspectId: sub.subAspectId, subAspectName: sub.subAspectName, averageScore: sub.averageScore, })), })); return c.json({ assessmentId, aspects: aspectsWithSubAspects, }); } ) .patch( "/updateOption", checkPermission("assessments.submitOption"), requestValidator("json", newOptionFormSchema), async (c) => { const optionData = c.req.valid("json"); // Temukan answerId yang sesuai berdasarkan assessmentId dan questionId const [targetAnswer] = await db .select({ id: answers.id }) .from(answers) .leftJoin(options, eq(answers.optionId, options.id)) .where( sql`answers."assessmentId" = ${optionData.assessmentId} AND options."questionId" = ${optionData.questionId}` ) .limit(1); if (!targetAnswer) { return c.json( { message: "Answer not found for given assessmentId and questionId" }, 404 ); } // Lakukan update pada answer_revisions menggunakan answerId yang ditemukan const [updatedRevision] = await db .update(answerRevisions) .set({ newOptionId: optionData.newOptionId, }) .where(sql`"answerId" = ${targetAnswer.id}`) .returning(); return c.json( { message: "Revision updated successfully", revision: updatedRevision, // Revisi yang baru saja diperbarui }, 200 ); } ) .patch( "/updateOption", checkPermission("assessments.submitOption"), requestValidator("json", newOptionFormSchema), async (c) => { const optionData = c.req.valid("json"); // Temukan answerId yang sesuai berdasarkan assessmentId dan questionId const [targetAnswer] = await db .select({ id: answers.id }) .from(answers) .leftJoin(options, eq(answers.optionId, options.id)) .where( sql`answers."assessmentId" = ${optionData.assessmentId} AND options."questionId" = ${optionData.questionId}` ) .limit(1); if (!targetAnswer) { return c.json( { message: "Answer not found for given assessmentId and questionId" }, 404 ); } // Lakukan update pada answer_revisions menggunakan answerId yang ditemukan const [updatedRevision] = await db .update(answerRevisions) .set({ newOptionId: optionData.newOptionId, }) .where(sql`"answerId" = ${targetAnswer.id}`) .returning(); return c.json( { message: "Revision updated successfully", revision: updatedRevision, // Revisi yang baru saja diperbarui }, 200 ); } ) export default assessmentsRoute;