jgi-dashboard/src/app/(dashboard)/absensi/page.tsx

330 lines
16 KiB
TypeScript

"use client"
import { useAppSelector } from "@/lib/hooks";
import { useGetAttendanceRangeQuery, useGetMonthlyAttendanceQuery, useGetOrganizationAttendanceQuery } from "@/services/api";
import { ExclamationCircleIcon } from "@heroicons/react/24/outline";
import { ComposedChart, Bar, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell, BarChart } from 'recharts';
import { formatDate } from "date-fns";
import { PageLoader } from "@/components/Loader";
import {CustomLegendHorizontal, CustomLegendVertical} from "@/components/CustomLegendPie";
import { useLocale } from "@/lib/hooks/useLocale";
export default function AbsensiPage() {
const { t } = useLocale();
const filter = useAppSelector(state => state.filter.filter);
const {data: attendanceSummary, isLoading: isLoadingAttendanceSummary, isFetching: isFetchingAttendanceSummary} = useGetOrganizationAttendanceQuery(filter);
const {data: attendanceRangeStaff, isLoading: isLoadingAttendanceRangeStaff, isFetching: isFetchingAttendanceRangeStaff} = useGetAttendanceRangeQuery({
...filter,
job_name: "STAFF"
});
const {data: attendanceRangeNonStaff, isLoading: isLoadingAttendanceRangeNonStaff, isFetching: isFetchingAttendanceRangeNonStaff} = useGetAttendanceRangeQuery({
...filter,
job_name: "NON-STAFF"
});
const {data: attendanceRangeHarvester, isLoading: isLoadingAttendanceRangeHarvester, isFetching: isFetchingAttendanceRangeHarvester} = useGetAttendanceRangeQuery({
...filter,
job_name: "HARVESTER"
});
const {data: attendanceRangeMaintenance, isLoading: isLoadingAttendanceRangeMaintenance, isFetching: isFetchingAttendanceRangeMaintenance} = useGetAttendanceRangeQuery({
...filter,
job_name: "MAINTENANCE"
});
const {data: montlyAttendance, isLoading: isLoadingMonthlyAttendance, isFetching: isFetchingMonthlyAttendance} = useGetMonthlyAttendanceQuery(filter);
// Combine all loading states (both initial loading and refetching)
const isLoading = isLoadingAttendanceSummary || isLoadingAttendanceRangeStaff ||
isLoadingAttendanceRangeNonStaff || isLoadingAttendanceRangeHarvester ||
isLoadingAttendanceRangeMaintenance || isLoadingMonthlyAttendance ||
isFetchingAttendanceSummary || isFetchingAttendanceRangeStaff ||
isFetchingAttendanceRangeNonStaff || isFetchingAttendanceRangeHarvester ||
isFetchingAttendanceRangeMaintenance || isFetchingMonthlyAttendance;
// Show loader while any data is loading
if (isLoading) {
return <PageLoader text={t('attendance.loadingData')} />;
}
return (
<div className="grid grid-cols-12 gap-4">
<div className="col-span-4 bg-white py-4 px-4 rounded-lg max-h-[640px] flex flex-col">
<div className="text-xl font-bold">{t('attendance.kehadiranStaff')}</div>
<div className="flex-1 flex">
{attendanceRangeStaff ? (
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={attendanceRangeStaff.map(e => ({name: e.range, value: e.count, label: `${e.range} HK`}))}
cx="50%"
cy="50%"
innerRadius="40%"
outerRadius="70%"
paddingAngle={0}
dataKey="value"
// label={({name, value}) => `${name} HK`}
labelLine={false}
>
{attendanceRangeStaff.map((entry, index) => {
const colors = ["#E65550","#8A5FB1","#69C9C9","#F08431","#F6C421", "#3B82F6"];
return <Cell key={`cell-${index}`} fill={colors[index % colors.length]} />;
})}
</Pie>
<Tooltip
formatter={(value, name) => [`${Number(value).toLocaleString('id-ID')} ${t('common.employees')}`, name]}
/>
<Legend
content={<CustomLegendHorizontal payload={attendanceRangeStaff.map(e => ({name: e.range, value: e.count, label: `${e.range} HK`}))}/>}
verticalAlign="bottom"
height={36}
iconType="circle"
/>
</PieChart>
</ResponsiveContainer>
) : (
<div className="flex-1 flex items-center justify-center text-gray-500">
{t('common.dataNotAvailable')}
</div>
)}
</div>
</div>
<div className="col-span-4 bg-white rounded-lg py-4 px-4 max-h-[640px] flex flex-col">
<div className="text-xl font-bold">{t('attendance.kehadiranNonStaff')}</div>
<div className="flex-1 flex">
{attendanceRangeNonStaff ? (
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={attendanceRangeNonStaff.map(e => ({name: e.range, value: e.count, label: `${e.range} HK`}))}
cx="50%"
cy="50%"
innerRadius="40%"
outerRadius="70%"
paddingAngle={0}
dataKey="value"
// label={({name, value}) => `${name} HK`}
labelLine={false}
>
{attendanceRangeNonStaff.map((entry, index) => {
const colors = ["#E65550","#8A5FB1","#69C9C9","#F08431","#F6C421", "#3B82F6"];
return <Cell key={`cell-${index}`} fill={colors[index % colors.length]} />;
})}
</Pie>
<Tooltip
formatter={(value, name) => [`${Number(value).toLocaleString('id-ID')} ${t('common.employees')}`, name]}
/>
<Legend
content={<CustomLegendHorizontal payload={attendanceRangeNonStaff.map(e => ({name: e.range, value: Number(e.count).toLocaleString('id-ID'), label: `${e.range} HK`}))}/>}
verticalAlign="bottom"
height={36}
iconType="circle"
/>
</PieChart>
</ResponsiveContainer>
) : (
<div className="flex-1 flex items-center justify-center text-gray-500">
{t('common.dataNotAvailable')}
</div>
)}
</div>
</div>
<div className="col-span-4 max-h-[640px] flex flex-col gap-4">
<div className="bg-white rounded-lg py-4 px-4 flex flex-col flex-1 min-h-[300px]">
<div className="text-xl font-bold">{t('attendance.kehadiranPemanen')}</div>
<div className="flex-1 flex min-h-[250px]">
{attendanceRangeHarvester ? (
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={attendanceRangeHarvester.map(e => ({name: e.range, value: e.count, label: `${e.range} HK`}))}
cx="50%"
cy="50%"
innerRadius="65%"
outerRadius="90%"
paddingAngle={0}
dataKey="value"
// label={({name, value}) => `${name} HK`}
labelLine={false}
>
{attendanceRangeHarvester.map((entry, index) => {
const colors = ["#E65550","#8A5FB1","#69C9C9","#F08431","#F6C421","#3B82F6"];
return <Cell key={`cell-${index}`} fill={colors[index % colors.length]} />;
})}
</Pie>
<Tooltip
formatter={(value, name) => [`${value} ${t('common.employees')}`, name]}
/>
<Legend
content={<CustomLegendVertical payload={attendanceRangeHarvester.map(e => ({name: e.range, value: e.count, label: `${e.range} HK`}))}/>}
verticalAlign="middle"
align="right"
layout="vertical"
iconType="circle"
wrapperStyle={{ paddingLeft: '20px' }}
/>
</PieChart>
</ResponsiveContainer>
) : (
<div className="flex-1 flex items-center justify-center text-gray-500">
{t('common.dataNotAvailable')}
</div>
)}
</div>
</div>
<div className="bg-white rounded-lg py-4 px-4 flex flex-col flex-1 min-h-[300px]">
<div className="text-xl font-bold">{t('attendance.kehadiranPerawatan')}</div>
<div className="flex-1 flex min-h-[250px]">
{attendanceRangeMaintenance ? (
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={attendanceRangeMaintenance.map(e => ({name: e.range, value: e.count, label: `${e.range} HK`}))}
cx="50%"
cy="50%"
innerRadius="65%"
outerRadius="90%"
paddingAngle={0}
dataKey="value"
// label={({name, value}) => `${name} HK`}
labelLine={false}
>
{attendanceRangeMaintenance.map((entry, index) => {
const colors = ["#E65550","#8A5FB1","#69C9C9","#F08431","#F6C421","#3B82F6"];
return <Cell key={`cell-${index}`} fill={colors[index % colors.length]} />;
})}
</Pie>
<Tooltip
formatter={(value, name) => [`${Number(value).toLocaleString('id-ID')} ${t('common.employees')}`, name]}
/>
<Legend
content={<CustomLegendVertical payload={attendanceRangeMaintenance.map(e => ({name: e.range, value: Number(e.count).toLocaleString('id-ID'), label: `${e.range} HK`}))}/>}
verticalAlign="middle"
align="right"
layout="vertical"
iconType="circle"
wrapperStyle={{ paddingLeft: '20px' }}
/>
</PieChart>
</ResponsiveContainer>
) : (
<div className="flex-1 flex items-center justify-center text-gray-500">
{t('common.dataNotAvailable')}
</div>
)}
</div>
</div>
</div>
<div className="col-span-8 bg-white py-4 pl-4 rounded-lg max-h-[420px] flex flex-col">
<div className="text-xl font-bold">{t('attendance.dataKehadiranKaryawan')}</div>
<div className="flex-1 min-h-[300px]">
{attendanceSummary ? (
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={attendanceSummary.map(item => ({
...item,
countPercent: item.workdays > 0 ? (item.count / item.workdays) * 100 : 0,
absentPercent: item.workdays > 0 ? (item.absent / item.workdays) * 100 : 0
})).sort((a, b) => b.countPercent - a.countPercent)}
margin={{ top: 20, right: 30, left: 20, bottom: 5 }}
>
{/* <CartesianGrid strokeDasharray="3 3" /> */}
<XAxis
dataKey="organization_code"
tick={{ fontSize: 12 }}
/>
<YAxis
tickFormatter={(value) => `${value}%`}
domain={[0, 100]}
hide
/>
<Tooltip
formatter={(value, name, props) => {
const orgData = attendanceSummary.find(e => e.organization_code === props.payload.organization_code);
if (name === t('attendance.hadir')) {
return [`${Number(orgData?.count || 0).toLocaleString('id-ID')} ${t('common.hariKerja')}`, t('attendance.hadir')];
} else {
return [`${Number(orgData?.absent || 0).toLocaleString('id-ID')} ${t('common.hariKerja')}`, t('attendance.tidakHadir')];
}
}}
labelFormatter={(label) => {
const orgData = attendanceSummary.find(e => e.organization_code === label);
const percentage = Math.round(orgData?.count! / orgData?.workdays! * 100);
return `${orgData?.organization_name} (${percentage}%)`;
}}
/>
<Legend />
<Bar
dataKey="countPercent"
stackId="attendance"
fill="#2385DE"
name={t('attendance.hadir')}
/>
<Bar
dataKey="absentPercent"
stackId="attendance"
fill="#F7B500"
name={t('attendance.tidakHadir')}
/>
</BarChart>
</ResponsiveContainer>
) : (
<div className="flex-1 flex items-center justify-center text-gray-500">
{t('common.dataNotAvailable')}
</div>
)}
</div>
</div>
<div className="col-span-4 bg-white rounded-lg py-4 pl-4 max-h-[420px] flex flex-col">
<div className="text-xl font-bold">{t('attendance.dataAbsensiPerbulan')}</div>
<div className="flex-1 min-h-[300px]">
{montlyAttendance && (
<ResponsiveContainer width="100%" height="100%">
<ComposedChart
data={montlyAttendance.map(item => ({
...item,
percentage: item.workdays > 0 ? Math.round((item.count / item.workdays) * 100) : 0,
month: formatDate(new Date(item.date), "MMM")
}))}
margin={{ top: 20, right: 30, left: 20, bottom: 5 }}
>
{/* <CartesianGrid strokeDasharray="3 3" /> */}
<XAxis
dataKey="month"
tick={{ fontSize: 12 }}
/>
<YAxis
yAxisId="left"
tick={{ fontSize: 12 }}
label={{ value: t('common.jumlah'), angle: -90, position: 'insideLeft' }}
/>
<YAxis
yAxisId="right"
orientation="right"
tick={{ fontSize: 12 }}
label={{ value: t('attendance.persentase'), angle: 90, position: 'insideRight' }}
/>
<Tooltip
labelFormatter={(value, payload) => {
if (payload && payload[0]) {
return formatDate(new Date(payload[0].payload.date), "MMMM yyyy");
}
return value;
}}
formatter={(value, name) => {
if (name === t('attendance.persentase')) {
return [`${value}%`, name];
}
return [value, name];
}}
/>
<Legend />
<Bar yAxisId="left" dataKey="count" fill="#F7CAA9" name={t('attendance.kehadiran')} />
<Bar yAxisId="left" dataKey="workdays" fill="#2385DE" name={t('attendance.mandays')} />
<Line yAxisId="right" type="monotone" dataKey="percentage" stroke="#10B981" strokeWidth={3} name={t('attendance.persentase')} />
</ComposedChart>
</ResponsiveContainer>
)}
</div>
</div>
</div>
);
}