diff --git a/apps/frontend/src/modules/questionsManagement/modals/QuestionDeleteModal.tsx b/apps/frontend/src/modules/questionsManagement/modals/QuestionDeleteModal.tsx
new file mode 100644
index 0000000..9ce9da2
--- /dev/null
+++ b/apps/frontend/src/modules/questionsManagement/modals/QuestionDeleteModal.tsx
@@ -0,0 +1,99 @@
+import client from "@/honoClient";
+import { Button, Flex, Modal, Text } from "@mantine/core";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { getRouteApi, useSearch } from "@tanstack/react-router";
+import { deleteQuestion } from "../queries/questionQueries";
+import { notifications } from "@mantine/notifications";
+import fetchRPC from "@/utils/fetchRPC";
+
+const routeApi = getRouteApi("/_dashboardLayout/questions/");
+
+export default function QuestionDeleteModal() {
+ const queryClient = useQueryClient();
+
+ const searchParams = useSearch({ from: "/_dashboardLayout/questions/" }) as {
+ delete: string;
+ };
+
+ const questionId = searchParams.delete;
+ const navigate = routeApi.useNavigate();
+
+ const questionQuery = useQuery({
+ queryKey: ["questions", questionId],
+ queryFn: async () => {
+ if (!questionId) return null;
+ return await fetchRPC(
+ client.questions[":id"].$get({
+ param: {
+ id: questionId,
+ },
+ query: {},
+ })
+ );
+ },
+ });
+
+ const mutation = useMutation({
+ mutationKey: ["deleteQuestionMutation"],
+ mutationFn: async ({ id }: { id: string }) => {
+ return await deleteQuestion(id);
+ },
+ onError: (error: unknown) => {
+ if (error instanceof Error) {
+ notifications.show({
+ message: error.message,
+ color: "red",
+ });
+ }
+ },
+ onSuccess: () => {
+ notifications.show({
+ message: "Question deleted successfully.",
+ color: "green",
+ });
+ queryClient.removeQueries({ queryKey: ["question", questionId] });
+ queryClient.invalidateQueries({ queryKey: ["questions"] });
+ navigate({ search: {} });
+ },
+ });
+
+ const isModalOpen = Boolean(searchParams.delete && questionQuery.data);
+
+ return (
+ navigate({ search: {} })}
+ title={`Delete confirmation`}
+ >
+
+ Are you sure you want to delete question{" "}
+
+ "{questionQuery.data?.question}"
+
+ {" "}? This action is irreversible.
+
+
+ {/* {errorMessage && {errorMessage}} */}
+ {/* Buttons */}
+
+
+ }
+ type="submit"
+ color="red"
+ loading={mutation.isPending}
+ onClick={() => mutation.mutate({ id: questionId })}
+ >
+ Delete Question
+
+
+
+ );
+}
diff --git a/apps/frontend/src/modules/questionsManagement/modals/QuestionFormModal.tsx b/apps/frontend/src/modules/questionsManagement/modals/QuestionFormModal.tsx
new file mode 100644
index 0000000..486ee96
--- /dev/null
+++ b/apps/frontend/src/modules/questionsManagement/modals/QuestionFormModal.tsx
@@ -0,0 +1,261 @@
+import { useForm } from "@mantine/form";
+import {
+ Modal,
+ Stack,
+ Button,
+ Flex,
+ Avatar,
+ Center,
+ ScrollArea,
+ TextInput,
+ NumberInput,
+ Group,
+ ActionIcon,
+} from "@mantine/core";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { getRouteApi } from "@tanstack/react-router";
+import { TbDeviceFloppy, TbPlus, TbTrash } from "react-icons/tb";
+import { useEffect } from "react";
+import { notifications } from "@mantine/notifications";
+import FormResponseError from "@/errors/FormResponseError";
+import createInputComponents from "@/utils/createInputComponents";
+import stringToColorHex from "@/utils/stringToColorHex";
+import {
+ createQuestion,
+ getQuestionByIdQueryOptions,
+ updateQuestion,
+} from "../queries/questionQueries";
+
+/**
+ * Change this
+ */
+const routeApi = getRouteApi("/_dashboardLayout/questions/");
+
+export default function QuestionFormModal() {
+ /**
+ * DON'T CHANGE FOLLOWING:
+ */
+ const queryClient = useQueryClient();
+
+ const navigate = routeApi.useNavigate();
+
+ const searchParams = routeApi.useSearch();
+
+ const dataId = searchParams.detail || searchParams.edit;
+
+ const isModalOpen = Boolean(dataId || searchParams.create);
+
+ const detailId = searchParams.detail;
+ const editId = searchParams.edit;
+
+ const formType = detailId ? "detail" : editId ? "edit" : "create";
+
+ /**
+ * CHANGE FOLLOWING:
+ */
+ const questionQuery = useQuery(getQuestionByIdQueryOptions(dataId));
+
+ const modalTitle =
+ formType.charAt(0).toUpperCase() + formType.slice(1) + " Question";
+
+ const form = useForm({
+ initialValues: {
+ id: "",
+ question: "",
+ needFile: false,
+ aspectName: "",
+ subAspectName: "",
+ options: [] as { id: string; text: string; score: number }[],
+ },
+ });
+
+ useEffect(() => {
+ const data = questionQuery.data;
+
+ if (!data) {
+ form.reset();
+ return;
+ }
+
+ form.setValues({
+ id: data.id,
+ question: data.question ?? "",
+ needFile: data.needFile ?? false,
+ aspectName: data.aspectName ?? "",
+ subAspectName: data.subAspectName ?? "",
+ options: data.options ?? [],
+ });
+
+ form.setErrors({});
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [questionQuery.data]);
+
+ const mutation = useMutation({
+ mutationKey: ["questionsMutation"],
+ mutationFn: async (
+ options:
+ | { action: "edit"; data: Record }
+ | { action: "create"; data: Record }
+ ) => {
+ // return options.action === "edit"
+ // ? await updateQuestion(options.data)
+ // : await createQuestion(options.data);
+ },
+ onError: (error: unknown) => {
+ if (error instanceof FormResponseError) {
+ form.setErrors(error.formErrors);
+ return;
+ }
+
+ if (error instanceof Error) {
+ notifications.show({
+ message: error.message,
+ color: "red",
+ });
+ }
+ },
+ });
+
+ const handleSubmit = async (values: typeof form.values) => {
+ if (formType === "detail") return;
+
+ await mutation.mutateAsync({
+ action: formType,
+ data: {
+ id: values.id,
+ question: values.question,
+ needFile: values.needFile,
+ options: values.options.map((option) => ({
+ id: option.id,
+ text: option.text,
+ score: option.score,
+ })),
+ },
+ });
+
+ queryClient.invalidateQueries({ queryKey: ["questions"] });
+ notifications.show({
+ message: `The question is ${formType === "create" ? "created" : "edited"
+ }`,
+ });
+
+ navigate({ search: {} });
+ };
+
+ const handleAddOption = () => {
+ form.insertListItem("options", { id: "", text: "", score: 0 });
+ };
+
+ const handleRemoveOption = (index: number) => {
+ form.removeListItem("options", index);
+ };
+
+ return (
+ navigate({ search: {} })}
+ title={modalTitle}
+ scrollAreaComponent={ScrollArea.Autosize}
+ size="md"
+ >
+
+
+ );
+}
diff --git a/apps/frontend/src/modules/questionsManagement/queries/questionQueries.ts b/apps/frontend/src/modules/questionsManagement/queries/questionQueries.ts
new file mode 100644
index 0000000..1a0da8a
--- /dev/null
+++ b/apps/frontend/src/modules/questionsManagement/queries/questionQueries.ts
@@ -0,0 +1,68 @@
+import client from "@/honoClient";
+import fetchRPC from "@/utils/fetchRPC";
+import { queryOptions } from "@tanstack/react-query";
+import { InferRequestType } from "hono";
+
+export const questionQueryOptions = (page: number, limit: number, q?: string) =>
+ queryOptions({
+ queryKey: ["questions", { page, limit, q }],
+ queryFn: () =>
+ fetchRPC(
+ client.questions.$get({
+ query: {
+ limit: String(limit),
+ page: String(page),
+ q,
+ },
+ })
+ ),
+ });
+
+export const getQuestionByIdQueryOptions = (questionId: string | undefined) =>
+ queryOptions({
+ queryKey: ["question", questionId],
+ queryFn: () =>
+ fetchRPC(
+ client.questions[":id"].$get({
+ param: {
+ id: questionId!,
+ },
+ query: {},
+ })
+ ),
+ enabled: Boolean(questionId),
+ });
+
+export const createQuestion = async (
+ form: InferRequestType["form"]
+) => {
+ return await fetchRPC(
+ client.users.$post({
+ form,
+ })
+ );
+};
+
+export const updateQuestion = 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 deleteQuestion = async (id: string) => {
+ return await fetchRPC(
+ client.questions[":id"].$delete({
+ param: { id },
+ query: {},
+ })
+ );
+};
diff --git a/apps/frontend/src/routes/_dashboardLayout/questions/index.lazy.tsx b/apps/frontend/src/routes/_dashboardLayout/questions/index.lazy.tsx
new file mode 100644
index 0000000..1e97992
--- /dev/null
+++ b/apps/frontend/src/routes/_dashboardLayout/questions/index.lazy.tsx
@@ -0,0 +1,85 @@
+import { questionQueryOptions } from "@/modules/questionsManagement/queries/questionQueries";
+import PageTemplate from "@/components/PageTemplate";
+import { createLazyFileRoute } from "@tanstack/react-router";
+import ExtractQueryDataType from "@/types/ExtractQueryDataType";
+import { createColumnHelper } from "@tanstack/react-table";
+import { Badge, Flex } from "@mantine/core";
+import createActionButtons from "@/utils/createActionButton";
+import { TbEye, TbPencil, TbTrash } from "react-icons/tb";
+import QuestionDeleteModal from "@/modules/questionsManagement/modals/QuestionDeleteModal";
+import QuestionFormModal from "@/modules/questionsManagement/modals/QuestionFormModal";
+
+export const Route = createLazyFileRoute("/_dashboardLayout/questions/")({
+ component: QuestionsPage,
+});
+
+type DataType = ExtractQueryDataType;
+
+const columnHelper = createColumnHelper();
+
+export default function QuestionsPage() {
+ return (
+ , ]}
+ columnDefs={[
+ columnHelper.display({
+ header: "#",
+ cell: (props) => props.row.index + 1,
+ }),
+
+ columnHelper.display({
+ header: "Nama Aspek",
+ cell: (props) => props.row.original.aspectName,
+ }),
+
+ columnHelper.display({
+ header: "Nama Sub Aspek",
+ cell: (props) => props.row.original.subAspectName,
+ }),
+
+ columnHelper.display({
+ header: "Pertanyaan",
+ cell: (props) => props.row.original.question,
+ }),
+
+ columnHelper.display({
+ header: "Aksi",
+ cell: (props) => (
+
+ {createActionButtons([
+ {
+ label: "Detail",
+ permission: true,
+ action: `?detail=${props.row.original.id}`,
+ color: "green",
+ icon: ,
+ },
+ {
+ label: "Edit",
+ permission: true,
+ action: `?edit=${props.row.original.id}`,
+ color: "orange",
+ icon: ,
+ },
+ {
+ label: "Delete",
+ permission: true,
+ action: `?delete=${props.row.original.id}`,
+ color: "red",
+ icon: ,
+ },
+ ])}
+
+ ),
+ }),
+
+ columnHelper.display({
+ header: "Hasil Rata Rata",
+ // cell: (props) => props.row.original.question,
+ }),
+ ]}
+ />
+ );
+}
diff --git a/apps/frontend/src/routes/_dashboardLayout/questions/index.tsx b/apps/frontend/src/routes/_dashboardLayout/questions/index.tsx
new file mode 100644
index 0000000..a95dd4c
--- /dev/null
+++ b/apps/frontend/src/routes/_dashboardLayout/questions/index.tsx
@@ -0,0 +1,18 @@
+import { questionQueryOptions } from "@/modules/questionsManagement/queries/questionQueries";
+import { createFileRoute } from "@tanstack/react-router";
+import { z } from "zod";
+
+const searchParamSchema = z.object({
+ create: z.boolean().default(false).optional(),
+ edit: z.string().default("").optional(),
+ delete: z.string().default("").optional(),
+ detail: z.string().default("").optional(),
+});
+
+export const Route = createFileRoute("/_dashboardLayout/questions/")({
+ validateSearch: searchParamSchema,
+
+ loader: ({ context: { queryClient } }) => {
+ queryClient.ensureQueryData(questionQueryOptions(0, 10));
+ },
+});