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
5 changed files with 531 additions and 0 deletions
Showing only changes of commit 7db6a10b31 - Show all commits

View File

@ -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 (
<Modal
opened={isModalOpen}
onClose={() => navigate({ search: {} })}
title={`Delete confirmation`}
>
<Text size="sm">
Are you sure you want to delete question{" "}
<Text span fw={700}>
"{questionQuery.data?.question}"
</Text>
{" "}? This action is irreversible.
</Text>
{/* {errorMessage && <Alert color="red">{errorMessage}</Alert>} */}
{/* Buttons */}
<Flex justify="flex-end" align="center" gap="lg" mt="lg">
<Button
variant="outline"
onClick={() => navigate({ search: {} })}
disabled={mutation.isPending}
>
Cancel
</Button>
<Button
variant="subtle"
// leftSection={<TbDeviceFloppy size={20} />}
type="submit"
color="red"
loading={mutation.isPending}
onClick={() => mutation.mutate({ id: questionId })}
>
Delete Question
</Button>
</Flex>
</Modal>
);
}

View File

@ -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<string, any> }
| { action: "create"; data: Record<string, any> }
) => {
// 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 (
<Modal
opened={isModalOpen}
onClose={() => navigate({ search: {} })}
title={modalTitle}
scrollAreaComponent={ScrollArea.Autosize}
size="md"
>
<form onSubmit={form.onSubmit((values) => 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 */}
<Stack mt="sm">
{form.values.options.map((option, index) => (
<Group key={index} mb="sm">
<TextInput
label={`Option ${index + 1}`}
placeholder="Option Text"
{...form.getInputProps(`options.${index}.text`)}
required
/>
<NumberInput
label="Score"
placeholder="Score"
{...form.getInputProps(`options.${index}.score`)}
required
/>
{/* Render Trash Icon only if formType is 'create' or 'edit' */}
{formType !== "detail" && (
<ActionIcon
color="red"
onClick={() => handleRemoveOption(index)}
>
<TbTrash />
</ActionIcon>
)}
</Group>
))}
{/* Render Add Option Button only if formType is 'create' or 'edit' */}
{formType !== "detail" && (
<Button
onClick={handleAddOption}
leftSection={<TbPlus />}
>
Add Option
</Button>
)}
</Stack>
{/* Buttons */}
<Flex justify="flex-end" align="center" gap="lg" mt="lg">
<Button
variant="outline"
onClick={() => navigate({ search: {} })}
disabled={mutation.isPending}
>
Close
</Button>
{formType !== "detail" && (
<Button
variant="filled"
leftSection={<TbDeviceFloppy size={20} />}
type="submit"
loading={mutation.isPending}
>
Save
</Button>
)}
</Flex>
</form>
</Modal>
);
}

View File

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

View File

@ -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<typeof questionQueryOptions>;
const columnHelper = createColumnHelper<DataType>();
export default function QuestionsPage() {
return (
<PageTemplate
title="Questions"
queryOptions={questionQueryOptions}
modals={[<QuestionFormModal />, <QuestionDeleteModal />]}
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) => (
<Flex gap="xs">
{createActionButtons([
{
label: "Detail",
permission: true,
action: `?detail=${props.row.original.id}`,
color: "green",
icon: <TbEye />,
},
{
label: "Edit",
permission: true,
action: `?edit=${props.row.original.id}`,
color: "orange",
icon: <TbPencil />,
},
{
label: "Delete",
permission: true,
action: `?delete=${props.row.original.id}`,
color: "red",
icon: <TbTrash />,
},
])}
</Flex>
),
}),
columnHelper.display({
header: "Hasil Rata Rata",
// cell: (props) => props.row.original.question,
}),
]}
/>
);
}

View File

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