update: add sheet component on left side and right side

This commit is contained in:
abiyasa05 2024-11-14 14:04:24 +07:00
parent 0b416be90c
commit 45e1501762

View File

@ -12,6 +12,7 @@ import {
Card, Card,
CardContent, CardContent,
CardDescription, CardDescription,
CardTitle,
} from "@/shadcn/components/ui/card"; } from "@/shadcn/components/ui/card";
import { Button } from "@/shadcn/components/ui/button"; import { Button } from "@/shadcn/components/ui/button";
import { Textarea } from "@/shadcn/components/ui/textarea"; import { Textarea } from "@/shadcn/components/ui/textarea";
@ -33,12 +34,33 @@ import {
getQuestionsAllQueryOptions, getQuestionsAllQueryOptions,
toggleFlagAnswer, toggleFlagAnswer,
} from "@/modules/assessmentManagement/queries/assessmentQueries"; } 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 FinishAssessmentModal from "@/modules/assessmentManagement/modals/ConfirmModal";
import FileUpload from "@/modules/assessmentManagement/fileUpload/fileUpload"; import FileUpload from "@/modules/assessmentManagement/fileUpload/fileUpload";
import ValidationModal from "@/modules/assessmentManagement/modals/ValidationModal"; import ValidationModal from "@/modules/assessmentManagement/modals/ValidationModal";
import FileSizeValidationModal from "@/modules/assessmentManagement/modals/FileSizeValidationModal"; 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 { useState, useRef, useEffect } from "react";
import AppHeader from "@/components/AppHeader";
const getQueryParam = (param: string) => { const getQueryParam = (param: string) => {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
@ -66,8 +88,8 @@ interface ToggleFlagResponse {
export interface SubmitOptionResponse { export interface SubmitOptionResponse {
message: string; message: string;
answer: { answer: {
id: string; id: string;
isFlagged: boolean | null; isFlagged: boolean | null;
}; };
} }
@ -95,6 +117,29 @@ export default function AssessmentPage() {
const [currentPagePerSubAspect, setCurrentPagePerSubAspect] = useState<{ [subAspectId: string]: number }>({}); const [currentPagePerSubAspect, setCurrentPagePerSubAspect] = useState<{ [subAspectId: string]: number }>({});
const currentPage = currentPagePerSubAspect[selectedSubAspectId || ""] || 1; const currentPage = currentPagePerSubAspect[selectedSubAspectId || ""] || 1;
const questionsPerPage = 10; 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 // Fetch aspects and sub-aspects
const aspectsQuery = useQuery({ const aspectsQuery = useQuery({
@ -169,41 +214,37 @@ export default function AssessmentPage() {
if (answersData && answersData.data) { if (answersData && answersData.data) {
const transformedData = answersData.data.reduce( const transformedData = answersData.data.reduce(
(acc: Record<string, string>, item: any) => { (acc: Record<string, string>, item: any) => {
if (item.questionId && item.optionId) { if (item.questionId && item.optionId) {
acc[item.questionId] = item.optionId; acc[item.questionId] = item.optionId;
} }
return acc; return acc;
}, },
{} {}
); );
} else {
console.error("No data found or data is undefined.");
} }
// Effect untuk mengatur answers dari data yang diambil // Effect untuk mengatur answers dari data yang diambil
useEffect(() => { useEffect(() => {
const assessmentId = getQueryParam("id"); const assessmentId = getQueryParam("id");
if (!assessmentId) { if (!assessmentId) {
console.error("Assessment ID tidak ditemukan"); console.error("Assessment ID tidak ditemukan");
return; return;
} }
// Set answers from `answersData` if data is available // Set answers from `answersData` if data is available
if (answersData && Array.isArray(answersData.data)) { if (answersData && Array.isArray(answersData.data)) {
const answersFromDatabase = answersData.data.reduce( const answersFromDatabase = answersData.data.reduce(
(acc: Record<string, string>, item: any) => { (acc: Record<string, string>, item: any) => {
if (item.questionId && item.optionId) { if (item.questionId && item.optionId) {
acc[item.questionId] = item.optionId; acc[item.questionId] = item.optionId;
} }
return acc; return acc;
}, },
{} {}
); );
setAnswers(answersFromDatabase); // Set the transformed data directly to state setAnswers(answersFromDatabase); // Set the transformed data directly to state
} else {
console.error("No data found or data is undefined.");
} }
}, [answersData]); }, [answersData]);
@ -212,34 +253,34 @@ export default function AssessmentPage() {
// Filter pertanyaan yang belum dijawab berdasarkan data `answers` // Filter pertanyaan yang belum dijawab berdasarkan data `answers`
const unanswered = data?.data.filter(question => { const unanswered = data?.data.filter(question => {
return question.questionId !== null && !answers[question.questionId]; return question.questionId !== null && !answers[question.questionId];
}) || []; }) || [];
setUnansweredQuestions(unanswered.length); setUnansweredQuestions(unanswered.length);
// Tampilkan modal berdasarkan jumlah pertanyaan yang belum dijawab // Tampilkan modal berdasarkan jumlah pertanyaan yang belum dijawab
if (unanswered.length > 0) { if (unanswered.length > 0) {
setValidationModalOpen(true); setValidationModalOpen(true);
} else { } else {
setModalOpen(true); setModalOpen(true);
} }
}; };
const handleConfirmFinish = async (assessmentId: string) => { const handleConfirmFinish = async (assessmentId: string) => {
try { try {
// Skip counting unanswered questions here to prevent duplication // Skip counting unanswered questions here to prevent duplication
const mutation = submitAssessmentMutationOptions(assessmentId); const mutation = submitAssessmentMutationOptions(assessmentId);
const response = await mutation.mutationFn(); const response = await mutation.mutationFn();
// Navigate to results // Navigate to results
const newUrl = `/assessmentResult?id=${assessmentId}`; const newUrl = `/assessmentResult?id=${assessmentId}`;
window.history.pushState({}, "", newUrl); window.history.pushState({}, "", newUrl);
console.log("Navigated to:", newUrl); console.log("Navigated to:", newUrl);
console.log(response.message); console.log(response.message);
} catch (error) { } catch (error) {
console.error("Error finishing assessment:", error); console.error("Error finishing assessment:", error);
} finally { } finally {
setModalOpen(false); setModalOpen(false);
} }
}; };
@ -259,8 +300,8 @@ export default function AssessmentPage() {
// Filter aspects by selected aspectId // Filter aspects by selected aspectId
const filteredAspects = selectedAspectId const filteredAspects = selectedAspectId
? aspects.filter((aspect) => aspect.aspectId === selectedAspectId) // Use 'id' instead of 'aspectId' ? aspects.filter((aspect) => aspect.aspectId === selectedAspectId) // Use 'id' instead of 'aspectId'
: aspects; : aspects;
// Get the currently selected aspect to show all related sub-aspects // Get the currently selected aspect to show all related sub-aspects
const currentAspect = aspects.find(aspect => aspect.aspectId === selectedAspectId); const currentAspect = aspects.find(aspect => aspect.aspectId === selectedAspectId);
@ -283,18 +324,18 @@ export default function AssessmentPage() {
// Mutation function to toggle flag // Mutation function to toggle flag
const { mutate: toggleFlag } = useMutation({ const { mutate: toggleFlag } = useMutation({
mutationFn: (formData: { assessmentId: string; questionId: string; isFlagged: boolean }) => mutationFn: (formData: { assessmentId: string; questionId: string; isFlagged: boolean }) =>
toggleFlagAnswer(formData), toggleFlagAnswer(formData),
onSuccess: (response: SubmitOptionResponse) => { onSuccess: (response: SubmitOptionResponse) => {
if (response.answer) { if (response.answer) {
const { answer } = response; const { answer } = response;
setFlaggedQuestions((prevFlags) => ({ setFlaggedQuestions((prevFlags) => ({
...prevFlags, ...prevFlags,
[answer.id]: answer.isFlagged ?? false, [answer.id]: answer.isFlagged ?? false,
})); }));
} }
}, },
onError: (error) => { 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"); const assessmentId = getQueryParam("id");
if (!assessmentId) { if (!assessmentId) {
console.error("Assessment ID tidak ditemukan"); console.error("Assessment ID tidak ditemukan");
return; return;
} }
// Update flaggedQuestions di state // Update flaggedQuestions di state
setFlaggedQuestions((prevFlags) => ({ setFlaggedQuestions((prevFlags) => ({
...prevFlags, ...prevFlags,
[questionId]: newFlagState, [questionId]: newFlagState,
})); }));
// Kirim perubahan flag ke server // Kirim perubahan flag ke server
toggleFlag({ toggleFlag({
assessmentId, assessmentId,
questionId, questionId,
isFlagged: newFlagState, isFlagged: newFlagState,
}); });
}; };
@ -326,19 +367,19 @@ export default function AssessmentPage() {
const submitOptionMutation = useMutation({ const submitOptionMutation = useMutation({
...submitOptionMutationOptions, // Spread the mutation options here ...submitOptionMutationOptions, // Spread the mutation options here
onSuccess: () => { onSuccess: () => {
// Refetch the average scores after a successful submission // Refetch the average scores after a successful submission
averageScoreQuery.refetch(); averageScoreQuery.refetch();
}, },
onError: (error) => { onError: (error) => {
console.error("Error submitting option:", error); console.error("Error submitting option:", error);
}, },
}); });
const handleAnswerChange = (questionId: string, optionId: string) => { const handleAnswerChange = (questionId: string, optionId: string) => {
const assessmentId = getQueryParam("id"); const assessmentId = getQueryParam("id");
if (!assessmentId) { if (!assessmentId) {
console.error("Assessment ID tidak ditemukan"); console.error("Assessment ID tidak ditemukan");
return; return;
} }
// Update answers in the state // Update answers in the state
@ -347,17 +388,17 @@ export default function AssessmentPage() {
// Send the updated answer to the backend // Send the updated answer to the backend
submitOptionMutation.mutate({ submitOptionMutation.mutate({
optionId, optionId,
assessmentId, assessmentId,
questionId, questionId,
isFlagged: false, isFlagged: false,
filename: undefined, filename: undefined,
}); });
}; };
const validationResult = answersData?.data.reduce((acc, item) => { const validationResult = answersData?.data.reduce((acc, item) => {
if (item.questionId != null && item.validationInformation != null) { if (item.questionId != null && item.validationInformation != null) {
acc[item.questionId] = item.validationInformation; acc[item.questionId] = item.validationInformation;
} }
return acc; return acc;
}, {} as Record<string, string>); }, {} as Record<string, string>);
@ -372,16 +413,16 @@ export default function AssessmentPage() {
// Mutation untuk mengirim data ke backend // Mutation untuk mengirim data ke backend
const { mutate: submitValidation } = useMutation({ const { mutate: submitValidation } = useMutation({
mutationFn: (form: { mutationFn: (form: {
assessmentId: string; assessmentId: string;
questionId: string; questionId: string;
validationInformation: string; validationInformation: string;
}) => submitValidationQuery(form), }) => submitValidationQuery(form),
onSuccess: () => { onSuccess: () => {
// Tindakan yang diambil setelah berhasil // Tindakan yang diambil setelah berhasil
console.log("Validation updated successfully!"); console.log("Validation updated successfully!");
}, },
onError: (error) => { 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) => { const handleTextareaChange = (questionId: string, value: string) => {
// Memperbarui state validationInformation // Memperbarui state validationInformation
setValidationInformation((prev) => ({ setValidationInformation((prev) => ({
...prev, ...prev,
[questionId]: value, [questionId]: value,
})); }));
// Pastikan assessmentId tidak null sebelum mengirimkan data ke server // Pastikan assessmentId tidak null sebelum mengirimkan data ke server
if (assessmentId) { if (assessmentId) {
// Kirim data validasi ke server // Kirim data validasi ke server
submitValidation({ submitValidation({
assessmentId, assessmentId,
questionId, questionId,
validationInformation: value, validationInformation: value,
}); });
} else { } else {
console.error("Assessment ID tidak ditemukan"); console.error("Assessment ID tidak ditemukan");
} }
}; };
@ -445,39 +486,39 @@ export default function AssessmentPage() {
const droppedFiles = Array.from(event.dataTransfer.files); const droppedFiles = Array.from(event.dataTransfer.files);
if (droppedFiles.length > 0) { if (droppedFiles.length > 0) {
const file = droppedFiles[0]; const file = droppedFiles[0];
// Validate file size // Validate file size
if (file.size > MAX_FILE_SIZE) { if (file.size > MAX_FILE_SIZE) {
setExceededFileName(file.name); setExceededFileName(file.name);
setModalOpenFileSize(true); setModalOpenFileSize(true);
return; return;
} }
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
if (assessmentId) { if (assessmentId) {
formData.append('assessmentId', assessmentId); formData.append('assessmentId', assessmentId);
} else { } else {
console.error("assessmentId is null"); console.error("assessmentId is null");
return; return;
} }
if (question.questionId) { if (question.questionId) {
formData.append('questionId', question.questionId); formData.append('questionId', question.questionId);
} else { } else {
console.error("questionId is null"); console.error("questionId is null");
return; return;
} }
uploadFileMutation.mutate(formData); // Upload file uploadFileMutation.mutate(formData); // Upload file
// Update state to reflect the uploaded file (store the File object, not just the name) // Update state to reflect the uploaded file (store the File object, not just the name)
setUploadedFiles(prev => ({ setUploadedFiles(prev => ({
...prev, ...prev,
[question.questionId]: file, // Store the file itself [question.questionId]: file, // Store the file itself
})); }));
} }
}; };
@ -489,42 +530,42 @@ export default function AssessmentPage() {
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>, question: { questionId: string }) => { const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>, question: { questionId: string }) => {
if (event.target.files) { if (event.target.files) {
const fileArray = Array.from(event.target.files); const fileArray = Array.from(event.target.files);
if (fileArray.length > 0) { if (fileArray.length > 0) {
const file = fileArray[0]; const file = fileArray[0];
// Validate file size // Validate file size
if (file.size > MAX_FILE_SIZE) { if (file.size > MAX_FILE_SIZE) {
setExceededFileName(file.name); setExceededFileName(file.name);
setModalOpenFileSize(true); setModalOpenFileSize(true);
return; 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 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
}));
}
} }
}; };
@ -601,62 +642,120 @@ export default function AssessmentPage() {
{/* LEFT-SIDE */} {/* LEFT-SIDE */}
{/* Aspek dan Sub-Aspek */} {/* Aspek dan Sub-Aspek */}
<div className="fixed h-screen w-64 overflow-auto"> <TbMenu2
<Flex direction="column" gap="xs" className="w-64"> onClick={toggleLeftSidebar}
<div className="space-y-2"> className="ml-4 w-6 h-fit pb-4 mb-4 cursor-pointer"
{/* Aspek */} />
{aspectsQuery.data?.data
.filter((aspect) => {/* Sidebar for Mobile */}
aspect.subAspects.some((subAspect) => {isMobile && (
data?.data.some((question) => question.subAspectId === subAspect.id) <LeftSheet open={isLeftSidebarOpen} onOpenChange={(open) => setIsLeftSidebarOpen(open)}>
) <LeftSheetContent className="h-full w-75 overflow-auto mt-8">
) <Flex direction="column" gap="xs" className="w-64">
.map((aspect) => ( <div className="space-y-2">
<div {/* Aspek */}
key={aspect.id} {aspectsQuery.data?.data
className="p-2 " .filter((aspect) =>
> aspect.subAspects.some((subAspect) =>
<div data?.data.some((question) => question.subAspectId === subAspect.id)
className="flex justify-between cursor-pointer" )
onClick={() => toggleAspect(aspect.id)} )
> .map((aspect) => (
<div className="text-sm font-bold px-3">{aspect.name}</div> <div key={aspect.id} className="p-2 ">
<div> <div
{openAspects[aspect.id] ? ( className="flex justify-between cursor-pointer"
<TbChevronDown size={25} /> onClick={() => toggleAspect(aspect.id)}
) : ( >
<TbChevronRight size={25} /> <div className="text-sm font-bold px-3">{aspect.name}</div>
<div>
{openAspects[aspect.id] ? (
<TbChevronDown size={25} />
) : (
<TbChevronRight size={25} />
)}
</div>
</div>
{/* Sub-Aspek */}
{openAspects[aspect.id] && (
<div className="mt-2 space-y-2">
{aspect.subAspects
.filter((subAspect) =>
data?.data.some((question) => question.subAspectId === subAspect.id)
)
.map((subAspect) => (
<div
key={subAspect.id}
className={`flex justify-between cursor-pointer p-2 px-6 rounded-sm transition-colors duration-150 ${selectedSubAspectId === subAspect.id ? 'text-black font-medium bg-gray-200' : 'text-gray-500'}`}
onClick={() => setSelectedSubAspectId(subAspect.id)}
>
<div className="text-xs">{subAspect.name}</div>
</div>
))}
</div>
)} )}
</div> </div>
</div> ))}
</div>
</Flex>
</LeftSheetContent>
</LeftSheet>
)}
{/* Sub-Aspek */} {/* Sidebar for Desktop (Always Visible) */}
{openAspects[aspect.id] && ( <div className="hidden md:block fixed h-screen w-64 overflow-auto mt-8">
<div className="mt-2 space-y-2"> <Flex direction="column" gap="xs" className="w-64">
{aspect.subAspects <div className="space-y-2">
.filter((subAspect) => {/* Aspek */}
data?.data.some((question) => question.subAspectId === subAspect.id) {aspectsQuery.data?.data
) .filter((aspect) =>
.map((subAspect) => ( aspect.subAspects.some((subAspect) =>
<div data?.data.some((question) => question.subAspectId === subAspect.id)
key={subAspect.id} )
className={`flex justify-between cursor-pointer p-2 px-6 rounded-sm transition-colors duration-150 ${selectedSubAspectId === subAspect.id ? 'text-black font-medium bg-gray-200' : 'text-gray-500'}`} )
onClick={() => setSelectedSubAspectId(subAspect.id)} .map((aspect) => (
> <div key={aspect.id} className="p-2 ">
<div className="text-xs">{subAspect.name}</div> <div
</div> className="flex justify-between cursor-pointer"
))} onClick={() => toggleAspect(aspect.id)}
>
<div className="text-sm font-bold px-3">{aspect.name}</div>
<div>
{openAspects[aspect.id] ? (
<TbChevronDown size={25} />
) : (
<TbChevronRight size={25} />
)}
</div>
</div> </div>
)}
</div> {/* Sub-Aspek */}
))} {openAspects[aspect.id] && (
</div> <div className="mt-2 space-y-2">
</Flex> {aspect.subAspects
</div> .filter((subAspect) =>
data?.data.some((question) => question.subAspectId === subAspect.id)
)
.map((subAspect) => (
<div
key={subAspect.id}
className={`flex justify-between cursor-pointer p-2 px-6 rounded-sm transition-colors duration-150 ${selectedSubAspectId === subAspect.id ? 'text-black font-medium bg-gray-200' : 'text-gray-500'}`}
onClick={() => setSelectedSubAspectId(subAspect.id)}
>
<div className="text-xs">{subAspect.name}</div>
</div>
))}
</div>
)}
</div>
))}
</div>
</Flex>
</div>
{/* MIDDLE */} {/* MIDDLE */}
{/* Pertanyaan */} {/* Pertanyaan */}
<div className="ml-64 mr-60 flex-1 overflow-y-auto h-full"> <div className="ml-0 md:ml-64 mr-0 md:mr-60 flex-1 overflow-y-auto h-full">
<Stack gap="sm" style={{ flex: 1 }}> <Stack gap="sm" style={{ flex: 1 }}>
<Text className="text-2xl font-bold ml-6"> <Text className="text-2xl font-bold ml-6">
Harap menjawab semua pertanyaan yang tersedia Harap menjawab semua pertanyaan yang tersedia
@ -678,14 +777,12 @@ export default function AssessmentPage() {
className="space-y-4" className="space-y-4"
> >
<Stack gap="sm"> <Stack gap="sm">
<Flex justify="space-between" align="flex-start" style={{ width: "100%" }}> <div className="grid grid-cols-[auto_1fr_auto] gap-2 w-full items-start">
{/* Question */} {/* Question Number */}
<Text className="font-bold mx-3 p-1 text-sm">{startIndex + index + 1}.</Text> <Text className="font-bold p-1 text-sm">{startIndex + index + 1}.</Text>
<div className="flex-grow">
<Text className="font-bold break-words text-sm p-1"> {/* Question Text */}
{question.questionText} <Text className="font-bold break-words text-sm p-1">{question.questionText}</Text>
</Text>
</div>
{/* Action Icon/Flag */} {/* Action Icon/Flag */}
<ActionIcon <ActionIcon
@ -695,14 +792,14 @@ export default function AssessmentPage() {
> >
<TbFlagFilled <TbFlagFilled
size={25} size={25}
className={`p-1 ${flaggedQuestions[questionId] ? "text-white" : "text-black"}`} // Mengurangi padding untuk ikon className={`p-1 ${flaggedQuestions[questionId] ? "text-white" : "text-black"}`}
/> />
</ActionIcon> </ActionIcon>
</Flex> </div>
{/* Opsi Radio Button */} {/* Radio Button Options */}
{question.options?.length > 0 ? ( {question.options?.length > 0 ? (
<div className="mx-11"> <div className="mx-8">
<RadioGroup <RadioGroup
value={answers[question.questionId] || ""} value={answers[question.questionId] || ""}
onValueChange={(value) => handleAnswerChange(question.questionId, value)} onValueChange={(value) => handleAnswerChange(question.questionId, value)}
@ -711,11 +808,10 @@ export default function AssessmentPage() {
{question.options.map((option: any) => ( {question.options.map((option: any) => (
<div <div
key={option.optionId} key={option.optionId}
className={`cursor-pointer transition-transform transform hover:scale-105 shadow-md hover:shadow-lg flex items-center border-4 rounded-lg p-3 text-sm ${ className={`cursor-pointer transition-transform transform hover:scale-105 shadow-md hover:shadow-lg flex items-center border-4 rounded-lg p-3 text-sm ${answers[question.questionId] === option.optionId
answers[question.questionId] === option.optionId ? "bg-[--primary-color] text-white border-[--primary-color]"
? "bg-[--primary-color] text-white border-[--primary-color]" : "bg-gray-200 text-black border-gray-200"
: "bg-gray-200 text-black border-gray-200" }`}
}`}
onClick={() => handleAnswerChange(question.questionId, option.optionId)} onClick={() => handleAnswerChange(question.questionId, option.optionId)}
> >
<RadioGroupItem <RadioGroupItem
@ -738,7 +834,7 @@ export default function AssessmentPage() {
<Text color="red">Tidak ada opsi untuk pertanyaan ini.</Text> <Text color="red">Tidak ada opsi untuk pertanyaan ini.</Text>
)} )}
{/* Textarea */} {/* Textarea for additional information */}
<div className="mx-11"> <div className="mx-11">
<Textarea <Textarea
placeholder="Berikan keterangan terkait jawaban di atas" placeholder="Berikan keterangan terkait jawaban di atas"
@ -763,7 +859,7 @@ export default function AssessmentPage() {
handleClick={handleClick} handleClick={handleClick}
/> />
{/* Garis pembatas setiap soal */} {/* Divider between questions */}
<div> <div>
<hr className="border-t-2 border-gray-300 mx-11 mt-6 mb-6" /> <hr className="border-t-2 border-gray-300 mx-11 mt-6 mb-6" />
</div> </div>
@ -777,7 +873,174 @@ export default function AssessmentPage() {
{/* RIGHT-SIDE */} {/* RIGHT-SIDE */}
{/* Navigasi dan Pagination */} {/* Navigasi dan Pagination */}
<div className="fixed h-screen right-0 w-60 overflow-auto mr-4"> {isMobile && (
<button
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
className="fixed bottom-4 right-4 bg-[--primary-color] text-white p-3 rounded-full shadow-md z-50"
>
<TbLayoutSidebarLeftCollapseFilled size={30} />
</button>
)}
{/* Sidebar for mobile (only when toggled) */}
<div className="hidden md:block">
<Sheet open={isSidebarOpen} onOpenChange={(open) => setIsSidebarOpen(open)}>
<SheetContent className="h-full w-70 overflow-auto mr-4">
<SheetTitle className="font-medium text-lg text-gray-800 mb-2">
Nomor Soal
</SheetTitle>
{/* <Text className="font-medium text-lg text-gray-800 mb-2">Nomor Soal</Text> */}
{/* Navigasi (Number of Questions) */}
<div className="grid grid-cols-5 gap-2">
{Array.from({ length: totalQuestionsInSubAspect }).map((_, i) => {
const questionNumber = startIndex + i + 1;
const questionId = filteredQuestions[i]?.questionId;
return questionId ? (
<div key={questionId} className="flex justify-center relative">
<button
className={`
w-9 h-9 border rounded-sm flex items-center justify-center relative text-md
${answers[questionId] && flaggedQuestions[questionId] ? "bg-white text-black" : ""}
${answers[questionId] && !flaggedQuestions[questionId] ? "bg-[--primary-color] text-white" : ""}
${!answers[questionId] && !flaggedQuestions[questionId] ? "bg-transparent text-black" : ""}
${flaggedQuestions[questionId] ? "border-gray-50" : ""}
`}
onClick={() => scrollToQuestion(questionId)}
>
{questionNumber}
</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-red-600 rounded-tr-sm" />
)}
</div>
) : null;
})}
</div>
<div className="mt-4 flex justify-center">
<Pagination
page={currentPage}
totalPages={totalPages}
onPageChange={(newPage) => {
if (selectedSubAspectId) {
handlePageChange(selectedSubAspectId, newPage);
}
}}
>
<Text className="text-xs m-0">Halaman {currentPage} dari {totalPages}</Text>
</Pagination>
</div>
{/* Skor Aspek dan Sub-Aspek */}
<div className="mt-4">
<Card>
<CardTitle className="text-lg font-extrabold text-center mt-4 mb-2">
Nilai Sementara
</CardTitle>
<CardContent className="max-h-full overflow-hidden">
<ScrollArea className="h-[200px] w-full rounded-md p-2">
<CardDescription>
{filteredAspects.length > 0 ? (
filteredAspects.map((aspect) => {
const aspectScore = parseFloat(aspect.averageScore).toFixed(2);
const aspectScoreValue = parseFloat(aspectScore);
return (
<div key={aspect.aspectId} className="flex justify-between items-center">
<Text className="text-lg text-gray-700">{aspect.aspectName}</Text>
<Text
className={`text-xl font-bold ${
aspectScoreValue >= 4.5
? "text-green-700"
: aspectScoreValue >= 3.5
? "text-green-400"
: aspectScoreValue >= 2.5
? "text-yellow-400"
: aspectScoreValue >= 1.5
? "text-orange-500"
: "text-red-500"
}`}
>
{aspectScore}
</Text>
</div>
);
})
) : (
<Text className="text-base text-gray-700">Data aspek ini kosong</Text>
)}
</CardDescription>
{/* Divider */}
<div className="border-t-2 border-gray-300 my-4" />
{/* Skor Sub-Aspek */}
{filteredSubAspects.length > 0 ? (
filteredSubAspects.map((subAspect) => {
const subAspectScore = parseFloat(subAspect.averageScore).toFixed(2);
const subAspectScoreValue = parseFloat(subAspectScore);
return (
<div key={subAspect.subAspectId} className="flex justify-between items-center my-2">
<Text className="text-sm text-gray-700">{subAspect.subAspectName}</Text>
<Text
className={`text-sm font-bold ${
subAspectScoreValue >= 4.5
? "text-green-700"
: subAspectScoreValue >= 3.5
? "text-green-400"
: subAspectScoreValue >= 2.5
? "text-yellow-400"
: subAspectScoreValue >= 1.5
? "text-orange-500"
: "text-red-500"
}`}
>
{subAspectScore}
</Text>
</div>
);
})
) : (
<Text className="text-sm text-gray-700">Data sub-aspek ini kosong</Text>
)}
</ScrollArea>
{/* Finish Button */}
<div>
<Button
onClick={handleFinishClick}
className="bg-[--primary-color] text-white font-bold rounded-md w-full text-sm"
>
Selesai
</Button>
</div>
</CardContent>
{/* Finish Assessment Modal */}
<FinishAssessmentModal
opened={modalOpen}
onClose={() => setModalOpen(false)}
onConfirm={handleConfirmFinish}
assessmentId={assessmentId}
/>
{/* Validation Modal */}
<ValidationModal
opened={validationModalOpen}
onClose={() => setValidationModalOpen(false)}
unansweredQuestions={unansweredQuestions}
/>
</Card>
</div>
</SheetContent>
</Sheet>
</div>
{/* Sidebar for desktop (always visible) */}
<div className="hidden md:block fixed h-screen right-0 w-60 overflow-auto mr-4">
<Flex direction="column" gap="xs" className="mx-4"> <Flex direction="column" gap="xs" className="mx-4">
<Text className="font-medium text-lg text-gray-800 mb-2"> <Text className="font-medium text-lg text-gray-800 mb-2">
Nomor Soal Nomor Soal