update: index lazy for upload file on database and local storage
This commit is contained in:
parent
665de19688
commit
521d39f7f6
|
|
@ -14,6 +14,7 @@ import {
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
|
uploadFileMutationOptions,
|
||||||
submitValidationMutationOptions,
|
submitValidationMutationOptions,
|
||||||
submitOptionMutationOptions,
|
submitOptionMutationOptions,
|
||||||
getAverageScoreQueryOptions,
|
getAverageScoreQueryOptions,
|
||||||
|
|
@ -63,6 +64,7 @@ export default function AssessmentPage() {
|
||||||
const [assessmentId, setAssessmentId] = useState<string | null>(null);
|
const [assessmentId, setAssessmentId] = useState<string | null>(null);
|
||||||
const [answers, setAnswers] = useState<{ [key: string]: string }>({});
|
const [answers, setAnswers] = useState<{ [key: string]: string }>({});
|
||||||
const [validationInformation, setValidationInformation] = useState<{ [key: string]: string }>({});
|
const [validationInformation, setValidationInformation] = useState<{ [key: string]: string }>({});
|
||||||
|
const [uploadedFiles, setUploadedFiles] = useState<{ [key: string]: File | null }>({});
|
||||||
|
|
||||||
// Fetch aspects and sub-aspects
|
// Fetch aspects and sub-aspects
|
||||||
const aspectsQuery = useQuery({
|
const aspectsQuery = useQuery({
|
||||||
|
|
@ -233,6 +235,9 @@ export default function AssessmentPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mutation for file upload
|
||||||
|
const uploadFileMutation = useMutation(uploadFileMutationOptions());
|
||||||
|
|
||||||
// Drag and Drop handlers
|
// Drag and Drop handlers
|
||||||
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -243,12 +248,59 @@ export default function AssessmentPage() {
|
||||||
setDragActive(false);
|
setDragActive(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
// 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();
|
event.preventDefault();
|
||||||
setDragActive(false);
|
setDragActive(false);
|
||||||
const droppedFiles = Array.from(event.dataTransfer.files);
|
const droppedFiles = Array.from(event.dataTransfer.files);
|
||||||
setFiles((prevFiles) => [...prevFiles, ...droppedFiles]);
|
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 = () => {
|
const handleClick = () => {
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
|
|
@ -256,17 +308,51 @@ export default function AssessmentPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>, question: { questionId: string }) => {
|
||||||
if (event.target.files) {
|
if (event.target.files) {
|
||||||
const fileArray = Array.from(event.target.files); // Ubah ke array hanya jika files tidak null
|
const fileArray = Array.from(event.target.files);
|
||||||
setFiles((prevFiles) => [...prevFiles, ...fileArray]);
|
if (fileArray.length > 0) {
|
||||||
}
|
const formData = new FormData();
|
||||||
};
|
formData.append('file', fileArray[0]); // Hanya menyertakan file pertama
|
||||||
|
|
||||||
const handleRemoveFile = (fileIndex: number) => {
|
// Tambahkan assessmentId ke FormData
|
||||||
setFiles((prevFiles) =>
|
if (assessmentId) {
|
||||||
prevFiles.filter((_, index) => index !== fileIndex)
|
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
|
// Function to scroll to the specific question
|
||||||
|
|
@ -316,7 +402,7 @@ export default function AssessmentPage() {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card shadow="sm" p="lg" radius="md" withBorder>
|
<div>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Flex justify="space-between" align="flex-start" mt="lg">
|
<Flex justify="space-between" align="flex-start" mt="lg">
|
||||||
|
|
||||||
|
|
@ -372,10 +458,10 @@ export default function AssessmentPage() {
|
||||||
</Flex>
|
</Flex>
|
||||||
{/* Pertanyaan */}
|
{/* Pertanyaan */}
|
||||||
<Stack gap="sm" style={{ flex: 1 }}>
|
<Stack gap="sm" style={{ flex: 1 }}>
|
||||||
<Text className="text-2xl font-bold">
|
<Text className="text-2xl font-bold ml-6">
|
||||||
Harap menjawab semua pertanyaan yang tersedia
|
Harap menjawab semua pertanyaan yang tersedia
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-gray-400">Semua jawaban Anda akan ditinjau</Text>
|
<Text className="text-gray-400 ml-6">Semua jawaban Anda akan ditinjau</Text>
|
||||||
{filteredQuestions.length === 0 ? (
|
{filteredQuestions.length === 0 ? (
|
||||||
<Text color="black" className="text-center p-3">
|
<Text color="black" className="text-center p-3">
|
||||||
Pertanyaan tidak ada untuk sub-aspek yang dipilih.
|
Pertanyaan tidak ada untuk sub-aspek yang dipilih.
|
||||||
|
|
@ -386,28 +472,21 @@ export default function AssessmentPage() {
|
||||||
if (!questionId) return null;
|
if (!questionId) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<div
|
||||||
key={questionId}
|
key={questionId}
|
||||||
shadow="sm"
|
|
||||||
p="lg"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
ref={(el) => (questionRefs.current[questionId] = el)}
|
ref={(el) => (questionRefs.current[questionId] = el)}
|
||||||
style={{ position: "relative" }}
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<Flex justify="space-between" align="flex-start" style={{ width: "100%" }}>
|
<Flex justify="space-between" align="flex-start" style={{ width: "100%" }}>
|
||||||
<Text
|
<Text className="font-bold mr-3">{startIndex + index + 1}.</Text>
|
||||||
className="font-bold"
|
<div className="flex-grow">
|
||||||
style={{
|
<Text className="font-bold break-words">
|
||||||
flexGrow: 1,
|
{question.questionText}
|
||||||
wordBreak: "break-word",
|
</Text>
|
||||||
marginRight: "40px",
|
</div>
|
||||||
}}
|
|
||||||
>
|
|
||||||
{startIndex + index + 1}. {question.questionText}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
|
{/* Action Icon */}
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFlaggedQuestions((prevFlags) => ({
|
setFlaggedQuestions((prevFlags) => ({
|
||||||
|
|
@ -440,92 +519,109 @@ export default function AssessmentPage() {
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
|
{/* Opsi Radio Button */}
|
||||||
{question.options?.length > 0 ? (
|
{question.options?.length > 0 ? (
|
||||||
<Radio.Group value={answers[question.questionId] || ""}>
|
<div className="ml-6">
|
||||||
<div className="flex flex-col gap-4">
|
<Radio.Group value={answers[question.questionId] || ""}>
|
||||||
{question.options.map((option: any) => (
|
<div className="flex flex-col gap-4">
|
||||||
<label
|
{question.options.map((option: any) => (
|
||||||
key={option.optionId}
|
<label
|
||||||
className="bg-gray-200 border rounded-lg p-4 cursor-pointer transition-transform transform hover:scale-105 shadow-md hover:shadow-lg flex items-center"
|
key={option.optionId}
|
||||||
onClick={() => document.getElementById(option.optionId)?.click()}
|
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
|
||||||
<Radio
|
? "bg-blue-500 text-white"
|
||||||
id={option.optionId}
|
: "bg-gray-200 text-black"
|
||||||
className="font-bold"
|
}`}
|
||||||
value={option.optionId}
|
onClick={() => document.getElementById(option.optionId)?.click()}
|
||||||
label={option.optionText}
|
>
|
||||||
size="md"
|
<Radio
|
||||||
radius="xl"
|
id={option.optionId}
|
||||||
style={{ pointerEvents: "none" }}
|
className="font-bold"
|
||||||
checked={answers[question.questionId] === option.optionId} // Untuk menampilkan jawaban yang sudah dipilih
|
value={option.optionId}
|
||||||
onChange={() => handleAnswerChange(question.questionId, option.optionId)} // Memanggil handleAnswerChange dengan questionId dan optionId
|
label={option.optionText}
|
||||||
/>
|
size="md"
|
||||||
</label>
|
radius="xl"
|
||||||
))}
|
style={{ pointerEvents: "none" }}
|
||||||
</div>
|
checked={answers[question.questionId] === option.optionId} // Untuk menampilkan jawaban yang sudah dipilih
|
||||||
</Radio.Group>
|
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>
|
<Text color="red">Tidak ada opsi untuk pertanyaan ini.</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Textarea
|
{/* Textarea */}
|
||||||
placeholder="Berikan keterangan terkait jawaban di atas"
|
<div className="ml-6">
|
||||||
value={validationInformation[question.questionId] || ""}
|
<Textarea
|
||||||
onChange={(event) => handleTextareaChange(question.questionId, event.currentTarget.value)}
|
placeholder="Berikan keterangan terkait jawaban di atas"
|
||||||
disabled={!answers[question.questionId]}
|
value={validationInformation[question.questionId] || ""}
|
||||||
/>
|
onChange={(event) => handleTextareaChange(question.questionId, event.currentTarget.value)}
|
||||||
|
disabled={!answers[question.questionId]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* File Upload */}
|
{/* File Upload */}
|
||||||
{question.needFile === true && (
|
<div className="ml-6">
|
||||||
<div
|
{question.needFile === true && (
|
||||||
className={`pt-5 pb-5 pr-5 pl-2 border-2 border-dashed ${dragActive ? "bg-gray-100" : "bg-transparent"
|
<div
|
||||||
} shadow-lg`}
|
className={`pt-5 pb-5 pr-5 pl-5 border-2 rounded-lg border-dashed ${dragActive ? "bg-gray-100" : "bg-transparent"
|
||||||
onDragOver={handleDragOver}
|
} shadow-lg`}
|
||||||
onDragLeave={handleDragLeave}
|
onDragOver={handleDragOver}
|
||||||
onDrop={handleDrop}
|
onDragLeave={handleDragLeave}
|
||||||
onClick={handleClick}
|
onDrop={(event) => handleDrop(event, question)} // Mengoper question sebagai argumen
|
||||||
>
|
onClick={handleClick}
|
||||||
<Flex align="center" justify="space-between" gap="sm">
|
>
|
||||||
<TbUpload
|
<Flex align="center" justify="space-between" gap="sm">
|
||||||
size={24}
|
<TbUpload
|
||||||
style={{ marginLeft: "8px", marginRight: "8px" }}
|
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={handleFileChange}
|
|
||||||
style={{ display: "none" }}
|
|
||||||
accept="image/png, image/jpeg, application/pdf"
|
|
||||||
disabled={!answers[question.questionId]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{files.length > 0 && (
|
|
||||||
<Stack gap="sm" mt="sm">
|
|
||||||
<Text className="font-bold">File yang diunggah:</Text>
|
|
||||||
{files.map((file, fileIndex) => (
|
|
||||||
<Group key={fileIndex} align="center">
|
|
||||||
<Text>{file.name}</Text>
|
|
||||||
<CloseButton
|
|
||||||
title="Hapus file"
|
|
||||||
onClick={() => handleRemoveFile(fileIndex)}
|
|
||||||
/>
|
/>
|
||||||
</Group>
|
<div className="flex-grow text-right">
|
||||||
))}
|
<Text className="font-bold">
|
||||||
</Stack>
|
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>
|
</Stack>
|
||||||
</Card>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
|
|
@ -626,7 +722,7 @@ export default function AssessmentPage() {
|
||||||
|
|
||||||
{/* Tombol Selesai */}
|
{/* Tombol Selesai */}
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<button onClick={handleFinishClick} className="bg-gray-200 text-black font-bold py-2 w-full">
|
<button onClick={handleFinishClick} className="bg-blue-500 text-white font-bold rounded-md py-2 w-full">
|
||||||
Selesai
|
Selesai
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -643,6 +739,6 @@ export default function AssessmentPage() {
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user