import { createLazyFileRoute } from "@tanstack/react-router"; import { Flex, Stack, Text, Loader, ActionIcon, CloseButton, Group, } from "@mantine/core"; import { Card, CardContent, CardDescription, } from "@/shadcn/components/ui/card"; import { Button } from "@/shadcn/components/ui/button"; import { Textarea } from "@/shadcn/components/ui/textarea"; import { Label } from "@/shadcn/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/shadcn/components/ui/radio-group"; import { ScrollArea } from "@/shadcn/components/ui/scroll-area"; import { Pagination, } from "@/shadcn/components/ui/pagination-assessment"; import { useQuery, useMutation } from "@tanstack/react-query"; import { getAnswersQueryOptions, submitAssessmentMutationOptions, uploadFileMutationOptions, submitValidationQuery, submitOptionMutationOptions, getAverageScoreQueryOptions, fetchAspects, getQuestionsAllQueryOptions, toggleFlagAnswer, } from "@/modules/assessmentManagement/queries/assessmentQueries"; import { TbFlagFilled, TbUpload, TbChevronRight, TbChevronDown } from "react-icons/tb"; import FinishAssessmentModal from "@/modules/assessmentManagement/modals/ConfirmModal"; import FileUpload from "@/modules/assessmentManagement/fileUpload/fileUpload"; import ValidationModal from "@/modules/assessmentManagement/modals/ValidationModal"; import FileSizeValidationModal from "@/modules/assessmentManagement/modals/FileSizeValidationModal"; import { useState, useRef, useEffect } from "react"; const getQueryParam = (param: string) => { const urlParams = new URLSearchParams(window.location.search); return urlParams.get(param); }; export const Route = createLazyFileRoute("/_assessmentLayout/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 interface SubmitOptionResponse { message: string; answer: { id: string; isFlagged: boolean | null; }; } export default function AssessmentPage() { const [page, setPage] = useState(1); const limit = 10; const questionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); const [files, setFiles] = useState([]); const [dragActive, setDragActive] = useState(false); const [flaggedQuestions, setFlaggedQuestions] = useState<{ [key: string]: boolean; }>({}); const fileInputRef = useRef(null); const [modalOpen, setModalOpen] = useState(false); const [modalOpenFileSize, setModalOpenFileSize] = useState(false); const [selectedAspectId, setSelectedAspectId] = useState(null); const [selectedSubAspectId, setSelectedSubAspectId] = useState(null); const [assessmentId, setAssessmentId] = useState(null); const [answers, setAnswers] = useState<{ [key: string]: string }>({}); const [validationInformation, setValidationInformation] = useState<{ [key: string]: string }>({}); const [uploadedFiles, setUploadedFiles] = useState<{ [key: string]: File | null }>({}); const [unansweredQuestions, setUnansweredQuestions] = useState(0); const [validationModalOpen, setValidationModalOpen] = useState(false); const [exceededFileName, setExceededFileName] = useState(""); const [currentPagePerSubAspect, setCurrentPagePerSubAspect] = useState<{ [subAspectId: string]: number }>({}); const currentPage = currentPagePerSubAspect[selectedSubAspectId || ""] || 1; const questionsPerPage = 10; // Fetch aspects and sub-aspects const aspectsQuery = useQuery({ queryKey: ["aspects"], queryFn: fetchAspects, }); // Fetching questions data using useQuery const { data, isLoading, isError, error } = useQuery( getQuestionsAllQueryOptions(page, limit) ); const handleFinishClick = () => { // Memanggil fungsi untuk memeriksa pertanyaan yang belum dijawab checkUnansweredQuestions(); }; useEffect(() => { const id = getQueryParam("id"); if (!id) { setAssessmentId(null); } else { setAssessmentId(id); } // Check if aspectsQuery.data is defined if (aspectsQuery.data?.data && aspectsQuery.data.data.length > 0) { // If no sub-aspect is selected, find a suitable default if (selectedSubAspectId === null) { const firstMatchingSubAspect = aspectsQuery.data.data .flatMap((aspect) => aspect.subAspects) // Get all sub-aspects .find((subAspect) => data?.data.some((question) => question.subAspectId === subAspect.id) ); if (firstMatchingSubAspect) { setSelectedSubAspectId(firstMatchingSubAspect.id); // Find the parent aspect and set its id as the selectedAspectId const parentAspect = aspectsQuery.data.data.find((aspect) => aspect.subAspects.some((sub) => sub.id === firstMatchingSubAspect.id) ); if (parentAspect) { setSelectedAspectId(parentAspect.id); // Use `id` from the parent aspect setOpenAspects({ [parentAspect.id]: true }); // Open only relevant aspects } } } else { // Update the aspectId based on the selected sub-aspect const matchingAspect = aspectsQuery.data.data.find((aspect) => aspect.subAspects.some((subAspect) => subAspect.id === selectedSubAspectId) ); if (matchingAspect) { setSelectedAspectId(matchingAspect.id); // Use `id` from the matching aspect setOpenAspects({ [matchingAspect.id]: true }); // Close all other dropdowns and open only the newly selected aspect } else { console.warn("No matching aspect found for selected sub-aspect."); setSelectedAspectId(null); setOpenAspects({}); // Close all dropdowns if none of them match } } } }, [aspectsQuery.data, selectedSubAspectId, data?.data]); // Fetching answers for the assessment const { data: answersData } = useQuery( getAnswersQueryOptions(assessmentId || "", page, limit), ); if (answersData && answersData.data) { const transformedData = answersData.data.reduce( (acc: Record, item: any) => { if (item.questionId && item.optionId) { acc[item.questionId] = item.optionId; } return acc; }, {} ); } else { console.error("No data found or data is undefined."); } // Effect untuk mengatur answers dari data yang diambil useEffect(() => { const assessmentId = getQueryParam("id"); if (!assessmentId) { console.error("Assessment ID tidak ditemukan"); return; } // Set answers from `answersData` if data is available if (answersData && Array.isArray(answersData.data)) { const answersFromDatabase = answersData.data.reduce( (acc: Record, item: any) => { if (item.questionId && item.optionId) { acc[item.questionId] = item.optionId; } return acc; }, {} ); setAnswers(answersFromDatabase); // Set the transformed data directly to state } else { console.error("No data found or data is undefined."); } }, [answersData]); // Fungsi untuk memeriksa pertanyaan yang belum dijawab const checkUnansweredQuestions = () => { // Filter pertanyaan yang belum dijawab berdasarkan data `answers` const unanswered = data?.data.filter(question => { return question.questionId !== null && !answers[question.questionId]; }) || []; setUnansweredQuestions(unanswered.length); // Tampilkan modal berdasarkan jumlah pertanyaan yang belum dijawab if (unanswered.length > 0) { setValidationModalOpen(true); } else { setModalOpen(true); } }; const handleConfirmFinish = async (assessmentId: string) => { try { // Skip counting unanswered questions here to prevent duplication const mutation = submitAssessmentMutationOptions(assessmentId); const response = await mutation.mutationFn(); // Navigate to results const newUrl = `/assessmentResult?id=${assessmentId}`; window.history.pushState({}, "", newUrl); console.log("Navigated to:", newUrl); console.log(response.message); } catch (error) { console.error("Error finishing assessment:", error); } finally { setModalOpen(false); } }; // 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 average scores by aspect const averageScoreQuery = useQuery(getAverageScoreQueryOptions(assessmentId || "")); const aspects = averageScoreQuery.data?.aspects || []; // Filter aspects by selected aspectId const filteredAspects = selectedAspectId ? aspects.filter((aspect) => aspect.aspectId === selectedAspectId) // Use 'id' instead of 'aspectId' : aspects; // Get the currently selected aspect to show all related sub-aspects const currentAspect = aspects.find(aspect => aspect.aspectId === selectedAspectId); const filteredSubAspects = currentAspect ? currentAspect.subAspects : []; // Inisialisasi flaggedQuestions dari database saat komponen dimuat useEffect(() => { const initialFlagData = answersData?.data.reduce((acc, item) => { if (item.questionId != null && item.isFlagged != null) { acc[item.questionId] = item.isFlagged; } return acc; }, {} as Record); if (initialFlagData) { setFlaggedQuestions(initialFlagData); } }, [answersData]); // Mutation function to toggle flag const { mutate: toggleFlag } = useMutation({ mutationFn: (formData: { assessmentId: string; questionId: string; isFlagged: boolean }) => toggleFlagAnswer(formData), onSuccess: (response: SubmitOptionResponse) => { if (response.answer) { const { answer } = response; setFlaggedQuestions((prevFlags) => ({ ...prevFlags, [answer.id]: answer.isFlagged ?? false, })); } }, onError: (error) => { console.error("Error toggling flag:", error); }, }); // Fungsi untuk toggle flag const handleToggleFlag = (questionId: string) => { const newFlagState = !flaggedQuestions[questionId]; const assessmentId = getQueryParam("id"); if (!assessmentId) { console.error("Assessment ID tidak ditemukan"); return; } // Update flaggedQuestions di state setFlaggedQuestions((prevFlags) => ({ ...prevFlags, [questionId]: newFlagState, })); // Kirim perubahan flag ke server toggleFlag({ assessmentId, questionId, isFlagged: newFlagState, }); }; // Usage of the mutation in your component const submitOptionMutation = useMutation({ ...submitOptionMutationOptions, // Spread the mutation options here onSuccess: () => { // Refetch the average scores after a successful submission averageScoreQuery.refetch(); }, onError: (error) => { console.error("Error submitting option:", error); }, }); const handleAnswerChange = (questionId: string, optionId: string) => { const assessmentId = getQueryParam("id"); if (!assessmentId) { console.error("Assessment ID tidak ditemukan"); return; } // Update answers in the state const updatedAnswers = { ...answers, [questionId]: optionId }; setAnswers(updatedAnswers); // Send the updated answer to the backend submitOptionMutation.mutate({ optionId, assessmentId, questionId, isFlagged: false, filename: undefined, }); }; const validationResult = answersData?.data.reduce((acc, item) => { if (item.questionId != null && item.validationInformation != null) { acc[item.questionId] = item.validationInformation; } return acc; }, {} as Record); // Mengambil data dari database saat komponen dimuat useEffect(() => { if (validationResult) { setValidationInformation(validationResult); } }, [answersData, assessmentId]); // Mutation untuk mengirim data ke backend const { mutate: submitValidation } = useMutation({ mutationFn: (form: { assessmentId: string; questionId: string; validationInformation: string; }) => submitValidationQuery(form), onSuccess: () => { // Tindakan yang diambil setelah berhasil console.log("Validation updated successfully!"); }, onError: (error) => { console.error("Error updating validation:", error); }, }); // Handle perubahan di Textarea const handleTextareaChange = (questionId: string, value: string) => { // Memperbarui state validationInformation setValidationInformation((prev) => ({ ...prev, [questionId]: value, })); // Pastikan assessmentId tidak null sebelum mengirimkan data ke server if (assessmentId) { // Kirim data validasi ke server submitValidation({ assessmentId, questionId, validationInformation: value, }); } else { console.error("Assessment ID tidak ditemukan"); } }; // Mutation for file upload const uploadFileMutation = useMutation(uploadFileMutationOptions()); // Inisialisasi uploadedFiles dari data yang diterima (answersData) useEffect(() => { if (answersData && answersData.data) { const transformedFileData = answersData.data.reduce((acc, item) => { if (item.questionId && item.filename) { acc[item.questionId] = new File([""], item.filename, { type: "application/pdf" }); } return acc; }, {} as Record); setUploadedFiles(transformedFileData); } }, [answersData]); // Drag and Drop handlers const handleDragOver = (event: React.DragEvent) => { event.preventDefault(); setDragActive(true); }; const handleDragLeave = () => { setDragActive(false); }; // Max file size in bytes (64 MB) const MAX_FILE_SIZE = 64 * 1024 * 1024; const handleDrop = (event: React.DragEvent, question: { questionId: string }) => { event.preventDefault(); setDragActive(false); const droppedFiles = Array.from(event.dataTransfer.files); if (droppedFiles.length > 0) { const file = droppedFiles[0]; // Validate file size if (file.size > MAX_FILE_SIZE) { setExceededFileName(file.name); setModalOpenFileSize(true); return; } const formData = new FormData(); formData.append('file', file); if (assessmentId) { formData.append('assessmentId', assessmentId); } else { console.error("assessmentId is null"); return; } if (question.questionId) { formData.append('questionId', question.questionId); } else { console.error("questionId is null"); return; } uploadFileMutation.mutate(formData); // Upload file // Update state to reflect the uploaded file (store the File object, not just the name) setUploadedFiles(prev => ({ ...prev, [question.questionId]: file, // Store the file itself })); } }; const handleClick = () => { if (fileInputRef.current) { fileInputRef.current.click(); } }; const handleFileChange = (event: React.ChangeEvent, question: { questionId: string }) => { if (event.target.files) { const fileArray = Array.from(event.target.files); if (fileArray.length > 0) { const file = fileArray[0]; // Validate file size if (file.size > MAX_FILE_SIZE) { setExceededFileName(file.name); setModalOpenFileSize(true); return; } const formData = new FormData(); formData.append('file', file); if (assessmentId) { formData.append('assessmentId', assessmentId); } else { console.error("assessmentId is null"); return; } if (question.questionId) { formData.append('questionId', question.questionId); } else { console.error("questionId is null"); return; } uploadFileMutation.mutate(formData); // Upload file // Update state to reflect the uploaded file (store the File object) setUploadedFiles(prev => ({ ...prev, [question.questionId]: file, // Store the File object, not just the name })); } } }; const handleRemoveFile = (question: { questionId: string }) => { setUploadedFiles((prev) => ({ ...prev, [question.questionId]: null, })); }; // Function to scroll to the specific question const scrollToQuestion = (questionId: string) => { const questionElement = questionRefs.current[questionId]; if (questionElement) { questionElement.scrollIntoView({ behavior: "smooth" }); } }; // Render conditions if (isLoading) { return ; } if (isError) { return ( Error: {error?.message || "Terjadi kesalahan saat memuat pertanyaan."} ); } if (!assessmentId) { return ( Error: Data Asesmen tidak ditemukan. Harap akses halaman melalui link yang valid. ); } const startIndex = (currentPage - 1) * questionsPerPage; // Fungsi untuk mengubah halaman pada sub-aspek const handlePageChange = (subAspectId: string, newPage: number) => { setCurrentPagePerSubAspect((prev) => ({ ...prev, [subAspectId]: newPage, })); }; // Filter pertanyaan berdasarkan halaman saat ini const filteredQuestions = data?.data?.filter((question) => { // Filter berdasarkan sub-aspek yang dipilih return question.subAspectId === selectedSubAspectId; })?.slice( (currentPage - 1) * questionsPerPage, currentPage * questionsPerPage ) || []; // Perbarui jumlah halaman untuk sub-aspek saat ini const totalQuestionsInSubAspect = data?.data?.filter( (question) => question.subAspectId === selectedSubAspectId )?.length || 0; const totalPages = Math.ceil(totalQuestionsInSubAspect / questionsPerPage); return (
{/* LEFT-SIDE */} {/* Aspek dan Sub-Aspek */}
{/* Aspek */} {aspectsQuery.data?.data .filter((aspect) => aspect.subAspects.some((subAspect) => data?.data.some((question) => question.subAspectId === subAspect.id) ) ) .map((aspect) => (
toggleAspect(aspect.id)} >
{aspect.name}
{openAspects[aspect.id] ? ( ) : ( )}
{/* Sub-Aspek */} {openAspects[aspect.id] && (
{aspect.subAspects .filter((subAspect) => data?.data.some((question) => question.subAspectId === subAspect.id) ) .map((subAspect) => (
setSelectedSubAspectId(subAspect.id)} >
{subAspect.name}
))}
)}
))}
{/* MIDDLE */} {/* Pertanyaan */}
Harap menjawab semua pertanyaan yang tersedia Semua jawaban Anda akan ditinjau {filteredQuestions.length === 0 ? ( Pertanyaan tidak ada untuk sub-aspek yang dipilih. ) : ( filteredQuestions.map((question: any, index: number) => { const questionId = question.questionId; if (!questionId) return null; return (
(questionRefs.current[questionId] = el)} className="space-y-4" > {/* Question */} {startIndex + index + 1}.
{question.questionText}
{/* Action Icon/Flag */} handleToggleFlag(questionId)} title="Tandai" className={`m-2 rounded-md border-1 flex items-center justify-center h-7 w-7 ${flaggedQuestions[questionId] ? "border-white bg-red-500" : "border-gray-100 bg-white"}`} >
{/* Opsi Radio Button */} {question.options?.length > 0 ? (
handleAnswerChange(question.questionId, value)} className="flex flex-col gap-2" > {question.options.map((option: any) => (
handleAnswerChange(question.questionId, option.optionId)} >
))}
) : ( Tidak ada opsi untuk pertanyaan ini. )} {/* Textarea */}