import { and, eq, ilike, or, sql } 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"; 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), }); export const answerUpdateSchema = answerFormSchema.partial(); // Helper function to save the file async function saveFile(filePath: string, fileBuffer: Buffer): Promise { await fs.promises.writeFile(filePath, fileBuffer); } // Function to update the filename in the database async function updateFilenameInDatabase(answerId: string, filename: string): Promise { await db.update(answers) .set({ filename }) .where(eq(answers.id, answerId)); } const assessmentsRoute = new Hono() .use(authInfo) // 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) => { 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 )`; 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, optionText: options.text, 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`) return c.json({ data: result.map((d) => ( { ...d, fullCount: undefined } )), }); } ) // 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 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, page, limit, q } = 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, optionId: answers.optionId, isFlagged: answers.isFlagged, filename: answers.filename, validationInformation: answers.validationInformation, fullCount: totalCountQuery, }) .from(answers) .where( and( eq(answers.assessmentId, assessmentId), // Filter by assessmentId q ? or( ilike(answers.filename, q), ilike(answers.validationInformation, q), eq(answers.id, q) ) : undefined ) ) .offset(page * limit) .limit(limit); return c.json({ data: result.map((d) => ({ ...d, fullCount: undefined })), _metadata: { currentPage: page, totalPages: Math.ceil( (Number(result[0]?.fullCount) ?? 0) / limit ), totalItems: Number(result[0]?.fullCount) ?? 0, perPage: limit, }, }); } ) // Toggles the isFlagged field between true and false .patch( "/:id/toggleFlag", checkPermission("assessments.toggleFlag"), async (c) => { const answerId = c.req.param("id"); // Retrieve the current state of isFlagged const currentAnswer = await db .select({ isFlagged: answers.isFlagged, }) .from(answers) .where(eq(answers.id, answerId)) .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, answerId)) .returning(); if (!updatedAnswer.length) { throw notFound( { message: "Failed to update answer", } ) } return c.json( { message: "Answer flag toggled successfully", answer: updatedAnswer[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) => { // Get the Content-Type header const contentType = c.req.header('content-type'); if (!contentType || !contentType.includes('multipart/form-data')) { throw notFound({ message: "Invalid Content-Type", }); } // Extract boundary const boundary = contentType.split('boundary=')[1]; if (!boundary) { throw notFound({ message: "Boundary not found", }); } // Get the raw body const body = await c.req.arrayBuffer(); const bodyString = Buffer.from(body).toString(); // Split the body by the boundary const parts = bodyString.split(`--${boundary}`); let fileUrl = null; for (const part of parts) { if (part.includes('Content-Disposition: form-data;')) { // Extract file name 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'); // Extract file content as Buffer const fileBuffer = Buffer.from(part.slice(fileContentStart, fileContentEnd), 'binary'); // Define file path and save the file const filePath = path.join('images', Date.now() + '-' + fileName); await saveFile(filePath, fileBuffer); // Assuming answerId is passed as a query parameter or in the form-data const answerId = c.req.query('answerId'); if (!answerId) { throw notFound({ message: "answerId is required", }); } await updateFilenameInDatabase(answerId, path.basename(filePath)); // Set the file URL for the final response fileUrl = `/images/${path.basename(filePath)}`; } } } if (!fileUrl) { throw notFound({ message: 'No file uploaded', }); } 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 ); } ) // 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.readAverageSubAspect"), 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.readAverageAllSubAspects"), async (c) => { const { assessmentId } = c.req.param(); const averageScores = await db .select({ 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 })) }); } ) // Get data for One Aspect average score By Aspect Id and Assessment Id .get( "/average-score/aspects/:aspectId/assessments/:assessmentId", checkPermission("assessments.readAverageAspect"), 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 All Aspects average score By Assessment Id .get( '/average-score/aspects/assessments/:assessmentId', checkPermission("assessments.readAverageAllAspects"), async (c) => { const { assessmentId } = c.req.param(); const averageScores = await db .select({ AspectId: aspects.id, 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(eq(assessments.id, assessmentId)) .groupBy(aspects.id); return c.json({ assessmentId, aspects: averageScores.map(score => ({ AspectId: score.AspectId, AspectName: score.AspectName, averageScore: score.average })) }); } ) export default assessmentsRoute;