fix
This commit is contained in:
parent
7ca52e1092
commit
b02e84b48b
|
|
@ -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 />
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
}
|
}
|
||||||
] : [];
|
] : [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -263,7 +263,8 @@
|
||||||
"idKaryawan": "员工ID",
|
"idKaryawan": "员工ID",
|
||||||
"nama": "姓名",
|
"nama": "姓名",
|
||||||
"alamat": "地址",
|
"alamat": "地址",
|
||||||
"gajiIdr": "薪资 (印尼盾)"
|
"gajiIdr": "薪资 (印尼盾)",
|
||||||
|
"thr": " THR"
|
||||||
},
|
},
|
||||||
"forms": {
|
"forms": {
|
||||||
"required": "必填项",
|
"required": "必填项",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue