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

1102 lines
40 KiB
TypeScript

import { and, eq, ilike, isNull, inArray, or, sql, is } 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";
import { answerRevisions } from "../../drizzle/schema/answerRevisions";
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),
});
// 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(),
});
// newOptionFormSchema: untuk /updateOption
export const newOptionFormSchema = z.object({
newOptionId: z.string().min(1),
assessmentId: z.string().min(1),
questionId: z.string().min(1),
});
// validationFormSchema: untuk /submitValidation
export const validationFormSchema = z.object({
assessmentId: z.string().min(1),
questionId: z.string().min(1),
validationInformation: z.string().min(1, "Validation information is required"),
});
// newValidationFormSchema: untuk /updateValidation
export const newValidationFormSchema = z.object({
assessmentId: z.string().min(1),
questionId: z.string().min(1),
newValidationInformation: z.string().min(1, "Validation information is required"),
});
// validationFormSchema: untuk /submitValidation
export const flagFormSchema = z.object({
assessmentId: z.string().min(1),
questionId: z.string().min(1),
isFlagged: z.boolean().optional().default(false),
});
export const answerUpdateSchema = answerFormSchema.partial();
// Helper untuk menyimpan file
async function saveFile(filePath: string, fileBuffer: Buffer): Promise<void> {
await fs.promises.writeFile(filePath, fileBuffer);
}
// Cari answer berdasarkan assessmentId dan questionId
async function findAnswerId(assessmentId: string, questionId: string): Promise<string | null> {
const result = await db
.select({ answerId: answers.id })
.from(answers)
.leftJoin(options, eq(answers.optionId, options.id))
.where(
and(
eq(answers.assessmentId, assessmentId),
eq(options.questionId, questionId)
)
)
.limit(1);
return result.length > 0 ? result[0].answerId : null;
}
// Update filename di tabel answers
async function updateFilename(answerId: string, filename: string): Promise<void> {
// Dapatkan tanggal dan waktu saat ini
const currentDate = new Date();
await db
.update(answers)
.set({
filename,
updatedAt: currentDate,
})
.where(eq(answers.id, answerId));
}
const assessmentsRoute = new Hono<HonoEnv>()
.use(authInfo)
// Get all aspects
.get(
"/aspect",
checkPermission("assessments.readAspect"),
requestValidator(
"query",
z.object({
includeTrashed: z
.string()
.optional()
.transform((v) => v?.toLowerCase() === "true"),
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(10),
q: z.string().default(""),
})
),
async (c) => {
const { includeTrashed, page, limit, q } = c.req.valid("query");
const totalCountQuery = includeTrashed
? sql<number>`(SELECT count(DISTINCT ${aspects.id}) FROM ${aspects})`
: sql<number>`(SELECT count(DISTINCT ${aspects.id}) FROM ${aspects} WHERE ${aspects.deletedAt} IS NULL)`;
const aspectIdsQuery = await db
.select({
id: aspects.id,
})
.from(aspects)
.where(
and(
includeTrashed ? undefined : isNull(aspects.deletedAt),
q ? or(ilike(aspects.name, q), eq(aspects.id, q)) : undefined
)
)
.offset(page * limit)
.limit(limit);
const aspectIds = aspectIdsQuery.map(a => a.id);
if (aspectIds.length === 0) {
return c.json({
data: [],
_metadata: {
currentPage: page,
totalPages: 0,
totalItems: 0,
perPage: limit,
},
});
}
// Main query to get aspects, sub-aspects, and number of questions
const result = await db
.select({
id: aspects.id,
name: aspects.name,
createdAt: aspects.createdAt,
updatedAt: aspects.updatedAt,
...(includeTrashed ? { deletedAt: aspects.deletedAt } : {}),
subAspectId: subAspects.id,
subAspectName: subAspects.name,
// Increase the number of questions related to sub aspects
questionCount: sql<number>`(
SELECT count(*)
FROM ${questions}
WHERE ${questions.subAspectId} = ${subAspects.id}
)`.as('questionCount'),
fullCount: totalCountQuery,
})
.from(aspects)
.leftJoin(subAspects, eq(subAspects.aspectId, aspects.id))
.where(inArray(aspects.id, aspectIds));
// Grouping sub aspects by aspect ID
const groupedResult = result.reduce((acc, curr) => {
const aspectId = curr.id;
if (!acc[aspectId]) {
acc[aspectId] = {
id: curr.id,
name: curr.name,
createdAt: curr.createdAt ? new Date(curr.createdAt).toISOString() : null,
updatedAt: curr.updatedAt ? new Date(curr.updatedAt).toISOString() : null,
subAspects: curr.subAspectName
? [{ id: curr.subAspectId!, name: curr.subAspectName, questionCount: curr.questionCount }]
: [],
};
} else {
if (curr.subAspectName) {
const exists = acc[aspectId].subAspects.some(sub => sub.id === curr.subAspectId);
if (!exists) {
acc[aspectId].subAspects.push({
id: curr.subAspectId!,
name: curr.subAspectName,
questionCount: curr.questionCount,
});
}
}
}
return acc;
}, {} as Record<string, {
id: string;
name: string;
createdAt: string | null;
updatedAt: string | null;
subAspects: { id: string; name: string; questionCount: number }[];
}>);
const groupedArray = Object.values(groupedResult);
return c.json({
data: groupedArray,
_metadata: {
currentPage: page,
totalPages: Math.ceil((Number(result[0]?.fullCount) ?? 0) / limit),
totalItems: Number(result[0]?.fullCount) ?? 0,
perPage: limit,
},
});
}
)
// 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<number>`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) => {
// Definisikan tipe untuk hasil query dan izinkan nilai null
type QuestionWithOptions = {
aspectsId: string | null;
aspectsName: string | null;
subAspectId: string | null;
subAspectName: string | null;
questionId: string | null;
questionText: string | null;
optionId: string;
optionText: string;
needFile: boolean | null;
optionScore: number;
fullCount?: number;
};
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}
WHERE ${questions.deletedAt} IS NULL
)`;
// Sesuaikan tipe hasil query
const result: QuestionWithOptions[] = await db
.select({
aspectsId: aspects.id,
aspectsName: aspects.name,
subAspectId: subAspects.id,
subAspectName: subAspects.name,
questionId: questions.id,
questionText: questions.question,
optionId: options.id,
optionText: options.text,
needFile: questions.needFile,
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`);
// Definisikan tipe untuk hasil pengelompokan
type GroupedQuestion = {
questionId: string | null;
questionText: string | null;
needFile: boolean | null;
aspectsId: string | null;
aspectsName: string | null;
subAspectId: string | null;
subAspectName: string | null;
options: {
optionId: string;
optionText: string;
optionScore: number;
}[];
};
// Mengelompokkan berdasarkan questionId
const groupedResult: GroupedQuestion[] = result.reduce((acc, current) => {
const { questionId, questionText, needFile, aspectsId, aspectsName, subAspectId, subAspectName, optionId, optionText, optionScore } = current;
// Cek apakah questionId sudah ada dalam accumulator
const existingQuestion = acc.find(q => q.questionId === questionId);
if (existingQuestion) {
// Tambahkan opsi baru ke array options dari pertanyaan yang ada
existingQuestion.options.push({
optionId,
optionText,
optionScore
});
} else {
// Jika pertanyaan belum ada, tambahkan objek baru
acc.push({
questionId,
questionText,
needFile,
aspectsId,
aspectsName,
subAspectId,
subAspectName,
options: [
{
optionId,
optionText,
optionScore
}
]
});
}
return acc;
}, [] as GroupedQuestion[]); // Pastikan tipe untuk accumulator didefinisikan
return c.json({
data: groupedResult,
});
}
)
// 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
})
),
async (c) => {
const { assessmentId } = c.req.valid("query");
// Query to count total answers for the specific assessmentId
const totalCountQuery =
sql<number>`(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,
questionId: options.questionId,
optionId: answers.optionId,
isFlagged: answers.isFlagged,
filename: answers.filename,
validationInformation: answers.validationInformation,
fullCount: totalCountQuery,
})
.from(answers)
.leftJoin(options, eq(answers.optionId, options.id))
.where(
and(
eq(answers.assessmentId, assessmentId), // Filter by assessmentId
)
)
return c.json({
data: result.map((d) => ({ ...d, fullCount: undefined })),
});
}
)
.get(
"/getAllAnswers/:assessmentId", // Use :assessmentId in the URL path
checkPermission("assessments.readAnswers"),
async (c) => {
const assessmentId = c.req.param("assessmentId"); // Retrieve assessmentId from the URL
// Query to retrieve answers for the specific assessmentId, including the associated questionId
const result = await db
.select({
id: answers.id,
assessmentId: answers.assessmentId,
questionId: options.questionId, // Get the questionId from the options table
optionId: answers.optionId,
isFlagged: answers.isFlagged,
filename: answers.filename,
validationInformation: answers.validationInformation,
})
.from(answers)
.leftJoin(options, eq(answers.optionId, options.id)) // Join with the options table
.where(eq(answers.assessmentId, assessmentId)); // Filter by assessmentId
return c.json({
data: result,
});
}
)
// Toggles the isFlagged field between true and false
// .patch(
// "/:questionId/toggleFlag",
// checkPermission("assessments.toggleFlag"),
// async (c) => {
// const questionId = c.req.param("questionId");
// // Join answers and options to retrieve answer based on questionId
// const currentAnswer = await db
// .select({
// isFlagged: answers.isFlagged,
// answerId: answers.id,
// })
// .from(answers)
// .innerJoin(options, eq(answers.optionId, options.id))
// .where(eq(options.questionId, questionId))
// .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, currentAnswer[0].answerId))
// .returning();
// if (!updatedAnswer.length) {
// throw notFound({
// message: "Failed to update answer",
// });
// }
// return c.json(
// {
// message: "Answer flag toggled successfully",
// answer: updatedAnswer[0],
// },
// 200
// );
// }
// )
// Toggles the isFlagged field between true and false
.patch(
"/toggleFlag",
checkPermission("assessments.submitOption"),
requestValidator("json", flagFormSchema),
async (c) => {
const flagData = c.req.valid("json");
// Update jawaban yang ada berdasarkan assessmentId dan questionId
const answer = await db
.update(answers)
.set({
isFlagged: flagData.isFlagged, // Ubah ke pilihan baru
})
.where(
and(
eq(answers.assessmentId, flagData.assessmentId),
eq(answers.questionId, flagData.questionId)
)
)
.returning();
return c.json(
{
message: "Flag changed successfully",
answer: answer[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) => {
const contentType = c.req.header('content-type');
if (!contentType || !contentType.includes('multipart/form-data')) {
return c.json({ message: "Invalid Content-Type" }, 400);
}
const boundary = contentType.split('boundary=')[1];
if (!boundary) {
return c.json({ message: "Boundary not found" }, 400);
}
const body = await c.req.arrayBuffer();
const bodyString = Buffer.from(body).toString();
const parts = bodyString.split(`--${boundary}`);
let fileUrl: string | null = null;
for (const part of parts) {
if (part.includes('Content-Disposition: form-data;')) {
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');
const fileBuffer = Buffer.from(part.slice(fileContentStart, fileContentEnd), 'binary');
const filePath = path.join('files', `${Date.now()}-${fileName}`);
await saveFile(filePath, fileBuffer);
const assessmentId = c.req.query('assessmentId');
const questionId = c.req.query('questionId');
if (!assessmentId || !questionId) {
return c.json({ message: "assessmentId and questionId are required" }, 400);
}
const answerId = await findAnswerId(assessmentId, questionId);
if (!answerId) {
return c.json({ message: 'Answer not found' }, 404);
}
await updateFilename(answerId, path.basename(filePath));
fileUrl = `/files/${path.basename(filePath)}`;
}
}
}
if (!fileUrl) {
return c.json({ message: 'No file uploaded' }, 400);
}
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
);
}
)
// .post(
// "/submitOption",
// checkPermission("assessments.submitOption"),
// requestValidator("json", optionFormSchema),
// async (c) => {
// const optionData = c.req.valid("json");
// // Cek apakah jawaban sudah ada berdasarkan assessmentId dan questionId
// const existingAnswer = await db
// .select()
// .from(answers)
// .leftJoin(options, eq(answers.optionId, options.id))
// .leftJoin(questions, eq(options.questionId, questions.id))
// .where(
// sql`answers."assessmentId" = ${optionData.assessmentId}
// AND questions.id = ${optionData.questionId}`
// )
// .limit(1);
// let answer;
// if (existingAnswer.length > 0) {
// // Update jika jawaban sudah ada
// answer = await db
// .update(answers)
// .set({
// optionId: optionData.optionId, // Ubah ke pilihan baru
// })
// .where(
// sql`answers."assessmentId" = ${optionData.assessmentId}
// AND answers."optionId" IN (
// SELECT id FROM options WHERE "questionId" = ${optionData.questionId}
// )` // Mendapatkan optionId berdasarkan questionId
// )
// .returning();
// } else {
// // Insert jika belum ada jawaban
// answer = await db
// .insert(answers)
// .values({
// optionId: optionData.optionId,
// assessmentId: optionData.assessmentId,
// isFlagged: optionData.isFlagged ?? false,
// filename: optionData.filename ?? null,
// validationInformation: "", // Placeholder untuk not-null constraint
// })
// .returning();
// }
// return c.json(
// {
// message: "Option submitted successfully",
// answer: answer[0],
// },
// 201
// );
// }
// )
.post(
"/submitOption",
checkPermission("assessments.submitOption"),
requestValidator("json", optionFormSchema),
async (c) => {
const optionData = c.req.valid("json");
// Update jawaban yang ada berdasarkan assessmentId dan questionId
const answer = await db
.update(answers)
.set({
optionId: optionData.optionId, // Ubah ke pilihan baru
})
.where(
and(
eq(answers.assessmentId, optionData.assessmentId),
eq(answers.questionId, optionData.questionId)
)
)
.returning();
return c.json(
{
message: "Option submitted successfully",
answer: answer[0],
},
200
);
}
)
.post(
"/submitValidation",
checkPermission("assessments.submitValidation"),
requestValidator("json", validationFormSchema),
async (c) => {
const validationData = c.req.valid("json");
// Cek apakah jawaban ada berdasarkan assessmentId dan questionId
const existingAnswer = await db
.select()
.from(answers)
.leftJoin(options, eq(answers.optionId, options.id))
.leftJoin(questions, eq(options.questionId, questions.id))
.where(
sql`answers."assessmentId" = ${validationData.assessmentId}
AND questions.id = ${validationData.questionId}`
)
.limit(1);
if (existingAnswer.length === 0) {
return c.json(
{
message: "No existing answer found for the given assessmentId and questionId.",
},
404
);
}
// Dapatkan tanggal dan waktu saat ini
const currentDate = new Date();
// Update dengan melakukan JOIN yang sama
const updatedAnswer = await db
.update(answers)
.set({
validationInformation: validationData.validationInformation,
updatedAt: currentDate,
})
.where(
sql`answers."assessmentId" = ${validationData.assessmentId}
AND answers."optionId" IN (
SELECT id FROM options WHERE "questionId" = ${validationData.questionId}
)`
)
.returning();
return c.json(
{
message: "Validation information updated successfully",
answer: updatedAnswer[0],
},
200
);
}
)
.patch(
"/submitAssessment/:id",
checkPermission("assessments.submitAssessment"),
async (c) => {
const assessmentId = c.req.param("id");
const status = "belum diverifikasi";
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.",
});
}
)
// 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.readAssessmentScore"),
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.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(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,
aspectId: score.aspectId
}))
});
}
)
// Get data for One Aspect average score By Aspect Id and Assessment Id
.get(
"/average-score/aspects/:aspectId/assessments/:assessmentId",
checkPermission("assessments.readAssessmentScore"),
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 Aspects average score and all related Sub Aspects average score By Assessment Id
.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(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);
// 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(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);
// 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,
});
}
)
.patch(
"/updateOption",
checkPermission("assessments.submitOption"),
requestValidator("json", newOptionFormSchema),
async (c) => {
const optionData = c.req.valid("json");
// Temukan answerId yang sesuai 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" = ${optionData.assessmentId}
AND options."questionId" = ${optionData.questionId}`
)
.limit(1);
if (!targetAnswer) {
return c.json(
{ message: "Answer not found for given assessmentId and questionId" },
404
);
}
// Lakukan update pada answer_revisions menggunakan answerId yang ditemukan
const [updatedRevision] = await db
.update(answerRevisions)
.set({
newOptionId: optionData.newOptionId,
})
.where(sql`"answerId" = ${targetAnswer.id}`)
.returning();
return c.json(
{
message: "Revision updated successfully",
revision: updatedRevision, // Revisi yang baru saja diperbarui
},
200
);
}
)
.patch(
"/updateOption",
checkPermission("assessments.submitOption"),
requestValidator("json", newOptionFormSchema),
async (c) => {
const optionData = c.req.valid("json");
// Temukan answerId yang sesuai 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" = ${optionData.assessmentId}
AND options."questionId" = ${optionData.questionId}`
)
.limit(1);
if (!targetAnswer) {
return c.json(
{ message: "Answer not found for given assessmentId and questionId" },
404
);
}
// Lakukan update pada answer_revisions menggunakan answerId yang ditemukan
const [updatedRevision] = await db
.update(answerRevisions)
.set({
newOptionId: optionData.newOptionId,
})
.where(sql`"answerId" = ${targetAnswer.id}`)
.returning();
return c.json(
{
message: "Revision updated successfully",
revision: updatedRevision, // Revisi yang baru saja diperbarui
},
200
);
}
)
export default assessmentsRoute;