Pull Request branch dev-clone to main #1

Merged
gitea merged 429 commits from dev-clone into main 2024-12-23 09:31:34 +00:00
Showing only changes of commit e4473bf15a - Show all commits

View File

@ -24,9 +24,10 @@ import {
import { useQuery, useMutation } from "@tanstack/react-query";
import {
getAnswersQueryOptions,
getAllAnswer,
submitAssessmentMutationOptions,
uploadFileMutationOptions,
submitValidationMutationOptions,
submitValidationQuery,
submitOptionMutationOptions,
getAverageScoreQueryOptions,
fetchAspects,
@ -35,6 +36,7 @@ import {
} 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";
@ -98,23 +100,6 @@ export default function AssessmentPage() {
getQuestionsAllQueryOptions(page, limit)
);
// Fungsi untuk memeriksa pertanyaan yang belum dijawab
const checkUnansweredQuestions = () => {
// Misalkan data berisi pertanyaan dan jawaban
const unanswered = data?.data.filter(question => {
// Pastikan questionId tidak null dan tidak ada jawaban untuk questionId tersebut
return question.questionId !== null && !answers[question.questionId];
}) || []; // Ganti question.id dengan question.questionId dan tambahkan pengecekan null
setUnansweredQuestions(unanswered.length); // Aman, karena unanswered selalu array
// Jika ada pertanyaan yang belum dijawab, buka modal peringatan
if (unanswered.length > 0) {
setValidationModalOpen(true);
} else {
setModalOpen(true); // Jika tidak ada, buka modal konfirmasi selesai asesmen
}
};
const handleFinishClick = () => {
// Memanggil fungsi untuk memeriksa pertanyaan yang belum dijawab
checkUnansweredQuestions();
@ -172,65 +157,74 @@ export default function AssessmentPage() {
// Fetching answers for the assessment
const { data: answersData } = useQuery(
getAnswersQueryOptions(assessmentId || "", page, limit) // Memanggil query untuk mengambil jawaban dengan argumen yang diperlukan
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"); // Ambil assessmentId dari query params
const assessmentId = getQueryParam("id");
if (!assessmentId) {
console.error("Assessment ID tidak ditemukan");
return;
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)); // Set state answers dengan data dari local storage
}
}, []);
// Mengambil jawaban dari local storage saat komponen dimuat
useEffect(() => {
const storedAnswers = localStorage.getItem('assessmentAnswers');
if (storedAnswers) {
setAnswers(JSON.parse(storedAnswers)); // Set state answers dengan data dari local storage
// 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 {
// Cek pertanyaan yang belum dijawab
let unansweredCount = 0;
// Cek radio button
data?.data.forEach((question) => {
// Pastikan questionId tidak null sebelum memeriksa answers
if (question.questionId && !answers[question.questionId]) {
unansweredCount += 1;
}
});
// Cek textarea
Object.keys(validationInformation).forEach((key) => {
// Pastikan key tidak null dan tidak ada validasi informasi untuk key tersebut
if (key && !validationInformation[key]) {
unansweredCount += 1;
}
});
if (unansweredCount > 0) {
// Tampilkan modal validasi jika ada pertanyaan yang belum dijawab
setUnansweredQuestions(unansweredCount);
setValidationModalOpen(true);
return;
}
// Memanggil mutation untuk mengubah status asesmen menjadi 'selesai' di backend
// Skip counting unanswered questions here to prevent duplication
const mutation = submitAssessmentMutationOptions(assessmentId);
const response = await mutation.mutationFn();
// Setelah status diubah, navigasikan ke halaman hasil asesmen
// Navigate to results
const newUrl = `/assessmentResult?id=${assessmentId}`;
window.history.pushState({}, "", newUrl);
console.log("Navigated to:", newUrl);
@ -238,7 +232,7 @@ export default function AssessmentPage() {
} catch (error) {
console.error("Error finishing assessment:", error);
} finally {
setModalOpen(false); // Menutup modal setelah selesai
setModalOpen(false);
}
};
@ -265,44 +259,50 @@ export default function AssessmentPage() {
const currentAspect = aspects.find(aspect => aspect.aspectId === selectedAspectId);
const filteredSubAspects = currentAspect ? currentAspect.subAspects : [];
// Inisialisasi flaggedQuestions dari localStorage saat komponen dimuat
// Inisialisasi flaggedQuestions dari database saat komponen dimuat
useEffect(() => {
const savedFlags = localStorage.getItem("flaggedQuestions");
if (savedFlags) {
setFlaggedQuestions(JSON.parse(savedFlags));
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);
}
}, []);
// Simpan perubahan flag ke localStorage setiap kali flaggedQuestions berubah
useEffect(() => {
if (Object.keys(flaggedQuestions).length > 0) {
localStorage.setItem("flaggedQuestions", JSON.stringify(flaggedQuestions));
}
}, [flaggedQuestions]);
}, [answersData]);
// Mutation function to toggle flag
const toggleFlagMutation = useMutation<ToggleFlagResponse, Error, string>({
mutationFn: toggleFlagAnswer,
const { mutate: toggleFlag } = useMutation({
mutationFn: (questionId: string) => toggleFlagAnswer(questionId),
onSuccess: (response) => {
if (response && response.answer) {
const { answer } = response;
setFlaggedQuestions((prevFlags) => {
const newFlags = {
...prevFlags,
[answer.id]: answer.isFlagged !== null ? answer.isFlagged : false,
};
// Simpan perubahan ke localStorage
localStorage.setItem("flaggedQuestions", JSON.stringify(newFlags));
return newFlags;
});
setFlaggedQuestions((prevFlags) => ({
...prevFlags,
[answer.id]: answer.isFlagged !== null ? answer.isFlagged : false,
}));
}
},
onError: (error) => {
console.error("Error toggling flag:", error);
},
});
// Fungsi untuk toggle flag
const handleToggleFlag = (questionId: string) => {
const newFlagState = !flaggedQuestions[questionId];
// Update flaggedQuestions dan kirim ke server
setFlaggedQuestions((prevFlags) => ({
...prevFlags,
[questionId]: newFlagState,
}));
toggleFlag(questionId);
};
// Usage of the mutation in your component
const submitOptionMutation = useMutation({
...submitOptionMutationOptions, // Spread the mutation options here
@ -315,115 +315,108 @@ export default function AssessmentPage() {
},
});
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;
console.error("Assessment ID tidak ditemukan");
return;
}
// Simpan jawaban ke localStorage dengan ID assessment
// Update answers in the state
const updatedAnswers = { ...answers, [questionId]: optionId };
localStorage.setItem(`assessmentAnswers_${assessmentId}`, JSON.stringify(updatedAnswers)); // Simpan berdasarkan ID assessment
// Update state
setAnswers(updatedAnswers);
// Call the mutation to submit the option
// Send the updated answer to the backend
submitOptionMutation.mutate({
optionId,
assessmentId,
questionId,
isFlagged: false,
filename: undefined,
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);
}
const validationResult = answersData?.data.reduce((acc, item) => {
if (item.questionId != null && item.validationInformation != null) {
acc[item.questionId] = item.validationInformation;
}
}, [assessmentId]);
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,
...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,
});
// 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);
};
// 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]);
// Max file size in bytes (64 MB)
const MAX_FILE_SIZE = 64 * 1024 * 1024;
@ -437,45 +430,37 @@ export default function AssessmentPage() {
// Validate file size
if (file.size > MAX_FILE_SIZE) {
setExceededFileName(file.name); // Simpan nama file yang melebihi ukuran
setModalOpenFileSize(true); // Tampilkan modal
setExceededFileName(file.name);
setModalOpenFileSize(true);
return;
}
const formData = new FormData();
formData.append('file', file); // Hanya menyertakan file pertama
formData.append('file', file);
// 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
return;
}
// Tambahkan questionId ke FormData
if (question.questionId) {
formData.append('questionId', question.questionId);
} else {
console.error("questionId is null");
return; // Atau tangani sesuai kebutuhan
return;
}
uploadFileMutation.mutate(formData); // Unggah file
uploadFileMutation.mutate(formData); // Upload file
// Simpan file dalam state dan local storage menggunakan questionId dan assessmentId sebagai kunci
// Update state to reflect the uploaded file (store the File object, not just the name)
setUploadedFiles(prev => ({
...prev,
[question.questionId]: file, // Simpan file berdasarkan questionId
}));
localStorage.setItem(`uploadedFile_${assessmentId}_${question.questionId}`, JSON.stringify({
name: file.name,
type: file.type,
lastModified: file.lastModified,
[question.questionId]: file, // Store the file itself
}));
}
};
};
const handleClick = () => {
if (fileInputRef.current) {
@ -491,53 +476,44 @@ export default function AssessmentPage() {
// Validate file size
if (file.size > MAX_FILE_SIZE) {
setExceededFileName(file.name); // Simpan nama file yang melebihi ukuran
setModalOpenFileSize(true); // Tampilkan modal
return; // Hentikan eksekusi fungsi jika ukuran file melebihi batas
setExceededFileName(file.name);
setModalOpenFileSize(true);
return;
}
const formData = new FormData();
formData.append('file', file); // Hanya menyertakan file pertama
formData.append('file', file);
// Tambahkan assessmentId ke FormData
if (assessmentId) {
formData.append('assessmentId', assessmentId);
} else {
console.error("assessmentId is null");
return; // Atau tangani sesuai kebutuhan
return;
}
// Tambahkan questionId ke FormData
if (question.questionId) {
formData.append('questionId', question.questionId);
} else {
console.error("questionId is null");
return; // Atau tangani sesuai kebutuhan
return;
}
uploadFileMutation.mutate(formData); // Unggah file
uploadFileMutation.mutate(formData); // Upload file
// Simpan file dalam state dan local storage menggunakan questionId dan assessmentId sebagai kunci
// Update state to reflect the uploaded file (store the File object)
setUploadedFiles(prev => ({
...prev,
[question.questionId]: file, // Simpan file berdasarkan questionId
}));
localStorage.setItem(`uploadedFile_${assessmentId}_${question.questionId}`, JSON.stringify({
name: file.name,
type: file.type,
lastModified: file.lastModified,
[question.questionId]: file, // Store the File object, not just the name
}));
}
}
};
const handleRemoveFile = (question: { questionId: string }) => {
setUploadedFiles(prev => ({
...prev,
[question.questionId]: null, // Hapus file yang diunggah untuk pertanyaan ini
setUploadedFiles((prev) => ({
...prev,
[question.questionId]: null,
}));
localStorage.removeItem(`uploadedFile_${assessmentId}_${question.questionId}`); // Hapus info file dari local storage
};
// Function to scroll to the specific question
@ -694,18 +670,7 @@ export default function AssessmentPage() {
{/* Action Icon/Flag */}
<ActionIcon
onClick={() => {
setFlaggedQuestions((prevFlags) => {
const newFlags = {
...prevFlags,
[questionId]: !prevFlags[questionId],
};
// Simpan perubahan ke localStorage
localStorage.setItem("flaggedQuestions", JSON.stringify(newFlags));
return newFlags;
});
toggleFlagMutation.mutate(questionId);
}}
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"}`}
>
@ -760,70 +725,23 @@ export default function AssessmentPage() {
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="mx-11">
{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>
<Text className="text-sm text-gray-400">
(Max.File size : 64 MB)
</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="mx-11 px-1">
{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>
{/* File Size Validation Modal */}
<FileSizeValidationModal
opened={modalOpenFileSize}
onClose={() => setModalOpenFileSize(false)}
fileName={exceededFileName}
<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 */}