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 */} + + + + + + ); +} 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" + > +
handleSubmit(values))}> + + {createInputComponents({ + disableAll: mutation.isPending, + readonlyAll: formType === "detail", + inputs: [ + { + type: "textarea", + label: "Question", + ...form.getInputProps("question"), + }, + formType === "detail" + ? { + type: "text", + label: "Need File", + readOnly: true, + value: form.values.needFile ? "Ya" : "Tidak", + } + : { + type: "checkbox", + label: "Need File", + ...form.getInputProps("needFile"), + }, + { + type: "text", + label: "Aspect Name", + readOnly: true, + ...form.getInputProps("aspectName"), + }, + { + type: "text", + label: "Sub-Aspect Name", + readOnly: true, + ...form.getInputProps("subAspectName"), + }, + ], + })} + + {/* Options */} + + {form.values.options.map((option, index) => ( + + + + {/* Render Trash Icon only if formType is 'create' or 'edit' */} + {formType !== "detail" && ( + handleRemoveOption(index)} + > + + + )} + + ))} + + {/* Render Add Option Button only if formType is 'create' or 'edit' */} + {formType !== "detail" && ( + + )} + + + {/* Buttons */} + + + {formType !== "detail" && ( + + )} + +
+
+ ); +} 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)); + }, +});