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
3 changed files with 241 additions and 170 deletions
Showing only changes of commit 0c5e777273 - Show all commits

View File

@ -0,0 +1,65 @@
import { Navigate, Outlet, createFileRoute } from "@tanstack/react-router";
import AppHeader from "../components/AppHeader";
import AppNavbar from "../components/AppNavbar";
import useAuth from "@/hooks/useAuth";
import { useQuery } from "@tanstack/react-query";
import fetchRPC from "@/utils/fetchRPC";
import client from "@/honoClient";
import { useState } from "react";
export const Route = createFileRoute("/_assessmentLayout")({
component: AssessmentLayout,
// beforeLoad: ({ location }) => {
// if (true) {
// throw redirect({
// to: "/login",
// });
// }
// },
});
function AssessmentLayout() {
const { isAuthenticated, saveAuthData } = useAuth();
useQuery({
queryKey: ["my-profile"],
queryFn: async () => {
const response = await fetchRPC(client.auth["my-profile"].$get());
saveAuthData({
id: response.id,
name: response.name,
permissions: response.permissions,
});
return response;
},
enabled: isAuthenticated,
});
const [openNavbar, setNavbarOpen] = useState(true);
const toggle = () => {
setNavbarOpen(!openNavbar);
};
return isAuthenticated ? (
<div className="flex flex-col w-full h-screen overflow-hidden">
{/* Header */}
<AppHeader toggle={toggle} openNavbar={openNavbar} />
{/* Main Content Area */}
<div className="flex h-full w-screen overflow-hidden">
{/* Sidebar */}
<AppNavbar />
{/* Main Content */}
<main className="relative w-full mt-16 bg-white overflow-auto">
<Outlet />
</main>
</div>
</div>
) : (
<Navigate to="/login" />
);
}

View File

@ -25,13 +25,14 @@ import {
import { TbFlagFilled, TbUpload, TbChevronRight, TbChevronUp } from "react-icons/tb"; import { TbFlagFilled, TbUpload, TbChevronRight, TbChevronUp } from "react-icons/tb";
import FinishAssessmentModal from "@/modules/assessmentManagement/modals/ConfirmModal"; import FinishAssessmentModal from "@/modules/assessmentManagement/modals/ConfirmModal";
import { useState, useRef, useEffect } from "react"; import { useState, useRef, useEffect } from "react";
import AppHeader from "@/components/AppHeader";
const getQueryParam = (param: string) => { const getQueryParam = (param: string) => {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(param); return urlParams.get(param);
}; };
export const Route = createLazyFileRoute("/_dashboardLayout/assessment/")({ export const Route = createLazyFileRoute("/_assessmentLayout/assessment/")({
component: AssessmentPage, component: AssessmentPage,
}); });
@ -446,8 +447,9 @@ export default function AssessmentPage() {
{/* LEFT-SIDE */} {/* LEFT-SIDE */}
{/* Aspek dan Sub-Aspek */} {/* Aspek dan Sub-Aspek */}
<Flex direction="column" gap="xs" className="mr-4 w-52"> <Flex direction="column" gap="xs" className="w-52">
<div className="space-y-4"> <div className="space-y-2">
{/* Aspek */}
{aspectsQuery.data?.data {aspectsQuery.data?.data
.filter((aspect) => .filter((aspect) =>
aspect.subAspects.some((subAspect) => aspect.subAspects.some((subAspect) =>
@ -457,13 +459,13 @@ export default function AssessmentPage() {
.map((aspect) => ( .map((aspect) => (
<div <div
key={aspect.id} key={aspect.id}
className="p-4 bg-gray-50 rounded-lg shadow-md" className="p-2 "
> >
<div <div
className="flex justify-between cursor-pointer" className="flex justify-between cursor-pointer"
onClick={() => toggleAspect(aspect.id)} onClick={() => toggleAspect(aspect.id)}
> >
<div className="text-lg text-gray-700">{aspect.name}</div> <div className="text-lg font-bold px-3">{aspect.name}</div>
<div> <div>
{openAspects[aspect.id] ? ( {openAspects[aspect.id] ? (
<TbChevronUp size={25} /> <TbChevronUp size={25} />
@ -473,6 +475,7 @@ export default function AssessmentPage() {
</div> </div>
</div> </div>
{/* Sub-Aspek */}
{openAspects[aspect.id] && ( {openAspects[aspect.id] && (
<div className="mt-2 space-y-2"> <div className="mt-2 space-y-2">
{aspect.subAspects {aspect.subAspects
@ -482,7 +485,7 @@ export default function AssessmentPage() {
.map((subAspect) => ( .map((subAspect) => (
<div <div
key={subAspect.id} key={subAspect.id}
className={`flex justify-between text-gray-600 cursor-pointer ${selectedSubAspectId === subAspect.id ? 'font-bold' : ''}`} 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)} onClick={() => setSelectedSubAspectId(subAspect.id)}
> >
<div>{subAspect.name}</div> <div>{subAspect.name}</div>
@ -494,179 +497,182 @@ export default function AssessmentPage() {
))} ))}
</div> </div>
</Flex> </Flex>
{/* Pertanyaan */} {/* Pertanyaan */}
<Stack gap="sm" style={{ flex: 1 }}> <Stack gap="sm" style={{ flex: 1 }}>
<Text className="text-2xl font-bold ml-6"> <Text className="text-2xl font-bold ml-6">
Harap menjawab semua pertanyaan yang tersedia 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> </Text>
) : ( <Text className="text-gray-400 ml-6">Semua jawaban Anda akan ditinjau</Text>
filteredQuestions.map((question: any, index: number) => { {filteredQuestions.length === 0 ? (
const questionId = question.questionId; <Text color="black" className="text-center p-3">
if (!questionId) return null; Pertanyaan tidak ada untuk sub-aspek yang dipilih.
</Text>
) : (
filteredQuestions.map((question: any, index: number) => {
const questionId = question.questionId;
if (!questionId) return null;
return ( return (
<div <div
key={questionId} key={questionId}
ref={(el) => (questionRefs.current[questionId] = el)} ref={(el) => (questionRefs.current[questionId] = el)}
className="space-y-4" 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 className="font-bold mr-3">{startIndex + index + 1}.</Text> {/* Question */}
<div className="flex-grow"> <Text className="font-bold mx-3 p-1">{startIndex + index + 1}.</Text>
<Text className="font-bold break-words"> <div className="flex-grow">
{question.questionText} <Text className="font-bold break-words">
</Text> {question.questionText}
</div> </Text>
{/* 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>
<div className="ml-6"> {/* Action Icon/Flag */}
{uploadedFiles[question.questionId] && ( <ActionIcon
<Stack gap="sm" mt="sm"> onClick={() => {
<Text className="font-bold">File yang diunggah:</Text> setFlaggedQuestions((prevFlags) => ({
<Group align="center"> ...prevFlags,
<Text>{uploadedFiles[question.questionId]?.name}</Text> {/* Tampilkan nama file yang diunggah */} [questionId]: !prevFlags[questionId],
<CloseButton }));
title="Hapus file" toggleFlagMutation.mutate(questionId);
onClick={() => handleRemoveFile(question)} // Mengoper question sebagai argumen }}
title={
!answers[question.questionId]
? "Pilih jawaban terlebih dahulu"
: "Tandai"
}
color={flaggedQuestions[questionId] ? "red" : "white"}
style={{
margin: "2px 10px",
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="mx-12">
<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
/> />
</Group> </label>
</Stack> ))}
</div>
</Radio.Group>
</div>
) : (
<Text color="red">Tidak ada opsi untuk pertanyaan ini.</Text>
)} )}
</div>
{/* Garis pembatas setiap soal */} {/* Textarea */}
<div> <div className="mx-12">
<hr className="border-t-2 border-gray-300 ml-6 mx-auto mt-6 mb-6" /> <Textarea
</div> placeholder="Berikan keterangan terkait jawaban di atas"
</Stack> value={validationInformation[question.questionId] || ""}
</div> onChange={(event) => handleTextareaChange(question.questionId, event.currentTarget.value)}
); disabled={!answers[question.questionId]}
}) />
)} </div>
</Stack>
{/* File Upload */}
<div className="mx-12">
{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 mx-12 mt-6 mb-6" />
</div>
</Stack>
</div>
);
})
)}
</Stack>
{/* Navigasi dan Pagination */} {/* Navigasi dan Pagination */}
<Flex direction="column" gap="xs" className="ml-4"> <Flex direction="column" gap="xs" className="mx-4">
{/* Navigasi (Number of Questions) */} {/* Navigasi (Number of Questions) */}
<div className="grid grid-cols-5 gap-2"> <div className="grid grid-cols-5 gap-2">

View File

@ -9,7 +9,7 @@ const searchParamSchema = z.object({
detail: z.string().default("").optional(), detail: z.string().default("").optional(),
}); });
export const Route = createFileRoute("/_dashboardLayout/assessment/")({ export const Route = createFileRoute("/_assessmentLayout/assessment/")({
validateSearch: searchParamSchema, validateSearch: searchParamSchema,
loader: ({ context: { queryClient } }) => { loader: ({ context: { queryClient } }) => {