update: change assessment and verifying, add border on left and right side

This commit is contained in:
abiyasa05 2024-11-16 09:04:16 +07:00
parent 563251f8c6
commit ca4ec0a538
2 changed files with 748 additions and 788 deletions

View File

@ -1,10 +1,6 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import {
Flex,
Stack,
Text,
Loader,
ActionIcon,
} from "@mantine/core";
import {
Card,
@ -569,9 +565,9 @@ export default function AssessmentPage() {
if (isError) {
return (
<Text color="red">
<p color="red">
Error: {error?.message || "Terjadi kesalahan saat memuat pertanyaan."}
</Text>
</p>
);
}
@ -579,9 +575,9 @@ export default function AssessmentPage() {
return (
<Card>
<CardContent>
<Text color="red" className="text-center">
<p color="red" className="text-center">
Error: Data Asesmen tidak ditemukan. Harap akses halaman melalui link yang valid.
</Text>
</p>
</CardContent>
</Card>
);
@ -615,23 +611,23 @@ export default function AssessmentPage() {
return (
<div>
<Stack gap="md">
<Flex justify="space-between" align="flex-start" mt="lg">
<div className="space-y-4">
<div className="flex justify-between items-start">
{/* LEFT-SIDE */}
{/* Aspek dan Sub-Aspek */}
<AppHeader
openNavbar={isLeftSidebarOpen}
toggle={toggleLeftSidebar}
toggleLeftSidebar={toggleLeftSidebar}
/>
<AppHeader
openNavbar={isLeftSidebarOpen}
toggle={toggleLeftSidebar}
toggleLeftSidebar={toggleLeftSidebar}
/>
{/* Sidebar for Mobile */}
{isMobile && (
<LeftSheet open={isLeftSidebarOpen} onOpenChange={(open) => setIsLeftSidebarOpen(open)}>
<LeftSheetContent className="h-full w-75 overflow-auto">
<Label className="text-gray-800 p-5 text-sm font-normal">Aspek Menu</Label>
<Flex direction="column" gap="xs" className="w-64">
<div className="w-64">
<div className="space-y-2">
{/* Aspek */}
{aspectsQuery.data?.data
@ -666,7 +662,7 @@ export default function AssessmentPage() {
.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'}`}
className={`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>
@ -677,75 +673,75 @@ export default function AssessmentPage() {
</div>
))}
</div>
</Flex>
</div>
</LeftSheetContent>
</LeftSheet>
)}
{/* Sidebar for Desktop (Always Visible) */}
<div className="hidden md:block fixed h-screen w-64 overflow-auto">
<Label className="text-gray-800 p-5 text-sm font-normal">Aspek Menu</Label>
<Flex direction="column" gap="xs" className="w-64">
<div className="space-y-2">
{/* Aspek */}
{aspectsQuery.data?.data
.filter((aspect) =>
aspect.subAspects.some((subAspect) =>
data?.data.some((question) => question.subAspectId === subAspect.id)
)
<div className="hidden md:block fixed h-full w-66 overflow-auto border-x">
<Label className="text-gray-800 p-5 text-sm font-normal">Aspek Menu</Label>
<div className="w-64">
<div className="space-y-2">
{/* Aspek */}
{aspectsQuery.data?.data
.filter((aspect) =>
aspect.subAspects.some((subAspect) =>
data?.data.some((question) => question.subAspectId === subAspect.id)
)
.map((aspect) => (
<div key={aspect.id} className="p-2 ">
<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>
)
.map((aspect) => (
<div key={aspect.id} className="p-2 ">
<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>
{/* 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>
</Flex>
{/* 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>
{/* MIDDLE */}
{/* Pertanyaan */}
<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 }}>
<Text className="text-2xl font-bold ml-6">
<div className="space-y-6 flex flex-col">
<Label className="text-2xl font-bold ml-6 mt-4">
Harap menjawab semua pertanyaan yang tersedia
</Text>
<Text className="text-gray-400 ml-6 mb-7">Semua jawaban Anda akan ditinjau</Text>
</Label>
<Label className="text-gray-400 ml-6 mb-7">Semua jawaban Anda akan ditinjau</Label>
{filteredQuestions.length === 0 ? (
<Text className="text-center p-3">
<Label className="text-center p-3">
Pertanyaan tidak ada untuk sub-aspek yang dipilih.
</Text>
</Label>
) : (
filteredQuestions.map((question: any, index: number) => {
const questionId = question.questionId;
@ -757,99 +753,97 @@ export default function AssessmentPage() {
ref={(el) => (questionRefs.current[questionId] = el)}
className="space-y-4"
>
<Stack gap="sm">
<div className="grid grid-cols-[auto_1fr_auto] gap-2 w-full items-start">
{/* Question Number */}
<Text className="font-bold p-2 text-sm">{startIndex + index + 1}.</Text>
<div className="grid grid-cols-[auto_1fr_auto] gap-2 w-full items-start">
{/* Question Number */}
<Label className="font-bold p-2 text-sm">{startIndex + index + 1}.</Label>
{/* Question Text */}
<Text className="font-bold break-words text-sm p-2">{question.questionText}</Text>
{/* Question Text */}
<Label className="font-bold break-words text-sm p-2">{question.questionText}</Label>
{/* Action Icon/Flag */}
<ActionIcon
onClick={() => 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"}`}
>
<TbFlagFilled
size={25}
className={`p-1 ${flaggedQuestions[questionId] ? "text-white" : "text-black"}`}
/>
</ActionIcon>
</div>
{/* Radio Button Options */}
{question.options?.length > 0 ? (
<div className="mx-11">
<RadioGroup
value={answers[question.questionId] || ""}
onValueChange={(value) => handleAnswerChange(question.questionId, value)}
className="flex flex-col gap-2"
>
{question.options.map((option: any) => (
<div
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 ${answers[question.questionId] === option.optionId
? "bg-[--primary-color] text-white border-[--primary-color]"
: "bg-gray-200 text-black border-gray-200"
}`}
onClick={() => handleAnswerChange(question.questionId, option.optionId)}
>
<RadioGroupItem
value={option.optionId}
id={option.optionId}
checked={answers[question.questionId] === option.optionId}
className="bg-white checked:bg-white checked:border-[--primary-color] pointer-events-none rounded-full"
/>
<Label
htmlFor={option.optionId}
className="ml-2 font-bold cursor-pointer flex-1"
>
{option.optionText}
</Label>
</div>
))}
</RadioGroup>
</div>
) : (
<Text color="red">Tidak ada opsi untuk pertanyaan ini.</Text>
)}
{/* Textarea for additional information */}
<div className="mx-11">
<Textarea
placeholder="Berikan keterangan terkait jawaban di atas"
value={validationInformation[question.questionId] || ""}
onChange={(event) => handleTextareaChange(question.questionId, event.currentTarget.value)}
{/* Action Button/Flag */}
<button
onClick={() => 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"}`}
>
<TbFlagFilled
size={25}
className={`p-1 ${flaggedQuestions[questionId] ? "text-white" : "text-black"}`}
/>
</div>
</button>
</div>
{/* File Upload */}
<FileUpload
question={question}
handleFileChange={handleFileChange}
handleRemoveFile={handleRemoveFile}
uploadedFiles={uploadedFiles}
dragActive={dragActive}
handleDragOver={handleDragOver}
handleDragLeave={handleDragLeave}
handleDrop={handleDrop}
modalOpenFileSize={modalOpenFileSize}
setModalOpenFileSize={setModalOpenFileSize}
exceededFileName={exceededFileName}
handleClick={handleClick}
{/* Radio Button Options */}
{question.options?.length > 0 ? (
<div className="mx-11">
<RadioGroup
value={answers[question.questionId] || ""}
onValueChange={(value) => handleAnswerChange(question.questionId, value)}
className="flex flex-col gap-2"
>
{question.options.map((option: any) => (
<div
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 ${answers[question.questionId] === option.optionId
? "bg-[--primary-color] text-white border-[--primary-color]"
: "bg-gray-200 text-black border-gray-200"
}`}
onClick={() => handleAnswerChange(question.questionId, option.optionId)}
>
<RadioGroupItem
value={option.optionId}
id={option.optionId}
checked={answers[question.questionId] === option.optionId}
className="bg-white checked:bg-white checked:border-[--primary-color] pointer-events-none rounded-full"
/>
<Label
htmlFor={option.optionId}
className="ml-2 font-bold cursor-pointer flex-1"
>
{option.optionText}
</Label>
</div>
))}
</RadioGroup>
</div>
) : (
<Label color="red">Tidak ada opsi untuk pertanyaan ini.</Label>
)}
{/* Textarea for additional information */}
<div className="mx-11">
<Textarea
placeholder="Berikan keterangan terkait jawaban di atas"
value={validationInformation[question.questionId] || ""}
onChange={(event) => handleTextareaChange(question.questionId, event.currentTarget.value)}
/>
</div>
{/* Divider between questions */}
<div>
<hr className="border-t-2 border-gray-300 mx-11 mt-6 mb-6" />
</div>
</Stack>
{/* File Upload */}
<FileUpload
question={question}
handleFileChange={handleFileChange}
handleRemoveFile={handleRemoveFile}
uploadedFiles={uploadedFiles}
dragActive={dragActive}
handleDragOver={handleDragOver}
handleDragLeave={handleDragLeave}
handleDrop={handleDrop}
modalOpenFileSize={modalOpenFileSize}
setModalOpenFileSize={setModalOpenFileSize}
exceededFileName={exceededFileName}
handleClick={handleClick}
/>
{/* Divider between questions */}
<div>
<hr className="border-t-2 border-gray-300 mx-11 mt-6 mb-6" />
</div>
</div>
);
})
)}
</Stack>
</div>
{/* Pagination for mobile */}
<div className="md:hidden mb-4 flex justify-center">
<Pagination
@ -861,7 +855,7 @@ export default function AssessmentPage() {
}
}}
>
<Text className="text-sm m-0">Halaman {currentPage} dari {totalPages}</Text>
<Label className="text-sm m-0">Halaman {currentPage} dari {totalPages}</Label>
</Pagination>
</div>
</div>
@ -920,73 +914,71 @@ export default function AssessmentPage() {
<div className="mt-4">
<h2 className="text-lg font-extrabold mt-4 mb-2">Nilai Sementara</h2>
<div className="max-h-full overflow-hidden">
<div>
{filteredAspects.length > 0 ? (
filteredAspects.map((aspect) => {
const aspectScore = parseFloat(aspect.averageScore).toFixed(2);
const aspectScoreValue = parseFloat(aspectScore);
<div>
{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 break-words whitespace-normal">{aspect.aspectName}</Text>
<Text
className={`text-xl font-bold ${
aspectScoreValue >= 4.5
? "text-green-700"
: aspectScoreValue >= 3.5
? "text-green-400"
: aspectScoreValue >= 2.5
return (
<div key={aspect.aspectId} className="flex justify-between items-center">
<Label className="text-lg text-gray-700 break-words whitespace-normal">{aspect.aspectName}</Label>
<Label
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"
? "text-orange-500"
: "text-red-500"
}`}
>
{aspectScore}
</Text>
</div>
);
})
) : (
<Text className="text-base text-gray-700">Data aspek ini kosong</Text>
)}
</div>
>
{aspectScore}
</Label>
</div>
);
})
) : (
<Label className="text-base text-gray-700">Data aspek ini kosong</Label>
)}
</div>
{/* Divider */}
<div className="border-t-2 border-gray-300 my-4" />
{/* Divider */}
<div className="border-t-2 border-gray-300 my-4" />
{/* Skor Sub-Aspek */}
<div>
{filteredSubAspects.length > 0 ? (
filteredSubAspects.map((subAspect) => {
const subAspectScore = parseFloat(subAspect.averageScore).toFixed(2);
const subAspectScoreValue = parseFloat(subAspectScore);
{/* Skor Sub-Aspek */}
<div>
{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 break-words whitespace-normal">{subAspect.subAspectName}</Text>
<Text
className={`text-sm font-bold ${
subAspectScoreValue >= 4.5
? "text-green-700"
: subAspectScoreValue >= 3.5
? "text-green-400"
: subAspectScoreValue >= 2.5
return (
<div key={subAspect.subAspectId} className="flex justify-between items-center my-2">
<Label className="text-sm text-gray-700 break-words whitespace-normal">{subAspect.subAspectName}</Label>
<Label
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"
? "text-orange-500"
: "text-red-500"
}`}
>
{subAspectScore}
</Text>
</div>
);
})
) : (
<Text className="text-sm text-gray-700">Data sub-aspek ini kosong</Text>
)}
</div>
>
{subAspectScore}
</Label>
</div>
);
})
) : (
<Label className="text-sm text-gray-700">Data sub-aspek ini kosong</Label>
)}
</div>
{/* Finish Button */}
<div className="mt-4">
@ -1019,11 +1011,11 @@ export default function AssessmentPage() {
</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">
<Text className="font-medium text-lg text-gray-800 mb-2">
Nomor Soal
</Text>
<div className="hidden md:block fixed h-screen right-0 w-60 overflow-auto mr-4 border-x">
<div className="mx-4 space-y-2">
<Label className="font-medium text-lg text-gray-800 mb-2">
Nomor Soal
</Label>
{/* Navigasi (Number of Questions) */}
<div className="grid grid-cols-5 gap-2">
@ -1050,126 +1042,128 @@ export default function AssessmentPage() {
) : null;
})}
</div>
</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>
{/* Pagination */}
<div className="mt-6 py-2 flex justify-center border-y">
<Pagination
page={currentPage}
totalPages={totalPages}
onPageChange={(newPage) => {
if (selectedSubAspectId) {
handlePageChange(selectedSubAspectId, newPage);
}
}}
>
<Label className="text-xs m-0">Halaman {currentPage} dari {totalPages}</Label>
</Pagination>
</div>
<div className="mx-4 space-y-2">
{/* Skor Aspek dan Sub-Aspek */}
<div className="mt-4">
<Card>
<Text className="text-lg font-extrabold text-center mt-4 mb-2">
<p className="text-lg font-extrabold text-center mt-4 mb-2">
Nilai Sementara
</Text>
<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>
{/* Garis pembatas */}
<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);
</p>
<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={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
<div key={aspect.aspectId} className="flex justify-between items-center">
<Label className="text-lg text-gray-700">{aspect.aspectName}</Label>
<Label
className={`text-xl font-bold ${aspectScoreValue >= 4.5
? "text-green-700"
: aspectScoreValue >= 3.5
? "text-green-400"
: subAspectScoreValue >= 2.5
? "text-yellow-400"
: subAspectScoreValue >= 1.5
? "text-orange-500"
: "text-red-500"
}`}
: aspectScoreValue >= 2.5
? "text-yellow-400"
: aspectScoreValue >= 1.5
? "text-orange-500"
: "text-red-500"
}`}
>
{subAspectScore}
</Text>
{aspectScore}
</Label>
</div>
);
})
) : (
<Text className="text-sm text-gray-700">Data sub-aspek ini kosong</Text>
<Label className="text-base text-gray-700">Data aspek ini kosong</Label>
)}
</ScrollArea>
{/* Tombol Selesai */}
<div>
<Button
onClick={handleFinishClick}
className="bg-[--primary-color] text-white font-bold rounded-md w-full text-sm"
>
Selesai
</Button>
</div>
</CardContent>
</CardDescription>
{/* Modal untuk konfirmasi selesai asesmen */}
<FinishAssessmentModal
opened={modalOpen}
onClose={() => setModalOpen(false)}
onConfirm={handleConfirmFinish}
assessmentId={assessmentId}
/>
{/* Garis pembatas */}
<div className="border-t-2 border-gray-300 my-4" />
{/* Modal untuk peringatan jika ada pertanyaan yang belum dijawab */}
<ValidationModal
opened={validationModalOpen}
onClose={() => setValidationModalOpen(false)}
unansweredQuestions={unansweredQuestions}
/>
{/* 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">
<Label className="text-sm text-gray-700">{subAspect.subAspectName}</Label>
<Label
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}
</Label>
</div>
);
})
) : (
<Label className="text-sm text-gray-700">Data sub-aspek ini kosong</Label>
)}
</ScrollArea>
{/* Tombol Selesai */}
<div>
<Button
onClick={handleFinishClick}
className="bg-[--primary-color] text-white font-bold rounded-md w-full text-sm"
>
Selesai
</Button>
</div>
</CardContent>
{/* Modal untuk konfirmasi selesai asesmen */}
<FinishAssessmentModal
opened={modalOpen}
onClose={() => setModalOpen(false)}
onConfirm={handleConfirmFinish}
assessmentId={assessmentId}
/>
{/* Modal untuk peringatan jika ada pertanyaan yang belum dijawab */}
<ValidationModal
opened={validationModalOpen}
onClose={() => setValidationModalOpen(false)}
unansweredQuestions={unansweredQuestions}
/>
</Card>
</div>
</Flex>
</div>
</div>
</Flex>
</Stack>
</div>
</div>
</div>
);
}