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,
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<MutationResponse, Error, MutationOptions>({
mutationKey: ["questionsMutation"],
mutationFn: async (
options:
| { action: "edit"; data: Record<string, any> }
| { action: "create"; data: Record<string, any> }
) => {
// 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"
>
<form onSubmit={form.onSubmit((values) => 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) => (
<Group key={index} mb="sm">
<TextInput
label={`Option ${index + 1}`}
placeholder="Option Text"
label={`Jawaban ${index + 1}`}
placeholder="Teks Jawaban"
{...form.getInputProps(`options.${index}.text`)}
required
/>
<NumberInput
label="Score"
placeholder="Score"
label="Skor"
placeholder="Skor"
{...form.getInputProps(`options.${index}.score`)}
required
/>
{/* Render Trash Icon only if formType is 'create' or 'edit' */}
{formType !== "detail" && (
<ActionIcon
color="red"
@ -224,13 +292,12 @@ export default function QuestionFormModal() {
</Group>
))}
{/* Render Add Option Button only if formType is 'create' or 'edit' */}
{formType !== "detail" && (
<Button
onClick={handleAddOption}
leftSection={<TbPlus />}
>
Add Option
Tambah Jawaban
</Button>
)}
</Stack>
@ -242,7 +309,7 @@ export default function QuestionFormModal() {
onClick={() => navigate({ search: {} })}
disabled={mutation.isPending}
>
Close
Tutup
</Button>
{formType !== "detail" && (
<Button
@ -251,7 +318,7 @@ export default function QuestionFormModal() {
type="submit"
loading={mutation.isPending}
>
Save
Simpan
</Button>
)}
</Flex>

View File

@ -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<typeof client.users.$post>["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
})
);
};

View File

@ -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,
}),
]}
/>