Pull Request branch dev-clone to main #1
|
|
@ -0,0 +1,450 @@
|
|||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import {
|
||||
Card,
|
||||
Flex,
|
||||
Pagination,
|
||||
Stack,
|
||||
Radio,
|
||||
Text,
|
||||
Textarea,
|
||||
Loader,
|
||||
ActionIcon,
|
||||
CloseButton,
|
||||
Group,
|
||||
} from "@mantine/core";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import {
|
||||
getAverageScoreSubAspectQueryOptions,
|
||||
getAverageScoreQueryOptions,
|
||||
fetchAspects,
|
||||
submitAnswerMutationOptions,
|
||||
getQuestionsAllQueryOptions,
|
||||
toggleFlagAnswer,
|
||||
} from "@/modules/assessmentManagement/queries/assessmentQueries";
|
||||
import { TbFlag, TbUpload, TbChevronRight, TbChevronUp } from "react-icons/tb";
|
||||
import { useState, useRef } from "react";
|
||||
|
||||
export const Route = createLazyFileRoute("/_dashboardLayout/assessment/")({
|
||||
component: AssessmentPage,
|
||||
});
|
||||
|
||||
interface ToggleFlagResponse {
|
||||
message: string;
|
||||
answer: {
|
||||
id: string;
|
||||
createdAt: string | null;
|
||||
updatedAt: string | null;
|
||||
optionId: string | null;
|
||||
assessmentId: string | null;
|
||||
isFlagged: boolean | null;
|
||||
filename: string | null;
|
||||
validationInformation: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function AssessmentPage() {
|
||||
const [page, setPage] = useState(1);
|
||||
const limit = 2;
|
||||
const questionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
const [flaggedQuestions, setFlaggedQuestions] = useState<{
|
||||
[key: string]: boolean;
|
||||
}>({});
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Fetching questions data using useQuery
|
||||
const { data, isLoading, isError, error } = useQuery(
|
||||
getQuestionsAllQueryOptions(page, limit)
|
||||
);
|
||||
|
||||
// Tambahkan state untuk aspek yang terbuka
|
||||
const [openAspects, setOpenAspects] = useState<{ [key: string]: boolean }>({});
|
||||
|
||||
const toggleAspect = (aspectId: string) => {
|
||||
setOpenAspects((prev) => ({
|
||||
...prev,
|
||||
[aspectId]: !prev[aspectId], // Toggle state untuk aspek yang diklik
|
||||
}));
|
||||
};
|
||||
|
||||
// Fetch aspects and sub-aspects
|
||||
const aspectsQuery = useQuery({
|
||||
queryKey: ["aspects"],
|
||||
queryFn: fetchAspects,
|
||||
});
|
||||
|
||||
// Fetch average scores
|
||||
const assessmentId = "aqduqcdc1mhnbz8zrpnmx9oj"; // Replace with actual assessment ID
|
||||
const averageScoreQuery = useQuery(
|
||||
getAverageScoreQueryOptions(assessmentId)
|
||||
);
|
||||
|
||||
// Fetch average scores for sub-aspects
|
||||
const averageScoreSubAspectQuery = useQuery(
|
||||
getAverageScoreSubAspectQueryOptions(assessmentId)
|
||||
);
|
||||
|
||||
// Mutation function to toggle flag
|
||||
const toggleFlagMutation = useMutation<ToggleFlagResponse, Error, string>({
|
||||
mutationFn: toggleFlagAnswer,
|
||||
onSuccess: (response) => {
|
||||
if (response && response.answer) {
|
||||
const { answer } = response;
|
||||
setFlaggedQuestions((prevFlags) => ({
|
||||
...prevFlags,
|
||||
[answer.id]: answer.isFlagged !== null ? answer.isFlagged : false,
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
onError: (error) => {
|
||||
console.error("Error toggling flag:", error);
|
||||
},
|
||||
});
|
||||
|
||||
// Inside the AssessmentPage function:
|
||||
const submitAnswerMutation = useMutation(submitAnswerMutationOptions());
|
||||
|
||||
// Drag and Drop handlers
|
||||
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
setDragActive(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = () => {
|
||||
setDragActive(false);
|
||||
};
|
||||
|
||||
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
setDragActive(false);
|
||||
const droppedFiles = Array.from(event.dataTransfer.files);
|
||||
setFiles((prevFiles) => [...prevFiles, ...droppedFiles]);
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.files) {
|
||||
const fileArray = Array.from(event.target.files); // Ubah ke array hanya jika files tidak null
|
||||
setFiles((prevFiles) => [...prevFiles, ...fileArray]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveFile = (fileIndex: number) => {
|
||||
setFiles((prevFiles) =>
|
||||
prevFiles.filter((_, index) => index !== fileIndex)
|
||||
);
|
||||
};
|
||||
|
||||
// Function to scroll to the specific question
|
||||
const scrollToQuestion = (questionId: string) => {
|
||||
const questionElement = questionRefs.current[questionId];
|
||||
if (questionElement) {
|
||||
questionElement.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
};
|
||||
|
||||
// Handle pagination
|
||||
const handlePageChange = (newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
// Render conditions
|
||||
if (isLoading) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Text color="red">
|
||||
Error: {error?.message || "Terjadi kesalahan saat memuat pertanyaan."}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const totalQuestions = data?.data?.length || 0;
|
||||
const averageScores = averageScoreQuery.data?.aspects;
|
||||
const averageScoresSubAspect = averageScoreSubAspectQuery.data?.subAspects;
|
||||
|
||||
return (
|
||||
<Card shadow="sm" p="lg" radius="md" withBorder>
|
||||
<Stack gap="md">
|
||||
<Text className="text-2xl font-bold">
|
||||
Harap menjawab semua pertanyaan yang tersedia
|
||||
</Text>
|
||||
<Text className="text-gray-400">Semua jawaban Anda akan ditinjau</Text>
|
||||
|
||||
<Flex justify="space-between" align="flex-start" mt="lg">
|
||||
{/* Pertanyaan */}
|
||||
<Stack gap="sm" style={{ flex: 1 }}>
|
||||
{data?.data?.map((question: any, index: number) => {
|
||||
const questionId = question.questionId;
|
||||
if (!questionId) return null;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={questionId}
|
||||
shadow="sm"
|
||||
p="lg"
|
||||
radius="md"
|
||||
withBorder
|
||||
ref={(el) => (questionRefs.current[questionId] = el)}
|
||||
style={{ position: "relative" }}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<Flex
|
||||
justify="space-between"
|
||||
align="flex-start"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<Text
|
||||
className="font-bold"
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
wordBreak: "break-word",
|
||||
marginRight: "40px",
|
||||
}}
|
||||
>
|
||||
{index + 1}. {question.questionText}
|
||||
</Text>
|
||||
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
setFlaggedQuestions((prevFlags) => ({
|
||||
...prevFlags,
|
||||
[questionId]: !prevFlags[questionId],
|
||||
}));
|
||||
toggleFlagMutation.mutate(questionId);
|
||||
}}
|
||||
title="Tandai"
|
||||
color={flaggedQuestions[questionId] ? "red" : "gray"}
|
||||
>
|
||||
<TbFlag
|
||||
size={24}
|
||||
color={flaggedQuestions[questionId] ? "black" : "inherit"}
|
||||
/>
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
|
||||
{question.options?.length > 0 ? (
|
||||
<Radio.Group>
|
||||
<div className="flex flex-col gap-4">
|
||||
{question.options.map((option: any) => (
|
||||
<label
|
||||
key={option.optionId}
|
||||
className="bg-gray-200 border rounded-lg p-4 cursor-pointer transition-transform transform hover:scale-105 shadow-md hover:shadow-lg flex items-center"
|
||||
onClick={() =>
|
||||
document.getElementById(option.optionId)?.click()
|
||||
}
|
||||
>
|
||||
<Radio
|
||||
id={option.optionId}
|
||||
className="font-bold"
|
||||
value={option.optionId}
|
||||
label={option.optionText}
|
||||
size="md"
|
||||
radius="xl"
|
||||
style={{ pointerEvents: "none" }}
|
||||
onChange={() => {
|
||||
submitAnswerMutation.mutate({
|
||||
optionId: option.optionId,
|
||||
assessmentId: "aqduqcdc1mhnbz8zrpnmx9oj",
|
||||
validationInformation: "jfjforjfocn",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</Radio.Group>
|
||||
) : (
|
||||
<Text color="red">Tidak ada opsi untuk pertanyaan ini.</Text>
|
||||
)}
|
||||
|
||||
<Textarea placeholder="Berikan keterangan terkait jawaban di atas" />
|
||||
|
||||
{/* File Upload */}
|
||||
<div
|
||||
className={`pt-5 pb-5 pr-5 pl-2 border-2 border-dashed ${dragActive ? "bg-gray-100" : "bg-transparent"
|
||||
} shadow-lg`} // Tambah shadow-lg
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Flex align="center" justify="space-between" gap="sm">
|
||||
<TbUpload
|
||||
size={24}
|
||||
style={{ marginLeft: "8px", marginRight: "8px" }}
|
||||
/>{" "}
|
||||
{/* Tambah marginLeft */}
|
||||
<div className="flex-grow text-right">
|
||||
<Text className="font-bold">
|
||||
Klik untuk unggah atau geser file disini
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-400">
|
||||
PNG, JPG, PDF
|
||||
</Text>
|
||||
</div>
|
||||
</Flex>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
style={{ display: "none" }}
|
||||
accept="image/png, image/jpeg, application/pdf"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{files.length > 0 && (
|
||||
<Stack gap="sm" mt="sm">
|
||||
<Text className="font-bold">File yang diunggah:</Text>
|
||||
{files.map((file, fileIndex) => (
|
||||
<Group key={fileIndex} align="center">
|
||||
<Text>{file.name}</Text>
|
||||
<CloseButton
|
||||
title="Hapus file"
|
||||
onClick={() => handleRemoveFile(fileIndex)}
|
||||
/>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
|
||||
{/* Navigasi dan Pagination */}
|
||||
<Flex direction="column" gap="xs" className="ml-4">
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
{Array.from({ length: totalQuestions }, (_, index) => {
|
||||
const questionId = data?.data[index]?.questionId;
|
||||
if (!questionId) return null;
|
||||
|
||||
return (
|
||||
<div key={questionId} className="flex justify-center relative">
|
||||
<button
|
||||
className={`w-10 h-10 border rounded-lg flex items-center justify-center relative
|
||||
${flaggedQuestions[questionId] ? "text-black" : "bg-transparent text-black"}`}
|
||||
onClick={() => scrollToQuestion(questionId)}
|
||||
>
|
||||
{index + 1}
|
||||
</button>
|
||||
|
||||
{flaggedQuestions[questionId] && (
|
||||
<div className="absolute top-0 right-0 w-0 h-0 border-b-[20px] border-b-transparent border-r-[20px] border-r-black rounded-e-md" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex justify-center">
|
||||
<Pagination
|
||||
value={page}
|
||||
total={Math.ceil(totalQuestions / limit)}
|
||||
onChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Aspek dan Sub-Aspek */}
|
||||
<div className="space-y-4">
|
||||
{aspectsQuery.data?.data.map((aspect) => (
|
||||
<div
|
||||
key={aspect.id}
|
||||
className="p-4 bg-gray-50 rounded-lg shadow-md"
|
||||
>
|
||||
<div
|
||||
className="flex justify-between cursor-pointer"
|
||||
onClick={() => toggleAspect(aspect.id)}
|
||||
>
|
||||
<div className="text-lg text-gray-700">{aspect.name}</div>
|
||||
<div>
|
||||
{openAspects[aspect.id] ? (
|
||||
<TbChevronUp size={25} />
|
||||
) : (
|
||||
<TbChevronRight size={25} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{openAspects[aspect.id] && (
|
||||
<div className="mt-2 space-y-2">
|
||||
{aspect.subAspects.map((subAspect) => (
|
||||
<div
|
||||
key={subAspect.id}
|
||||
className="flex justify-between text-gray-600"
|
||||
>
|
||||
<div>{subAspect.name}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Skor Aspek dan Sub-Aspek */}
|
||||
<div className="mt-4">
|
||||
<Card shadow="sm" p="md" radius="md" withBorder>
|
||||
<Stack>
|
||||
{averageScores && averageScores.length > 0 ? (
|
||||
averageScores.map((aspect: any) => (
|
||||
<div key={aspect.AspectId} className="flex justify-between items-center">
|
||||
<Text className="text-xl text-gray-400">
|
||||
{aspect.AspectName}
|
||||
</Text>
|
||||
<Text className="text-xl font-bold">
|
||||
{parseFloat(aspect.averageScore).toFixed(1)}
|
||||
</Text>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<Text>Tidak ada data skor.</Text>
|
||||
)}
|
||||
|
||||
{/* Garis pembatas */}
|
||||
<div>
|
||||
<hr className="border-t-2 border-gray-300 w-full mx-auto" />
|
||||
</div>
|
||||
|
||||
{averageScoresSubAspect && averageScoresSubAspect.length > 0 && (
|
||||
<Stack>
|
||||
{averageScoresSubAspect.map((subAspects: any) => {
|
||||
return (
|
||||
<div key={subAspects.subAspectId} className="flex justify-between items-center">
|
||||
<Text className="text-lg text-gray-400">
|
||||
{subAspects.subAspectName}
|
||||
</Text>
|
||||
<Text className="text-lg font-bold">
|
||||
{parseFloat(subAspects.averageScore).toFixed(1)}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{/* Tombol Selesai */}
|
||||
<div className="mt-6">
|
||||
<button className="bg-gray-200 text-black font-bold py-2 w-full">
|
||||
Selesai
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { getQuestionsAllQueryOptions } from "@/modules/assessmentManagement/queries/assessmentQueries.ts";
|
||||
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/assessment/")({
|
||||
validateSearch: searchParamSchema,
|
||||
|
||||
loader: ({ context: { queryClient } }) => {
|
||||
queryClient.ensureQueryData(getQuestionsAllQueryOptions(0, 10));
|
||||
},
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user