amati/apps/backend/src/routes/assessmentResult/route.ts

653 lines
21 KiB
TypeScript

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<HonoEnv>()
.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<number>`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<string>`
CASE
WHEN ${assessments.verifiedAt} IS NOT NULL THEN 'sudah diverifikasi'
ELSE 'belum diverifikasi'
END`
.as("statusVerification"),
assessmentsResult: sql<number>`
(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<string>`
CASE
WHEN ${assessments.verifiedAt} IS NOT NULL THEN 'sudah diverifikasi'
ELSE 'belum diverifikasi'
END`
.as("statusVerification"),
assessmentsResult: sql<number>`
(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<string>`
CASE
WHEN ${assessments.verifiedAt} IS NOT NULL THEN 'sudah diverifikasi'
ELSE 'belum diverifikasi'
END`.as("statusVerification"),
verifiedAssessmentsResult: sql<number>`
(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<number>`
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<number>`(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;