diff --git a/apps/backend/src/data/permissions.ts b/apps/backend/src/data/permissions.ts index ccfdff6..d0a7c76 100644 --- a/apps/backend/src/data/permissions.ts +++ b/apps/backend/src/data/permissions.ts @@ -63,6 +63,18 @@ const permissionsData = [ code: "managementAspect.restore", }, { + code: "assessmentResult.readAll", + }, + { + code: "assessmentResult.read", + }, + { + code: "assessmentResult.readAllQuestions", + }, + { + code: "assessmentResult.create", + }, + { code: "assessmentRequest.read", }, { diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 8c06f6f..7133f40 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -18,6 +18,7 @@ import HonoEnv from "./types/HonoEnv"; import devRoutes from "./routes/dev/route"; import appEnv from "./appEnv"; import questionsRoute from "./routes/questions/route"; +import assessmentResultRoute from "./routes/assessmentResult/route"; import assessmentRequestRoute from "./routes/assessmentRequest/route"; import forgotPasswordRoutes from "./routes/forgotPassword/route"; import assessmentsRoute from "./routes/assessments/route"; @@ -87,6 +88,7 @@ const routes = app .route("/questions", questionsRoute) .route("/management-aspect", managementAspectsRoute) .route("/register", respondentsRoute) + .route("/assessmentResult", assessmentResultRoute) .route("/assessmentRequest", assessmentRequestRoute) .route("/forgot-password", forgotPasswordRoutes) .route("/assessments", assessmentsRoute) diff --git a/apps/backend/src/routes/assessmentResult/route.ts b/apps/backend/src/routes/assessmentResult/route.ts new file mode 100644 index 0000000..5b9e989 --- /dev/null +++ b/apps/backend/src/routes/assessmentResult/route.ts @@ -0,0 +1,238 @@ +import { and, eq, isNull, sql } from "drizzle-orm"; +import { Hono } from "hono"; +import { z } from "zod"; +import db from "../../drizzle"; +import { assessments } 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"; + +const assessmentRoute = new Hono() + .use(authInfo) + + // Get All List of Assessment Results + .get( + "/", + checkPermission("assessmentResult.readAll"), + requestValidator( + "query", + z.object({ + page: z.coerce.number().int().min(0).default(0), + limit: z.coerce.number().int().min(1).max(1000).default(40), + }) + ), + async (c) => { + const { page, limit } = c.req.valid("query"); + + const result = await db + .select({ + id: assessments.id, + respondentName: users.name, + companyName: respondents.companyName, + statusAssessments: assessments.status, + statusVerification: sql` + CASE + WHEN ${assessments.validatedAt} 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)) + .offset(page * limit) + .limit(limit); + + const totalItems = await db + .select({ + count: sql`COUNT(*)`, + }) + .from(assessments); + + return c.json({ + data: result, + _metadata: { + currentPage: page, + totalPages: Math.ceil(totalItems[0].count / limit), + totalItems: totalItems[0].count, + 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, + 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 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 + }); + } + ) + + // POST Endpoint for creating a new answer revision + .post( + "/answer-revisions", + checkPermission("assessmentResult.create"), + requestValidator( + "json", + z.object({ + answerId: z.string(), + newOptionId: z.string(), + revisedBy: z.string(), + newValidationInformation: z.string(), + }) + ), + async (c) => { + const { answerId, newOptionId, revisedBy, newValidationInformation } = c.req.valid("json"); + + // Check if the answer exists + const existingAnswer = await db + .select() + .from(answers) + .where(eq(answers.id, answerId)); + + if (!existingAnswer.length) { + throw notFound({ + message: "Answer not found", + }); + } + + // Insert new revision + const [newRevision] = await db + .insert(answerRevisions) + .values({ + answerId, + newOptionId, + revisedBy, + newValidationInformation + }) + .returning(); + + return c.json( + { + message: "Answer revision created successfully", + data: newRevision + }, + 201 + ); + } + ); + +export default assessmentRoute;