import { and, eq, ilike, isNull, or, sql } from "drizzle-orm"; import { Hono } from "hono"; import { z } from "zod"; import db from "../../drizzle"; import { assessments, statusEnum } from "../../drizzle/schema/assessments"; import { respondents } from "../../drizzle/schema/respondents"; import { users } from "../../drizzle/schema/users"; import { aspects } from "../../drizzle/schema/aspects"; import { subAspects } from "../../drizzle/schema/subAspects"; import { questions } from "../../drizzle/schema/questions"; import { options } from "../../drizzle/schema/options"; import { answers } from "../../drizzle/schema/answers"; import { answerRevisions } from "../../drizzle/schema/answerRevisions"; import HonoEnv from "../../types/HonoEnv"; import authInfo from "../../middlewares/authInfo"; import checkPermission from "../../middlewares/checkPermission"; import requestValidator from "../../utils/requestValidator"; import { notFound } from "../../errors/DashboardError"; // 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(), }); // validationFormSchema: untuk /submitValidation export const validationFormSchema = z.object({ assessmentId: z.string().min(1), questionId: z.string().min(1), newValidationInformation: z.string().min(1, "Validation information is required"), }); const assessmentRoute = new Hono() .use(authInfo) // Get All List of Assessment Results .get( "/", checkPermission("assessmentResult.readAll"), requestValidator( "query", z.object({ 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(40), q: z.string().default(""), }) ), async (c) => { const { page, limit, q } = c.req.valid("query"); const totalItems = await db .select({ count: sql`COUNT(*)`, }) .from(assessments) .leftJoin(respondents, eq(assessments.respondentId, respondents.id)) .leftJoin(users, eq(respondents.userId, users.id)) .where( and( or( q ? or( ilike(users.name, q), ilike(respondents.companyName, q) ) : undefined, ), or( eq(assessments.status, 'belum diverifikasi'), eq(assessments.status, 'selesai') ) ) ); const result = await db .select({ id: assessments.id, respondentName: users.name, companyName: respondents.companyName, statusAssessments: assessments.status, statusVerification: sql` CASE WHEN ${assessments.verifiedAt} IS NOT NULL THEN 'sudah diverifikasi' ELSE 'belum diverifikasi' END` .as("statusVerification"), assessmentsResult: sql` (SELECT ROUND(AVG(${options.score}), 2) FROM ${answers} JOIN ${options} ON ${options.id} = ${answers.optionId} JOIN ${questions} ON ${questions.id} = ${options.questionId} JOIN ${subAspects} ON ${subAspects.id} = ${questions.subAspectId} JOIN ${aspects} ON ${aspects.id} = ${subAspects.aspectId} WHERE ${answers.assessmentId} = ${assessments.id})` .as("assessmentsResult"), }) .from(assessments) .leftJoin(respondents, eq(assessments.respondentId, respondents.id)) .leftJoin(users, eq(respondents.userId, users.id)) .where( and( or( q ? or( ilike(users.name, q), ilike(respondents.companyName, q), ) : undefined, ), or( eq(assessments.status, 'belum diverifikasi'), eq(assessments.status, 'selesai') ) ) ) .orderBy( sql`CASE WHEN ${assessments.status} = 'belum diverifikasi' THEN 1 WHEN ${assessments.status} = 'selesai' THEN 2 ELSE 3 END` ) .offset(page * limit) .limit(limit); const totalCountResult = await totalItems; const totalCount = totalCountResult[0]?.count || 0; return c.json({ data: result, _metadata: { currentPage: page, totalPages: Math.ceil(totalCount / limit), totalItems: totalCount, perPage: limit, }, }); } ) // Get Assessment Result by ID .get( "/:id", checkPermission("assessmentResult.read"), async (c) => { const assessmentId = c.req.param("id"); const result = await db .select({ respondentName: users.name, position: respondents.position, workExperience: respondents.workExperience, email: users.email, companyName: respondents.companyName, address: respondents.address, phoneNumber: respondents.phoneNumber, username: users.username, assessmentDate: assessments.createdAt, statusAssessment: assessments.status, statusVerification: sql` CASE WHEN ${assessments.verifiedAt} IS NOT NULL THEN 'sudah diverifikasi' ELSE 'belum diverifikasi' END` .as("statusVerification"), assessmentsResult: sql` (SELECT ROUND(AVG(${options.score}), 2) FROM ${answers} JOIN ${options} ON ${options.id} = ${answers.optionId} JOIN ${questions} ON ${questions.id} = ${options.questionId} JOIN ${subAspects} ON ${subAspects.id} = ${questions.subAspectId} JOIN ${aspects} ON ${aspects.id} = ${subAspects.aspectId} WHERE ${answers.assessmentId} = ${assessments.id})` .as("assessmentsResult"), }) .from(assessments) .leftJoin(respondents, eq(assessments.respondentId, respondents.id)) .leftJoin(users, eq(respondents.userId, users.id)) .where(eq(assessments.id, assessmentId)); if (!result.length) { throw notFound({ message: "Assessment not found", }); } return c.json(result[0]); } ) .get( "/verified/:id", checkPermission("assessmentResult.read"), async (c) => { const assessmentId = c.req.param("id"); const result = await db .select({ respondentName: users.name, position: respondents.position, workExperience: respondents.workExperience, email: users.email, companyName: respondents.companyName, address: respondents.address, phoneNumber: respondents.phoneNumber, username: users.username, assessmentDate: assessments.createdAt, statusAssessment: assessments.status, statusVerification: sql` CASE WHEN ${assessments.verifiedAt} IS NOT NULL THEN 'sudah diverifikasi' ELSE 'belum diverifikasi' END`.as("statusVerification"), verifiedAssessmentsResult: sql` (SELECT ROUND(AVG(${options.score}), 2) FROM ${answerRevisions} JOIN ${answers} ON ${answers.id} = ${answerRevisions.answerId} JOIN ${options} ON ${options.id} = ${answerRevisions.newOptionId} JOIN ${questions} ON ${questions.id} = ${options.questionId} JOIN ${subAspects} ON ${subAspects.id} = ${questions.subAspectId} JOIN ${aspects} ON ${aspects.id} = ${subAspects.aspectId} WHERE ${answers.assessmentId} = ${assessments.id})`.as("verifiedAssessmentsResult"), }) .from(assessments) .leftJoin(respondents, eq(assessments.respondentId, respondents.id)) .leftJoin(users, eq(respondents.userId, users.id)) .where(eq(assessments.id, assessmentId)); if (!result.length) { throw notFound({ message: "Assessment not found", }); } return c.json(result[0]); } ) // Get all Questions and Options that relate to Sub Aspects and Aspects based on Assessment ID .get( "getAllQuestion/:id", checkPermission("assessmentResult.readAllQuestions"), async (c) => { const assessmentId = c.req.param("id"); if (!assessmentId) { throw notFound({ message: "Assessment ID is missing", }); } // Total count of options related to the assessment 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} LEFT JOIN ${answers} ON ${options.id} = ${answers.optionId} WHERE ${questions.deletedAt} IS NULL AND ${answers.assessmentId} = ${assessmentId} `; // Query to get detailed information about options const result = await db .select({ optionId: options.id, aspectsId: aspects.id, aspectsName: aspects.name, subAspectId: subAspects.id, subAspectName: subAspects.name, questionId: questions.id, questionText: questions.question, answerId: answers.id, answerText: options.text, answerScore: options.score, }) .from(options) .leftJoin(questions, eq(options.questionId, questions.id)) .leftJoin(subAspects, eq(questions.subAspectId, subAspects.id)) .leftJoin(aspects, eq(subAspects.aspectId, aspects.id)) .leftJoin(answers, eq(options.id, answers.optionId)) .where(sql`${questions.deletedAt} IS NULL AND ${answers.assessmentId} = ${assessmentId}`); // Execute the total count query const totalCountResult = await db.execute(totalCountQuery); if (result.length === 0) { throw notFound({ message: "Data does not exist", }); } return c.json({ data: result, totalCount: totalCountResult[0]?.count || 0 }); } ) .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(answerRevisions) .innerJoin(answers, eq(answers.id, answerRevisions.answerId)) .innerJoin(options, eq(answerRevisions.newOptionId, 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( '/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(answerRevisions) .innerJoin(answers, eq(answers.id, answerRevisions.answerId)) .innerJoin(options, eq(answerRevisions.newOptionId, 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(answerRevisions) .innerJoin(answers, eq(answers.id, answerRevisions.answerId)) .innerJoin(options, eq(answerRevisions.newOptionId, 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, }); } ) // Get all Answers Data by Assessment Id .get( "/getAnswers/:id", checkPermission("assessments.readAnswers"), requestValidator( "query", z.object({ // assessmentId: z.string().min(1), 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(1000), q: z.string().default(""), }) ), async (c) => { const assessmentId = c.req.param("id").toString(); const { page, limit, q, withMetadata } = c.req.valid("query"); // Query to count total answers for the specific assessmentId const totalCountQuery = sql`(SELECT count(*) FROM ${answerRevisions} JOIN ${answers} ON ${answers.id} = ${answerRevisions.answerId} JOIN ${assessments} ON ${answers.assessmentId} = ${assessments.id} WHERE ${assessments.id} = ${assessmentId})`; // Query to retrieve answers for the specific assessmentId const result = await db .select({ id: answerRevisions.id, answerId: answerRevisions.answerId, newOptionId: answerRevisions.newOptionId, newValidationInformation: answerRevisions.newValidationInformation, fullCount: totalCountQuery, }) .from(answerRevisions) .innerJoin(answers, eq(answers.id, answerRevisions.answerId)) .innerJoin(assessments, eq(answers.assessmentId, assessments.id)) .where( and( eq(assessments.id, assessmentId), q ? or( ilike(answers.filename, q), ilike(answerRevisions.newValidationInformation, q), eq(answerRevisions.answerId, q) ) : undefined ) ) .offset(page * limit) .limit(limit); return c.json({ data: result.map((d) => ({ ...d, fullCount: undefined })), _metadata: { currentPage: page, totalPages: withMetadata ? Math.ceil((Number(result[0]?.fullCount) ?? 0) / limit) : null, totalItems: withMetadata ? Number(result[0]?.fullCount) ?? 0 : null, perPage: limit, }, }); } ) // POST Endpoint for creating multiple answer revisions based on assessmentId .post( "/answer-revisions", checkPermission("assessmentResult.create"), requestValidator( "json", z.object({ assessmentId: z.string(), revisedBy: z.string(), // assuming this will come from the session or auth context }) ), async (c) => { const { assessmentId, revisedBy } = c.req.valid("json"); // Fetch answers related to the given assessmentId const existingAnswers = await db .select() .from(answers) .where(eq(answers.assessmentId, assessmentId)); if (!existingAnswers.length) { throw notFound({ message: "No answers found for the given assessment ID", }); } // Fetch already existing revisions for the given answer IDs const existingRevisions = await db .select({ answerId: answerRevisions.answerId }) .from(answerRevisions) .where( or( ...existingAnswers.map((answer) => eq(answerRevisions.answerId, answer.id) ) ) ); // Create a Set of existing revision IDs for quick lookup const existingRevisionIds = new Set( existingRevisions.map(revision => revision.answerId) ); // Prepare revisions to be inserted, excluding those that already exist const revisions = existingAnswers .filter(answer => !existingRevisionIds.has(answer.id)) // Filter out existing revisions .map(answer => ({ answerId: answer.id, newOptionId: answer.optionId, // Assuming you want to keep the existing optionId newValidationInformation: answer.validationInformation, // Keep the existing validation information revisedBy: revisedBy })); if (revisions.length === 0) { return c.json({ message: "No new revisions to create, as all answers are already revised.", }); } // Insert all new revisions in a single operation const newRevisions = await db .insert(answerRevisions) .values(revisions) .returning(); return c.json( { message: "Answer revisions created successfully", data: newRevisions, }, 201 ); } ) .patch( "/:id", checkPermission("assessmentResult.update"), async (c) => { const assessmentId = c.req.param("id"); if (!assessmentId) { throw notFound({ message: "Assessment tidak ada", }); } const assessment = await db .select() .from(assessments) .where(and(eq(assessments.id, assessmentId))); if (!assessment[0]) throw notFound(); await db .update(assessments) .set({ verifiedAt: new Date(), }) .where(eq(assessments.id, assessmentId)); console.log("Verified Success"); return c.json({ message: "Assessment berhasil diverifikasi", data: assessment, }); } ) .patch( "/submitAssessmentRevision/:id", checkPermission("assessments.submitAssessment"), async (c) => { const assessmentId = c.req.param("id"); const status = "selesai"; 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.", }); } ) .post( "/updateValidation", checkPermission("assessments.submitValidation"), requestValidator("json", validationFormSchema), async (c) => { const validationData = c.req.valid("json"); // Cek apakah jawaban ada 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" = ${validationData.assessmentId} AND options."questionId" = ${validationData.questionId}` ) .limit(1); if (!targetAnswer) { return c.json( { message: "Answer not found for given assessmentId and questionId" }, 404 ); } // Dapatkan tanggal dan waktu saat ini const currentDate = new Date(); // Update dengan melakukan JOIN yang sama const [updatedRevision] = await db .update(answerRevisions) .set({ newValidationInformation: validationData.newValidationInformation, }) .where(sql`"answerId" = ${targetAnswer.id}`) .returning(); return c.json( { message: "Revision updated successfully", revision: updatedRevision, // Revisi yang baru saja diperbarui }, 200 ); } ) export default assessmentRoute;