330 lines
16 KiB
TypeScript
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>
|
|
);
|
|
} |