831 lines
36 KiB
TypeScript
831 lines
36 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import html2pdf from "html2pdf.js";
|
|
import useAuth from "@/hooks/useAuth";
|
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
|
import { getAllAspectsAverageScore, getAllSubAspectsAverageScore, getAllVerifiedAspectsAverageScore, getAllVerifiedSubAspectsAverageScore } from "@/modules/assessmentResult/queries/assessmentResultQueries";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { getAssessmentResultByIdQueryOptions, getVerifiedAssessmentResultByIdQueryOptions } from "@/modules/assessmentResultsManagement/queries/assessmentResultsManagaementQueries";
|
|
import { PieChart, Pie, Label, BarChart, Bar, CartesianGrid, XAxis, YAxis } from "recharts";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/shadcn/components/ui/card"
|
|
import {
|
|
ChartConfig,
|
|
ChartContainer,
|
|
ChartTooltip,
|
|
ChartTooltipContent,
|
|
} from "@/shadcn/components/ui/chart"
|
|
import { aspectQueryOptions } from "@/modules/aspectManagement/queries/aspectQueries";
|
|
import { TbChevronDown, TbChevronLeft, TbChevronRight, TbChevronUp, TbFileTypePdf } from "react-icons/tb";
|
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/shadcn/components/ui/dropdown-menu";
|
|
import clsx from "clsx";
|
|
import React from "react";
|
|
import AppHeader from "@/components/AppHeader";
|
|
import { LeftSheet, LeftSheetContent } from "@/shadcn/components/ui/leftsheet";
|
|
import { ScrollArea } from "@mantine/core";
|
|
import data from "node_modules/backend/src/appEnv";
|
|
|
|
const getQueryParam = (param: string) => {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
return urlParams.get(param);
|
|
};
|
|
|
|
export const Route = createLazyFileRoute("/_dashboardLayout/assessmentResult/")({
|
|
component: AssessmentResultPage,
|
|
});
|
|
|
|
export default function AssessmentResultPage() {
|
|
const { user } = useAuth();
|
|
const isSuperAdmin = user?.role === "super-admin";
|
|
|
|
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);
|
|
const toggle = () => {
|
|
setOpenNavbar((prevState) => !prevState);
|
|
};
|
|
|
|
// Adjust layout on screen resize
|
|
window.addEventListener('resize', () => {
|
|
setIsMobile(window.innerWidth <= 768);
|
|
});
|
|
|
|
const toggleLeftSidebar = () => setIsLeftSidebarOpen(!isLeftSidebarOpen);
|
|
|
|
const [assessmentId, setAssessmentId] = useState<string | undefined>(undefined);
|
|
|
|
useEffect(() => {
|
|
const id = getQueryParam("id");
|
|
setAssessmentId(id ?? undefined);
|
|
}, []);
|
|
|
|
//fetch data from API
|
|
const { data: aspectsData } = useQuery(aspectQueryOptions(0, 10));
|
|
const { data: assessmentResult } = useQuery(getAssessmentResultByIdQueryOptions(assessmentId));
|
|
const { data: verifiedAssessmentResult } = useQuery(getVerifiedAssessmentResultByIdQueryOptions(assessmentId));
|
|
const { data: allAspectsScoreData } = useQuery(getAllAspectsAverageScore(assessmentId));
|
|
const { data: allSubAspectsScoreData } = useQuery(getAllSubAspectsAverageScore(assessmentId));
|
|
const { data: allVerifiedAspectsScoreData } = useQuery(getAllVerifiedAspectsAverageScore(assessmentId));
|
|
const { data: allVerifiedSubAspectsScoreData } = useQuery(getAllVerifiedSubAspectsAverageScore(assessmentId));
|
|
|
|
// Pastikan status tersedia
|
|
const assessmentStatus = assessmentResult?.statusAssessment;
|
|
|
|
const getAspectScore = (aspectId: string) => {
|
|
return allAspectsScoreData?.aspects?.find((score) => score.aspectId === aspectId)?.averageScore || undefined;
|
|
};
|
|
|
|
const getSubAspectScore = (subAspectId: string) => {
|
|
return allSubAspectsScoreData?.subAspects?.find((score) => score.subAspectId === subAspectId)?.averageScore || undefined;
|
|
};
|
|
|
|
const getVerifiedAspectScore = (aspectId: string, assessmentStatus: string) => {
|
|
if (assessmentStatus !== "selesai") return undefined;
|
|
return allVerifiedAspectsScoreData?.aspects?.find((score) => score.aspectId === aspectId)?.averageScore || undefined;
|
|
};
|
|
|
|
const getVerifiedSubAspectScore = (subAspectId: string, assessmentStatus: string) => {
|
|
if (assessmentStatus !== "selesai") return undefined;
|
|
return allVerifiedSubAspectsScoreData?.subAspects?.find((score) => score.subAspectId === subAspectId)?.averageScore || undefined;
|
|
};
|
|
|
|
const formatScore = (score: string | number | undefined) => {
|
|
if (score === null || score === undefined) return '0';
|
|
const parsedScore = typeof score === 'number' ? score : parseFloat(score || "NaN");
|
|
return !isNaN(parsedScore) ? parsedScore.toFixed(2) : '0';
|
|
};
|
|
|
|
// Total score
|
|
const totalScore = parseFloat(formatScore(assessmentResult?.assessmentsResult));
|
|
const totalVerifiedScore =
|
|
assessmentStatus === "selesai"
|
|
? parseFloat(formatScore(verifiedAssessmentResult?.verifiedAssessmentsResult))
|
|
: 0;
|
|
|
|
// Mengatur warna dan level maturitas berdasarkan skor
|
|
const getScoreStyleClass = (score: number | undefined, isBg: boolean = false) => {
|
|
if (score === undefined || score === null) return { color: 'grey' };
|
|
|
|
let colorVar = '--levelOne-color';
|
|
let descLevel = '1';
|
|
|
|
if (score >= 1.50 && score < 2.50) {
|
|
colorVar = '--levelTwo-color';
|
|
descLevel = '2';
|
|
} else if (score >= 2.50 && score < 3.50) {
|
|
colorVar = '--levelThree-color';
|
|
descLevel = '3';
|
|
} else if (score >= 3.50 && score < 4.50) {
|
|
colorVar = '--levelFour-color';
|
|
descLevel = '4';
|
|
} else if (score >= 4.50 && score <= 5) {
|
|
colorVar = '--levelFive-color';
|
|
descLevel = '5';
|
|
}
|
|
|
|
return isBg
|
|
? { backgroundColor: `var(${colorVar})`, descLevel }
|
|
: { color: `var(${colorVar})`, descLevel };
|
|
};
|
|
|
|
// Warna aspek
|
|
const aspectsColors = [
|
|
"#37DCCC",
|
|
"#FF8C8C",
|
|
"#51D0FD",
|
|
"#FEA350",
|
|
"#AD8AFC",
|
|
];
|
|
|
|
// Data diagram
|
|
const chartData = aspectsData?.data?.map((aspect, index) => ({
|
|
aspectName: aspect.name,
|
|
score: Number(formatScore(getAspectScore(aspect.id))),
|
|
fill: aspectsColors[index % aspectsColors.length],
|
|
})) || [];
|
|
|
|
const verifiedChartData =
|
|
assessmentStatus === "selesai"
|
|
? aspectsData?.data?.map((aspect, index) => ({
|
|
aspectName: aspect.name,
|
|
score: Number(formatScore(getVerifiedAspectScore(aspect.id, assessmentStatus))),
|
|
fill: aspectsColors[index % aspectsColors.length],
|
|
})) || []
|
|
: [];
|
|
|
|
const barChartData = aspectsData?.data?.flatMap((aspect) =>
|
|
aspect.subAspects.map((subAspect) => ({
|
|
subAspectName: subAspect.name,
|
|
score: Number(formatScore(getSubAspectScore(subAspect.id))),
|
|
fill: "#005BFF",
|
|
aspectId: aspect.id,
|
|
aspectName: aspect.name
|
|
}))
|
|
) || [];
|
|
|
|
const verifiedBarChartData =
|
|
assessmentStatus === "selesai"
|
|
? aspectsData?.data?.flatMap((aspect) =>
|
|
aspect.subAspects.map((subAspect) => ({
|
|
subAspectName: subAspect.name,
|
|
score: Number(formatScore(getVerifiedSubAspectScore(subAspect.id, assessmentStatus))),
|
|
fill: "#005BFF",
|
|
aspectId: aspect.id,
|
|
aspectName: aspect.name,
|
|
}))
|
|
) || []
|
|
: [];
|
|
|
|
const sortedBarChartData = barChartData.sort((a, b) => (a.aspectId ?? '').localeCompare(b.aspectId ?? ''));
|
|
const sortedVerifiedBarChartData = verifiedBarChartData.sort((a, b) => (a.aspectId ?? '').localeCompare(b.aspectId ?? ''));
|
|
|
|
const chartConfig =
|
|
assessmentStatus === "selesai"
|
|
? aspectsData?.data?.reduce((config, aspect, index) => {
|
|
config[aspect.name.toLowerCase()] = {
|
|
label: aspect.name,
|
|
color: aspectsColors[index % aspectsColors.length],
|
|
};
|
|
return config;
|
|
}, {} as ChartConfig) || {}
|
|
: {};
|
|
|
|
const barChartConfig =
|
|
assessmentStatus === "selesai"
|
|
? aspectsData?.data?.reduce((config, aspect, index) => {
|
|
aspect.subAspects.forEach((subAspect) => {
|
|
config[subAspect.name.toLowerCase()] = {
|
|
label: subAspect.name,
|
|
color: aspectsColors[index % aspectsColors.length],
|
|
};
|
|
});
|
|
return config;
|
|
}, {} as ChartConfig) || {}
|
|
: {};
|
|
|
|
// Dropdown State
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [selectedItem, setSelectedItem] = useState('Hasil Asesmen');
|
|
|
|
const handleDropdownToggle = () => {
|
|
setIsOpen((prev) => !prev);
|
|
};
|
|
|
|
const handleItemClick = () => {
|
|
setSelectedItem(prev =>
|
|
prev === 'Hasil Asesmen' ? 'Hasil Terverifikasi' : 'Hasil Asesmen'
|
|
);
|
|
setIsOpen(false);
|
|
};
|
|
|
|
// Pie Chart Component
|
|
function PieChartComponent({ chartData, totalScore, chartConfig }: { chartData: { aspectName: string, score: number, fill: string }[], totalScore: number, chartConfig: ChartConfig }) {
|
|
return (
|
|
<div className="flex flex-col w-full border-none">
|
|
<div className="flex-1 pb-0 w-72">
|
|
<ChartContainer
|
|
config={chartConfig}
|
|
className="-ml-6 -mb-6 lg:mb-6 aspect-square max-h-60 lg:max-h-64"
|
|
>
|
|
<PieChart>
|
|
<ChartTooltip
|
|
cursor={false}
|
|
content={<ChartTooltipContent hideLabel />}
|
|
/>
|
|
<Pie
|
|
data={chartData}
|
|
dataKey="score"
|
|
nameKey="aspectName"
|
|
innerRadius={50}
|
|
strokeWidth={5}
|
|
label={({ cx, cy, midAngle, innerRadius, outerRadius, index }) => {
|
|
const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
|
|
const x = cx + radius * Math.cos(-midAngle * (Math.PI / 180));
|
|
const y = cy + radius * Math.sin(-midAngle * (Math.PI / 180));
|
|
return (
|
|
<text
|
|
x={x}
|
|
y={y}
|
|
fill="black"
|
|
textAnchor="middle"
|
|
dominantBaseline="middle"
|
|
className="text-xs lg:text-md"
|
|
>
|
|
{chartData[index]?.score || ""}
|
|
</text>
|
|
);
|
|
}}
|
|
labelLine={false}
|
|
>
|
|
<Label
|
|
content={({ viewBox }) => {
|
|
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
|
return (
|
|
<text
|
|
x={viewBox.cx}
|
|
y={viewBox.cy}
|
|
textAnchor="middle"
|
|
dominantBaseline="middle"
|
|
>
|
|
<tspan
|
|
x={viewBox.cx}
|
|
y={viewBox.cy}
|
|
className="fill-foreground text-2xl lg:text-3xl font-bold"
|
|
>
|
|
{totalScore.toLocaleString()}
|
|
</tspan>
|
|
</text>
|
|
);
|
|
}
|
|
}}
|
|
/>
|
|
</Pie>
|
|
</PieChart>
|
|
</ChartContainer>
|
|
</div>
|
|
|
|
{/* Legend */}
|
|
<div className="flex flex-col lg:grid lg:grid-cols-2 gap-x-4 text-xs justify-center gap-2 lg:gap-0">
|
|
<div className="flex flex-col gap-2">
|
|
{chartData.map((entry, index) => (
|
|
<div key={index} className="flex items-center gap-2">
|
|
<span
|
|
className="pl-4 w-4 h-4"
|
|
style={{ backgroundColor: entry.fill }}
|
|
/>
|
|
<span className="font-medium whitespace-nowrap">{entry.aspectName}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
// Mengatur tampilan label sumbu X
|
|
const customizedAxisTick = (props: any) => {
|
|
const { x, y, payload } = props;
|
|
return (
|
|
<g transform={`translate(${x},${y})`}>
|
|
<text
|
|
x={0}
|
|
y={0}
|
|
dy={3}
|
|
textAnchor="end"
|
|
fill="#666"
|
|
transform="rotate(-90)"
|
|
className="lg:text-[0.6rem] text-[0.3rem]"
|
|
>
|
|
{payload.value.slice(0, 3)}
|
|
</text>
|
|
</g>
|
|
);
|
|
};
|
|
|
|
// Bar Chart Component
|
|
function BarChartComponent({ barChartData, barChartConfig }: { barChartData: { subAspectName: string, score: number, fill: string, aspectId: string, aspectName: string }[], barChartConfig: ChartConfig }) {
|
|
return (
|
|
<div className="w-full h-full">
|
|
<ChartContainer config={barChartConfig}>
|
|
<BarChart accessibilityLayer data={barChartData} margin={{ top: 5, left: -40 }}>
|
|
<CartesianGrid vertical={false} horizontal={true} />
|
|
<XAxis
|
|
dataKey="subAspectName"
|
|
tickLine={false}
|
|
tickMargin={0}
|
|
axisLine={false}
|
|
interval={0}
|
|
tick={customizedAxisTick}
|
|
/>
|
|
<YAxis
|
|
domain={[0, 5]}
|
|
tickCount={6}
|
|
/>
|
|
<ChartTooltip
|
|
cursor={false}
|
|
content={({ active, payload }) => {
|
|
if (active && payload && payload.length > 0) {
|
|
const { subAspectName, score } = payload[0].payload;
|
|
return (
|
|
<div className="tooltip bg-white p-1 rounded-md shadow-lg">
|
|
<p>{`${subAspectName} : ${score}`}</p>
|
|
</div>
|
|
);
|
|
}
|
|
return null;
|
|
}}
|
|
/>
|
|
<Bar dataKey="score" radius={2} fill="#007BFF" />
|
|
</BarChart>
|
|
</ChartContainer>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const handlePrintPDF = async () => {
|
|
const pdfContainer = document.getElementById("pdfContainer");
|
|
|
|
if (pdfContainer) {
|
|
// Sembunyikan elemen yang tidak ingin dicetak
|
|
const buttonPrint = document.getElementById("button-print");
|
|
const noPrint = document.getElementById("no-print");
|
|
if (buttonPrint) buttonPrint.style.visibility = 'hidden';
|
|
if (noPrint) noPrint.style.visibility = 'hidden';
|
|
|
|
const options = {
|
|
margin: [10, 10, 10, -220],
|
|
image: { type: 'jpeg', quality: 0.98 },
|
|
html2canvas: {
|
|
scale: 2,
|
|
width: 1510,
|
|
height: pdfContainer.scrollHeight,
|
|
ignoreElements: (element: { tagName: string; }) => {
|
|
// Abaikan elemen <header> dalam pdfContainer
|
|
return element.tagName === 'HEADER';
|
|
},
|
|
},
|
|
jsPDF: {
|
|
unit: 'pt',
|
|
format: 'a4',
|
|
orientation: 'portrait',
|
|
}
|
|
};
|
|
|
|
try {
|
|
const pdfBlob: Blob = await html2pdf()
|
|
.set(options)
|
|
.from(pdfContainer)
|
|
.toPdf()
|
|
.get('pdf')
|
|
.then((pdf: any) => {
|
|
pdf.setProperties({
|
|
title: 'Hasil_Asesemen_Level_Maturitas',
|
|
});
|
|
return pdf.output('blob');
|
|
});
|
|
|
|
const pdfURL = URL.createObjectURL(pdfBlob);
|
|
window.open(pdfURL, '_blank');
|
|
} catch (err) {
|
|
console.error("Error generating PDF:", err);
|
|
} finally {
|
|
// Tampilkan kembali elemen yang disembunyikan
|
|
if (buttonPrint) buttonPrint.style.visibility = 'visible';
|
|
if (noPrint) noPrint.style.visibility = 'visible';
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Card className="flex flex-row w-full h-full border-none shadow-none" id="pdfContainer">
|
|
<AppHeader
|
|
openNavbar={isLeftSidebarOpen}
|
|
toggle={toggleLeftSidebar}
|
|
toggleLeftSidebar={toggleLeftSidebar}
|
|
/>
|
|
{isMobile && (
|
|
<LeftSheet open={isLeftSidebarOpen} onOpenChange={(open) => setIsLeftSidebarOpen(open)}>
|
|
<LeftSheetContent className="h-full w-75 overflow-auto">
|
|
<ScrollArea className="h-full w-75 rounded-md p-2">
|
|
<Label className="text-gray-800 p-5 text-sm font-normal">Tingkatan Level Maturitas</Label>
|
|
<div className="flex flex-col w-64 h-fit border-none shadow-none -mt-6 pt-6">
|
|
<div className="flex flex-col mr-2 h-full">
|
|
<p className="font-bold mb-4 text-lg">Tingkatan Level Maturitas</p>
|
|
{[
|
|
{ level: 5, colorVar: '--levelFive-color', title: 'Implementasi Optimal', details: ['Otomatisasi', 'Terintegrasi', 'Membudaya'], textColor: 'white' },
|
|
{ level: 4, colorVar: '--levelFour-color', title: 'Implementasi Terkelola', details: ['Terorganisir', 'Review Berkala', 'Berkelanjutan'], textColor: 'white' },
|
|
{ level: 3, colorVar: '--levelThree-color', title: 'Implementasi Terdefinisi', details: ['Terorganisir', 'Konsisten', 'Review Berkala'], textColor: 'white' },
|
|
{ level: 2, colorVar: '--levelTwo-color', title: 'Implementasi Berulang', details: ['Terorganisir', 'Tidak Konsisten', 'Berulang'], textColor: 'white' },
|
|
{ level: 1, colorVar: '--levelOne-color', title: 'Implementasi Awal', details: ['Tidak Terukur', 'Tidak Konsisten', 'Risiko Tinggi'], textColor: 'white' }
|
|
].map(({ level, colorVar, title, details }) => (
|
|
<div key={level} className="flex flex-row h-full border-none">
|
|
<div
|
|
className="w-20 h-20 text-white font-medium text-lg flex justify-center items-center"
|
|
style={{ background: `var(${colorVar})` }}
|
|
>
|
|
<p className="text-center">Level {level}</p>
|
|
</div>
|
|
<div className="flex flex-col items-start justify-center p-2">
|
|
<p className="text-xs font-bold whitespace-nowrap">{title}</p>
|
|
{details.map((detail) => (
|
|
<p key={detail} className="text-xs">{detail}</p>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Total verified score */}
|
|
<div className="pt-14">
|
|
{selectedItem === 'Hasil Asesmen' ? (
|
|
<>
|
|
<div className="flex flex-row h-16 border-t font-semibold justify-between items-center gap-2 pl-8 pr-6 -ml-6">
|
|
<p className="text-lg">Nilai Maturitas</p>
|
|
<span className="text-xl">{totalScore}</span>
|
|
</div>
|
|
<div className="flex flex-row h-16 font-semibold justify-between items-center gap-2 pl-8 pr-6 text-white -ml-6" style={getScoreStyleClass(Number(totalScore), true)}>
|
|
<p className="text-lg">Level Maturitas</p>
|
|
<span className="ml-2 text-xl">{getScoreStyleClass(Number(totalScore), true).descLevel}</span>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<>
|
|
<div className="flex flex-row h-16 border-t font-semibold justify-between items-center gap-2 pl-8 pr-6 -ml-6">
|
|
<p className="text-lg">Nilai Maturitas</p>
|
|
<span className="text-xl">{totalVerifiedScore}</span>
|
|
</div>
|
|
<div className="flex flex-row h-16 font-semibold justify-between items-center gap-2 pl-8 pr-6 text-white -ml-6" style={getScoreStyleClass(Number(totalVerifiedScore), true)}>
|
|
<p className="text-lg">Level Maturitas</p>
|
|
<span className="ml-2 text-xl">{getScoreStyleClass(Number(totalVerifiedScore), true).descLevel}</span>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</ScrollArea>
|
|
</LeftSheetContent>
|
|
</LeftSheet>
|
|
)}
|
|
|
|
<div className="hidden md:block lg:flex flex-col w-fit h-fit border-r shadow-none -mt-6 pt-6">
|
|
<div className="flex flex-col mr-2 h-full">
|
|
<p className="font-bold mb-4 text-lg">Tingkatan Level Maturitas</p>
|
|
{[
|
|
{ level: 5, colorVar: '--levelFive-color', title: 'Implementasi Optimal', details: ['Otomatisasi', 'Terintegrasi', 'Membudaya'], textColor: 'white' },
|
|
{ level: 4, colorVar: '--levelFour-color', title: 'Implementasi Terkelola', details: ['Terorganisir', 'Review Berkala', 'Berkelanjutan'], textColor: 'white' },
|
|
{ level: 3, colorVar: '--levelThree-color', title: 'Implementasi Terdefinisi', details: ['Terorganisir', 'Konsisten', 'Review Berkala'], textColor: 'white' },
|
|
{ level: 2, colorVar: '--levelTwo-color', title: 'Implementasi Berulang', details: ['Terorganisir', 'Tidak Konsisten', 'Berulang'], textColor: 'white' },
|
|
{ level: 1, colorVar: '--levelOne-color', title: 'Implementasi Awal', details: ['Tidak Terukur', 'Tidak Konsisten', 'Risiko Tinggi'], textColor: 'white' }
|
|
].map(({ level, colorVar, title, details }) => (
|
|
<div key={level} className="flex flex-row h-full border-none">
|
|
<div
|
|
className="w-20 h-20 text-white font-medium text-lg flex justify-center items-center"
|
|
style={{ background: `var(${colorVar})` }}
|
|
>
|
|
<p className="text-center">Level {level}</p>
|
|
</div>
|
|
<div className="flex flex-col items-start justify-center p-2">
|
|
<p className="text-xs font-bold whitespace-nowrap">{title}</p>
|
|
{details.map((detail) => (
|
|
<p key={detail} className="text-xs">{detail}</p>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Total verified score */}
|
|
<div className="pt-14">
|
|
{selectedItem === 'Hasil Asesmen' ? (
|
|
<>
|
|
<div className="flex flex-row h-16 border-t font-semibold justify-between items-center gap-2 pl-8 pr-6 -ml-6">
|
|
<p className="text-lg">Nilai Maturitas</p>
|
|
<span className="text-xl">{totalScore}</span>
|
|
</div>
|
|
<div className="flex flex-row h-16 font-semibold justify-between items-center gap-2 pl-8 pr-6 text-white -ml-6" style={getScoreStyleClass(Number(totalScore), true)}>
|
|
<p className="text-lg">Level Maturitas</p>
|
|
<span className="ml-2 text-xl">{getScoreStyleClass(Number(totalScore), true).descLevel}</span>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<>
|
|
<div className="flex flex-row h-16 border-t font-semibold justify-between items-center gap-2 pl-8 pr-6 -ml-6">
|
|
<p className="text-lg">Nilai Maturitas</p>
|
|
<span className="text-xl">{totalVerifiedScore}</span>
|
|
</div>
|
|
<div className="flex flex-row h-16 font-semibold justify-between items-center gap-2 pl-8 pr-6 text-white -ml-6" style={getScoreStyleClass(Number(totalVerifiedScore), true)}>
|
|
<p className="text-lg">Level Maturitas</p>
|
|
<span className="ml-2 text-xl">{getScoreStyleClass(Number(totalVerifiedScore), true).descLevel}</span>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<Card className="flex flex-col w-screen h-fit border-none shadow-none -mt-6 lg:-mr-6 lg:-ml-0 -mx-3 lg:bg-stone-50 md:bg-white overflow-hidden">
|
|
<div className="flex flex-col w-full h-fit mb-4 justify-center items-start bg-white p-4 border-b">
|
|
{/* Konten Header */}
|
|
<div className="flex flex-col lg:flex-row justify-between items-center w-full">
|
|
{isSuperAdmin ? (
|
|
<div className="flex flex-col">
|
|
<button
|
|
className="flex items-center text-sm text-blue-600 gap-2 mb-2" id="no-print"
|
|
onClick={() => window.close()}
|
|
>
|
|
<TbChevronLeft size={20} className="mr-1" />
|
|
Kembali
|
|
</button>
|
|
<p className="text-md lg:text-2xl font-bold">Detail Hasil Asesmen</p>
|
|
<p className="text-xs text-muted-foreground">Kelola dan Pantau Semua Permohonan Asesmen Dengan Mudah</p>
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col">
|
|
<p className="text-md lg:text-2xl font-bold">Dasboard Hasil Tingkat Kematangan Keamanan Cyber</p>
|
|
<p className="text-xs text-muted-foreground">Kelola dan Pantau Semua Permohonan Asesmen Dengan Mudah</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex flex-row gap-2 mt-4 lg:mt-0" id="button-print">
|
|
<div className="flex">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger
|
|
className="text-black flex w-44 p-2 pl-4 rounded-lg text-sm items-start justify-between border outline-none ring-offset-0"
|
|
onClick={handleDropdownToggle}
|
|
>
|
|
{selectedItem}
|
|
{isOpen ? (
|
|
<TbChevronUp size={20} className="justify-center items-center" />
|
|
) : (
|
|
<TbChevronDown size={20} className="justify-center items-center" />
|
|
)}
|
|
</DropdownMenuTrigger>
|
|
{isOpen && (
|
|
<DropdownMenuContent className="bg-white text-black flex w-44 rounded-sm text-xs lg:text-sm items-start">
|
|
<DropdownMenuItem className="w-full" onClick={handleItemClick}>
|
|
{selectedItem === 'Hasil Asesmen' ? 'Hasil Terverifikasi' : 'Hasil Asesmen'}
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
)}
|
|
</DropdownMenu>
|
|
</div>
|
|
<button
|
|
onClick={() => handlePrintPDF()}
|
|
className="bg-blue-600 text-white flex w-fit py-2 px-4 rounded-lg text-xs lg:text-sm items-start justify-between outline-none ring-offset-0"
|
|
>
|
|
Cetak PDF
|
|
<TbFileTypePdf size={20} className="ml-2" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{isSuperAdmin &&
|
|
<Card className="flex flex-col h-full mb-6 justify-center items-start mx-4">
|
|
<div className="flex lg:flex-row flex-col text-xs h-full w-full justify-between p-6 gap-4 lg:gap-0">
|
|
<div className="flex flex-col gap-4">
|
|
<div>
|
|
<p className="text-muted-foreground">Nama Responden</p>
|
|
<p>{assessmentResult?.respondentName}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-muted-foreground">Posisi</p>
|
|
<p>{assessmentResult?.position}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-muted-foreground">Nama Pengguna</p>
|
|
<p>{assessmentResult?.username}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col gap-4">
|
|
<div>
|
|
<p className="text-muted-foreground">Nama Perusahaan</p>
|
|
<p>{assessmentResult?.companyName}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-muted-foreground">Pengalaman Kerja</p>
|
|
<p>{assessmentResult?.workExperience}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-muted-foreground">Email</p>
|
|
<p>{assessmentResult?.email}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col gap-4">
|
|
<div>
|
|
<p className="text-muted-foreground">No. HP</p>
|
|
<p>{assessmentResult?.phoneNumber}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-muted-foreground">Alamat</p>
|
|
<p>{assessmentResult?.address}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-muted-foreground">Tanggal Asesmen</p>
|
|
<p>
|
|
{assessmentResult?.assessmentDate ? (
|
|
new Intl.DateTimeFormat("id-ID", {
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
hour12: true,
|
|
})
|
|
.format(new Date(assessmentResult.assessmentDate))
|
|
.replace(/\./g, ':')
|
|
.replace('pukul ', '')
|
|
) : (
|
|
'N/A'
|
|
)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col gap-4">
|
|
<div>
|
|
<p className="text-muted-foreground">Status Asesmen</p>
|
|
<p>{assessmentResult?.statusAssessment}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
}
|
|
|
|
{/* Conditional rendering based on selectedItem */}
|
|
{selectedItem === 'Hasil Asesmen' ? (
|
|
<>
|
|
{/* Score Table */}
|
|
<Card className="flex flex-col h-fit my-2 mb-6 overflow-hidden lg:mx-4">
|
|
<p className="text-sm lg:text-lg font-bold p-2 border-b">Tabel Nilai Hasil Asesmen</p>
|
|
<table className="w-full table-fixed border-collapse border rounded-lg overflow-hidden">
|
|
<thead>
|
|
<tr>
|
|
{aspectsData?.data?.map((aspect) => (
|
|
<th
|
|
key={aspect.id}
|
|
colSpan={2}
|
|
className="text-start font-normal bg-white border border-gray-200 p-2 lg:p-4 w-1/5 whitespace-nowrap"
|
|
>
|
|
<div className="flex flex-col items-start">
|
|
<p className="text-[0.6rem] lg:text-sm text-black">{aspect.name}</p>
|
|
<span
|
|
className={clsx(
|
|
"text-sm lg:text-3xl font-bold",
|
|
)}
|
|
style={getScoreStyleClass(getAspectScore(aspect.id))}
|
|
>
|
|
{formatScore(getAspectScore(aspect.id))}
|
|
</span>
|
|
</div>
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{aspectsData && Array.from({ length: Math.max(...aspectsData.data.map(aspect => aspect.subAspects.length)) }).map((_, rowIndex) => (
|
|
<tr key={rowIndex} className={rowIndex % 2 === 0 ? "bg-slate-100" : "bg-white"}>
|
|
{aspectsData?.data?.map((aspect) => (
|
|
<React.Fragment key={aspect.id}>
|
|
{/* Sub-aspect Name Column (No Right Border) */}
|
|
<td className="text-[0.33rem] md:text-[0.5rem] lg:text-xs text-black p-1 py-0 lg:py-2 border-t border-l border-b border-gray-200 w-full text-left">
|
|
{aspect.subAspects[rowIndex]?.name || ""}
|
|
</td>
|
|
{/* Sub-aspect Score Column (No Left Border and w-fit for flexible width) */}
|
|
<td className="text-[0.4rem] lg:text-sm font-bold text-right p-1 lg:p-2 border-t border-b border-r border-gray-200 w-fit">
|
|
{aspect.subAspects[rowIndex] ? formatScore(getSubAspectScore(aspect.subAspects[rowIndex].id)) : ""}
|
|
</td>
|
|
</React.Fragment>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</Card>
|
|
</>
|
|
) : (
|
|
<>
|
|
{/* Verified Result Table */}
|
|
<Card className="flex flex-col h-fit my-2 mb-6 overflow-hidden border-y lg:mx-4">
|
|
<p className="text-sm lg:text-lg font-bold p-2 border-b">Tabel Nilai Hasil Asesmen Terverifikasi</p>
|
|
<table className="w-full table-fixed border-collapse border rounded-lg overflow-hidden">
|
|
<thead>
|
|
<tr>
|
|
{aspectsData?.data?.map((aspect) => (
|
|
<th
|
|
key={aspect.id}
|
|
colSpan={2}
|
|
className="text-start font-normal bg-white border border-gray-200 p-2 lg:p-4 w-1/5 whitespace-nowrap"
|
|
>
|
|
<div className="flex flex-col items-start">
|
|
<p className="text-[0.6rem] lg:text-sm text-black">{aspect.name}</p>
|
|
<span
|
|
className={clsx(
|
|
"text-sm lg:text-3xl font-bold",
|
|
)}
|
|
style={getScoreStyleClass(getVerifiedAspectScore(aspect.id, assessmentStatus ?? ''))}
|
|
>
|
|
{formatScore(getVerifiedAspectScore(aspect.id, assessmentStatus ?? ''))}
|
|
</span>
|
|
</div>
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{aspectsData && Array.from({ length: Math.max(...aspectsData.data.map(aspect => aspect.subAspects.length)) }).map((_, rowIndex) => (
|
|
<tr key={rowIndex} className={rowIndex % 2 === 0 ? "bg-slate-100" : "bg-white"}>
|
|
{aspectsData?.data?.map((aspect) => (
|
|
<React.Fragment key={aspect.id}>
|
|
{/* Sub-aspect Name Column (No Right Border) */}
|
|
<td className="text-[0.33rem] md:text-[0.5rem] lg:text-xs text-black p-1 py-0 lg:py-2 border-t border-l border-b border-gray-200 w-full text-left">
|
|
{aspect.subAspects[rowIndex]?.name || ""}
|
|
</td>
|
|
{/* Sub-aspect Score Column (No Left Border and w-fit for flexible width) */}
|
|
<td className="text-[0.4rem] lg:text-sm font-bold text-right p-1 lg:p-2 border-t border-b border-r border-gray-200 w-fit">
|
|
{aspect.subAspects[rowIndex] ? formatScore(getVerifiedSubAspectScore(aspect.subAspects[rowIndex].id, assessmentStatus ?? '')) : ""}
|
|
</td>
|
|
</React.Fragment>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</Card>
|
|
</>
|
|
)}
|
|
|
|
<Card className="flex flex-col lg:flex-row gap-4 border-none shadow-none lg:mx-4 bg-transparent mb-4">
|
|
{/* Bar Chart */}
|
|
{selectedItem === 'Hasil Asesmen' ? (
|
|
<>
|
|
<Card className="w-full">
|
|
<CardHeader className="items-start">
|
|
<CardTitle className="text-sm lg:text-lg">Diagram Nilai Hasil Asesmen</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<BarChartComponent barChartData={sortedBarChartData} barChartConfig={barChartConfig} />
|
|
</CardContent>
|
|
</Card>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Card className="w-full">
|
|
<CardHeader className="items-start">
|
|
<CardTitle className="text-sm lg:text-lg">Diagram Nilai Hasil Asesmen Terverifikasi</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<BarChartComponent barChartData={sortedVerifiedBarChartData} barChartConfig={barChartConfig} />
|
|
</CardContent>
|
|
</Card>
|
|
</>
|
|
)}
|
|
|
|
{/* Pie Chart */}
|
|
{selectedItem === 'Hasil Asesmen' ? (
|
|
<Card className="flex flex-col w-full lg:w-64">
|
|
<CardContent>
|
|
<PieChartComponent
|
|
chartData={chartData}
|
|
totalScore={totalScore}
|
|
chartConfig={chartConfig}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<Card className="flex flex-col w-full lg:w-64">
|
|
<CardContent>
|
|
<PieChartComponent
|
|
chartData={verifiedChartData}
|
|
totalScore={totalVerifiedScore}
|
|
chartConfig={chartConfig}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</Card>
|
|
</Card>
|
|
</Card>
|
|
);
|
|
}
|