Pull Request branch dev-clone to main #1

Merged
gitea merged 429 commits from dev-clone into main 2024-12-23 09:31:34 +00:00
Showing only changes of commit 0f0f974c5e - Show all commits

View File

@ -1,4 +1,4 @@
import { and, eq, ilike, or, sql } from "drizzle-orm";
import { and, eq, ilike, isNull, inArray, or, sql } from "drizzle-orm";
import { Hono } from "hono";
import { z } from "zod";
import db from "../../drizzle";
@ -42,6 +42,133 @@ async function updateFilenameInDatabase(answerId: string, filename: string): Pro
const assessmentsRoute = new Hono<HonoEnv>()
.use(authInfo)
// Get all aspects
.get(
"/aspect",
// checkPermission("managementAspect.readAll"),
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",
@ -54,7 +181,7 @@ const assessmentsRoute = new Hono<HonoEnv>()
),
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({
@ -64,20 +191,34 @@ const assessmentsRoute = new Hono<HonoEnv>()
.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 =
// 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;
optionScore: number;
fullCount?: number;
};
const totalCountQuery =
sql<number>`(SELECT count(*)
FROM ${options}
LEFT JOIN ${questions} ON ${options.questionId} = ${questions.id}
@ -86,15 +227,16 @@ const assessmentsRoute = new Hono<HonoEnv>()
WHERE ${questions.deletedAt} IS NULL
)`;
const result = await db
// Sesuaikan tipe hasil query
const result: QuestionWithOptions[] = await db
.select({
optionId: options.id,
aspectsId: aspects.id,
aspectsName: aspects.name,
subAspectId: subAspects.id,
subAspectName: subAspects.name,
questionId: questions.id,
questionText: questions.question,
optionId: options.id,
optionText: options.text,
optionScore: options.score,
fullCount: totalCountQuery,
@ -103,15 +245,61 @@ const assessmentsRoute = new Hono<HonoEnv>()
.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`)
.where(sql`${questions.deletedAt} IS NULL`);
// Definisikan tipe untuk hasil pengelompokan
type GroupedQuestion = {
questionId: string | null;
questionText: string | 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, 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,
aspectsId,
aspectsName,
subAspectId,
subAspectName,
options: [
{
optionId,
optionText,
optionScore
}
]
});
}
return acc;
}, [] as GroupedQuestion[]); // Pastikan tipe untuk accumulator didefinisikan
return c.json({
data: result.map((d) => (
{
...d,
fullCount: undefined
}
)),
data: groupedResult,
});
}
)
@ -135,13 +323,13 @@ const assessmentsRoute = new Hono<HonoEnv>()
),
async (c) => {
const { assessmentId, page, limit, q } = c.req.valid("query");
// Query to count total answers for the specific assessmentId
const totalCountQuery =
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({
@ -159,16 +347,16 @@ const assessmentsRoute = new Hono<HonoEnv>()
eq(answers.assessmentId, assessmentId), // Filter by assessmentId
q
? or(
ilike(answers.filename, q),
ilike(answers.validationInformation, q),
eq(answers.id, q)
)
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: {
@ -185,48 +373,46 @@ const assessmentsRoute = new Hono<HonoEnv>()
// Toggles the isFlagged field between true and false
.patch(
"/:id/toggleFlag",
"/:questionId/toggleFlag",
checkPermission("assessments.toggleFlag"),
async (c) => {
const answerId = c.req.param("id");
// Retrieve the current state of isFlagged
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)
.where(eq(answers.id, answerId))
.innerJoin(options, eq(answers.optionId, options.id))
.where(eq(options.questionId, questionId))
.limit(1);
if (!currentAnswer.length) {
throw notFound(
{
message: "Answer not found",
}
)
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))
.where(eq(answers.id, currentAnswer[0].answerId))
.returning();
if (!updatedAnswer.length) {
throw notFound(
{
message: "Failed to update answer",
}
)
throw notFound({
message: "Failed to update answer",
});
}
return c.json(
{
message: "Answer flag toggled successfully",
@ -235,7 +421,7 @@ const assessmentsRoute = new Hono<HonoEnv>()
200
);
}
)
)
// Get data answers from table answers by optionId and assessmentId
.post(
@ -243,7 +429,7 @@ const assessmentsRoute = new Hono<HonoEnv>()
checkPermission("assessments.checkAnswer"),
async (c) => {
const { optionId, assessmentId } = await c.req.json();
const result = await db
.select()
.from(answers)
@ -251,24 +437,24 @@ const assessmentsRoute = new Hono<HonoEnv>()
and(eq(answers.optionId, optionId), eq(answers.assessmentId, assessmentId))
)
.execute();
const existingAnswer = result[0];
let response;
if (existingAnswer) {
response = {
exists: true,
answerId: existingAnswer.id
response = {
exists: true,
answerId: existingAnswer.id
};
} else {
response = {
exists: false
response = {
exists: false
};
}
return c.json(response);
}
)
)
// Upload filename to the table answers and save the file on the local storage
.post(
@ -282,7 +468,7 @@ const assessmentsRoute = new Hono<HonoEnv>()
message: "Invalid Content-Type",
});
}
// Extract boundary
const boundary = contentType.split('boundary=')[1];
if (!boundary) {
@ -290,16 +476,16 @@ const assessmentsRoute = new Hono<HonoEnv>()
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
@ -308,14 +494,14 @@ const assessmentsRoute = new Hono<HonoEnv>()
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) {
@ -323,29 +509,29 @@ const assessmentsRoute = new Hono<HonoEnv>()
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
{
success: true,
imageUrl: fileUrl
}
);
}
)
)
// Submit option to table answers from use-form in frontend
.post(
@ -384,7 +570,7 @@ const assessmentsRoute = new Hono<HonoEnv>()
async (c) => {
const answerId = c.req.param("id");
const answerData = c.req.valid("json");
const updatedAnswer = await db
.update(answers)
.set({
@ -392,27 +578,27 @@ const assessmentsRoute = new Hono<HonoEnv>()
})
.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],
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"),
'/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,
@ -427,7 +613,7 @@ const assessmentsRoute = new Hono<HonoEnv>()
sql`sub_aspects.id = ${subAspectId} AND assessments.id = ${assessmentId}`
)
.groupBy(subAspects.id);
return c.json({
subAspectId,
subAspectName: averageScore[0].subAspectName,
@ -437,13 +623,13 @@ const assessmentsRoute = new Hono<HonoEnv>()
}
)
// Get data for All Sub Aspects average score By Assessment Id
// Get data for All Sub Aspects average score By Assessment Id
.get(
'/average-score/sub-aspects/assessments/:assessmentId',
checkPermission("assessments.readAverageAllSubAspects"),
'/average-score/sub-aspects/assessments/:assessmentId',
// checkPermission("assessments.readAssessmentScore"),
async (c) => {
const { assessmentId } = c.req.param();
const averageScores = await db
.select({
subAspectId: subAspects.id,
@ -457,7 +643,7 @@ const assessmentsRoute = new Hono<HonoEnv>()
.innerJoin(assessments, eq(answers.assessmentId, assessments.id))
.where(eq(assessments.id, assessmentId))
.groupBy(subAspects.id);
return c.json({
assessmentId,
subAspects: averageScores.map(score => ({
@ -471,11 +657,11 @@ const assessmentsRoute = new Hono<HonoEnv>()
// Get data for One Aspect average score By Aspect Id and Assessment Id
.get(
"/average-score/aspects/:aspectId/assessments/:assessmentId",
checkPermission("assessments.readAverageAspect"),
"/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,
@ -491,7 +677,7 @@ const assessmentsRoute = new Hono<HonoEnv>()
sql`aspects.id = ${aspectId} AND assessments.id = ${assessmentId}`
)
.groupBy(aspects.id);
return c.json({
aspectId,
aspectName: averageScore[0].aspectName,
@ -503,11 +689,11 @@ const assessmentsRoute = new Hono<HonoEnv>()
// Get data for All Aspects average score By Assessment Id
.get(
'/average-score/aspects/assessments/:assessmentId',
checkPermission("assessments.readAverageAllAspects"),
'/average-score/aspects/assessments/:assessmentId',
// checkPermission("assessments.readAssessmentScore"),
async (c) => {
const { assessmentId } = c.req.param();
const averageScores = await db
.select({
AspectId: aspects.id,
@ -522,7 +708,7 @@ const assessmentsRoute = new Hono<HonoEnv>()
.innerJoin(assessments, eq(answers.assessmentId, assessments.id))
.where(eq(assessments.id, assessmentId))
.groupBy(aspects.id);
return c.json({
assessmentId,
aspects: averageScores.map(score => ({