From b30de1b6e3edc2ad8ef8dcadbed9048e792bd8f4 Mon Sep 17 00:00:00 2001 From: falendikategar Date: Thu, 15 Aug 2024 15:24:17 +0700 Subject: [PATCH 01/12] create: API for Assessments --- apps/backend/src/data/permissions.ts | 24 ++ apps/backend/src/routes/assessments/route.ts | 414 +++++++++++++++++++ 2 files changed, 438 insertions(+) create mode 100644 apps/backend/src/routes/assessments/route.ts diff --git a/apps/backend/src/data/permissions.ts b/apps/backend/src/data/permissions.ts index 1e91356..9490851 100644 --- a/apps/backend/src/data/permissions.ts +++ b/apps/backend/src/data/permissions.ts @@ -32,6 +32,30 @@ const permissionsData = [ { code: "roles.delete", }, + { + code: "assessments.readAssessmentScore", + }, + { + code: "assessments.readAllQuestions", + }, + { + code: "assessments.readAnswers", + }, + { + code: "assessments.toggleFlag", + }, + { + code: "assessments.checkAnswer", + }, + { + code: "assessments.uploadFile", + }, + { + code: "assessments.submitAnswer", + }, + { + code: "assessments.updateAnswer", + }, ] as const; export type SpecificPermissionCode = (typeof permissionsData)[number]["code"]; diff --git a/apps/backend/src/routes/assessments/route.ts b/apps/backend/src/routes/assessments/route.ts new file mode 100644 index 0000000..11b7a06 --- /dev/null +++ b/apps/backend/src/routes/assessments/route.ts @@ -0,0 +1,414 @@ +import { and, eq, ilike, or, sql } 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 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'; + +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), +}); + +export const answerUpdateSchema = answerFormSchema.partial(); + +// Helper function to save the file +async function saveFile(filePath: string, fileBuffer: Buffer): Promise { + await fs.promises.writeFile(filePath, fileBuffer); +} + +// Function to update the filename in the database +async function updateFilenameInDatabase(answerId: string, flname: string): Promise { + + await db.update(answers) + .set({ + filename: flname, + }) + .where(eq(answers.id, answerId)); +} + +const assessmentsRoute = new Hono() + .use(authInfo) + + // 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`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"), + requestValidator( + "query", + z.object({ + 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 { page, limit, q } = c.req.valid("query"); + + 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} + )`; + + 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, + optionText: options.text, + 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( + and( + q + ? or( + ilike(aspects.name, q), + ilike(subAspects.name, q), + ilike(questions.question, q), + ilike(options.text, q), + ilike(options.score, q), + eq(options.id, q), + + ) + : undefined + ) + ) + .offset(page * limit) + .limit(limit); + + return c.json({ + data: result.map((d) => ({ ...d, fullCount: undefined })), + _metadata: { + currentPage: page, + totalPages: Math.ceil( + (Number(result[0]?.fullCount) ?? 0) / limit + ), + totalItems: Number(result[0]?.fullCount) ?? 0, + perPage: limit, + }, + }); + } + ) + + // 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 + 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, page, limit, q } = c.req.valid("query"); + + // Query to count total answers for the specific assessmentId + const totalCountQuery = sql`(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, + optionId: answers.optionId, + isFlagged: answers.isFlagged, + filename: answers.filename, + validationInformation: answers.validationInformation, + fullCount: totalCountQuery, + }) + .from(answers) + .where( + and( + eq(answers.assessmentId, assessmentId), // Filter by assessmentId + q + ? or( + 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: { + currentPage: page, + totalPages: Math.ceil( + (Number(result[0]?.fullCount) ?? 0) / limit + ), + totalItems: Number(result[0]?.fullCount) ?? 0, + perPage: limit, + }, + }); + } + ) + + // Toggles the isFlagged field between true and false + .patch( + "/:id/toggleFlag", + checkPermission("assessments.toggleFlag"), + async (c) => { + const answerId = c.req.param("id"); + + // Retrieve the current state of isFlagged + const currentAnswer = await db + .select({ + isFlagged: answers.isFlagged, + }) + .from(answers) + .where(eq(answers.id, answerId)) + .limit(1); + + if (!currentAnswer.length) { + return c.json( + { + message: "Answer not found", + }, + 404 + ); + } + + // 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)) + .returning(); + + if (!updatedAnswer.length) { + return c.json( + { + message: "Failed to update answer", + }, + 500 + ); + } + + return c.json( + { + message: "Answer flag toggled successfully", + answer: updatedAnswer[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]; + + if (existingAnswer) { + return c.json({ exists: true, answerId: existingAnswer.id }); + } else { + return c.json({ exists: false }); + } + } + ) + + // Upload filename to the table answers and save the file on the local storage + .post( + "/uploadFile", + checkPermission("assessments.uploadFile"), + async (c) => { + // Get the Content-Type header + const contentType = c.req.header('content-type'); + if (!contentType || !contentType.includes('multipart/form-data')) { + return c.json({ success: false, message: 'Invalid Content-Type' }); + } + + // Extract boundary + const boundary = contentType.split('boundary=')[1]; + if (!boundary) { + return c.json({ success: false, 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}`); + + for (const part of parts) { + if (part.includes('Content-Disposition: form-data;')) { + // Extract file name + 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'); + + // 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) { + await updateFilenameInDatabase(answerId, path.basename(filePath)); + } + + // Return the file URL + const fileUrl = `/images/${path.basename(filePath)}`; + return c.json({ success: true, imageUrl: fileUrl }); + } + } + } + + return c.json({ success: false, message: 'No file uploaded' }); + } + ) + + // 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 + ); + } + ) + + // 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) { + return c.json( + { + message: "Answer not found or update failed", + }, + 404 + ); + } + + return c.json( + { + message: "Answer updated successfully", + answer: updatedAnswer[0], + }, + 200 + ); + } + ) + +export default assessmentsRoute; From 82ba52d5b452cdae7d03857ded7adbcbdeabe566 Mon Sep 17 00:00:00 2001 From: falendikategar Date: Thu, 15 Aug 2024 15:29:02 +0700 Subject: [PATCH 02/12] create: route for Assessments --- apps/backend/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index c0fbe56..380afe3 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -15,6 +15,7 @@ import DashboardError from "./errors/DashboardError"; import HonoEnv from "./types/HonoEnv"; import devRoutes from "./routes/dev/route"; import appEnv from "./appEnv"; +import assessmentsRoute from "./routes/assessments/route"; configDotenv(); @@ -78,6 +79,7 @@ const routes = app .route("/dashboard", dashboardRoutes) .route("/roles", rolesRoute) .route("/dev", devRoutes) + .route("/assessments", assessmentsRoute) .onError((err, c) => { if (err instanceof DashboardError) { return c.json( From 26b5b9228a84d4f01b5437a161d80d9742c084ce Mon Sep 17 00:00:00 2001 From: falendikategar Date: Fri, 16 Aug 2024 13:38:20 +0700 Subject: [PATCH 03/12] create: fetch data in assessmentsQueries to FE still error --- .../queries/assessmentsQueries.ts | 72 ++++++++++ .../assessments/index.lazy.tsx | 131 ++++++++++++++++++ .../_dashboardLayout/assessments/index.tsx | 5 + 3 files changed, 208 insertions(+) create mode 100644 apps/frontend/src/modules/assessmentsManagement/queries/assessmentsQueries.ts create mode 100644 apps/frontend/src/routes/_dashboardLayout/assessments/index.lazy.tsx create mode 100644 apps/frontend/src/routes/_dashboardLayout/assessments/index.tsx diff --git a/apps/frontend/src/modules/assessmentsManagement/queries/assessmentsQueries.ts b/apps/frontend/src/modules/assessmentsManagement/queries/assessmentsQueries.ts new file mode 100644 index 0000000..c15dd34 --- /dev/null +++ b/apps/frontend/src/modules/assessmentsManagement/queries/assessmentsQueries.ts @@ -0,0 +1,72 @@ +import client from "@/honoClient"; +import fetchRPC from "@/utils/fetchRPC"; +import { queryOptions } from "@tanstack/react-query"; +import { InferRequestType } from "hono"; + +export const questionsQueryOptions = (page: number, limit: number, q?: string) => + queryOptions({ + queryKey: ["questions", { page, limit, q }], + queryFn: async () => { + const result = await fetchRPC( + client.assessments.getAllQuestions.$get({ + query: { + limit: String(limit), + page: String(page), + q, + }, + }) + ); + console.log('Result from fetchRPC:', result); + return result; + }, + }); + + +export const getUserByIdQueryOptions = (userId: string | undefined) => + queryOptions({ + queryKey: ["user", userId], + queryFn: () => + fetchRPC( + client.users[":id"].$get({ + param: { + id: userId!, + }, + query: {}, + }) + ), + enabled: Boolean(userId), + }); + +export const createUser = async ( + form: InferRequestType["form"] +) => { + return await fetchRPC( + client.users.$post({ + form, + }) + ); +}; + +export const updateUser = async ( + form: InferRequestType<(typeof client.users)[":id"]["$patch"]>["form"] & { + id: string; + } +) => { + return await fetchRPC( + client.users[":id"].$patch({ + param: { + id: form.id, + }, + form, + }) + ); +}; + +export const deleteUser = async (id: string) => { + return await fetchRPC( + client.users[":id"].$delete({ + param: { id }, + form: {}, + }) + ); +}; diff --git a/apps/frontend/src/routes/_dashboardLayout/assessments/index.lazy.tsx b/apps/frontend/src/routes/_dashboardLayout/assessments/index.lazy.tsx new file mode 100644 index 0000000..15175be --- /dev/null +++ b/apps/frontend/src/routes/_dashboardLayout/assessments/index.lazy.tsx @@ -0,0 +1,131 @@ +import { createLazyFileRoute } from '@tanstack/react-router' + +import { Button } from "@/shadcn/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/shadcn/components/ui/card" +import { Label } from "@/shadcn/components/ui/label" +import { RadioGroup, RadioGroupItem } from "@/shadcn/components/ui/radio-group" +// import ExtractQueryDataType from '@/types/ExtractQueryDataType' +// import { createColumnHelper } from '@tanstack/react-table' +import { questionsQueryOptions } from "@/modules/assessmentsManagement/queries/assessmentsQueries"; +// import { useForm } from '@mantine/form' +import { useQuery } from '@tanstack/react-query' + +// type DataType = ExtractQueryDataType; +// const columnHelper = useForm(); + +export function CardWithAssessment() { + // const { handleSubmit, form } = useForm(); + + const { page = 1, limit = 10, q = '' } = {} + + const { data, isLoading, isError } = useQuery(questionsQueryOptions(page, limit, q)); + + console.log(data) + + if (isLoading) return
Loading...
; + if (isError) return
Error fetching data
; + + // const groupedQuestions = data?.reduce((acc, item) => { + // const { subAspectId, subAspectName } = item; + // if (!acc[subAspectId]) { + // acc[subAspectId] = { + // subAspectName, + // questions: [], + // }; + // } + // acc[subAspectId].questions.push(item); + // return acc; + // }, {} as Record); + + + const renderQuestions = () => { + return data?.data.map((subAspect) => ( +
+

{subAspect.subAspectName}

+ {data?.data.map((question) => ( +
+ + + {data?.data.map((option) => ( +
+ + +
+ ))} +
+
+ ))} +
+ )); + }; + + const renderQuestionNumbers = () => { + const questionNumbers: number[] = []; + data?.data.forEach((subAspect) => { + data?.data.forEach((question, index) => { + questionNumbers.push(index + 1); + }); + }); + + return questionNumbers.map((num) => ( + + )); + }; + + return ( + +
+ + Pertanyaan + + Bacalah pertanyaan dengan hati-hati dan jawablah dengan teliti! + + + +
+
+ {renderQuestions()} +
+
+
+ + + + +
+
+ + Daftar Nomor + + +
+
+ {renderQuestionNumbers()} +
+
+
+
+
+ ); +} + +export const Route = createLazyFileRoute('/_dashboardLayout/assessments/')({ + component: () => +
+

+ Halaman Assessments +

+ +
+}) \ No newline at end of file diff --git a/apps/frontend/src/routes/_dashboardLayout/assessments/index.tsx b/apps/frontend/src/routes/_dashboardLayout/assessments/index.tsx new file mode 100644 index 0000000..57a1b20 --- /dev/null +++ b/apps/frontend/src/routes/_dashboardLayout/assessments/index.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_dashboardLayout/assessments/')({ + component: () =>
Hello /_dashboardLayout/assessments/!
+}) \ No newline at end of file From 51d249f4c6f466ee42ef95ade704c597deabb849 Mon Sep 17 00:00:00 2001 From: falendikategar Date: Fri, 16 Aug 2024 14:10:02 +0700 Subject: [PATCH 04/12] Revert "create: fetch data in assessmentsQueries to FE still error" This reverts commit 26b5b9228a84d4f01b5437a161d80d9742c084ce. --- .../queries/assessmentsQueries.ts | 72 ---------- .../assessments/index.lazy.tsx | 131 ------------------ .../_dashboardLayout/assessments/index.tsx | 5 - 3 files changed, 208 deletions(-) delete mode 100644 apps/frontend/src/modules/assessmentsManagement/queries/assessmentsQueries.ts delete mode 100644 apps/frontend/src/routes/_dashboardLayout/assessments/index.lazy.tsx delete mode 100644 apps/frontend/src/routes/_dashboardLayout/assessments/index.tsx diff --git a/apps/frontend/src/modules/assessmentsManagement/queries/assessmentsQueries.ts b/apps/frontend/src/modules/assessmentsManagement/queries/assessmentsQueries.ts deleted file mode 100644 index c15dd34..0000000 --- a/apps/frontend/src/modules/assessmentsManagement/queries/assessmentsQueries.ts +++ /dev/null @@ -1,72 +0,0 @@ -import client from "@/honoClient"; -import fetchRPC from "@/utils/fetchRPC"; -import { queryOptions } from "@tanstack/react-query"; -import { InferRequestType } from "hono"; - -export const questionsQueryOptions = (page: number, limit: number, q?: string) => - queryOptions({ - queryKey: ["questions", { page, limit, q }], - queryFn: async () => { - const result = await fetchRPC( - client.assessments.getAllQuestions.$get({ - query: { - limit: String(limit), - page: String(page), - q, - }, - }) - ); - console.log('Result from fetchRPC:', result); - return result; - }, - }); - - -export const getUserByIdQueryOptions = (userId: string | undefined) => - queryOptions({ - queryKey: ["user", userId], - queryFn: () => - fetchRPC( - client.users[":id"].$get({ - param: { - id: userId!, - }, - query: {}, - }) - ), - enabled: Boolean(userId), - }); - -export const createUser = async ( - form: InferRequestType["form"] -) => { - return await fetchRPC( - client.users.$post({ - form, - }) - ); -}; - -export const updateUser = async ( - form: InferRequestType<(typeof client.users)[":id"]["$patch"]>["form"] & { - id: string; - } -) => { - return await fetchRPC( - client.users[":id"].$patch({ - param: { - id: form.id, - }, - form, - }) - ); -}; - -export const deleteUser = async (id: string) => { - return await fetchRPC( - client.users[":id"].$delete({ - param: { id }, - form: {}, - }) - ); -}; diff --git a/apps/frontend/src/routes/_dashboardLayout/assessments/index.lazy.tsx b/apps/frontend/src/routes/_dashboardLayout/assessments/index.lazy.tsx deleted file mode 100644 index 15175be..0000000 --- a/apps/frontend/src/routes/_dashboardLayout/assessments/index.lazy.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { createLazyFileRoute } from '@tanstack/react-router' - -import { Button } from "@/shadcn/components/ui/button" -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/shadcn/components/ui/card" -import { Label } from "@/shadcn/components/ui/label" -import { RadioGroup, RadioGroupItem } from "@/shadcn/components/ui/radio-group" -// import ExtractQueryDataType from '@/types/ExtractQueryDataType' -// import { createColumnHelper } from '@tanstack/react-table' -import { questionsQueryOptions } from "@/modules/assessmentsManagement/queries/assessmentsQueries"; -// import { useForm } from '@mantine/form' -import { useQuery } from '@tanstack/react-query' - -// type DataType = ExtractQueryDataType; -// const columnHelper = useForm(); - -export function CardWithAssessment() { - // const { handleSubmit, form } = useForm(); - - const { page = 1, limit = 10, q = '' } = {} - - const { data, isLoading, isError } = useQuery(questionsQueryOptions(page, limit, q)); - - console.log(data) - - if (isLoading) return
Loading...
; - if (isError) return
Error fetching data
; - - // const groupedQuestions = data?.reduce((acc, item) => { - // const { subAspectId, subAspectName } = item; - // if (!acc[subAspectId]) { - // acc[subAspectId] = { - // subAspectName, - // questions: [], - // }; - // } - // acc[subAspectId].questions.push(item); - // return acc; - // }, {} as Record); - - - const renderQuestions = () => { - return data?.data.map((subAspect) => ( -
-

{subAspect.subAspectName}

- {data?.data.map((question) => ( -
- - - {data?.data.map((option) => ( -
- - -
- ))} -
-
- ))} -
- )); - }; - - const renderQuestionNumbers = () => { - const questionNumbers: number[] = []; - data?.data.forEach((subAspect) => { - data?.data.forEach((question, index) => { - questionNumbers.push(index + 1); - }); - }); - - return questionNumbers.map((num) => ( - - )); - }; - - return ( - -
- - Pertanyaan - - Bacalah pertanyaan dengan hati-hati dan jawablah dengan teliti! - - - -
-
- {renderQuestions()} -
-
-
- - - - -
-
- - Daftar Nomor - - -
-
- {renderQuestionNumbers()} -
-
-
-
-
- ); -} - -export const Route = createLazyFileRoute('/_dashboardLayout/assessments/')({ - component: () => -
-

- Halaman Assessments -

- -
-}) \ No newline at end of file diff --git a/apps/frontend/src/routes/_dashboardLayout/assessments/index.tsx b/apps/frontend/src/routes/_dashboardLayout/assessments/index.tsx deleted file mode 100644 index 57a1b20..0000000 --- a/apps/frontend/src/routes/_dashboardLayout/assessments/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' - -export const Route = createFileRoute('/_dashboardLayout/assessments/')({ - component: () =>
Hello /_dashboardLayout/assessments/!
-}) \ No newline at end of file From 208a1a1fffa1f415570fe6b437fe3e4a983039be Mon Sep 17 00:00:00 2001 From: falendikategar Date: Fri, 16 Aug 2024 14:10:33 +0700 Subject: [PATCH 05/12] Reapply "create: fetch data in assessmentsQueries to FE still error" This reverts commit 51d249f4c6f466ee42ef95ade704c597deabb849. --- .../queries/assessmentsQueries.ts | 72 ++++++++++ .../assessments/index.lazy.tsx | 131 ++++++++++++++++++ .../_dashboardLayout/assessments/index.tsx | 5 + 3 files changed, 208 insertions(+) create mode 100644 apps/frontend/src/modules/assessmentsManagement/queries/assessmentsQueries.ts create mode 100644 apps/frontend/src/routes/_dashboardLayout/assessments/index.lazy.tsx create mode 100644 apps/frontend/src/routes/_dashboardLayout/assessments/index.tsx diff --git a/apps/frontend/src/modules/assessmentsManagement/queries/assessmentsQueries.ts b/apps/frontend/src/modules/assessmentsManagement/queries/assessmentsQueries.ts new file mode 100644 index 0000000..c15dd34 --- /dev/null +++ b/apps/frontend/src/modules/assessmentsManagement/queries/assessmentsQueries.ts @@ -0,0 +1,72 @@ +import client from "@/honoClient"; +import fetchRPC from "@/utils/fetchRPC"; +import { queryOptions } from "@tanstack/react-query"; +import { InferRequestType } from "hono"; + +export const questionsQueryOptions = (page: number, limit: number, q?: string) => + queryOptions({ + queryKey: ["questions", { page, limit, q }], + queryFn: async () => { + const result = await fetchRPC( + client.assessments.getAllQuestions.$get({ + query: { + limit: String(limit), + page: String(page), + q, + }, + }) + ); + console.log('Result from fetchRPC:', result); + return result; + }, + }); + + +export const getUserByIdQueryOptions = (userId: string | undefined) => + queryOptions({ + queryKey: ["user", userId], + queryFn: () => + fetchRPC( + client.users[":id"].$get({ + param: { + id: userId!, + }, + query: {}, + }) + ), + enabled: Boolean(userId), + }); + +export const createUser = async ( + form: InferRequestType["form"] +) => { + return await fetchRPC( + client.users.$post({ + form, + }) + ); +}; + +export const updateUser = async ( + form: InferRequestType<(typeof client.users)[":id"]["$patch"]>["form"] & { + id: string; + } +) => { + return await fetchRPC( + client.users[":id"].$patch({ + param: { + id: form.id, + }, + form, + }) + ); +}; + +export const deleteUser = async (id: string) => { + return await fetchRPC( + client.users[":id"].$delete({ + param: { id }, + form: {}, + }) + ); +}; diff --git a/apps/frontend/src/routes/_dashboardLayout/assessments/index.lazy.tsx b/apps/frontend/src/routes/_dashboardLayout/assessments/index.lazy.tsx new file mode 100644 index 0000000..15175be --- /dev/null +++ b/apps/frontend/src/routes/_dashboardLayout/assessments/index.lazy.tsx @@ -0,0 +1,131 @@ +import { createLazyFileRoute } from '@tanstack/react-router' + +import { Button } from "@/shadcn/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/shadcn/components/ui/card" +import { Label } from "@/shadcn/components/ui/label" +import { RadioGroup, RadioGroupItem } from "@/shadcn/components/ui/radio-group" +// import ExtractQueryDataType from '@/types/ExtractQueryDataType' +// import { createColumnHelper } from '@tanstack/react-table' +import { questionsQueryOptions } from "@/modules/assessmentsManagement/queries/assessmentsQueries"; +// import { useForm } from '@mantine/form' +import { useQuery } from '@tanstack/react-query' + +// type DataType = ExtractQueryDataType; +// const columnHelper = useForm(); + +export function CardWithAssessment() { + // const { handleSubmit, form } = useForm(); + + const { page = 1, limit = 10, q = '' } = {} + + const { data, isLoading, isError } = useQuery(questionsQueryOptions(page, limit, q)); + + console.log(data) + + if (isLoading) return
Loading...
; + if (isError) return
Error fetching data
; + + // const groupedQuestions = data?.reduce((acc, item) => { + // const { subAspectId, subAspectName } = item; + // if (!acc[subAspectId]) { + // acc[subAspectId] = { + // subAspectName, + // questions: [], + // }; + // } + // acc[subAspectId].questions.push(item); + // return acc; + // }, {} as Record); + + + const renderQuestions = () => { + return data?.data.map((subAspect) => ( +
+

{subAspect.subAspectName}

+ {data?.data.map((question) => ( +
+ + + {data?.data.map((option) => ( +
+ + +
+ ))} +
+
+ ))} +
+ )); + }; + + const renderQuestionNumbers = () => { + const questionNumbers: number[] = []; + data?.data.forEach((subAspect) => { + data?.data.forEach((question, index) => { + questionNumbers.push(index + 1); + }); + }); + + return questionNumbers.map((num) => ( + + )); + }; + + return ( + +
+ + Pertanyaan + + Bacalah pertanyaan dengan hati-hati dan jawablah dengan teliti! + + + +
+
+ {renderQuestions()} +
+
+
+ + + + +
+
+ + Daftar Nomor + + +
+
+ {renderQuestionNumbers()} +
+
+
+
+
+ ); +} + +export const Route = createLazyFileRoute('/_dashboardLayout/assessments/')({ + component: () => +
+

+ Halaman Assessments +

+ +
+}) \ No newline at end of file diff --git a/apps/frontend/src/routes/_dashboardLayout/assessments/index.tsx b/apps/frontend/src/routes/_dashboardLayout/assessments/index.tsx new file mode 100644 index 0000000..57a1b20 --- /dev/null +++ b/apps/frontend/src/routes/_dashboardLayout/assessments/index.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_dashboardLayout/assessments/')({ + component: () =>
Hello /_dashboardLayout/assessments/!
+}) \ No newline at end of file From 1b545abf5fdb585933305d3462e8ee6b64498c80 Mon Sep 17 00:00:00 2001 From: falendikategar Date: Fri, 16 Aug 2024 14:14:27 +0700 Subject: [PATCH 06/12] Revert "Reapply "create: fetch data in assessmentsQueries to FE still error"" This reverts commit 208a1a1fffa1f415570fe6b437fe3e4a983039be. --- .../queries/assessmentsQueries.ts | 72 ---------- .../assessments/index.lazy.tsx | 131 ------------------ .../_dashboardLayout/assessments/index.tsx | 5 - 3 files changed, 208 deletions(-) delete mode 100644 apps/frontend/src/modules/assessmentsManagement/queries/assessmentsQueries.ts delete mode 100644 apps/frontend/src/routes/_dashboardLayout/assessments/index.lazy.tsx delete mode 100644 apps/frontend/src/routes/_dashboardLayout/assessments/index.tsx diff --git a/apps/frontend/src/modules/assessmentsManagement/queries/assessmentsQueries.ts b/apps/frontend/src/modules/assessmentsManagement/queries/assessmentsQueries.ts deleted file mode 100644 index c15dd34..0000000 --- a/apps/frontend/src/modules/assessmentsManagement/queries/assessmentsQueries.ts +++ /dev/null @@ -1,72 +0,0 @@ -import client from "@/honoClient"; -import fetchRPC from "@/utils/fetchRPC"; -import { queryOptions } from "@tanstack/react-query"; -import { InferRequestType } from "hono"; - -export const questionsQueryOptions = (page: number, limit: number, q?: string) => - queryOptions({ - queryKey: ["questions", { page, limit, q }], - queryFn: async () => { - const result = await fetchRPC( - client.assessments.getAllQuestions.$get({ - query: { - limit: String(limit), - page: String(page), - q, - }, - }) - ); - console.log('Result from fetchRPC:', result); - return result; - }, - }); - - -export const getUserByIdQueryOptions = (userId: string | undefined) => - queryOptions({ - queryKey: ["user", userId], - queryFn: () => - fetchRPC( - client.users[":id"].$get({ - param: { - id: userId!, - }, - query: {}, - }) - ), - enabled: Boolean(userId), - }); - -export const createUser = async ( - form: InferRequestType["form"] -) => { - return await fetchRPC( - client.users.$post({ - form, - }) - ); -}; - -export const updateUser = async ( - form: InferRequestType<(typeof client.users)[":id"]["$patch"]>["form"] & { - id: string; - } -) => { - return await fetchRPC( - client.users[":id"].$patch({ - param: { - id: form.id, - }, - form, - }) - ); -}; - -export const deleteUser = async (id: string) => { - return await fetchRPC( - client.users[":id"].$delete({ - param: { id }, - form: {}, - }) - ); -}; diff --git a/apps/frontend/src/routes/_dashboardLayout/assessments/index.lazy.tsx b/apps/frontend/src/routes/_dashboardLayout/assessments/index.lazy.tsx deleted file mode 100644 index 15175be..0000000 --- a/apps/frontend/src/routes/_dashboardLayout/assessments/index.lazy.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { createLazyFileRoute } from '@tanstack/react-router' - -import { Button } from "@/shadcn/components/ui/button" -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/shadcn/components/ui/card" -import { Label } from "@/shadcn/components/ui/label" -import { RadioGroup, RadioGroupItem } from "@/shadcn/components/ui/radio-group" -// import ExtractQueryDataType from '@/types/ExtractQueryDataType' -// import { createColumnHelper } from '@tanstack/react-table' -import { questionsQueryOptions } from "@/modules/assessmentsManagement/queries/assessmentsQueries"; -// import { useForm } from '@mantine/form' -import { useQuery } from '@tanstack/react-query' - -// type DataType = ExtractQueryDataType; -// const columnHelper = useForm(); - -export function CardWithAssessment() { - // const { handleSubmit, form } = useForm(); - - const { page = 1, limit = 10, q = '' } = {} - - const { data, isLoading, isError } = useQuery(questionsQueryOptions(page, limit, q)); - - console.log(data) - - if (isLoading) return
Loading...
; - if (isError) return
Error fetching data
; - - // const groupedQuestions = data?.reduce((acc, item) => { - // const { subAspectId, subAspectName } = item; - // if (!acc[subAspectId]) { - // acc[subAspectId] = { - // subAspectName, - // questions: [], - // }; - // } - // acc[subAspectId].questions.push(item); - // return acc; - // }, {} as Record); - - - const renderQuestions = () => { - return data?.data.map((subAspect) => ( -
-

{subAspect.subAspectName}

- {data?.data.map((question) => ( -
- - - {data?.data.map((option) => ( -
- - -
- ))} -
-
- ))} -
- )); - }; - - const renderQuestionNumbers = () => { - const questionNumbers: number[] = []; - data?.data.forEach((subAspect) => { - data?.data.forEach((question, index) => { - questionNumbers.push(index + 1); - }); - }); - - return questionNumbers.map((num) => ( - - )); - }; - - return ( - -
- - Pertanyaan - - Bacalah pertanyaan dengan hati-hati dan jawablah dengan teliti! - - - -
-
- {renderQuestions()} -
-
-
- - - - -
-
- - Daftar Nomor - - -
-
- {renderQuestionNumbers()} -
-
-
-
-
- ); -} - -export const Route = createLazyFileRoute('/_dashboardLayout/assessments/')({ - component: () => -
-

- Halaman Assessments -

- -
-}) \ No newline at end of file diff --git a/apps/frontend/src/routes/_dashboardLayout/assessments/index.tsx b/apps/frontend/src/routes/_dashboardLayout/assessments/index.tsx deleted file mode 100644 index 57a1b20..0000000 --- a/apps/frontend/src/routes/_dashboardLayout/assessments/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' - -export const Route = createFileRoute('/_dashboardLayout/assessments/')({ - component: () =>
Hello /_dashboardLayout/assessments/!
-}) \ No newline at end of file From 7a9980ec73b89b98bfead1c200e8343859b1881c Mon Sep 17 00:00:00 2001 From: falendikategar Date: Fri, 16 Aug 2024 14:18:57 +0700 Subject: [PATCH 07/12] update: route in assessments --- apps/backend/src/routes/assessments/route.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/backend/src/routes/assessments/route.ts b/apps/backend/src/routes/assessments/route.ts index 11b7a06..95333c0 100644 --- a/apps/backend/src/routes/assessments/route.ts +++ b/apps/backend/src/routes/assessments/route.ts @@ -412,3 +412,4 @@ const assessmentsRoute = new Hono() ) export default assessmentsRoute; + From ab37456906115457e450f49589f29341bd057cbe Mon Sep 17 00:00:00 2001 From: falendikategar Date: Tue, 20 Aug 2024 12:24:28 +0700 Subject: [PATCH 08/12] fix: revise route for Assessments --- apps/backend/src/routes/assessments/route.ts | 170 ++++++++++--------- 1 file changed, 87 insertions(+), 83 deletions(-) diff --git a/apps/backend/src/routes/assessments/route.ts b/apps/backend/src/routes/assessments/route.ts index 95333c0..fea0cb6 100644 --- a/apps/backend/src/routes/assessments/route.ts +++ b/apps/backend/src/routes/assessments/route.ts @@ -13,6 +13,7 @@ import authInfo from "../../middlewares/authInfo"; import checkPermission from "../../middlewares/checkPermission"; import path from "path"; import fs from 'fs'; +import { notFound } from "../../errors/DashboardError"; export const answerFormSchema = z.object({ optionId: z.string().min(1), @@ -30,12 +31,10 @@ async function saveFile(filePath: string, fileBuffer: Buffer): Promise { } // Function to update the filename in the database -async function updateFilenameInDatabase(answerId: string, flname: string): Promise { +async function updateFilenameInDatabase(answerId: string, filename: string): Promise { await db.update(answers) - .set({ - filename: flname, - }) + .set({ filename }) .where(eq(answers.id, answerId)); } @@ -76,23 +75,14 @@ const assessmentsRoute = new Hono() .get( "/getAllQuestions", checkPermission("assessments.readAllQuestions"), - requestValidator( - "query", - z.object({ - 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 { page, limit, q } = c.req.valid("query"); - 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} + WHERE ${questions.deletedAt} IS NULL )`; const result = await db @@ -112,34 +102,15 @@ const assessmentsRoute = new Hono() .leftJoin(questions, eq(options.questionId, questions.id)) .leftJoin(subAspects, eq(questions.subAspectId, subAspects.id)) .leftJoin(aspects, eq(subAspects.aspectId, aspects.id)) - .where( - and( - q - ? or( - ilike(aspects.name, q), - ilike(subAspects.name, q), - ilike(questions.question, q), - ilike(options.text, q), - ilike(options.score, q), - eq(options.id, q), - - ) - : undefined - ) - ) - .offset(page * limit) - .limit(limit); + .where(sql`${questions.deletedAt} IS NULL`) return c.json({ - data: result.map((d) => ({ ...d, fullCount: undefined })), - _metadata: { - currentPage: page, - totalPages: Math.ceil( - (Number(result[0]?.fullCount) ?? 0) / limit - ), - totalItems: Number(result[0]?.fullCount) ?? 0, - perPage: limit, - }, + data: result.map((d) => ( + { + ...d, + fullCount: undefined + } + )), }); } ) @@ -165,7 +136,10 @@ const assessmentsRoute = new Hono() const { assessmentId, page, limit, q } = c.req.valid("query"); // Query to count total answers for the specific assessmentId - const totalCountQuery = sql`(SELECT count(*) FROM ${answers} WHERE ${answers.assessmentId} = ${assessmentId})`; + const totalCountQuery = + sql`(SELECT count(*) + FROM ${answers} + WHERE ${answers.assessmentId} = ${assessmentId})`; // Query to retrieve answers for the specific assessmentId const result = await db @@ -225,12 +199,11 @@ const assessmentsRoute = new Hono() .limit(1); if (!currentAnswer.length) { - return c.json( + throw notFound( { message: "Answer not found", - }, - 404 - ); + } + ) } // Toggle the isFlagged value @@ -246,12 +219,11 @@ const assessmentsRoute = new Hono() .returning(); if (!updatedAnswer.length) { - return c.json( + throw notFound( { message: "Failed to update answer", - }, - 500 - ); + } + ) } return c.json( @@ -270,7 +242,7 @@ const assessmentsRoute = new Hono() checkPermission("assessments.checkAnswer"), async (c) => { const { optionId, assessmentId } = await c.req.json(); - + const result = await db .select() .from(answers) @@ -278,16 +250,24 @@ const assessmentsRoute = new Hono() and(eq(answers.optionId, optionId), eq(answers.assessmentId, assessmentId)) ) .execute(); - + const existingAnswer = result[0]; - + let response; + if (existingAnswer) { - return c.json({ exists: true, answerId: existingAnswer.id }); + response = { + exists: true, + answerId: existingAnswer.id + }; } else { - return c.json({ exists: false }); + response = { + exists: false + }; } + + return c.json(response); } - ) + ) // Upload filename to the table answers and save the file on the local storage .post( @@ -297,22 +277,28 @@ const assessmentsRoute = new Hono() // Get the Content-Type header const contentType = c.req.header('content-type'); if (!contentType || !contentType.includes('multipart/form-data')) { - return c.json({ success: false, message: 'Invalid Content-Type' }); + throw notFound({ + message: "Invalid Content-Type", + }); } // Extract boundary const boundary = contentType.split('boundary=')[1]; if (!boundary) { - return c.json({ success: false, message: 'Boundary not found' }); + throw notFound({ + 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 @@ -321,30 +307,44 @@ const assessmentsRoute = new Hono() 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) { - await updateFilenameInDatabase(answerId, path.basename(filePath)); + if (!answerId) { + throw notFound({ + message: "answerId is required", + }); } - - // Return the file URL - const fileUrl = `/images/${path.basename(filePath)}`; - return c.json({ success: true, imageUrl: fileUrl }); + + await updateFilenameInDatabase(answerId, path.basename(filePath)); + + // Set the file URL for the final response + fileUrl = `/images/${path.basename(filePath)}`; } } } - return c.json({ success: false, message: 'No file uploaded' }); + if (!fileUrl) { + throw notFound({ + message: 'No file uploaded', + }); + } + + return c.json( + { + success: true, + imageUrl: fileUrl + } + ); } - ) + ) // Submit option to table answers from use-form in frontend .post( @@ -384,6 +384,8 @@ const assessmentsRoute = new Hono() const answerId = c.req.param("id"); const answerData = c.req.valid("json"); + let response; + let statusCode; const updatedAnswer = await db .update(answers) .set({ @@ -393,23 +395,25 @@ const assessmentsRoute = new Hono() .returning(); if (!updatedAnswer.length) { - return c.json( - { - message: "Answer not found or update failed", - }, - 404 - ); + response = { + message: "Answer not found or update failed", + }; + statusCode = 404; + } else { + response = { + message: "Answer updated successfully", + answer: updatedAnswer[0], + }; + statusCode = 200; } return c.json( - { - message: "Answer updated successfully", - answer: updatedAnswer[0], - }, - 200 + response, + { + status: statusCode + } ); } - ) + ) export default assessmentsRoute; - From c0e28b1019635b302c653b7f395daa5e93a8cad1 Mon Sep 17 00:00:00 2001 From: falendikategar Date: Tue, 20 Aug 2024 17:25:58 +0700 Subject: [PATCH 09/12] fix: revise dashboardError for updateAnswer on Assessments --- apps/backend/src/routes/assessments/route.ts | 29 ++++++-------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/apps/backend/src/routes/assessments/route.ts b/apps/backend/src/routes/assessments/route.ts index fea0cb6..02781f5 100644 --- a/apps/backend/src/routes/assessments/route.ts +++ b/apps/backend/src/routes/assessments/route.ts @@ -383,9 +383,7 @@ const assessmentsRoute = new Hono() async (c) => { const answerId = c.req.param("id"); const answerData = c.req.valid("json"); - - let response; - let statusCode; + const updatedAnswer = await db .update(answers) .set({ @@ -395,24 +393,15 @@ const assessmentsRoute = new Hono() .returning(); if (!updatedAnswer.length) { - response = { - message: "Answer not found or update failed", - }; - statusCode = 404; - } else { - response = { - message: "Answer updated successfully", - answer: updatedAnswer[0], - }; - statusCode = 200; + throw notFound({ + message: "Answer not found or update failed" + }) } - - return c.json( - response, - { - status: statusCode - } - ); + + return c.json({ + message: "Answer updated successfully", + answer: updatedAnswer[0], + }); } ) From 68eb7468e99811396221c118acaea680668ce575 Mon Sep 17 00:00:00 2001 From: abiyasa05 Date: Thu, 22 Aug 2024 15:00:05 +0700 Subject: [PATCH 10/12] update: status and validatedBy column --- apps/backend/src/drizzle/schema/assessments.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/backend/src/drizzle/schema/assessments.ts b/apps/backend/src/drizzle/schema/assessments.ts index 44eb4ed..14bbc12 100644 --- a/apps/backend/src/drizzle/schema/assessments.ts +++ b/apps/backend/src/drizzle/schema/assessments.ts @@ -4,7 +4,7 @@ import { relations } from "drizzle-orm"; import { respondents } from "./respondents"; import { users } from "./users"; -const statusEnum = pgEnum("status", ["tertunda", "disetujui", "ditolak", "selesai"]); +const statusEnum = pgEnum("status", ["menunggu konfirmasi", "disetujui", "ditolak", "selesai"]); export const assessments = pgTable("assessments", { id: varchar("id", { length: 50 }) @@ -14,10 +14,9 @@ export const assessments = pgTable("assessments", { status: statusEnum("status"), reviewedBy: varchar("reviewedBy"), reviewedAt: timestamp("reviewedAt", { mode: "date" }), - validatedBy: varchar("validatedBy").notNull(), + validatedBy: varchar("validatedBy"), validatedAt: timestamp("validatedAt", { mode: "date" }), createdAt: timestamp("createdAt", { mode: "date" }).defaultNow(), - }); // Query Tools in PosgreSQL // CREATE TYPE status AS ENUM ('tertunda', 'disetujui', 'ditolak', 'selesai'); \ No newline at end of file From d22e4c01612d761fa044566fed8236f8f360a42d Mon Sep 17 00:00:00 2001 From: abiyasa05 Date: Thu, 22 Aug 2024 15:23:59 +0700 Subject: [PATCH 11/12] update: Query Tools for status --- apps/backend/src/drizzle/schema/assessments.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/drizzle/schema/assessments.ts b/apps/backend/src/drizzle/schema/assessments.ts index 14bbc12..75b9ccd 100644 --- a/apps/backend/src/drizzle/schema/assessments.ts +++ b/apps/backend/src/drizzle/schema/assessments.ts @@ -19,4 +19,4 @@ export const assessments = pgTable("assessments", { createdAt: timestamp("createdAt", { mode: "date" }).defaultNow(), }); // Query Tools in PosgreSQL -// CREATE TYPE status AS ENUM ('tertunda', 'disetujui', 'ditolak', 'selesai'); \ No newline at end of file +// CREATE TYPE status AS ENUM ('menunggu konfirmasi', 'disetujui', 'ditolak', 'selesai'); \ No newline at end of file From 5a2fb8126173e88b6c4d74d26cd88eb164a91899 Mon Sep 17 00:00:00 2001 From: falendikategar Date: Fri, 30 Aug 2024 14:22:35 +0700 Subject: [PATCH 12/12] update: add Endpoint to Calculate Average Score By Sub Aspect and Aspect --- apps/backend/src/routes/assessments/route.ts | 130 ++++++++++++++++++- 1 file changed, 129 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/routes/assessments/route.ts b/apps/backend/src/routes/assessments/route.ts index 02781f5..604c903 100644 --- a/apps/backend/src/routes/assessments/route.ts +++ b/apps/backend/src/routes/assessments/route.ts @@ -7,6 +7,7 @@ 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"; @@ -403,6 +404,133 @@ const assessmentsRoute = new Hono() 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({ + 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 + })) + }); + } + ) + + // Get data for One Aspect average score By Aspect Id and Assessment Id + .get( + "/average-score/aspects/:aspectId/assessments/:assessmentId", + 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 All Aspects average score By Assessment Id + .get( + '/average-score/aspects/assessments/:assessmentId', + // checkPermission("assessments.readAssessmentScore"), + async (c) => { + const { assessmentId } = c.req.param(); + + const averageScores = await db + .select({ + AspectId: aspects.id, + 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(eq(assessments.id, assessmentId)) + .groupBy(aspects.id); + + return c.json({ + assessmentId, + aspects: averageScores.map(score => ({ + AspectId: score.AspectId, + AspectName: score.AspectName, + averageScore: score.average + })) + }); + } + ) export default assessmentsRoute;