653 lines
21 KiB
TypeScript
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;
|