From b02e84b48b2ecca39651613f27009c887e5f8875 Mon Sep 17 00:00:00 2001 From: Muhammad Eko Date: Thu, 16 Oct 2025 14:24:31 +0700 Subject: [PATCH] fix --- src/app/(dashboard)/absensi/page.tsx | 4 +- src/app/(dashboard)/hrcost/page.tsx | 6 + src/app/(dashboard)/productivity/page.tsx | 105 +++++++++++---- src/app/(dashboard)/turnover/page.tsx | 152 +++++++++++++++++----- src/locales/cn.json | 3 +- src/locales/id.json | 3 +- src/services/api.ts | 2 +- src/services/types.ts | 3 +- 8 files changed, 215 insertions(+), 63 deletions(-) diff --git a/src/app/(dashboard)/absensi/page.tsx b/src/app/(dashboard)/absensi/page.tsx index 0a35986..5c7c452 100644 --- a/src/app/(dashboard)/absensi/page.tsx +++ b/src/app/(dashboard)/absensi/page.tsx @@ -311,9 +311,9 @@ export default function AbsensiPage() { }} formatter={(value, name) => { 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]; }} /> diff --git a/src/app/(dashboard)/hrcost/page.tsx b/src/app/(dashboard)/hrcost/page.tsx index a5400e7..2051bcd 100644 --- a/src/app/(dashboard)/hrcost/page.tsx +++ b/src/app/(dashboard)/hrcost/page.tsx @@ -110,6 +110,12 @@ export default function HrcostPage() { value: hrCost.total_bpjs_ks, valueInMillions: hrCost.total_bpjs_ks / 1000000000, color: COLORS[2] + }, + { + name: t('hrcost.thr'), + value: hrCost.total_thr, + valueInMillions: hrCost.total_thr / 1000000000, + color: COLORS[3] } ] : []; diff --git a/src/app/(dashboard)/productivity/page.tsx b/src/app/(dashboard)/productivity/page.tsx index 252f1b2..af9b913 100644 --- a/src/app/(dashboard)/productivity/page.tsx +++ b/src/app/(dashboard)/productivity/page.tsx @@ -41,6 +41,18 @@ export default function ProductivityPage() { const totalTonnageAge = productivityByAge?.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) const ageData = productivityByAge?.map(item => ({ name: item.age_range, @@ -54,29 +66,36 @@ export default function ProductivityPage() { 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 => ({ ...item, 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 const ageBarData = productivityByAge?.map(item => ({ ...item, 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 const tenureBarData = productivityByTenure?.map(item => ({ ...item, 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 // Prepare data for tonnage harvest group employee pie chart const tonnageGroupData = tonnageHarvestGroupEmployee?.map(item => { // Parse tonnage range from kg format (e.g., "0.0 - 20499.9") and convert to tons const parseRange = (range: string) => { + if (!range || range.endsWith("+")){ + return (Number(range.replace("+", "")) / 1000).toFixed(1) + "+ ton" + } + const parts = range.split(' - '); if (parts.length === 2) { const min = parseFloat(parts[0]) / 1000; // Convert kg to tons @@ -243,13 +262,28 @@ export default function ProductivityPage() { {tonnageGroupData && tonnageGroupData.length > 0 ? ( + { + const totalEmployees = tonnageGroupData.reduce((sum, item) => sum + item.value, 0); + const calculatedPercentage = totalEmployees > 0 ? ((entry.payload?.value / totalEmployees) * 100).toFixed(2) : 0; + return ( + + {value}: {entry.payload?.value?.toLocaleString('id-ID', { maximumFractionDigits: 2 })} {t('common.ton')} ({calculatedPercentage}%) + + ) + }}/> @@ -265,14 +299,23 @@ export default function ProductivityPage() { labelFormatter={(label, payload) => { 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 ( +
+

{`${t('productivity.rangeTonase')}: ${payload[0].name}`}

+

{`${t('common.karyawan')}: ${data.value.toLocaleString('id-ID')} ${t('common.orang')}`}

+

{`${t('productivity.persentase')}: ${calculatedPercentage}%`}

+
+ ); + } + return null; + }} /> - +
) : ( @@ -287,12 +330,28 @@ export default function ProductivityPage() {
+ { + 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 ( + + {value}: {entry.payload?.value?.toLocaleString('id-ID', { maximumFractionDigits: 2 })} {t('common.ton')} ({calculatedPercentage}%) + + ) + }} + /> `${name}: ${percentage}%`} @@ -311,30 +370,20 @@ export default function ProductivityPage() { content={({ active, payload, label }) => { if (active && payload && payload.length) { 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 (

{`${t('productivity.gaji')}: ${payload[0].name}`}

{`${t('productivity.output')}: ${data.value.toLocaleString('id-ID')} ${t('common.ton')}`}

{`${t('common.karyawan')}: ${data.count.toLocaleString('id-ID')} ${t('common.orang')}`}

-

{`${t('productivity.persentase')}: ${data.percentage}%`}

+

{`${t('productivity.persentase')}: ${calculatedPercentage}%`}

); } return null; }} /> - ( - - {value}: {entry.payload?.value?.toLocaleString('id-ID')} {t('common.ton')} ({(entry.payload as any)?.percentage}%) - - )} - />
diff --git a/src/app/(dashboard)/turnover/page.tsx b/src/app/(dashboard)/turnover/page.tsx index 5c378ba..a532df2 100644 --- a/src/app/(dashboard)/turnover/page.tsx +++ b/src/app/(dashboard)/turnover/page.tsx @@ -2,7 +2,7 @@ import { useAppSelector } from "@/lib/hooks"; import { useDimensions } from "@/lib/hooks/dimension"; 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 { Loader, LucideLoader2 } from "lucide-react"; import React from "react"; @@ -23,11 +23,18 @@ export default function TurnoverPage() { const {data: resignReason, isLoading: isLoadingResignReason, isFetching: isFetchingResignReason} = useGetResignReasonQuery(filter); const {data: employeeMovement, isLoading: isLoadingEmployeeMovement, isFetching: isFetchingEmployeeMovement} = useGetEmployeeMovementQuery(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) const isLoading = isLoadingResignSummary || isLoadingResignType || isLoadingResignCategory || isLoadingResignReason || isLoadingEmployeeMovement || isLoadingMppRecruitment || + isLoadingActiveEmployees || isFetchingResignSummary || isFetchingResignType || isFetchingResignCategory || isFetchingResignReason || isFetchingEmployeeMovement || isFetchingMppRecruitment; @@ -41,7 +48,8 @@ export default function TurnoverPage() { const [totalRecruitment, setTotalRecruitment] = 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 const [mppRecruitmentSummary, setMppRecruitmentSummary] = React.useState<{ @@ -59,40 +67,125 @@ export default function TurnoverPage() { 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(() => { if(resignSummary){ 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); - setActive(totalActive); - // Sort by total employees (resign + active) and limit to top 10 - const sortedResignData = [...resignSummary] - .sort((a, b) => (b.count ) - (a.count )) - .slice(0, 10); + // Calculate active employees for all organizations in resignSummary + const organizations = resignSummary.map(item => ({ + organization_code: item.organization_code, + organization_name: item.organization_name + })); - setLimitedResignSummary(sortedResignData); + calculateActiveEmployees(organizations).then((activeEmployeesMap) => { + const totalActive = Object.values(activeEmployeesMap).reduce((acc, count) => acc + count, 0); + setActive(totalActive); + + // 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] + .sort((a, b) => (b.count ) - (a.count )) + .map(item => ({ + ...item, + active: activeEmployeesMap[item.organization_code] || 0 + })); + + setLimitedResignSummary(sortedResignData); + }); } - }, [resignSummary]); + }, [resignSummary, calculateActiveEmployees]); React.useEffect(() => { if(resignType){ - const max = resignType.reduce((acc, curr) => Math.max(acc, curr.count), 0); - setMaxResignType(max); + const total = resignType.reduce((acc, curr) => acc + curr.count, 0); + setTotalResignType(total); } + }, [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 React.useEffect(() => { - if(employeeMovement && resignSummary) { + if(employeeMovement && Object.keys(activeEmployeesByOrg).length > 0) { // Calculate total recruitment from employee movement data const totalRecruitmentCount = employeeMovement.reduce((acc, curr) => { return acc + (curr.recruitment || 0); }, 0); - // Use the same active employee data as resignation component - const totalActiveCount = resignSummary.reduce((acc, curr) => acc + curr.active, 0); + // Use the calculated active employee data + const totalActiveCount = Object.values(activeEmployeesByOrg).reduce((acc, count) => acc + count, 0); // Calculate recruitment ratio (recruitment / total active * 100) const ratio = totalActiveCount > 0 ? parseFloat(((totalRecruitmentCount / totalActiveCount) * 100).toFixed(2)) : 0; @@ -101,7 +194,7 @@ export default function TurnoverPage() { setTotalActiveEmployees(totalActiveCount); setRecruitmentRatio(ratio); } - }, [employeeMovement, resignSummary]); + }, [employeeMovement, activeEmployeesByOrg]); // Process MPP recruitment data for chart React.useEffect(() => { @@ -194,11 +287,11 @@ export default function TurnoverPage() {
{t('turnover.karyawanBaru')}
-
{totalRecruitment}
+
{totalRecruitment.toLocaleString('id-ID')}
{t('turnover.karyawanAktif')}
-
{totalActiveEmployees}
+
{totalActiveEmployees.toLocaleString('id-ID')}
@@ -262,11 +355,11 @@ export default function TurnoverPage() {
{t('turnover.karyawanResign')}
-
{resign}
+
{resign.toLocaleString('id-ID')}
{t('turnover.karyawanAktif')}
-
{active}
+
{active.toLocaleString('id-ID')}
@@ -292,9 +385,9 @@ export default function TurnoverPage() { formatter={(value, name, props) => { const orgData = limitedResignSummary.find(e => e.organization_code === props.payload.organization_code); 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 { - 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) => { @@ -316,7 +409,7 @@ export default function TurnoverPage() { )} -
+
{t('turnover.jenisPemutusanHubungan')}
{resignType && resignType.map((resign, index) => ( @@ -324,7 +417,7 @@ export default function TurnoverPage() {
{resign.type}
-
+
0 ? (resign.count / totalResignType * 100) : 0}%`}}/>
))} @@ -332,7 +425,7 @@ export default function TurnoverPage() {
-
+
{t('turnover.kategoriResign')}
{resignCategory && resignCategory.length > 0 ? ( @@ -342,7 +435,8 @@ export default function TurnoverPage() { data={resignCategory.map(e => ({name: e.category, value: e.count}))} cx="50%" cy="50%" - outerRadius={80} + outerRadius={120} + innerRadius={90} dataKey="value" label={({ name }) => name} labelLine={false} @@ -367,7 +461,7 @@ export default function TurnoverPage() { )}
-
+
{t('turnover.alasanPemutusanHubungan')}
{resignReason && resignReason.map((resign, index) => ( @@ -375,7 +469,7 @@ export default function TurnoverPage() {
{resign.reason}
-
+
0 ? (resign.count / totalResignReason * 100) : 0}%`}}/>
))} diff --git a/src/locales/cn.json b/src/locales/cn.json index dbaf913..8ed9da9 100644 --- a/src/locales/cn.json +++ b/src/locales/cn.json @@ -263,7 +263,8 @@ "idKaryawan": "员工ID", "nama": "姓名", "alamat": "地址", - "gajiIdr": "薪资 (印尼盾)" + "gajiIdr": "薪资 (印尼盾)", + "thr": " THR" }, "forms": { "required": "必填项", diff --git a/src/locales/id.json b/src/locales/id.json index fb2e4a2..86ad73d 100644 --- a/src/locales/id.json +++ b/src/locales/id.json @@ -280,7 +280,8 @@ "idKaryawan": "ID Karyawan", "nama": "Nama", "alamat": "Alamat", - "gajiIdr": "Gaji (IDR)" + "gajiIdr": "Gaji (IDR)", + "thr": "THR" }, "forms": { "required": "Wajib diisi", diff --git a/src/services/api.ts b/src/services/api.ts index 61aece4..68e74b5 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -257,7 +257,7 @@ export const api = createApi({ }) export const { useGetFilterOptionsQuery, useGetEmployeeSummaryQuery, useGetMonthlyEmployeeQuery, useGetMonthlyAttendanceQuery, - useGetOrganizationAttendanceQuery, useGetAttendanceRangeQuery, useGetResignSummaryQuery, useGetResignTypeQuery, + useGetOrganizationAttendanceQuery, useGetAttendanceRangeQuery, useGetResignSummaryQuery, useGetResignTypeQuery, useLazyGetMonthlyEmployeeQuery, useLoginMutation, useAuthCheckQuery, useGetSanctionSummaryQuery, useGetResignCategoryQuery, useGetResignReasonQuery, useGetEmployeeMovementQuery, useGetMppRecruitmentQuery, useGetProductivityByRegionQuery, useGetProductivityByAgeQuery, useGetProductivityByTenureQuery, useGetTonnageHarvestGroupEmployeeQuery, useGetTonnageHarvestByEmployeeOriginQuery, useGetTonnageHarvestByEmployeeSalaryQuery, useGetTargetTonnageQuery, useGetHrCostByJobQuery, useGetHrCostQuery, useGetHrCostByOrganizationQuery, useGetHrCostPerMonthQuery, useGetHrCostPerEmployeeQuery, useGetCostQuery } = api \ No newline at end of file diff --git a/src/services/types.ts b/src/services/types.ts index 5362319..18fca1b 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -80,7 +80,7 @@ export type ResignSummary = { organization_code: string; organization_name: string; count: number; - active: number; + // active: number; } export type ResignationType = { @@ -175,6 +175,7 @@ export type HrCost = { total_salary: number; total_bpjs_tk: number; total_bpjs_ks: number; + total_thr: number; } export type HrCostByOrganization = {