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 521d39f7f6 - Show all commits

View File

@ -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,11 +248,58 @@ 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 = () => {
@ -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
// 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 = (fileIndex: number) => { const handleRemoveFile = (question: { questionId: string }) => {
setFiles((prevFiles) => setUploadedFiles(prev => ({
prevFiles.filter((_, index) => index !== fileIndex) ...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>
); );
} }