diff --git a/apps/frontend/src/modules/questionsManagement/modals/QuestionFormModal.tsx b/apps/frontend/src/modules/questionsManagement/modals/QuestionFormModal.tsx index 486ee96..a66dee2 100644 --- a/apps/frontend/src/modules/questionsManagement/modals/QuestionFormModal.tsx +++ b/apps/frontend/src/modules/questionsManagement/modals/QuestionFormModal.tsx @@ -4,13 +4,12 @@ import { Stack, Button, Flex, - Avatar, - Center, + ActionIcon, + Select, ScrollArea, TextInput, NumberInput, Group, - ActionIcon, } from "@mantine/core"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { getRouteApi } from "@tanstack/react-router"; @@ -24,6 +23,8 @@ import { createQuestion, getQuestionByIdQueryOptions, updateQuestion, + fetchAspects, // Import fetchAspects query + fetchSubAspects, // Import fetchSubAspects query } from "../queries/questionQueries"; /** @@ -31,44 +32,71 @@ import { */ const routeApi = getRouteApi("/_dashboardLayout/questions/"); +interface Option { + questionId: string; + text: string; + score: number; +} + +interface CreateQuestionPayload { + id?: string; // Make id optional + subAspectId: string; // Ensure this matches the correct ID type + question: string; + needFile: boolean; + options: Option[]; // Array of options (text and score) +} + +interface UpdateQuestionPayload { + id: string; // The ID of the question to update + subAspectId: string; // Ensure this matches the correct ID type + question: string; + needFile: boolean; + options?: Option[]; // Optional array of options (text and score) +} + 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 }[], + aspectId: "", + subAspectId: "", + options: [] as { id: string; text: string; score: number; questionId: string }[], }, }); + // Fetch aspects and sub-aspects + const aspectsQuery = useQuery({ + queryKey: ["aspects"], + queryFn: fetchAspects, + }); + + const subAspectsQuery = useQuery({ + queryKey: ["subAspects"], + queryFn: fetchSubAspects, + }); + + // Check for form initialization and aspectId before filtering + const filteredSubAspects = form.values.aspectId + ? subAspectsQuery.data?.filter( + (subAspect) => subAspect.aspectId === form.values.aspectId + ) || [] + : []; + + const questionQuery = useQuery(getQuestionByIdQueryOptions(dataId)); + const modalTitle = + formType.charAt(0).toUpperCase() + formType.slice(1) + " Pertanyaan"; + useEffect(() => { const data = questionQuery.data; @@ -81,32 +109,48 @@ export default function QuestionFormModal() { id: data.id, question: data.question ?? "", needFile: data.needFile ?? false, - aspectName: data.aspectName ?? "", - subAspectName: data.subAspectName ?? "", - options: data.options ?? [], + aspectId: data.aspectId ?? "", + subAspectId: data.subAspectId ?? "", + options: data.options.map((option) => ({ + ...option, + questionId: data.id, + })), }); form.setErrors({}); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [questionQuery.data]); - const mutation = useMutation({ + interface MutationOptions { + action: "edit" | "create"; // Define possible actions + data: CreateQuestionPayload | UpdateQuestionPayload; // Depending on the action, it can be one or the other + } + + interface MutationResponse { + message: string; + } + + 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); + mutationFn: async (options) => { + if (options.action === "edit") { + return await updateQuestion(options.data as UpdateQuestionPayload); + } else { + return await createQuestion(options.data as CreateQuestionPayload); + } }, - onError: (error: unknown) => { + onSuccess: (data) => { + // You can use data.message here if you want to display success messages + notifications.show({ + message: data.message, + color: "green", + }); + }, + onError: (error) => { if (error instanceof FormResponseError) { form.setErrors(error.formErrors); return; } - + if (error instanceof Error) { notifications.show({ message: error.message, @@ -116,30 +160,48 @@ export default function QuestionFormModal() { }, }); - const handleSubmit = async (values: typeof form.values) => { + const handleSubmit = async (values: CreateQuestionPayload) => { 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 payload: CreateQuestionPayload = { + id: values.id, + question: values.question, + needFile: values.needFile, + subAspectId: values.subAspectId, + options: values.options.map((option) => ({ + questionId: values.id || "", // Ensure questionId is included + text: option.text, + score: option.score, + })), + }; + + try { + if (formType === "create") { + await mutation.mutateAsync({ action: "create", data: payload }); + notifications.show({ + message: "Question created successfully!", + color: "green", + }); + } else { + await mutation.mutateAsync({ action: "edit", data: payload }); + notifications.show({ + message: "Question updated successfully!", + color: "green", + }); + } + + queryClient.invalidateQueries({ queryKey: ["questions"] }); + navigate({ search: {} }); + } catch (error) { + if (error instanceof FormResponseError) { + form.setErrors(error.formErrors); + } else if (error instanceof Error) { + notifications.show({ + message: error.message, + color: "red", + }); + } + } }; const handleAddOption = () => { @@ -159,40 +221,47 @@ export default function QuestionFormModal() { size="md" >
handleSubmit(values))}> - {createInputComponents({ disableAll: mutation.isPending, readonlyAll: formType === "detail", inputs: [ + { + type: "select", + label: "Nama Aspek", + data: aspectsQuery.data?.map((aspect) => ({ + value: aspect.id, + label: aspect.name, + })) || [], + disabled: mutation.isPending || formType === "detail", + ...form.getInputProps("aspectId"), + }, + { + type: "select", + label: "Nama Sub Aspek", + data: filteredSubAspects.map((subAspect) => ({ + value: subAspect.id, + label: subAspect.name, + })), + disabled: mutation.isPending || formType === "detail", + ...form.getInputProps("subAspectId"), + }, { type: "textarea", - label: "Question", + label: "Teks Pertanyaan", ...form.getInputProps("question"), }, formType === "detail" ? { - type: "text", - label: "Need File", - readOnly: true, - value: form.values.needFile ? "Ya" : "Tidak", - } + type: "text", + label: "Dibutuhkan Upload 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"), - }, + type: "checkbox", + label: "Dibutuhkan Upload File?", + ...form.getInputProps("needFile"), + }, ], })} @@ -201,18 +270,17 @@ export default function QuestionFormModal() { {form.values.options.map((option, index) => ( - {/* Render Trash Icon only if formType is 'create' or 'edit' */} {formType !== "detail" && ( ))} - {/* Render Add Option Button only if formType is 'create' or 'edit' */} {formType !== "detail" && ( )} @@ -242,7 +309,7 @@ export default function QuestionFormModal() { onClick={() => navigate({ search: {} })} disabled={mutation.isPending} > - Close + Tutup {formType !== "detail" && ( )} diff --git a/apps/frontend/src/modules/questionsManagement/queries/questionQueries.ts b/apps/frontend/src/modules/questionsManagement/queries/questionQueries.ts index 1a0da8a..ea4b71c 100644 --- a/apps/frontend/src/modules/questionsManagement/queries/questionQueries.ts +++ b/apps/frontend/src/modules/questionsManagement/queries/questionQueries.ts @@ -3,6 +3,27 @@ import fetchRPC from "@/utils/fetchRPC"; import { queryOptions } from "@tanstack/react-query"; import { InferRequestType } from "hono"; +interface Option { + questionId: string; + text: string; + score: number; +} + +interface CreateQuestionPayload { + subAspectId: string; // Ensure this matches the correct ID type + question: string; + needFile: boolean; + options: Option[]; // Array of options (text and score) +} + +interface UpdateQuestionPayload { + id: string; // The ID of the question to update + subAspectId: string; // Ensure this matches the correct ID type + question: string; + needFile: boolean; + options?: Option[]; // Optional array of options (text and score) +} + export const questionQueryOptions = (page: number, limit: number, q?: string) => queryOptions({ queryKey: ["questions", { page, limit, q }], @@ -33,28 +54,38 @@ export const getQuestionByIdQueryOptions = (questionId: string | undefined) => enabled: Boolean(questionId), }); -export const createQuestion = async ( - form: InferRequestType["form"] -) => { +export const createQuestion = async (form: CreateQuestionPayload) => { return await fetchRPC( - client.users.$post({ - form, + client.questions.$post({ + json: { + question: form.question, + needFile: form.needFile, + subAspectId: form.subAspectId, + options: form.options.map((option) => ({ + text: option.text, + score: option.score, + })), + }, }) ); }; -export const updateQuestion = async ( - form: InferRequestType<(typeof client.users)[":id"]["$patch"]>["form"] & { - id: string; - } -) => { +export const updateQuestion = async (form: UpdateQuestionPayload) => { return await fetchRPC( - client.users[":id"].$patch({ - param: { - id: form.id, - }, - form, - }) + client.questions[":id"].$patch({ + param: { + id: form.id, + }, + json: { + question: form.question, + needFile: form.needFile, + subAspectId: form.subAspectId, + options: form.options?.map((option: Option) => ({ + text: option.text, + score: option.score, + })), + }, + }) ); }; @@ -66,3 +97,19 @@ export const deleteQuestion = async (id: string) => { }) ); }; + +export const fetchAspects = async () => { + return await fetchRPC( + client.questions.aspects.$get({ + query: {} // Provide an empty query if no parameters are needed + }) // Adjust this based on your API client structure + ); +}; + +export const fetchSubAspects = async () => { + return await fetchRPC( + client.questions.subAspects.$get({ + query: {} // Provide an empty query if no parameters are needed + }) + ); +}; diff --git a/apps/frontend/src/routes/_dashboardLayout/questions/index.lazy.tsx b/apps/frontend/src/routes/_dashboardLayout/questions/index.lazy.tsx index 1e97992..bdccd8d 100644 --- a/apps/frontend/src/routes/_dashboardLayout/questions/index.lazy.tsx +++ b/apps/frontend/src/routes/_dashboardLayout/questions/index.lazy.tsx @@ -77,7 +77,9 @@ export default function QuestionsPage() { columnHelper.display({ header: "Hasil Rata Rata", - // cell: (props) => props.row.original.question, + cell: (props) => props.row.original.averageScore !== null + ? props.row.original.averageScore + : 0, }), ]} />