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
3 changed files with 226 additions and 110 deletions
Showing only changes of commit cd32ac33c7 - Show all commits

View File

@ -4,13 +4,12 @@ import {
Stack, Stack,
Button, Button,
Flex, Flex,
Avatar, ActionIcon,
Center, Select,
ScrollArea, ScrollArea,
TextInput, TextInput,
NumberInput, NumberInput,
Group, Group,
ActionIcon,
} from "@mantine/core"; } from "@mantine/core";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { getRouteApi } from "@tanstack/react-router"; import { getRouteApi } from "@tanstack/react-router";
@ -24,6 +23,8 @@ import {
createQuestion, createQuestion,
getQuestionByIdQueryOptions, getQuestionByIdQueryOptions,
updateQuestion, updateQuestion,
fetchAspects, // Import fetchAspects query
fetchSubAspects, // Import fetchSubAspects query
} from "../queries/questionQueries"; } from "../queries/questionQueries";
/** /**
@ -31,44 +32,71 @@ import {
*/ */
const routeApi = getRouteApi("/_dashboardLayout/questions/"); 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() { export default function QuestionFormModal() {
/**
* DON'T CHANGE FOLLOWING:
*/
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const navigate = routeApi.useNavigate(); const navigate = routeApi.useNavigate();
const searchParams = routeApi.useSearch(); const searchParams = routeApi.useSearch();
const dataId = searchParams.detail || searchParams.edit; const dataId = searchParams.detail || searchParams.edit;
const isModalOpen = Boolean(dataId || searchParams.create); const isModalOpen = Boolean(dataId || searchParams.create);
const detailId = searchParams.detail; const detailId = searchParams.detail;
const editId = searchParams.edit; const editId = searchParams.edit;
const formType = detailId ? "detail" : editId ? "edit" : "create"; 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({ const form = useForm({
initialValues: { initialValues: {
id: "", id: "",
question: "", question: "",
needFile: false, needFile: false,
aspectName: "", aspectId: "",
subAspectName: "", subAspectId: "",
options: [] as { id: string; text: string; score: number }[], 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(() => { useEffect(() => {
const data = questionQuery.data; const data = questionQuery.data;
@ -81,27 +109,43 @@ export default function QuestionFormModal() {
id: data.id, id: data.id,
question: data.question ?? "", question: data.question ?? "",
needFile: data.needFile ?? false, needFile: data.needFile ?? false,
aspectName: data.aspectName ?? "", aspectId: data.aspectId ?? "",
subAspectName: data.subAspectName ?? "", subAspectId: data.subAspectId ?? "",
options: data.options ?? [], options: data.options.map((option) => ({
...option,
questionId: data.id,
})),
}); });
form.setErrors({}); form.setErrors({});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [questionQuery.data]); }, [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<MutationResponse, Error, MutationOptions>({
mutationKey: ["questionsMutation"], mutationKey: ["questionsMutation"],
mutationFn: async ( mutationFn: async (options) => {
options: if (options.action === "edit") {
| { action: "edit"; data: Record<string, any> } return await updateQuestion(options.data as UpdateQuestionPayload);
| { action: "create"; data: Record<string, any> } } else {
) => { return await createQuestion(options.data as CreateQuestionPayload);
// return options.action === "edit" }
// ? await updateQuestion(options.data)
// : await createQuestion(options.data);
}, },
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) { if (error instanceof FormResponseError) {
form.setErrors(error.formErrors); form.setErrors(error.formErrors);
return; return;
@ -116,30 +160,48 @@ export default function QuestionFormModal() {
}, },
}); });
const handleSubmit = async (values: typeof form.values) => { const handleSubmit = async (values: CreateQuestionPayload) => {
if (formType === "detail") return; if (formType === "detail") return;
await mutation.mutateAsync({ const payload: CreateQuestionPayload = {
action: formType,
data: {
id: values.id, id: values.id,
question: values.question, question: values.question,
needFile: values.needFile, needFile: values.needFile,
subAspectId: values.subAspectId,
options: values.options.map((option) => ({ options: values.options.map((option) => ({
id: option.id, questionId: values.id || "", // Ensure questionId is included
text: option.text, text: option.text,
score: option.score, 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"] }); queryClient.invalidateQueries({ queryKey: ["questions"] });
notifications.show({
message: `The question is ${formType === "create" ? "created" : "edited"
}`,
});
navigate({ search: {} }); 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 = () => { const handleAddOption = () => {
@ -159,40 +221,47 @@ export default function QuestionFormModal() {
size="md" size="md"
> >
<form onSubmit={form.onSubmit((values) => handleSubmit(values))}> <form onSubmit={form.onSubmit((values) => handleSubmit(values))}>
{createInputComponents({ {createInputComponents({
disableAll: mutation.isPending, disableAll: mutation.isPending,
readonlyAll: formType === "detail", readonlyAll: formType === "detail",
inputs: [ 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", type: "textarea",
label: "Question", label: "Teks Pertanyaan",
...form.getInputProps("question"), ...form.getInputProps("question"),
}, },
formType === "detail" formType === "detail"
? { ? {
type: "text", type: "text",
label: "Need File", label: "Dibutuhkan Upload File?",
readOnly: true, readOnly: true,
value: form.values.needFile ? "Ya" : "Tidak", value: form.values.needFile ? "Ya" : "Tidak",
} }
: { : {
type: "checkbox", type: "checkbox",
label: "Need File", label: "Dibutuhkan Upload File?",
...form.getInputProps("needFile"), ...form.getInputProps("needFile"),
}, },
{
type: "text",
label: "Aspect Name",
readOnly: true,
...form.getInputProps("aspectName"),
},
{
type: "text",
label: "Sub-Aspect Name",
readOnly: true,
...form.getInputProps("subAspectName"),
},
], ],
})} })}
@ -201,18 +270,17 @@ export default function QuestionFormModal() {
{form.values.options.map((option, index) => ( {form.values.options.map((option, index) => (
<Group key={index} mb="sm"> <Group key={index} mb="sm">
<TextInput <TextInput
label={`Option ${index + 1}`} label={`Jawaban ${index + 1}`}
placeholder="Option Text" placeholder="Teks Jawaban"
{...form.getInputProps(`options.${index}.text`)} {...form.getInputProps(`options.${index}.text`)}
required required
/> />
<NumberInput <NumberInput
label="Score" label="Skor"
placeholder="Score" placeholder="Skor"
{...form.getInputProps(`options.${index}.score`)} {...form.getInputProps(`options.${index}.score`)}
required required
/> />
{/* Render Trash Icon only if formType is 'create' or 'edit' */}
{formType !== "detail" && ( {formType !== "detail" && (
<ActionIcon <ActionIcon
color="red" color="red"
@ -224,13 +292,12 @@ export default function QuestionFormModal() {
</Group> </Group>
))} ))}
{/* Render Add Option Button only if formType is 'create' or 'edit' */}
{formType !== "detail" && ( {formType !== "detail" && (
<Button <Button
onClick={handleAddOption} onClick={handleAddOption}
leftSection={<TbPlus />} leftSection={<TbPlus />}
> >
Add Option Tambah Jawaban
</Button> </Button>
)} )}
</Stack> </Stack>
@ -242,7 +309,7 @@ export default function QuestionFormModal() {
onClick={() => navigate({ search: {} })} onClick={() => navigate({ search: {} })}
disabled={mutation.isPending} disabled={mutation.isPending}
> >
Close Tutup
</Button> </Button>
{formType !== "detail" && ( {formType !== "detail" && (
<Button <Button
@ -251,7 +318,7 @@ export default function QuestionFormModal() {
type="submit" type="submit"
loading={mutation.isPending} loading={mutation.isPending}
> >
Save Simpan
</Button> </Button>
)} )}
</Flex> </Flex>

View File

@ -3,6 +3,27 @@ import fetchRPC from "@/utils/fetchRPC";
import { queryOptions } from "@tanstack/react-query"; import { queryOptions } from "@tanstack/react-query";
import { InferRequestType } from "hono"; 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) => export const questionQueryOptions = (page: number, limit: number, q?: string) =>
queryOptions({ queryOptions({
queryKey: ["questions", { page, limit, q }], queryKey: ["questions", { page, limit, q }],
@ -33,27 +54,37 @@ export const getQuestionByIdQueryOptions = (questionId: string | undefined) =>
enabled: Boolean(questionId), enabled: Boolean(questionId),
}); });
export const createQuestion = async ( export const createQuestion = async (form: CreateQuestionPayload) => {
form: InferRequestType<typeof client.users.$post>["form"]
) => {
return await fetchRPC( return await fetchRPC(
client.users.$post({ client.questions.$post({
form, 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 ( export const updateQuestion = async (form: UpdateQuestionPayload) => {
form: InferRequestType<(typeof client.users)[":id"]["$patch"]>["form"] & {
id: string;
}
) => {
return await fetchRPC( return await fetchRPC(
client.users[":id"].$patch({ client.questions[":id"].$patch({
param: { param: {
id: form.id, id: form.id,
}, },
form, 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
})
);
};

View File

@ -77,7 +77,9 @@ export default function QuestionsPage() {
columnHelper.display({ columnHelper.display({
header: "Hasil Rata Rata", header: "Hasil Rata Rata",
// cell: (props) => props.row.original.question, cell: (props) => props.row.original.averageScore !== null
? props.row.original.averageScore
: 0,
}), }),
]} ]}
/> />