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 <noreply@anthropic.com>
This commit is contained in:
ValdiANS
2026-05-25 14:14:40 +07:00
parent d7a98a77ea
commit b8419a3f69
2 changed files with 111 additions and 2 deletions
@@ -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<string>[] = [
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'
/>
<Dropdown
align='end'
direction='bottom'
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
trigger={
<Button
variant='outline'
color='none'
isLoading={isExcelExportLoading}
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
>
<div className='flex flex-row items-center gap-1.5'>
<Icon
icon='heroicons:cloud-arrow-down'
width={20}
height={20}
/>
<span>Export</span>
<div className='w-px self-stretch bg-base-content/10' />
<Icon icon='heroicons:chevron-down' width={14} height={14} />
</div>
</Button>
}
>
<Button
variant='ghost'
color='none'
onClick={handleExportExcel}
isLoading={isExcelExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:table-cells' width={20} height={20} />
Ekspor ke Excel
</Button>
</Dropdown>
</div>
);
}, [tabId, setTabActions, tableFilterState, filterModal.openModal]);
}, [
tabId,
setTabActions,
tableFilterState,
filterModal.openModal,
isExcelExportLoading,
handleExportExcel,
]);
useEffect(() => {
return () => clearTabActions(tabId);