769 lines
30 KiB
TypeScript
769 lines
30 KiB
TypeScript
import { createLazyFileRoute } from "@tanstack/react-router";
|
|
import {
|
|
Card,
|
|
Flex,
|
|
Pagination,
|
|
Stack,
|
|
Radio,
|
|
Text,
|
|
Textarea,
|
|
Loader,
|
|
ActionIcon,
|
|
CloseButton,
|
|
Group,
|
|
} from "@mantine/core";
|
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
|
import {
|
|
uploadFileMutationOptions,
|
|
submitValidationMutationOptions,
|
|
submitOptionMutationOptions,
|
|
getAverageScoreQueryOptions,
|
|
fetchAspects,
|
|
getQuestionsAllQueryOptions,
|
|
toggleFlagAnswer,
|
|
} from "@/modules/assessmentManagement/queries/assessmentQueries";
|
|
import { TbFlagFilled, TbUpload, TbChevronRight, TbChevronUp } from "react-icons/tb";
|
|
import FinishAssessmentModal from "@/modules/assessmentManagement/modals/ConfirmModal";
|
|
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("/_dashboardLayout/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 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 [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 }>({});
|
|
|
|
// 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 = () => {
|
|
setModalOpen(true);
|
|
};
|
|
|
|
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
|
|
}
|
|
}
|
|
} 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
|
|
} else {
|
|
console.warn("No matching aspect found for selected sub-aspect.");
|
|
setSelectedAspectId(null);
|
|
}
|
|
}
|
|
}
|
|
}, [aspectsQuery.data, selectedSubAspectId, data?.data]);
|
|
|
|
const handleConfirmFinish = () => {
|
|
if (assessmentId) {
|
|
// Menggunakan history.pushState untuk mengubah URL tanpa reload
|
|
const newUrl = `/assessmentResult?id=${assessmentId}`;
|
|
window.history.pushState({}, "", newUrl);
|
|
// Setelah mengubah URL, Anda bisa menjalankan logika lain jika diperlukan
|
|
console.log("Navigated to:", newUrl);
|
|
}
|
|
};
|
|
|
|
// 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 : [];
|
|
|
|
// Mutation function to toggle flag
|
|
const toggleFlagMutation = useMutation<ToggleFlagResponse, Error, string>({
|
|
mutationFn: toggleFlagAnswer,
|
|
onSuccess: (response) => {
|
|
if (response && response.answer) {
|
|
const { answer } = response;
|
|
setFlaggedQuestions((prevFlags) => ({
|
|
...prevFlags,
|
|
[answer.id]: answer.isFlagged !== null ? answer.isFlagged : false,
|
|
}));
|
|
}
|
|
},
|
|
|
|
onError: (error) => {
|
|
console.error("Error toggling flag:", error);
|
|
},
|
|
});
|
|
|
|
// 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);
|
|
},
|
|
});
|
|
|
|
useEffect(() => {
|
|
const assessmentId = getQueryParam("id");
|
|
|
|
if (!assessmentId) {
|
|
console.error("Assessment ID tidak ditemukan");
|
|
return;
|
|
}
|
|
|
|
// Ambil jawaban dari localStorage berdasarkan ID assessment
|
|
const savedAnswers = localStorage.getItem(`assessmentAnswers_${assessmentId}`);
|
|
if (savedAnswers) {
|
|
setAnswers(JSON.parse(savedAnswers));
|
|
}
|
|
}, []);
|
|
|
|
const handleAnswerChange = (questionId: string, optionId: string) => {
|
|
const assessmentId = getQueryParam("id");
|
|
|
|
if (!assessmentId) {
|
|
console.error("Assessment ID tidak ditemukan");
|
|
return;
|
|
}
|
|
|
|
// Simpan jawaban ke localStorage dengan ID assessment
|
|
const updatedAnswers = { ...answers, [questionId]: optionId };
|
|
localStorage.setItem(`assessmentAnswers_${assessmentId}`, JSON.stringify(updatedAnswers));
|
|
|
|
// Update state
|
|
setAnswers(updatedAnswers);
|
|
|
|
// Call the mutation to submit the option
|
|
submitOptionMutation.mutate({
|
|
optionId,
|
|
assessmentId,
|
|
questionId,
|
|
isFlagged: false,
|
|
filename: undefined,
|
|
});
|
|
};
|
|
|
|
// Mutation untuk mengirim data ke backend
|
|
const { mutate: submitValidation } = useMutation(submitValidationMutationOptions());
|
|
|
|
// Mengambil data dari localStorage saat komponen dimuat
|
|
useEffect(() => {
|
|
const storedValidationInfo = localStorage.getItem(`validationInfo_${assessmentId}`);
|
|
if (storedValidationInfo) {
|
|
try {
|
|
const parsedValidationInfo = JSON.parse(storedValidationInfo);
|
|
setValidationInformation(parsedValidationInfo);
|
|
} catch (error) {
|
|
console.error("Error parsing validation information:", error);
|
|
}
|
|
}
|
|
}, [assessmentId]);
|
|
|
|
// Handle perubahan di Textarea
|
|
const handleTextareaChange = (questionId: string, value: string) => {
|
|
setValidationInformation((prev) => ({
|
|
...prev,
|
|
[questionId]: value,
|
|
}));
|
|
|
|
// Update the localStorage with the new validation information as JSON
|
|
const updatedValidationInformation = {
|
|
...validationInformation,
|
|
[questionId]: value,
|
|
};
|
|
localStorage.setItem(`validationInfo_${assessmentId}`, JSON.stringify(updatedValidationInformation));
|
|
|
|
// Ensure assessmentId and questionId are not null before submitting
|
|
if (assessmentId && questionId) {
|
|
// Send the validation data to the server
|
|
submitValidation({
|
|
assessmentId,
|
|
questionId,
|
|
validationInformation: value,
|
|
});
|
|
}
|
|
};
|
|
|
|
// Mutation for file upload
|
|
const uploadFileMutation = useMutation(uploadFileMutationOptions());
|
|
|
|
// Drag and Drop handlers
|
|
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
|
event.preventDefault();
|
|
setDragActive(true);
|
|
};
|
|
|
|
const handleDragLeave = () => {
|
|
setDragActive(false);
|
|
};
|
|
|
|
// Load uploaded files from local storage when the component mounts
|
|
useEffect(() => {
|
|
const keys = Object.keys(localStorage);
|
|
keys.forEach((key) => {
|
|
if (key.startsWith(`uploadedFile_${assessmentId}_`)) { // Menggunakan assessmentId
|
|
const fileData = JSON.parse(localStorage.getItem(key) || '{}');
|
|
const questionId = key.replace(`uploadedFile_${assessmentId}_`, ''); // Ambil questionId dari kunci
|
|
setUploadedFiles(prev => ({
|
|
...prev,
|
|
[questionId]: new File([fileData], fileData.name, { type: fileData.type }), // Buat objek File baru
|
|
}));
|
|
}
|
|
});
|
|
}, [assessmentId]);
|
|
|
|
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 formData = new FormData();
|
|
formData.append('file', droppedFiles[0]); // Hanya menyertakan file pertama
|
|
|
|
// Pastikan assessmentId tidak null sebelum menambahkannya ke FormData
|
|
if (assessmentId) {
|
|
formData.append('assessmentId', assessmentId);
|
|
} else {
|
|
console.error("assessmentId is null");
|
|
return; // Atau tangani sesuai kebutuhan
|
|
}
|
|
|
|
// Tambahkan questionId ke FormData
|
|
if (question.questionId) {
|
|
formData.append('questionId', question.questionId);
|
|
} else {
|
|
console.error("questionId is null");
|
|
return; // Atau tangani sesuai kebutuhan
|
|
}
|
|
|
|
uploadFileMutation.mutate(formData); // Unggah file
|
|
|
|
// Simpan file dalam state dan local storage menggunakan questionId dan assessmentId sebagai kunci
|
|
setUploadedFiles(prev => ({
|
|
...prev,
|
|
[question.questionId]: droppedFiles[0], // Simpan file berdasarkan questionId
|
|
}));
|
|
localStorage.setItem(`uploadedFile_${assessmentId}_${question.questionId}`, JSON.stringify({
|
|
name: droppedFiles[0].name,
|
|
type: droppedFiles[0].type,
|
|
lastModified: droppedFiles[0].lastModified,
|
|
})); // Simpan info file ke local storage
|
|
}
|
|
};
|
|
|
|
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 formData = new FormData();
|
|
formData.append('file', fileArray[0]); // Hanya menyertakan file pertama
|
|
|
|
// Tambahkan assessmentId ke FormData
|
|
if (assessmentId) {
|
|
formData.append('assessmentId', assessmentId);
|
|
} else {
|
|
console.error("assessmentId is null");
|
|
return; // Atau tangani sesuai kebutuhan
|
|
}
|
|
|
|
// Tambahkan questionId ke FormData
|
|
if (question.questionId) {
|
|
formData.append('questionId', question.questionId);
|
|
} else {
|
|
console.error("questionId is null");
|
|
return; // Atau tangani sesuai kebutuhan
|
|
}
|
|
|
|
uploadFileMutation.mutate(formData); // Unggah file
|
|
|
|
// Simpan file dalam state dan local storage menggunakan questionId dan assessmentId sebagai kunci
|
|
setUploadedFiles(prev => ({
|
|
...prev,
|
|
[question.questionId]: fileArray[0], // Simpan file berdasarkan questionId
|
|
}));
|
|
localStorage.setItem(`uploadedFile_${assessmentId}_${question.questionId}`, JSON.stringify({
|
|
name: fileArray[0].name,
|
|
type: fileArray[0].type,
|
|
lastModified: fileArray[0].lastModified,
|
|
})); // Simpan info file ke local storage
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleRemoveFile = (question: { questionId: string }) => {
|
|
setUploadedFiles(prev => ({
|
|
...prev,
|
|
[question.questionId]: null, // Hapus file yang diunggah untuk pertanyaan ini
|
|
}));
|
|
localStorage.removeItem(`uploadedFile_${assessmentId}_${question.questionId}`); // Hapus info file dari local storage
|
|
};
|
|
|
|
// Function to scroll to the specific question
|
|
const scrollToQuestion = (questionId: string) => {
|
|
const questionElement = questionRefs.current[questionId];
|
|
if (questionElement) {
|
|
questionElement.scrollIntoView({ behavior: "smooth" });
|
|
}
|
|
};
|
|
|
|
// Handle pagination
|
|
const handlePageChange = (newPage: number) => {
|
|
setPage(newPage);
|
|
};
|
|
|
|
// Render conditions
|
|
if (isLoading) {
|
|
return <Loader />;
|
|
}
|
|
|
|
if (isError) {
|
|
return (
|
|
<Text color="red">
|
|
Error: {error?.message || "Terjadi kesalahan saat memuat pertanyaan."}
|
|
</Text>
|
|
);
|
|
}
|
|
|
|
const totalQuestions = data?.data?.length || 0;
|
|
|
|
if (!assessmentId) {
|
|
return (
|
|
<Card shadow="sm" p="lg" radius="md" withBorder>
|
|
<Text color="red" className="text-center">
|
|
Error: Data Asesmen tidak ditemukan. Harap akses halaman melalui link yang valid.
|
|
</Text>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
const startIndex = (page - 1) * limit;
|
|
const endIndex = startIndex + limit;
|
|
const paginatedQuestions = data?.data.slice(startIndex, endIndex) || [];
|
|
|
|
const filteredQuestions = paginatedQuestions.filter((question) => {
|
|
return question.subAspectId === selectedSubAspectId; // Misalnya, jika `question` memiliki `subAspectId`
|
|
});
|
|
|
|
return (
|
|
<div>
|
|
<Stack gap="md">
|
|
<Flex justify="space-between" align="flex-start" mt="lg">
|
|
|
|
{/* LEFT-SIDE */}
|
|
{/* Aspek dan Sub-Aspek */}
|
|
<Flex direction="column" gap="xs" className="mr-4 w-52">
|
|
<div className="space-y-4">
|
|
{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-4 bg-gray-50 rounded-lg shadow-md"
|
|
>
|
|
<div
|
|
className="flex justify-between cursor-pointer"
|
|
onClick={() => toggleAspect(aspect.id)}
|
|
>
|
|
<div className="text-lg text-gray-700">{aspect.name}</div>
|
|
<div>
|
|
{openAspects[aspect.id] ? (
|
|
<TbChevronUp size={25} />
|
|
) : (
|
|
<TbChevronRight size={25} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{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 text-gray-600 cursor-pointer ${selectedSubAspectId === subAspect.id ? 'font-bold' : ''}`}
|
|
onClick={() => setSelectedSubAspectId(subAspect.id)}
|
|
>
|
|
<div>{subAspect.name}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Flex>
|
|
{/* Pertanyaan */}
|
|
<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">Semua jawaban Anda akan ditinjau</Text>
|
|
{filteredQuestions.length === 0 ? (
|
|
<Text color="black" 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%" }}>
|
|
<Text className="font-bold mr-3">{startIndex + index + 1}.</Text>
|
|
<div className="flex-grow">
|
|
<Text className="font-bold break-words">
|
|
{question.questionText}
|
|
</Text>
|
|
</div>
|
|
|
|
{/* Action Icon */}
|
|
<ActionIcon
|
|
onClick={() => {
|
|
setFlaggedQuestions((prevFlags) => ({
|
|
...prevFlags,
|
|
[questionId]: !prevFlags[questionId],
|
|
}));
|
|
toggleFlagMutation.mutate(questionId);
|
|
}}
|
|
title={
|
|
!answers[question.questionId]
|
|
? "Pilih jawaban terlebih dahulu"
|
|
: "Tandai"
|
|
}
|
|
color={flaggedQuestions[questionId] ? "red" : "white"}
|
|
style={{
|
|
border: "1px gray solid",
|
|
borderRadius: "4px",
|
|
backgroundColor: flaggedQuestions[questionId] ? "red" : "white",
|
|
}}
|
|
disabled={!answers[question.questionId]}
|
|
>
|
|
<TbFlagFilled
|
|
size={20}
|
|
color={flaggedQuestions[questionId] ? "white" : "black"}
|
|
style={{
|
|
padding: "2px",
|
|
}}
|
|
|
|
/>
|
|
</ActionIcon>
|
|
</Flex>
|
|
|
|
{/* Opsi Radio Button */}
|
|
{question.options?.length > 0 ? (
|
|
<div className="ml-6">
|
|
<Radio.Group value={answers[question.questionId] || ""}>
|
|
<div className="flex flex-col gap-4">
|
|
{question.options.map((option: any) => (
|
|
<label
|
|
key={option.optionId}
|
|
className={`cursor-pointer transition-transform transform hover:scale-105 shadow-md hover:shadow-lg flex items-center border rounded-lg p-4 ${
|
|
answers[question.questionId] === option.optionId
|
|
? "bg-blue-500 text-white"
|
|
: "bg-gray-200 text-black"
|
|
}`}
|
|
onClick={() => document.getElementById(option.optionId)?.click()}
|
|
>
|
|
<Radio
|
|
id={option.optionId}
|
|
className="font-bold"
|
|
value={option.optionId}
|
|
label={option.optionText}
|
|
size="md"
|
|
radius="xl"
|
|
style={{ pointerEvents: "none" }}
|
|
checked={answers[question.questionId] === option.optionId} // Untuk menampilkan jawaban yang sudah dipilih
|
|
onChange={() => handleAnswerChange(question.questionId, option.optionId)} // Memanggil handleAnswerChange dengan questionId dan optionId
|
|
/>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</Radio.Group>
|
|
</div>
|
|
) : (
|
|
<Text color="red">Tidak ada opsi untuk pertanyaan ini.</Text>
|
|
)}
|
|
|
|
{/* Textarea */}
|
|
<div className="ml-6">
|
|
<Textarea
|
|
placeholder="Berikan keterangan terkait jawaban di atas"
|
|
value={validationInformation[question.questionId] || ""}
|
|
onChange={(event) => handleTextareaChange(question.questionId, event.currentTarget.value)}
|
|
disabled={!answers[question.questionId]}
|
|
/>
|
|
</div>
|
|
|
|
{/* File Upload */}
|
|
<div className="ml-6">
|
|
{question.needFile === true && (
|
|
<div
|
|
className={`pt-5 pb-5 pr-5 pl-5 border-2 rounded-lg border-dashed ${dragActive ? "bg-gray-100" : "bg-transparent"
|
|
} shadow-lg`}
|
|
onDragOver={handleDragOver}
|
|
onDragLeave={handleDragLeave}
|
|
onDrop={(event) => handleDrop(event, question)} // Mengoper question sebagai argumen
|
|
onClick={handleClick}
|
|
>
|
|
<Flex align="center" justify="space-between" gap="sm">
|
|
<TbUpload
|
|
size={24}
|
|
style={{ marginLeft: "8px", marginRight: "8px" }}
|
|
/>
|
|
<div className="flex-grow text-right">
|
|
<Text className="font-bold">
|
|
Klik untuk unggah atau geser file disini
|
|
</Text>
|
|
<Text className="text-sm text-gray-400">
|
|
PNG, JPG, PDF
|
|
</Text>
|
|
</div>
|
|
</Flex>
|
|
<input
|
|
type="file"
|
|
ref={fileInputRef}
|
|
onChange={(event) => handleFileChange(event, question)} // Mengoper question sebagai argumen
|
|
style={{ display: "none" }}
|
|
accept="image/png, image/jpeg, application/pdf"
|
|
disabled={!answers[question.questionId]}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="ml-6">
|
|
{uploadedFiles[question.questionId] && (
|
|
<Stack gap="sm" mt="sm">
|
|
<Text className="font-bold">File yang diunggah:</Text>
|
|
<Group align="center">
|
|
<Text>{uploadedFiles[question.questionId]?.name}</Text> {/* Tampilkan nama file yang diunggah */}
|
|
<CloseButton
|
|
title="Hapus file"
|
|
onClick={() => handleRemoveFile(question)} // Mengoper question sebagai argumen
|
|
/>
|
|
</Group>
|
|
</Stack>
|
|
)}
|
|
</div>
|
|
|
|
{/* Garis pembatas setiap soal */}
|
|
<div>
|
|
<hr className="border-t-2 border-gray-300 ml-6 mx-auto mt-6 mb-6" />
|
|
</div>
|
|
</Stack>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</Stack>
|
|
|
|
{/* Navigasi dan Pagination */}
|
|
<Flex direction="column" gap="xs" className="ml-4">
|
|
|
|
{/* Navigasi (Number of Questions) */}
|
|
<div className="grid grid-cols-5 gap-2">
|
|
{filteredQuestions.map((question, index) => {
|
|
const questionId = question.questionId;
|
|
if (!questionId) return null;
|
|
|
|
// Menentukan nomor soal berdasarkan indeks pertanyaan yang difilter
|
|
const questionNumber = index + 1; // Nomor pertanyaan dimulai dari 1
|
|
|
|
return (
|
|
<div key={questionId} className="flex justify-center relative">
|
|
<button
|
|
className={`w-10 h-10 border rounded-lg flex items-center justify-center relative
|
|
${flaggedQuestions[questionId] ? "text-black" : "bg-transparent text-black"}`}
|
|
onClick={() => scrollToQuestion(questionId)}
|
|
>
|
|
{questionNumber} {/* Menampilkan nomor pertanyaan yang sudah difilter */}
|
|
</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-black rounded-e-md" />
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="mt-4 flex justify-center">
|
|
<Pagination
|
|
value={page}
|
|
total={Math.ceil(totalQuestions / limit)}
|
|
onChange={handlePageChange}
|
|
/>
|
|
</div>
|
|
|
|
{/* Skor Aspek dan Sub-Aspek */}
|
|
<div className="mt-4">
|
|
<Card shadow="sm" p="md" radius="md" withBorder>
|
|
<Stack>
|
|
{/* Skor Aspek */}
|
|
<div>
|
|
{filteredAspects.length > 0 ? (
|
|
filteredAspects.map((aspect) => (
|
|
<div key={aspect.aspectId} className="flex justify-between items-center">
|
|
<Text className="text-xl text-gray-400">{aspect.aspectName}</Text>
|
|
<Text className="text-xl font-bold">
|
|
{parseFloat(aspect.averageScore).toFixed(2)}
|
|
</Text>
|
|
</div>
|
|
))
|
|
) : (
|
|
<Text className="text-lg text-gray-400">Data aspek kosong</Text>
|
|
)}
|
|
</div>
|
|
|
|
{/* Garis pembatas */}
|
|
<div>
|
|
<hr className="border-t-2 border-gray-300 w-full mx-auto" />
|
|
</div>
|
|
|
|
{/* Skor Sub-Aspek */}
|
|
<div>
|
|
{filteredSubAspects.length > 0 ? (
|
|
filteredSubAspects.map((subAspect) => (
|
|
<div key={subAspect.subAspectId} className="flex justify-between items-center"> {/* Change key to 'id' */}
|
|
<Text className="text-lg text-gray-400">{subAspect.subAspectName}</Text> {/* Change to 'name' */}
|
|
<Text className="text-lg font-bold">
|
|
{parseFloat(subAspect.averageScore).toFixed(2)}
|
|
</Text>
|
|
</div>
|
|
))
|
|
) : (
|
|
<Text className="text-lg text-gray-400">Data sub-aspek kosong</Text>
|
|
)}
|
|
</div>
|
|
</Stack>
|
|
|
|
{/* Tombol Selesai */}
|
|
<div className="mt-6">
|
|
<button onClick={handleFinishClick} className="bg-blue-500 text-white font-bold rounded-md py-2 w-full">
|
|
Selesai
|
|
</button>
|
|
</div>
|
|
|
|
{/* Modal untuk konfirmasi selesai asesmen */}
|
|
<FinishAssessmentModal
|
|
opened={modalOpen}
|
|
onClose={() => setModalOpen(false)}
|
|
onConfirm={handleConfirmFinish}
|
|
assessmentId={assessmentId}
|
|
/>
|
|
</Card>
|
|
</div>
|
|
</Flex>
|
|
</Flex>
|
|
</Stack>
|
|
</div>
|
|
);
|
|
} |