933 lines
35 KiB
TypeScript
933 lines
35 KiB
TypeScript
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<File[]>([]);
|
|
const [dragActive, setDragActive] = useState(false);
|
|
const [flaggedQuestions, setFlaggedQuestions] = useState<{
|
|
[key: string]: boolean;
|
|
}>({});
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const [modalOpenFileSize, setModalOpenFileSize] = useState(false);
|
|
const [selectedAspectId, setSelectedAspectId] = useState<string | null>(null);
|
|
const [selectedSubAspectId, setSelectedSubAspectId] = useState<string | null>(null);
|
|
const [assessmentId, setAssessmentId] = useState<string | null>(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<string, string>, 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<string, string>, 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<string, boolean>);
|
|
|
|
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<string, string>);
|
|
|
|
// 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<string, File | null>);
|
|
|
|
setUploadedFiles(transformedFileData);
|
|
}
|
|
}, [answersData]);
|
|
|
|
// Drag and Drop handlers
|
|
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
|
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<HTMLDivElement>, 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<HTMLInputElement>, 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 <Loader />;
|
|
}
|
|
|
|
if (isError) {
|
|
return (
|
|
<Text color="red">
|
|
Error: {error?.message || "Terjadi kesalahan saat memuat pertanyaan."}
|
|
</Text>
|
|
);
|
|
}
|
|
|
|
if (!assessmentId) {
|
|
return (
|
|
<Card>
|
|
<CardContent>
|
|
<Text color="red" className="text-center">
|
|
Error: Data Asesmen tidak ditemukan. Harap akses halaman melalui link yang valid.
|
|
</Text>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
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 (
|
|
<div>
|
|
<Stack gap="md">
|
|
<Flex justify="space-between" align="flex-start" mt="lg">
|
|
|
|
{/* LEFT-SIDE */}
|
|
{/* Aspek dan Sub-Aspek */}
|
|
<div className="fixed h-screen w-64 overflow-auto">
|
|
<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)
|
|
)
|
|
)
|
|
.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>
|
|
</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>
|
|
</div>
|
|
|
|
{/* MIDDLE */}
|
|
{/* Pertanyaan */}
|
|
<div className="ml-64 mr-60 flex-1 overflow-y-auto h-full">
|
|
<Stack gap="sm" style={{ flex: 1 }}>
|
|
<Text className="text-2xl font-bold ml-6">
|
|
Harap menjawab semua pertanyaan yang tersedia
|
|
</Text>
|
|
<Text className="text-gray-400 ml-6 mb-7">Semua jawaban Anda akan ditinjau</Text>
|
|
{filteredQuestions.length === 0 ? (
|
|
<Text className="text-center p-3">
|
|
Pertanyaan tidak ada untuk sub-aspek yang dipilih.
|
|
</Text>
|
|
) : (
|
|
filteredQuestions.map((question: any, index: number) => {
|
|
const questionId = question.questionId;
|
|
if (!questionId) return null;
|
|
|
|
return (
|
|
<div
|
|
key={questionId}
|
|
ref={(el) => (questionRefs.current[questionId] = el)}
|
|
className="space-y-4"
|
|
>
|
|
<Stack gap="sm">
|
|
<Flex justify="space-between" align="flex-start" style={{ width: "100%" }}>
|
|
{/* Question */}
|
|
<Text className="font-bold mx-3 p-1 text-sm">{startIndex + index + 1}.</Text>
|
|
<div className="flex-grow">
|
|
<Text className="font-bold break-words text-sm p-1">
|
|
{question.questionText}
|
|
</Text>
|
|
</div>
|
|
|
|
{/* 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"}`} // Mengurangi padding untuk ikon
|
|
/>
|
|
</ActionIcon>
|
|
</Flex>
|
|
|
|
{/* Opsi Radio Button */}
|
|
{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 */}
|
|
<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>
|
|
|
|
{/* 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}
|
|
/>
|
|
|
|
{/* Garis pembatas setiap soal */}
|
|
<div>
|
|
<hr className="border-t-2 border-gray-300 mx-11 mt-6 mb-6" />
|
|
</div>
|
|
</Stack>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</Stack>
|
|
</div>
|
|
|
|
{/* RIGHT-SIDE */}
|
|
{/* Navigasi dan Pagination */}
|
|
<div className="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>
|
|
|
|
{/* 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>
|
|
<Text 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);
|
|
|
|
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>
|
|
{/* 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>
|
|
</Flex>
|
|
</Stack>
|
|
</div>
|
|
);
|
|
} |