This commit is contained in:
Muhammad Eko 2025-10-16 14:24:31 +07:00
parent 7ca52e1092
commit b02e84b48b
8 changed files with 215 additions and 63 deletions

View File

@ -311,9 +311,9 @@ export default function AbsensiPage() {
}} }}
formatter={(value, name) => { formatter={(value, name) => {
if (name === t('attendance.persentase')) { if (name === t('attendance.persentase')) {
return [`${value}%`, name]; return [`${Number(value).toLocaleString('id-ID', {maximumFractionDigits: 2})}%`, name];
} }
return [value, name]; return [`${Number(value).toLocaleString('id-ID')} ${t('common.hariKerja')}`, name];
}} }}
/> />
<Legend /> <Legend />

View File

@ -110,6 +110,12 @@ export default function HrcostPage() {
value: hrCost.total_bpjs_ks, value: hrCost.total_bpjs_ks,
valueInMillions: hrCost.total_bpjs_ks / 1000000000, valueInMillions: hrCost.total_bpjs_ks / 1000000000,
color: COLORS[2] color: COLORS[2]
},
{
name: t('hrcost.thr'),
value: hrCost.total_thr,
valueInMillions: hrCost.total_thr / 1000000000,
color: COLORS[3]
} }
] : []; ] : [];

View File

@ -41,6 +41,18 @@ export default function ProductivityPage() {
const totalTonnageAge = productivityByAge?.reduce((sum, item) => sum + (item.tonnage / 1000), 0) || 0; const totalTonnageAge = productivityByAge?.reduce((sum, item) => sum + (item.tonnage / 1000), 0) || 0;
const totalTonnageTenure = productivityByTenure?.reduce((sum, item) => sum + (item.tonnage / 1000), 0) || 0; const totalTonnageTenure = productivityByTenure?.reduce((sum, item) => sum + (item.tonnage / 1000), 0) || 0;
// Calculate number of days between start_date and end_date
const calculateDays = (startDate: string, endDate: string) => {
const start = new Date(startDate);
const end = new Date(endDate);
const timeDiff = end.getTime() - start.getTime();
const daysDiff = Math.ceil(timeDiff / (1000 * 3600 * 24)) + 1; // +1 to include both start and end dates
return daysDiff;
};
const filterDays = filter.start_date && filter.end_date ?
calculateDays(filter.start_date, filter.end_date) : 1;
// Prepare data for pie charts (convert kg to tons) // Prepare data for pie charts (convert kg to tons)
const ageData = productivityByAge?.map(item => ({ const ageData = productivityByAge?.map(item => ({
name: item.age_range, name: item.age_range,
@ -54,29 +66,36 @@ export default function ProductivityPage() {
percentage: totalTonnageTenure > 0 ? (((item.tonnage / 1000) / totalTonnageTenure) * 100).toFixed(1) : 0 percentage: totalTonnageTenure > 0 ? (((item.tonnage / 1000) / totalTonnageTenure) * 100).toFixed(1) : 0
})) || []; })) || [];
// Prepare data for charts (convert kg to tons and calculate ratio) // Prepare data for charts (convert kg to tons and calculate ratio per employee per day)
const regionData = productivityByRegion?.map(item => ({ const regionData = productivityByRegion?.map(item => ({
...item, ...item,
tonnage: item.tonnage / 1000, // Convert kg to tons tonnage: item.tonnage / 1000, // Convert kg to tons
ratioPerEmployee: item.count > 0 ? (item.tonnage / 1000) / item.count : 0 ratioPerEmployee: item.count > 0 && filterDays > 0 ?
((item.tonnage / 1000) / item.count) / filterDays : 0
})).sort((a, b) => b.ratioPerEmployee - a.ratioPerEmployee) || []; // Sort by ratio descending })).sort((a, b) => b.ratioPerEmployee - a.ratioPerEmployee) || []; // Sort by ratio descending
const ageBarData = productivityByAge?.map(item => ({ const ageBarData = productivityByAge?.map(item => ({
...item, ...item,
tonnage: item.tonnage / 1000, // Convert kg to tons tonnage: item.tonnage / 1000, // Convert kg to tons
ratioPerEmployee: item.count > 0 ? (item.tonnage / 1000) / item.count : 0 ratioPerEmployee: item.count > 0 && filterDays > 0 ?
((item.tonnage / 1000) / item.count) / filterDays : 0
})).sort((a, b) => b.ratioPerEmployee - a.ratioPerEmployee) || []; // Sort by ratio descending })).sort((a, b) => b.ratioPerEmployee - a.ratioPerEmployee) || []; // Sort by ratio descending
const tenureBarData = productivityByTenure?.map(item => ({ const tenureBarData = productivityByTenure?.map(item => ({
...item, ...item,
tonnage: item.tonnage / 1000, // Convert kg to tons tonnage: item.tonnage / 1000, // Convert kg to tons
ratioPerEmployee: item.count > 0 ? (item.tonnage / 1000) / item.count : 0 ratioPerEmployee: item.count > 0 && filterDays > 0 ?
((item.tonnage / 1000) / item.count) / filterDays : 0
})).sort((a, b) => b.ratioPerEmployee - a.ratioPerEmployee) || []; // Sort by ratio descending })).sort((a, b) => b.ratioPerEmployee - a.ratioPerEmployee) || []; // Sort by ratio descending
// Prepare data for tonnage harvest group employee pie chart // Prepare data for tonnage harvest group employee pie chart
const tonnageGroupData = tonnageHarvestGroupEmployee?.map(item => { const tonnageGroupData = tonnageHarvestGroupEmployee?.map(item => {
// Parse tonnage range from kg format (e.g., "0.0 - 20499.9") and convert to tons // Parse tonnage range from kg format (e.g., "0.0 - 20499.9") and convert to tons
const parseRange = (range: string) => { const parseRange = (range: string) => {
if (!range || range.endsWith("+")){
return (Number(range.replace("+", "")) / 1000).toFixed(1) + "+ ton"
}
const parts = range.split(' - '); const parts = range.split(' - ');
if (parts.length === 2) { if (parts.length === 2) {
const min = parseFloat(parts[0]) / 1000; // Convert kg to tons const min = parseFloat(parts[0]) / 1000; // Convert kg to tons
@ -243,13 +262,28 @@ export default function ProductivityPage() {
{tonnageGroupData && tonnageGroupData.length > 0 ? ( {tonnageGroupData && tonnageGroupData.length > 0 ? (
<ResponsiveContainer width="100%" height={300}> <ResponsiveContainer width="100%" height={300}>
<PieChart> <PieChart>
<Legend
verticalAlign="middle"
align="right"
layout="vertical"
iconType="circle"
wrapperStyle={{ paddingLeft: '20px' }}
formatter={(value, entry) => {
const totalEmployees = tonnageGroupData.reduce((sum, item) => sum + item.value, 0);
const calculatedPercentage = totalEmployees > 0 ? ((entry.payload?.value / totalEmployees) * 100).toFixed(2) : 0;
return (
<span style={{ color: entry.color }} className="z-0">
{value}: {entry.payload?.value?.toLocaleString('id-ID', { maximumFractionDigits: 2 })} {t('common.ton')} ({calculatedPercentage}%)
</span>
)
}}/>
<Pie <Pie
data={tonnageGroupData} data={tonnageGroupData}
cx="50%" cx="50%"
cy="50%" cy="50%"
labelLine={false} labelLine={false}
outerRadius={120} outerRadius={90}
innerRadius={80} innerRadius={60}
fill="#8884d8" fill="#8884d8"
dataKey="value" dataKey="value"
> >
@ -265,14 +299,23 @@ export default function ProductivityPage() {
labelFormatter={(label, payload) => { labelFormatter={(label, payload) => {
return `${t('productivity.rangeTonase')}: ${label}`; return `${t('productivity.rangeTonase')}: ${label}`;
}} }}
content={({ active, payload, label }) => {
if (active && payload && payload.length) {
const data = payload[0].payload;
const totalEmployees = tonnageGroupData.reduce((sum, item) => sum + item.value, 0);
const calculatedPercentage = totalEmployees > 0 ? ((data.value / totalEmployees) * 100).toFixed(2) : 0;
return (
<div className="bg-white p-3 border rounded shadow z-10">
<p className="font-semibold">{`${t('productivity.rangeTonase')}: ${payload[0].name}`}</p>
<p className="text-blue-600">{`${t('common.karyawan')}: ${data.value.toLocaleString('id-ID')} ${t('common.orang')}`}</p>
<p className="text-gray-600">{`${t('productivity.persentase')}: ${calculatedPercentage}%`}</p>
</div>
);
}
return null;
}}
/> />
<Legend
verticalAlign="middle"
align="right"
layout="vertical"
iconType="circle"
wrapperStyle={{ paddingLeft: '20px' }}
/>
</PieChart> </PieChart>
</ResponsiveContainer> </ResponsiveContainer>
) : ( ) : (
@ -287,12 +330,28 @@ export default function ProductivityPage() {
<div className="h-80"> <div className="h-80">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<PieChart> <PieChart>
<Legend
verticalAlign="middle"
align="right"
layout="vertical"
iconSize={12}
wrapperStyle={{ paddingLeft: '10px' }}
formatter={(value, entry) => {
const totalEmployees = employeeSalaryData.reduce((sum, item) => sum + item.count, 0);
const calculatedPercentage = totalEmployees > 0 ? (((entry.payload as any).count / totalEmployees) * 100).toFixed(2) : 0;
return (
<span style={{ color: entry.color }}>
{value}: {entry.payload?.value?.toLocaleString('id-ID', { maximumFractionDigits: 2 })} {t('common.ton')} ({calculatedPercentage}%)
</span>
)
}}
/>
<Pie <Pie
data={employeeSalaryData} data={employeeSalaryData}
cx="50%" cx="50%"
cy="50%" cy="50%"
outerRadius={"80%"} outerRadius={90}
innerRadius={"55%"} innerRadius={60}
fill="#8884d8" fill="#8884d8"
dataKey="value" dataKey="value"
// label={({ name, percentage }) => `${name}: ${percentage}%`} // label={({ name, percentage }) => `${name}: ${percentage}%`}
@ -311,30 +370,20 @@ export default function ProductivityPage() {
content={({ active, payload, label }) => { content={({ active, payload, label }) => {
if (active && payload && payload.length) { if (active && payload && payload.length) {
const data = payload[0].payload; const data = payload[0].payload;
const totalEmployees = employeeSalaryData.reduce((sum, item) => sum + item.count, 0);
const calculatedPercentage = totalEmployees > 0 ? ((data.count / totalEmployees) * 100).toFixed(2) : 0;
return ( return (
<div className="bg-white p-3 border rounded shadow"> <div className="bg-white p-3 border rounded shadow">
<p className="font-semibold">{`${t('productivity.gaji')}: ${payload[0].name}`}</p> <p className="font-semibold">{`${t('productivity.gaji')}: ${payload[0].name}`}</p>
<p className="text-blue-600">{`${t('productivity.output')}: ${data.value.toLocaleString('id-ID')} ${t('common.ton')}`}</p> <p className="text-blue-600">{`${t('productivity.output')}: ${data.value.toLocaleString('id-ID')} ${t('common.ton')}`}</p>
<p className="text-green-600">{`${t('common.karyawan')}: ${data.count.toLocaleString('id-ID')} ${t('common.orang')}`}</p> <p className="text-green-600">{`${t('common.karyawan')}: ${data.count.toLocaleString('id-ID')} ${t('common.orang')}`}</p>
<p className="text-gray-600">{`${t('productivity.persentase')}: ${data.percentage}%`}</p> <p className="text-gray-600">{`${t('productivity.persentase')}: ${calculatedPercentage}%`}</p>
</div> </div>
); );
} }
return null; return null;
}} }}
/> />
<Legend
verticalAlign="middle"
align="right"
layout="vertical"
iconSize={12}
wrapperStyle={{ paddingLeft: '20px' }}
formatter={(value, entry) => (
<span style={{ color: entry.color }}>
{value}: {entry.payload?.value?.toLocaleString('id-ID')} {t('common.ton')} ({(entry.payload as any)?.percentage}%)
</span>
)}
/>
</PieChart> </PieChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>

View File

@ -2,7 +2,7 @@
import { useAppSelector } from "@/lib/hooks"; import { useAppSelector } from "@/lib/hooks";
import { useDimensions } from "@/lib/hooks/dimension"; import { useDimensions } from "@/lib/hooks/dimension";
import { useLocale } from "@/lib/hooks/useLocale"; import { useLocale } from "@/lib/hooks/useLocale";
import { useGetEmployeeMovementQuery, useGetMppRecruitmentQuery, useGetResignCategoryQuery, useGetResignReasonQuery, useGetResignSummaryQuery, useGetResignTypeQuery } from "@/services/api"; import { useGetEmployeeMovementQuery, useGetMppRecruitmentQuery, useGetResignCategoryQuery, useGetResignReasonQuery, useGetResignSummaryQuery, useGetResignTypeQuery, useLazyGetMonthlyEmployeeQuery } from "@/services/api";
import { BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"; import { BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts";
import { Loader, LucideLoader2 } from "lucide-react"; import { Loader, LucideLoader2 } from "lucide-react";
import React from "react"; import React from "react";
@ -24,10 +24,17 @@ export default function TurnoverPage() {
const {data: employeeMovement, isLoading: isLoadingEmployeeMovement, isFetching: isFetchingEmployeeMovement} = useGetEmployeeMovementQuery(filter); const {data: employeeMovement, isLoading: isLoadingEmployeeMovement, isFetching: isFetchingEmployeeMovement} = useGetEmployeeMovementQuery(filter);
const {data: mppRecruitment, isLoading: isLoadingMppRecruitment, isFetching: isFetchingMppRecruitment} = useGetMppRecruitmentQuery(filter); const {data: mppRecruitment, isLoading: isLoadingMppRecruitment, isFetching: isFetchingMppRecruitment} = useGetMppRecruitmentQuery(filter);
// Lazy query for monthly employee data to calculate active employees
const [getMonthlyEmployee, {isLoading: isLoadingActiveEmployees}] = useLazyGetMonthlyEmployeeQuery();
// State for active employee data by organization
const [activeEmployeesByOrg, setActiveEmployeesByOrg] = React.useState<{[key: string]: number}>({});
// Combine all loading states (both initial loading and refetching) // Combine all loading states (both initial loading and refetching)
const isLoading = isLoadingResignSummary || isLoadingResignType || const isLoading = isLoadingResignSummary || isLoadingResignType ||
isLoadingResignCategory || isLoadingResignReason || isLoadingResignCategory || isLoadingResignReason ||
isLoadingEmployeeMovement || isLoadingMppRecruitment || isLoadingEmployeeMovement || isLoadingMppRecruitment ||
isLoadingActiveEmployees ||
isFetchingResignSummary || isFetchingResignType || isFetchingResignSummary || isFetchingResignType ||
isFetchingResignCategory || isFetchingResignReason || isFetchingResignCategory || isFetchingResignReason ||
isFetchingEmployeeMovement || isFetchingMppRecruitment; isFetchingEmployeeMovement || isFetchingMppRecruitment;
@ -41,7 +48,8 @@ export default function TurnoverPage() {
const [totalRecruitment, setTotalRecruitment] = React.useState(0); const [totalRecruitment, setTotalRecruitment] = React.useState(0);
const [totalActiveEmployees, setTotalActiveEmployees] = React.useState(0); const [totalActiveEmployees, setTotalActiveEmployees] = React.useState(0);
const [maxResignType, setMaxResignType] = React.useState(0); const [totalResignType, setTotalResignType] = React.useState(0);
const [totalResignReason, setTotalResignReason] = React.useState(0);
// MPP Recruitment data state // MPP Recruitment data state
const [mppRecruitmentSummary, setMppRecruitmentSummary] = React.useState<{ const [mppRecruitmentSummary, setMppRecruitmentSummary] = React.useState<{
@ -59,40 +67,125 @@ export default function TurnoverPage() {
active: number; active: number;
}[]>([]); }[]>([]);
// Function to calculate active employees for each organization
const calculateActiveEmployees = React.useCallback(async (organizations: {organization_code: string, organization_name: string}[]) => {
console.log('🔍 calculateActiveEmployees called with:', { organizations: organizations.map(o => o.organization_code), filter });
const activeEmployeesMap: {[key: string]: number} = {};
for (const org of organizations) {
try {
const result = await getMonthlyEmployee({
...filter,
organization_code: org.organization_code
});
console.log(`📊 Monthly employee data for ${org.organization_code}:`, result.data);
if (result.data && result.data.length > 0) {
// Find the record that matches the end_date from filter
let matchingData = null;
if (filter.end_date) {
// Convert filter end_date to match the format in the API response
const filterEndDate = new Date(filter.end_date);
const filterYear = filterEndDate.getFullYear();
const filterMonth = filterEndDate.getMonth() + 1; // getMonth() returns 0-11
console.log(`🎯 Looking for data matching: ${filterYear}-${filterMonth.toString().padStart(2, '0')}`);
matchingData = result.data.find(item => {
const itemDate = new Date(item.date);
const itemYear = itemDate.getFullYear();
const itemMonth = itemDate.getMonth() + 1;
console.log(`📅 Checking item: ${itemYear}-${itemMonth.toString().padStart(2, '0')} (count: ${item.count})`);
return itemYear === filterYear && itemMonth === filterMonth;
});
}
// If no matching date found, use the latest data as fallback
if (!matchingData) {
matchingData = result.data[result.data.length - 1];
console.log(`⚠️ No matching date found for ${org.organization_code}, using latest:`, matchingData);
} else {
console.log(`✅ Found matching data for ${org.organization_code}:`, matchingData);
}
activeEmployeesMap[org.organization_code] = matchingData.count;
} else {
console.log(`❌ No data found for ${org.organization_code}`);
activeEmployeesMap[org.organization_code] = 0;
}
} catch (error) {
console.error(`Error fetching active employees for ${org.organization_code}:`, error);
activeEmployeesMap[org.organization_code] = 0;
}
}
console.log('🎉 Final activeEmployeesMap:', activeEmployeesMap);
setActiveEmployeesByOrg(activeEmployeesMap);
return activeEmployeesMap;
}, [getMonthlyEmployee, filter]);
React.useEffect(() => { React.useEffect(() => {
if(resignSummary){ if(resignSummary){
const totalResign = resignSummary.reduce((acc, curr) => acc + curr.count, 0); const totalResign = resignSummary.reduce((acc, curr) => acc + curr.count, 0);
const totalActive = resignSummary.reduce((acc, curr) => acc + curr.active, 0);
setTurnOverRatio(parseFloat((totalResign / totalActive * 100).toFixed(2)));
setResign(totalResign); setResign(totalResign);
// Calculate active employees for all organizations in resignSummary
const organizations = resignSummary.map(item => ({
organization_code: item.organization_code,
organization_name: item.organization_name
}));
calculateActiveEmployees(organizations).then((activeEmployeesMap) => {
const totalActive = Object.values(activeEmployeesMap).reduce((acc, count) => acc + count, 0);
setActive(totalActive); setActive(totalActive);
// Sort by total employees (resign + active) and limit to top 10 // Calculate turnover ratio
if (totalActive > 0) {
setTurnOverRatio(parseFloat((totalResign / totalActive * 100).toFixed(2)));
} else {
setTurnOverRatio(0);
}
// Now update limitedResignSummary with active employee data
const sortedResignData = [...resignSummary] const sortedResignData = [...resignSummary]
.sort((a, b) => (b.count ) - (a.count )) .sort((a, b) => (b.count ) - (a.count ))
.slice(0, 10); .map(item => ({
...item,
active: activeEmployeesMap[item.organization_code] || 0
}));
setLimitedResignSummary(sortedResignData); setLimitedResignSummary(sortedResignData);
});
} }
}, [resignSummary]); }, [resignSummary, calculateActiveEmployees]);
React.useEffect(() => { React.useEffect(() => {
if(resignType){ if(resignType){
const max = resignType.reduce((acc, curr) => Math.max(acc, curr.count), 0); const total = resignType.reduce((acc, curr) => acc + curr.count, 0);
setMaxResignType(max); setTotalResignType(total);
} }
}, [resignType]); }, [resignType]);
React.useEffect(() => {
if(resignReason){
const total = resignReason.reduce((acc, curr) => acc + curr.count, 0);
setTotalResignReason(total);
}
}, [resignReason]);
// Process employee movement data for recruitment calculations // Process employee movement data for recruitment calculations
React.useEffect(() => { React.useEffect(() => {
if(employeeMovement && resignSummary) { if(employeeMovement && Object.keys(activeEmployeesByOrg).length > 0) {
// Calculate total recruitment from employee movement data // Calculate total recruitment from employee movement data
const totalRecruitmentCount = employeeMovement.reduce((acc, curr) => { const totalRecruitmentCount = employeeMovement.reduce((acc, curr) => {
return acc + (curr.recruitment || 0); return acc + (curr.recruitment || 0);
}, 0); }, 0);
// Use the same active employee data as resignation component // Use the calculated active employee data
const totalActiveCount = resignSummary.reduce((acc, curr) => acc + curr.active, 0); const totalActiveCount = Object.values(activeEmployeesByOrg).reduce((acc, count) => acc + count, 0);
// Calculate recruitment ratio (recruitment / total active * 100) // Calculate recruitment ratio (recruitment / total active * 100)
const ratio = totalActiveCount > 0 ? parseFloat(((totalRecruitmentCount / totalActiveCount) * 100).toFixed(2)) : 0; const ratio = totalActiveCount > 0 ? parseFloat(((totalRecruitmentCount / totalActiveCount) * 100).toFixed(2)) : 0;
@ -101,7 +194,7 @@ export default function TurnoverPage() {
setTotalActiveEmployees(totalActiveCount); setTotalActiveEmployees(totalActiveCount);
setRecruitmentRatio(ratio); setRecruitmentRatio(ratio);
} }
}, [employeeMovement, resignSummary]); }, [employeeMovement, activeEmployeesByOrg]);
// Process MPP recruitment data for chart // Process MPP recruitment data for chart
React.useEffect(() => { React.useEffect(() => {
@ -194,11 +287,11 @@ export default function TurnoverPage() {
</div> </div>
<div className="px-3 py-4 flex-1 flex flex-col border border-[#DDE8F8] rounded-lg"> <div className="px-3 py-4 flex-1 flex flex-col border border-[#DDE8F8] rounded-lg">
<div className="text-sm text-[#5A5A5A]">{t('turnover.karyawanBaru')}</div> <div className="text-sm text-[#5A5A5A]">{t('turnover.karyawanBaru')}</div>
<div className="text-xl font-bold">{totalRecruitment}</div> <div className="text-xl font-bold">{totalRecruitment.toLocaleString('id-ID')}</div>
</div> </div>
<div className="px-3 py-4 flex-1 flex flex-col border border-[#DDE8F8] rounded-lg"> <div className="px-3 py-4 flex-1 flex flex-col border border-[#DDE8F8] rounded-lg">
<div className="text-sm text-[#5A5A5A]">{t('turnover.karyawanAktif')}</div> <div className="text-sm text-[#5A5A5A]">{t('turnover.karyawanAktif')}</div>
<div className="text-xl font-bold">{totalActiveEmployees}</div> <div className="text-xl font-bold">{totalActiveEmployees.toLocaleString('id-ID')}</div>
</div> </div>
</div> </div>
</> </>
@ -262,11 +355,11 @@ export default function TurnoverPage() {
</div> </div>
<div className="px-3 py-4 flex-1 flex flex-col border border-[#DDE8F8] rounded-lg"> <div className="px-3 py-4 flex-1 flex flex-col border border-[#DDE8F8] rounded-lg">
<div className="text-sm text-[#5A5A5A]">{t('turnover.karyawanResign')}</div> <div className="text-sm text-[#5A5A5A]">{t('turnover.karyawanResign')}</div>
<div className="text-xl font-bold">{resign}</div> <div className="text-xl font-bold">{resign.toLocaleString('id-ID')}</div>
</div> </div>
<div className="px-3 py-4 flex-1 flex flex-col border border-[#DDE8F8] rounded-lg"> <div className="px-3 py-4 flex-1 flex flex-col border border-[#DDE8F8] rounded-lg">
<div className="text-sm text-[#5A5A5A]">{t('turnover.karyawanAktif')}</div> <div className="text-sm text-[#5A5A5A]">{t('turnover.karyawanAktif')}</div>
<div className="text-xl font-bold">{active}</div> <div className="text-xl font-bold">{active.toLocaleString('id-ID')}</div>
</div> </div>
</div> </div>
</div> </div>
@ -292,9 +385,9 @@ export default function TurnoverPage() {
formatter={(value, name, props) => { formatter={(value, name, props) => {
const orgData = limitedResignSummary.find(e => e.organization_code === props.payload.organization_code); const orgData = limitedResignSummary.find(e => e.organization_code === props.payload.organization_code);
if (name === t('turnover.resign')) { if (name === t('turnover.resign')) {
return [`${orgData?.count || 0} ${t('common.employees')}`, t('turnover.resign')]; return [`${orgData?.count?.toLocaleString('id-ID') || 0} ${t('common.employees')}`, t('turnover.resign')];
} else { } else {
return [`${orgData?.active || 0} ${t('common.employees')}`, t('turnover.active')]; return [`${orgData?.active?.toLocaleString('id-ID') || 0} ${t('common.employees')}`, t('turnover.active')];
} }
}} }}
labelFormatter={(label) => { labelFormatter={(label) => {
@ -316,7 +409,7 @@ export default function TurnoverPage() {
)} )}
</div> </div>
</div> </div>
<div className="col-span-4 bg-white py-4 px-4 rounded-lg max-h-[420px] flex flex-col"> <div className="col-span-4 bg-white py-4 px-4 rounded-lg max-h-[480px] flex flex-col">
<div className="text-xl font-bold">{t('turnover.jenisPemutusanHubungan')}</div> <div className="text-xl font-bold">{t('turnover.jenisPemutusanHubungan')}</div>
<div className={`flex-1 min-h-[300px] gap-3 flex flex-col mt-8 ${resignType && resignType.length === 0 ? "justify-center items-center" : ""}`}> <div className={`flex-1 min-h-[300px] gap-3 flex flex-col mt-8 ${resignType && resignType.length === 0 ? "justify-center items-center" : ""}`}>
{resignType && resignType.map((resign, index) => ( {resignType && resignType.map((resign, index) => (
@ -324,7 +417,7 @@ export default function TurnoverPage() {
<div className="text-sm col-span-3 text-gray-600">{resign.type}</div> <div className="text-sm col-span-3 text-gray-600">{resign.type}</div>
<div className="col-span-1"/> <div className="col-span-1"/>
<div className="relative col-span-8 h-6 bg-[#2385DE] rounded-lg"> <div className="relative col-span-8 h-6 bg-[#2385DE] rounded-lg">
<div className="absolute top-0 left-0 h-6 bg-[#F7B500] rounded-lg" style={{width: `${resign.count / maxResignType * 100}%`}}/> <div className="absolute top-0 left-0 h-6 bg-[#F7B500] rounded-lg" style={{width: `${totalResignType > 0 ? (resign.count / totalResignType * 100) : 0}%`}}/>
</div> </div>
</div> </div>
))} ))}
@ -332,7 +425,7 @@ export default function TurnoverPage() {
<ReactTooltip id="tooltip-tor-type" /> <ReactTooltip id="tooltip-tor-type" />
</div> </div>
</div> </div>
<div className="col-span-4 bg-white py-4 px-4 rounded-lg max-h-[420px] flex flex-col"> <div className="col-span-4 bg-white py-4 px-4 rounded-lg max-h-[480px] flex flex-col">
<div className="text-xl font-bold">{t('turnover.kategoriResign')}</div> <div className="text-xl font-bold">{t('turnover.kategoriResign')}</div>
<div className="w-4/5 flex-1 flex mx-auto"> <div className="w-4/5 flex-1 flex mx-auto">
{resignCategory && resignCategory.length > 0 ? ( {resignCategory && resignCategory.length > 0 ? (
@ -342,7 +435,8 @@ export default function TurnoverPage() {
data={resignCategory.map(e => ({name: e.category, value: e.count}))} data={resignCategory.map(e => ({name: e.category, value: e.count}))}
cx="50%" cx="50%"
cy="50%" cy="50%"
outerRadius={80} outerRadius={120}
innerRadius={90}
dataKey="value" dataKey="value"
label={({ name }) => name} label={({ name }) => name}
labelLine={false} labelLine={false}
@ -367,7 +461,7 @@ export default function TurnoverPage() {
)} )}
</div> </div>
</div> </div>
<div className="col-span-4 bg-white py-4 px-4 rounded-lg max-h-[420px] flex flex-col"> <div className="col-span-4 bg-white py-4 px-4 rounded-lg max-h-[480px] flex flex-col">
<div className="text-xl font-bold">{t('turnover.alasanPemutusanHubungan')}</div> <div className="text-xl font-bold">{t('turnover.alasanPemutusanHubungan')}</div>
<div className={`flex-1 min-h-[300px] gap-3 flex flex-col mt-8 ${resignReason && resignReason.length === 0 ? "justify-center items-center" : ""}`}> <div className={`flex-1 min-h-[300px] gap-3 flex flex-col mt-8 ${resignReason && resignReason.length === 0 ? "justify-center items-center" : ""}`}>
{resignReason && resignReason.map((resign, index) => ( {resignReason && resignReason.map((resign, index) => (
@ -375,7 +469,7 @@ export default function TurnoverPage() {
<div className="text-sm col-span-3 text-gray-600">{resign.reason}</div> <div className="text-sm col-span-3 text-gray-600">{resign.reason}</div>
<div className="col-span-1"/> <div className="col-span-1"/>
<div className="relative col-span-8 h-6 bg-[#2385DE] rounded-lg"> <div className="relative col-span-8 h-6 bg-[#2385DE] rounded-lg">
<div className="absolute top-0 left-0 h-6 bg-[#F7B500] rounded-lg" style={{width: `${resign.count / maxResignType * 100}%`}}/> <div className="absolute top-0 left-0 h-6 bg-[#F7B500] rounded-lg" style={{width: `${totalResignReason > 0 ? (resign.count / totalResignReason * 100) : 0}%`}}/>
</div> </div>
</div> </div>
))} ))}

View File

@ -263,7 +263,8 @@
"idKaryawan": "员工ID", "idKaryawan": "员工ID",
"nama": "姓名", "nama": "姓名",
"alamat": "地址", "alamat": "地址",
"gajiIdr": "薪资 (印尼盾)" "gajiIdr": "薪资 (印尼盾)",
"thr": " THR"
}, },
"forms": { "forms": {
"required": "必填项", "required": "必填项",

View File

@ -280,7 +280,8 @@
"idKaryawan": "ID Karyawan", "idKaryawan": "ID Karyawan",
"nama": "Nama", "nama": "Nama",
"alamat": "Alamat", "alamat": "Alamat",
"gajiIdr": "Gaji (IDR)" "gajiIdr": "Gaji (IDR)",
"thr": "THR"
}, },
"forms": { "forms": {
"required": "Wajib diisi", "required": "Wajib diisi",

View File

@ -257,7 +257,7 @@ export const api = createApi({
}) })
export const { useGetFilterOptionsQuery, useGetEmployeeSummaryQuery, useGetMonthlyEmployeeQuery, useGetMonthlyAttendanceQuery, export const { useGetFilterOptionsQuery, useGetEmployeeSummaryQuery, useGetMonthlyEmployeeQuery, useGetMonthlyAttendanceQuery,
useGetOrganizationAttendanceQuery, useGetAttendanceRangeQuery, useGetResignSummaryQuery, useGetResignTypeQuery, useGetOrganizationAttendanceQuery, useGetAttendanceRangeQuery, useGetResignSummaryQuery, useGetResignTypeQuery, useLazyGetMonthlyEmployeeQuery,
useLoginMutation, useAuthCheckQuery, useGetSanctionSummaryQuery, useLoginMutation, useAuthCheckQuery, useGetSanctionSummaryQuery,
useGetResignCategoryQuery, useGetResignReasonQuery, useGetEmployeeMovementQuery, useGetMppRecruitmentQuery, useGetResignCategoryQuery, useGetResignReasonQuery, useGetEmployeeMovementQuery, useGetMppRecruitmentQuery,
useGetProductivityByRegionQuery, useGetProductivityByAgeQuery, useGetProductivityByTenureQuery, useGetTonnageHarvestGroupEmployeeQuery, useGetTonnageHarvestByEmployeeOriginQuery, useGetTonnageHarvestByEmployeeSalaryQuery, useGetTargetTonnageQuery, useGetHrCostByJobQuery, useGetHrCostQuery, useGetHrCostByOrganizationQuery, useGetHrCostPerMonthQuery, useGetHrCostPerEmployeeQuery, useGetCostQuery } = api useGetProductivityByRegionQuery, useGetProductivityByAgeQuery, useGetProductivityByTenureQuery, useGetTonnageHarvestGroupEmployeeQuery, useGetTonnageHarvestByEmployeeOriginQuery, useGetTonnageHarvestByEmployeeSalaryQuery, useGetTargetTonnageQuery, useGetHrCostByJobQuery, useGetHrCostQuery, useGetHrCostByOrganizationQuery, useGetHrCostPerMonthQuery, useGetHrCostPerEmployeeQuery, useGetCostQuery } = api

View File

@ -80,7 +80,7 @@ export type ResignSummary = {
organization_code: string; organization_code: string;
organization_name: string; organization_name: string;
count: number; count: number;
active: number; // active: number;
} }
export type ResignationType = { export type ResignationType = {
@ -175,6 +175,7 @@ export type HrCost = {
total_salary: number; total_salary: number;
total_bpjs_tk: number; total_bpjs_tk: number;
total_bpjs_ks: number; total_bpjs_ks: number;
total_thr: number;
} }
export type HrCostByOrganization = { export type HrCostByOrganization = {