diff --git a/apps/frontend/src/routes/_assessmentLayout/assessment/index.lazy.tsx b/apps/frontend/src/routes/_assessmentLayout/assessment/index.lazy.tsx index 5653400..f33f79b 100644 --- a/apps/frontend/src/routes/_assessmentLayout/assessment/index.lazy.tsx +++ b/apps/frontend/src/routes/_assessmentLayout/assessment/index.lazy.tsx @@ -12,6 +12,7 @@ import { Card, CardContent, CardDescription, + CardTitle, } from "@/shadcn/components/ui/card"; import { Button } from "@/shadcn/components/ui/button"; import { Textarea } from "@/shadcn/components/ui/textarea"; @@ -33,12 +34,33 @@ import { getQuestionsAllQueryOptions, toggleFlagAnswer, } from "@/modules/assessmentManagement/queries/assessmentQueries"; -import { TbFlagFilled, TbUpload, TbChevronRight, TbChevronDown } from "react-icons/tb"; +import { TbFlagFilled, TbUpload, TbChevronRight, TbChevronDown, TbLayoutSidebarLeftCollapseFilled, TbMenu2 } 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 { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/shadcn/components/ui/sheet"; +import { + LeftSheet, + LeftSheetClose, + LeftSheetContent, + LeftSheetDescription, + LeftSheetFooter, + LeftSheetHeader, + LeftSheetTitle, + LeftSheetTrigger, +} from "@/shadcn/components/ui/leftsheet"; import { useState, useRef, useEffect } from "react"; +import AppHeader from "@/components/AppHeader"; const getQueryParam = (param: string) => { const urlParams = new URLSearchParams(window.location.search); @@ -66,8 +88,8 @@ interface ToggleFlagResponse { export interface SubmitOptionResponse { message: string; answer: { - id: string; - isFlagged: boolean | null; + id: string; + isFlagged: boolean | null; }; } @@ -95,6 +117,29 @@ export default function AssessmentPage() { const [currentPagePerSubAspect, setCurrentPagePerSubAspect] = useState<{ [subAspectId: string]: number }>({}); const currentPage = currentPagePerSubAspect[selectedSubAspectId || ""] || 1; const questionsPerPage = 10; + const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); // Check for mobile screen + const [isSidebarOpen, setIsSidebarOpen] = useState(false); + const [openNavbar, setOpenNavbar] = useState(false); + const [isLeftSidebarOpen, setIsLeftSidebarOpen] = useState(false); + + // Toggle the sidebar open/close on mobile + const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen); + + const toggleLeftSidebar = () => setIsLeftSidebarOpen(!isLeftSidebarOpen); + + const toggleSidebarLeftSide = () => { + setIsSidebarOpen((prevState) => !prevState); + }; + + // Fungsi toggle untuk membuka/menutup navbar + const toggle = () => { + setOpenNavbar((prevState) => !prevState); + }; + + // Adjust layout on screen resize + window.addEventListener('resize', () => { + setIsMobile(window.innerWidth <= 768); + }); // Fetch aspects and sub-aspects const aspectsQuery = useQuery({ @@ -114,13 +159,13 @@ export default function AssessmentPage() { 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 @@ -130,15 +175,15 @@ export default function AssessmentPage() { .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 @@ -149,7 +194,7 @@ export default function AssessmentPage() { 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 @@ -169,41 +214,37 @@ export default function AssessmentPage() { 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; - }, - {} + (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; + 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."); + 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 } }, [answersData]); @@ -212,34 +253,34 @@ export default function AssessmentPage() { // Filter pertanyaan yang belum dijawab berdasarkan data `answers` const unanswered = data?.data.filter(question => { - return question.questionId !== null && !answers[question.questionId]; + return question.questionId !== null && !answers[question.questionId]; }) || []; setUnansweredQuestions(unanswered.length); // Tampilkan modal berdasarkan jumlah pertanyaan yang belum dijawab if (unanswered.length > 0) { - setValidationModalOpen(true); + setValidationModalOpen(true); } else { - setModalOpen(true); + 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(); + // 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); + // 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); + console.error("Error finishing assessment:", error); } finally { - setModalOpen(false); + setModalOpen(false); } }; @@ -259,8 +300,8 @@ export default function AssessmentPage() { // Filter aspects by selected aspectId const filteredAspects = selectedAspectId - ? aspects.filter((aspect) => aspect.aspectId === selectedAspectId) // Use 'id' instead of 'aspectId' - : aspects; + ? 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); @@ -274,7 +315,7 @@ export default function AssessmentPage() { } return acc; }, {} as Record); - + if (initialFlagData) { setFlaggedQuestions(initialFlagData); } @@ -283,18 +324,18 @@ export default function AssessmentPage() { // Mutation function to toggle flag const { mutate: toggleFlag } = useMutation({ mutationFn: (formData: { assessmentId: string; questionId: string; isFlagged: boolean }) => - toggleFlagAnswer(formData), + toggleFlagAnswer(formData), onSuccess: (response: SubmitOptionResponse) => { - if (response.answer) { - const { answer } = response; - setFlaggedQuestions((prevFlags) => ({ - ...prevFlags, - [answer.id]: answer.isFlagged ?? false, - })); - } + if (response.answer) { + const { answer } = response; + setFlaggedQuestions((prevFlags) => ({ + ...prevFlags, + [answer.id]: answer.isFlagged ?? false, + })); + } }, onError: (error) => { - console.error("Error toggling flag:", error); + console.error("Error toggling flag:", error); }, }); @@ -304,21 +345,21 @@ export default function AssessmentPage() { const assessmentId = getQueryParam("id"); if (!assessmentId) { - console.error("Assessment ID tidak ditemukan"); - return; + console.error("Assessment ID tidak ditemukan"); + return; } // Update flaggedQuestions di state setFlaggedQuestions((prevFlags) => ({ - ...prevFlags, - [questionId]: newFlagState, + ...prevFlags, + [questionId]: newFlagState, })); // Kirim perubahan flag ke server toggleFlag({ - assessmentId, - questionId, - isFlagged: newFlagState, + assessmentId, + questionId, + isFlagged: newFlagState, }); }; @@ -326,19 +367,19 @@ export default function AssessmentPage() { const submitOptionMutation = useMutation({ ...submitOptionMutationOptions, // Spread the mutation options here onSuccess: () => { - // Refetch the average scores after a successful submission - averageScoreQuery.refetch(); + // Refetch the average scores after a successful submission + averageScoreQuery.refetch(); }, onError: (error) => { - console.error("Error submitting option:", 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; + console.error("Assessment ID tidak ditemukan"); + return; } // Update answers in the state @@ -347,23 +388,23 @@ export default function AssessmentPage() { // Send the updated answer to the backend submitOptionMutation.mutate({ - optionId, - assessmentId, - questionId, - isFlagged: false, - filename: undefined, + 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; + acc[item.questionId] = item.validationInformation; } return acc; }, {} as Record); // Mengambil data dari database saat komponen dimuat - useEffect(() => { + useEffect(() => { if (validationResult) { setValidationInformation(validationResult); } @@ -372,16 +413,16 @@ export default function AssessmentPage() { // Mutation untuk mengirim data ke backend const { mutate: submitValidation } = useMutation({ mutationFn: (form: { - assessmentId: string; - questionId: string; - validationInformation: string; + assessmentId: string; + questionId: string; + validationInformation: string; }) => submitValidationQuery(form), onSuccess: () => { - // Tindakan yang diambil setelah berhasil - console.log("Validation updated successfully!"); + // Tindakan yang diambil setelah berhasil + console.log("Validation updated successfully!"); }, onError: (error) => { - console.error("Error updating validation:", error); + console.error("Error updating validation:", error); }, }); @@ -389,20 +430,20 @@ export default function AssessmentPage() { const handleTextareaChange = (questionId: string, value: string) => { // Memperbarui state validationInformation setValidationInformation((prev) => ({ - ...prev, - [questionId]: value, + ...prev, + [questionId]: value, })); // Pastikan assessmentId tidak null sebelum mengirimkan data ke server if (assessmentId) { - // Kirim data validasi ke server - submitValidation({ - assessmentId, - questionId, - validationInformation: value, - }); + // Kirim data validasi ke server + submitValidation({ + assessmentId, + questionId, + validationInformation: value, + }); } else { - console.error("Assessment ID tidak ditemukan"); + console.error("Assessment ID tidak ditemukan"); } }; @@ -412,16 +453,16 @@ export default function AssessmentPage() { // 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]); @@ -431,7 +472,7 @@ export default function AssessmentPage() { event.preventDefault(); setDragActive(true); }; - + const handleDragLeave = () => { setDragActive(false); }; @@ -443,43 +484,43 @@ export default function AssessmentPage() { 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 file = droppedFiles[0]; - const formData = new FormData(); - formData.append('file', file); + // Validate file size + if (file.size > MAX_FILE_SIZE) { + setExceededFileName(file.name); + setModalOpenFileSize(true); + return; + } - if (assessmentId) { - formData.append('assessmentId', assessmentId); - } else { - console.error("assessmentId is null"); - return; - } + const formData = new FormData(); + formData.append('file', file); - if (question.questionId) { - formData.append('questionId', question.questionId); - } else { - console.error("questionId is null"); - return; - } + if (assessmentId) { + formData.append('assessmentId', assessmentId); + } else { + console.error("assessmentId is null"); + return; + } - uploadFileMutation.mutate(formData); // Upload file + if (question.questionId) { + formData.append('questionId', question.questionId); + } else { + console.error("questionId is null"); + return; + } - // 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 - })); + 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) { @@ -489,44 +530,44 @@ export default function AssessmentPage() { 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]; + 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 - })); + // 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) => ({ @@ -591,7 +632,7 @@ export default function AssessmentPage() { const totalQuestionsInSubAspect = data?.data?.filter( (question) => question.subAspectId === selectedSubAspectId )?.length || 0; - + const totalPages = Math.ceil(totalQuestionsInSubAspect / questionsPerPage); return ( @@ -601,62 +642,120 @@ export default function AssessmentPage() { {/* 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] ? ( - - ) : ( - + + + {/* Sidebar for Mobile */} + {isMobile && ( + setIsLeftSidebarOpen(open)}> + + +
+ {/* 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}
+
+ ))} +
)}
-
+ ))} +
+ + + + )} - {/* Sub-Aspek */} - {openAspects[aspect.id] && ( -
- {aspect.subAspects - .filter((subAspect) => - data?.data.some((question) => question.subAspectId === subAspect.id) - ) - .map((subAspect) => ( -
setSelectedSubAspectId(subAspect.id)} - > -
{subAspect.name}
-
- ))} + {/* Sidebar for Desktop (Always Visible) */} +
+ +
+ {/* 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 @@ -678,14 +777,12 @@ export default function AssessmentPage() { className="space-y-4" > - - {/* Question */} - {startIndex + index + 1}. -
- - {question.questionText} - -
+
+ {/* Question Number */} + {startIndex + index + 1}. + + {/* Question Text */} + {question.questionText} {/* Action Icon/Flag */} - +
- {/* Opsi Radio Button */} + {/* Radio Button Options */} {question.options?.length > 0 ? ( -
+
handleAnswerChange(question.questionId, value)} @@ -711,11 +808,10 @@ export default function AssessmentPage() { {question.options.map((option: any) => (
handleAnswerChange(question.questionId, option.optionId)} > Tidak ada opsi untuk pertanyaan ini. )} - {/* Textarea */} + {/* Textarea for additional information */}