Pull Request branch dev-clone to main #1
|
|
@ -12,6 +12,7 @@ import {
|
|||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardTitle,
|
||||
} from "@/shadcn/components/ui/card";
|
||||
import { Button } from "@/shadcn/components/ui/button";
|
||||
import { Textarea } from "@/shadcn/components/ui/textarea";
|
||||
|
|
@ -33,12 +34,33 @@ import {
|
|||
getQuestionsAllQueryOptions,
|
||||
toggleFlagAnswer,
|
||||
} from "@/modules/assessmentManagement/queries/assessmentQueries";
|
||||
import { TbFlagFilled, TbUpload, TbChevronRight, TbChevronDown } from "react-icons/tb";
|
||||
import { TbFlagFilled, TbUpload, TbChevronRight, TbChevronDown, TbLayoutSidebarLeftCollapseFilled, TbMenu2 } 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 {
|
||||
Sheet,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/shadcn/components/ui/sheet";
|
||||
import {
|
||||
LeftSheet,
|
||||
LeftSheetClose,
|
||||
LeftSheetContent,
|
||||
LeftSheetDescription,
|
||||
LeftSheetFooter,
|
||||
LeftSheetHeader,
|
||||
LeftSheetTitle,
|
||||
LeftSheetTrigger,
|
||||
} from "@/shadcn/components/ui/leftsheet";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import AppHeader from "@/components/AppHeader";
|
||||
|
||||
const getQueryParam = (param: string) => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
|
@ -66,8 +88,8 @@ interface ToggleFlagResponse {
|
|||
export interface SubmitOptionResponse {
|
||||
message: string;
|
||||
answer: {
|
||||
id: string;
|
||||
isFlagged: boolean | null;
|
||||
id: string;
|
||||
isFlagged: boolean | null;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -95,6 +117,29 @@ export default function AssessmentPage() {
|
|||
const [currentPagePerSubAspect, setCurrentPagePerSubAspect] = useState<{ [subAspectId: string]: number }>({});
|
||||
const currentPage = currentPagePerSubAspect[selectedSubAspectId || ""] || 1;
|
||||
const questionsPerPage = 10;
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); // Check for mobile screen
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
const [openNavbar, setOpenNavbar] = useState(false);
|
||||
const [isLeftSidebarOpen, setIsLeftSidebarOpen] = useState(false);
|
||||
|
||||
// Toggle the sidebar open/close on mobile
|
||||
const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen);
|
||||
|
||||
const toggleLeftSidebar = () => setIsLeftSidebarOpen(!isLeftSidebarOpen);
|
||||
|
||||
const toggleSidebarLeftSide = () => {
|
||||
setIsSidebarOpen((prevState) => !prevState);
|
||||
};
|
||||
|
||||
// Fungsi toggle untuk membuka/menutup navbar
|
||||
const toggle = () => {
|
||||
setOpenNavbar((prevState) => !prevState);
|
||||
};
|
||||
|
||||
// Adjust layout on screen resize
|
||||
window.addEventListener('resize', () => {
|
||||
setIsMobile(window.innerWidth <= 768);
|
||||
});
|
||||
|
||||
// Fetch aspects and sub-aspects
|
||||
const aspectsQuery = useQuery({
|
||||
|
|
@ -169,41 +214,37 @@ export default function AssessmentPage() {
|
|||
|
||||
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;
|
||||
},
|
||||
{}
|
||||
(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;
|
||||
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.");
|
||||
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
|
||||
}
|
||||
}, [answersData]);
|
||||
|
||||
|
|
@ -212,34 +253,34 @@ export default function AssessmentPage() {
|
|||
|
||||
// Filter pertanyaan yang belum dijawab berdasarkan data `answers`
|
||||
const unanswered = data?.data.filter(question => {
|
||||
return question.questionId !== null && !answers[question.questionId];
|
||||
return question.questionId !== null && !answers[question.questionId];
|
||||
}) || [];
|
||||
|
||||
setUnansweredQuestions(unanswered.length);
|
||||
|
||||
// Tampilkan modal berdasarkan jumlah pertanyaan yang belum dijawab
|
||||
if (unanswered.length > 0) {
|
||||
setValidationModalOpen(true);
|
||||
setValidationModalOpen(true);
|
||||
} else {
|
||||
setModalOpen(true);
|
||||
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();
|
||||
// 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);
|
||||
// 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);
|
||||
console.error("Error finishing assessment:", error);
|
||||
} finally {
|
||||
setModalOpen(false);
|
||||
setModalOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -259,8 +300,8 @@ export default function AssessmentPage() {
|
|||
|
||||
// Filter aspects by selected aspectId
|
||||
const filteredAspects = selectedAspectId
|
||||
? aspects.filter((aspect) => aspect.aspectId === selectedAspectId) // Use 'id' instead of 'aspectId'
|
||||
: aspects;
|
||||
? 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);
|
||||
|
|
@ -283,18 +324,18 @@ export default function AssessmentPage() {
|
|||
// Mutation function to toggle flag
|
||||
const { mutate: toggleFlag } = useMutation({
|
||||
mutationFn: (formData: { assessmentId: string; questionId: string; isFlagged: boolean }) =>
|
||||
toggleFlagAnswer(formData),
|
||||
toggleFlagAnswer(formData),
|
||||
onSuccess: (response: SubmitOptionResponse) => {
|
||||
if (response.answer) {
|
||||
const { answer } = response;
|
||||
setFlaggedQuestions((prevFlags) => ({
|
||||
...prevFlags,
|
||||
[answer.id]: answer.isFlagged ?? false,
|
||||
}));
|
||||
}
|
||||
if (response.answer) {
|
||||
const { answer } = response;
|
||||
setFlaggedQuestions((prevFlags) => ({
|
||||
...prevFlags,
|
||||
[answer.id]: answer.isFlagged ?? false,
|
||||
}));
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error toggling flag:", error);
|
||||
console.error("Error toggling flag:", error);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -304,21 +345,21 @@ export default function AssessmentPage() {
|
|||
const assessmentId = getQueryParam("id");
|
||||
|
||||
if (!assessmentId) {
|
||||
console.error("Assessment ID tidak ditemukan");
|
||||
return;
|
||||
console.error("Assessment ID tidak ditemukan");
|
||||
return;
|
||||
}
|
||||
|
||||
// Update flaggedQuestions di state
|
||||
setFlaggedQuestions((prevFlags) => ({
|
||||
...prevFlags,
|
||||
[questionId]: newFlagState,
|
||||
...prevFlags,
|
||||
[questionId]: newFlagState,
|
||||
}));
|
||||
|
||||
// Kirim perubahan flag ke server
|
||||
toggleFlag({
|
||||
assessmentId,
|
||||
questionId,
|
||||
isFlagged: newFlagState,
|
||||
assessmentId,
|
||||
questionId,
|
||||
isFlagged: newFlagState,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -326,19 +367,19 @@ export default function AssessmentPage() {
|
|||
const submitOptionMutation = useMutation({
|
||||
...submitOptionMutationOptions, // Spread the mutation options here
|
||||
onSuccess: () => {
|
||||
// Refetch the average scores after a successful submission
|
||||
averageScoreQuery.refetch();
|
||||
// Refetch the average scores after a successful submission
|
||||
averageScoreQuery.refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error submitting option:", 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;
|
||||
console.error("Assessment ID tidak ditemukan");
|
||||
return;
|
||||
}
|
||||
|
||||
// Update answers in the state
|
||||
|
|
@ -347,17 +388,17 @@ export default function AssessmentPage() {
|
|||
|
||||
// Send the updated answer to the backend
|
||||
submitOptionMutation.mutate({
|
||||
optionId,
|
||||
assessmentId,
|
||||
questionId,
|
||||
isFlagged: false,
|
||||
filename: undefined,
|
||||
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;
|
||||
acc[item.questionId] = item.validationInformation;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
|
|
@ -372,16 +413,16 @@ export default function AssessmentPage() {
|
|||
// Mutation untuk mengirim data ke backend
|
||||
const { mutate: submitValidation } = useMutation({
|
||||
mutationFn: (form: {
|
||||
assessmentId: string;
|
||||
questionId: string;
|
||||
validationInformation: string;
|
||||
assessmentId: string;
|
||||
questionId: string;
|
||||
validationInformation: string;
|
||||
}) => submitValidationQuery(form),
|
||||
onSuccess: () => {
|
||||
// Tindakan yang diambil setelah berhasil
|
||||
console.log("Validation updated successfully!");
|
||||
// Tindakan yang diambil setelah berhasil
|
||||
console.log("Validation updated successfully!");
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error updating validation:", error);
|
||||
console.error("Error updating validation:", error);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -389,20 +430,20 @@ export default function AssessmentPage() {
|
|||
const handleTextareaChange = (questionId: string, value: string) => {
|
||||
// Memperbarui state validationInformation
|
||||
setValidationInformation((prev) => ({
|
||||
...prev,
|
||||
[questionId]: value,
|
||||
...prev,
|
||||
[questionId]: value,
|
||||
}));
|
||||
|
||||
// Pastikan assessmentId tidak null sebelum mengirimkan data ke server
|
||||
if (assessmentId) {
|
||||
// Kirim data validasi ke server
|
||||
submitValidation({
|
||||
assessmentId,
|
||||
questionId,
|
||||
validationInformation: value,
|
||||
});
|
||||
// Kirim data validasi ke server
|
||||
submitValidation({
|
||||
assessmentId,
|
||||
questionId,
|
||||
validationInformation: value,
|
||||
});
|
||||
} else {
|
||||
console.error("Assessment ID tidak ditemukan");
|
||||
console.error("Assessment ID tidak ditemukan");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -445,39 +486,39 @@ export default function AssessmentPage() {
|
|||
const droppedFiles = Array.from(event.dataTransfer.files);
|
||||
|
||||
if (droppedFiles.length > 0) {
|
||||
const file = droppedFiles[0];
|
||||
const file = droppedFiles[0];
|
||||
|
||||
// Validate file size
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
setExceededFileName(file.name);
|
||||
setModalOpenFileSize(true);
|
||||
return;
|
||||
}
|
||||
// Validate file size
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
setExceededFileName(file.name);
|
||||
setModalOpenFileSize(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
if (assessmentId) {
|
||||
formData.append('assessmentId', assessmentId);
|
||||
} else {
|
||||
console.error("assessmentId is null");
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
if (question.questionId) {
|
||||
formData.append('questionId', question.questionId);
|
||||
} else {
|
||||
console.error("questionId is null");
|
||||
return;
|
||||
}
|
||||
|
||||
uploadFileMutation.mutate(formData); // Upload file
|
||||
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
|
||||
}));
|
||||
// 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
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -489,42 +530,42 @@ export default function AssessmentPage() {
|
|||
|
||||
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];
|
||||
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
|
||||
}));
|
||||
// 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
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -601,62 +642,120 @@ export default function AssessmentPage() {
|
|||
|
||||
{/* 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} />
|
||||
<TbMenu2
|
||||
onClick={toggleLeftSidebar}
|
||||
className="ml-4 w-6 h-fit pb-4 mb-4 cursor-pointer"
|
||||
/>
|
||||
|
||||
{/* Sidebar for Mobile */}
|
||||
{isMobile && (
|
||||
<LeftSheet open={isLeftSidebarOpen} onOpenChange={(open) => setIsLeftSidebarOpen(open)}>
|
||||
<LeftSheetContent className="h-full w-75 overflow-auto mt-8">
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
</Flex>
|
||||
</LeftSheetContent>
|
||||
</LeftSheet>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
))}
|
||||
{/* Sidebar for Desktop (Always Visible) */}
|
||||
<div className="hidden md:block fixed h-screen w-64 overflow-auto mt-8">
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Flex>
|
||||
</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">
|
||||
<div className="ml-0 md:ml-64 mr-0 md: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
|
||||
|
|
@ -678,14 +777,12 @@ export default function AssessmentPage() {
|
|||
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>
|
||||
<div className="grid grid-cols-[auto_1fr_auto] gap-2 w-full items-start">
|
||||
{/* Question Number */}
|
||||
<Text className="font-bold p-1 text-sm">{startIndex + index + 1}.</Text>
|
||||
|
||||
{/* Question Text */}
|
||||
<Text className="font-bold break-words text-sm p-1">{question.questionText}</Text>
|
||||
|
||||
{/* Action Icon/Flag */}
|
||||
<ActionIcon
|
||||
|
|
@ -695,14 +792,14 @@ export default function AssessmentPage() {
|
|||
>
|
||||
<TbFlagFilled
|
||||
size={25}
|
||||
className={`p-1 ${flaggedQuestions[questionId] ? "text-white" : "text-black"}`} // Mengurangi padding untuk ikon
|
||||
className={`p-1 ${flaggedQuestions[questionId] ? "text-white" : "text-black"}`}
|
||||
/>
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
{/* Opsi Radio Button */}
|
||||
{/* Radio Button Options */}
|
||||
{question.options?.length > 0 ? (
|
||||
<div className="mx-11">
|
||||
<div className="mx-8">
|
||||
<RadioGroup
|
||||
value={answers[question.questionId] || ""}
|
||||
onValueChange={(value) => handleAnswerChange(question.questionId, value)}
|
||||
|
|
@ -711,11 +808,10 @@ export default function AssessmentPage() {
|
|||
{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"
|
||||
}`}
|
||||
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
|
||||
|
|
@ -738,7 +834,7 @@ export default function AssessmentPage() {
|
|||
<Text color="red">Tidak ada opsi untuk pertanyaan ini.</Text>
|
||||
)}
|
||||
|
||||
{/* Textarea */}
|
||||
{/* Textarea for additional information */}
|
||||
<div className="mx-11">
|
||||
<Textarea
|
||||
placeholder="Berikan keterangan terkait jawaban di atas"
|
||||
|
|
@ -763,7 +859,7 @@ export default function AssessmentPage() {
|
|||
handleClick={handleClick}
|
||||
/>
|
||||
|
||||
{/* Garis pembatas setiap soal */}
|
||||
{/* Divider between questions */}
|
||||
<div>
|
||||
<hr className="border-t-2 border-gray-300 mx-11 mt-6 mb-6" />
|
||||
</div>
|
||||
|
|
@ -777,7 +873,174 @@ export default function AssessmentPage() {
|
|||
|
||||
{/* RIGHT-SIDE */}
|
||||
{/* Navigasi dan Pagination */}
|
||||
<div className="fixed h-screen right-0 w-60 overflow-auto mr-4">
|
||||
{isMobile && (
|
||||
<button
|
||||
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||
className="fixed bottom-4 right-4 bg-[--primary-color] text-white p-3 rounded-full shadow-md z-50"
|
||||
>
|
||||
<TbLayoutSidebarLeftCollapseFilled size={30} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Sidebar for mobile (only when toggled) */}
|
||||
<div className="hidden md:block">
|
||||
<Sheet open={isSidebarOpen} onOpenChange={(open) => setIsSidebarOpen(open)}>
|
||||
<SheetContent className="h-full w-70 overflow-auto mr-4">
|
||||
<SheetTitle className="font-medium text-lg text-gray-800 mb-2">
|
||||
Nomor Soal
|
||||
</SheetTitle>
|
||||
{/* <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>
|
||||
<CardTitle className="text-lg font-extrabold text-center mt-4 mb-2">
|
||||
Nilai Sementara
|
||||
</CardTitle>
|
||||
<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>
|
||||
|
||||
{/* Divider */}
|
||||
<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>
|
||||
|
||||
{/* Finish Button */}
|
||||
<div>
|
||||
<Button
|
||||
onClick={handleFinishClick}
|
||||
className="bg-[--primary-color] text-white font-bold rounded-md w-full text-sm"
|
||||
>
|
||||
Selesai
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
{/* Finish Assessment Modal */}
|
||||
<FinishAssessmentModal
|
||||
opened={modalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
onConfirm={handleConfirmFinish}
|
||||
assessmentId={assessmentId}
|
||||
/>
|
||||
|
||||
{/* Validation Modal */}
|
||||
<ValidationModal
|
||||
opened={validationModalOpen}
|
||||
onClose={() => setValidationModalOpen(false)}
|
||||
unansweredQuestions={unansweredQuestions}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
|
||||
{/* Sidebar for desktop (always visible) */}
|
||||
<div className="hidden md:block 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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user