From b8419a3f69361b5419a64c23ad4dcde5db4ca5d7 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 25 May 2026 14:14:40 +0700 Subject: [PATCH] feat: add Excel export to BalanceMonitoringTab Add exportBalanceMonitoringToExcel to FinanceApiService (server-side blob download hitting reports/balance-monitoring?export=excel). Wire it into BalanceMonitoringTab via a Dropdown export button in the tab actions. Wrap the handler in useCallback to prevent an infinite setTabActions loop caused by a new function reference on every render. Co-Authored-By: Claude Sonnet 4.6 --- .../finance/tab/BalanceMonitoringTab.tsx | 79 ++++++++++++++++++- src/services/api/report/finance-report.ts | 34 ++++++++ 2 files changed, 111 insertions(+), 2 deletions(-) diff --git a/src/components/pages/report/finance/tab/BalanceMonitoringTab.tsx b/src/components/pages/report/finance/tab/BalanceMonitoringTab.tsx index b2920008..e7ec6cb3 100644 --- a/src/components/pages/report/finance/tab/BalanceMonitoringTab.tsx +++ b/src/components/pages/report/finance/tab/BalanceMonitoringTab.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useMemo, useEffect } from 'react'; +import { useState, useMemo, useEffect, useCallback } from 'react'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; import { useFormik } from 'formik'; @@ -24,6 +24,7 @@ import { BalanceMonitoringRow } from '@/types/api/report/balance-monitoring'; import { CustomerPaymentRow } from '@/types/api/report/customer-payment'; import Modal, { useModal } from '@/components/Modal'; import Button from '@/components/Button'; +import Dropdown from '@/components/Dropdown'; import DateInput from '@/components/input/DateInput'; import Table from '@/components/Table'; import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton'; @@ -40,6 +41,7 @@ const filterByOptions: OptionType[] = [ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { const [hasDateError, setHasDateError] = useState(false); const [dateErrorShown, setDateErrorShown] = useState(false); + const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); const filterModal = useModal(); @@ -230,6 +232,33 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { ? balanceMonitoringsResponse.meta : null; + const handleExportExcel = useCallback(async () => { + setIsExcelExportLoading(true); + try { + const customer_ids = + tableFilterState.customers.length > 0 + ? tableFilterState.customers.map((o) => String(o.value)).join(',') + : undefined; + const sales_ids = + tableFilterState.salesPersons.length > 0 + ? tableFilterState.salesPersons.map((o) => String(o.value)).join(',') + : undefined; + + await FinanceApi.exportBalanceMonitoringToExcel( + customer_ids, + sales_ids, + tableFilterState.filterBy?.value, + tableFilterState.start_date || undefined, + tableFilterState.end_date || undefined + ); + toast.success('Excel berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat Excel. Silakan coba lagi.'); + } finally { + setIsExcelExportLoading(false); + } + }, [tableFilterState]); + // Inject tab actions directly — no nested component, no remount cycle useEffect(() => { setTabActions( @@ -248,9 +277,55 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { variant='outline' className='px-3 py-2.5' /> + + +
+ + Export +
+ +
+ + } + > + +
); - }, [tabId, setTabActions, tableFilterState, filterModal.openModal]); + }, [ + tabId, + setTabActions, + tableFilterState, + filterModal.openModal, + isExcelExportLoading, + handleExportExcel, + ]); useEffect(() => { return () => clearTabActions(tabId); diff --git a/src/services/api/report/finance-report.ts b/src/services/api/report/finance-report.ts index 132e3063..171e59d0 100644 --- a/src/services/api/report/finance-report.ts +++ b/src/services/api/report/finance-report.ts @@ -86,6 +86,40 @@ export class FinanceApiService extends BaseApiService< link.remove(); } + async exportBalanceMonitoringToExcel( + customer_ids?: string, + sales_ids?: string, + filter_by?: string, + start_date?: string, + end_date?: string + ) { + const params = new URLSearchParams(); + if (customer_ids) params.set('customer_ids', customer_ids); + if (sales_ids) params.set('sales_ids', sales_ids); + if (filter_by) params.set('filter_by', filter_by); + if (start_date) params.set('start_date', start_date); + if (end_date) params.set('end_date', end_date); + params.set('export', 'excel'); + params.set('page', '1'); + params.set('limit', '9999999999'); + + const res = await httpClient( + `${this.basePath}/balance-monitoring?${params.toString()}`, + { method: 'GET', responseType: 'blob' } + ); + + const url = window.URL.createObjectURL(new Blob([res])); + const link = document.createElement('a'); + link.href = url; + link.setAttribute( + 'download', + `laporan-balance-monitoring-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx` + ); + document.body.appendChild(link); + link.click(); + link.remove(); + } + async getBalanceMonitoringReport(params: { start_date?: string; end_date?: string;