import { useState, useCallback, useEffect } from 'react'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; import { AxiosError } from 'axios'; import Card from '@/components/Card'; import StatusBadge from '@/components/helper/StatusBadge'; import { useSelect, OptionType } from '@/components/input/SelectInput'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; import DateInput from '@/components/input/DateInput'; import { CustomerApi } from '@/services/api/master-data'; import { FinanceApi } from '@/services/api/report/finance-report'; import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; import { BaseApiResponse } from '@/types/api/api-general'; import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; import { formatCurrency, formatDate, formatNumber, formatTitleCase, } from '@/lib/helper'; import { CustomerPaymentReport, CustomerPaymentSummary, } from '@/types/api/report/customer-payment'; import { isResponseSuccess } from '@/lib/api-helper'; import Button from '@/components/Button'; import Dropdown from '@/components/Dropdown'; import Modal, { useModal } from '@/components/Modal'; import toast from 'react-hot-toast'; import { useFormik } from 'formik'; import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/export/CustomerPaymentExportPDF'; import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton'; import { Color } from '@/types/theme'; import ButtonFilter from '@/components/helper/ButtonFilter'; import Pagination from '@/components/Pagination'; import { useTableFilter } from '@/services/hooks/useTableFilter'; interface CustomerPaymentTabProps { tabId: string; } const dataTypeOptions: OptionType[] = [ { value: 'trans_date', label: 'Tanggal Jual/Bayar' }, { value: 'realization_date', label: 'Tanggal Realisasi' }, ]; const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // ===== STATE MANAGEMENT ===== const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); const [isExcelGeneralExportLoading, setIsExcelGeneralExportLoading] = useState(false); const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading || isExcelGeneralExportLoading; const [dateErrorShown, setDateErrorShown] = useState(false); const [hasDateError, setHasDateError] = useState(false); const filterModal = useModal(); const setTabActions = useTabActionsStore((state) => state.setTabActions); const clearTabActions = useTabActionsStore((state) => state.clearTabActions); const { state: tableFilterState, updateFilter, setPage, setPageSize, toQueryString: getTableFilterQueryString, reset: resetFilter, } = useTableFilter<{ start_date: string; end_date: string; customers: OptionType[]; filterBy?: OptionType; }>({ initial: { start_date: '', end_date: '', customers: [], filterBy: undefined, }, paramMap: { page: 'page', pageSize: 'limit', start_date: 'start_date', end_date: 'end_date', customers: 'customer_ids', filterBy: 'filter_by', }, persist: true, storeName: 'customer-payment-report-table', }); const { options: customerOptions, setInputValue: setCustomerInputValue, isLoadingOptions: isLoadingCustomers, loadMore: loadMoreCustomers, } = useSelect(CustomerApi.basePath, 'id', 'name', 'search'); // ===== FORMIK SETUP ===== const formik = useFormik({ initialValues: { start_date: tableFilterState.start_date, end_date: tableFilterState.end_date, customers: tableFilterState.customers, filterBy: tableFilterState.filterBy, }, onSubmit: (values) => { updateFilter('start_date', values.start_date, true); updateFilter('end_date', values.end_date, true); updateFilter('customers', values.customers, true); updateFilter('filterBy', values.filterBy, true); filterModal.closeModal(); }, }); const formikResetHandler = () => { resetFilter(); setHasDateError(false); if (dateErrorShown) { toast.dismiss(); setDateErrorShown(false); } formik.resetForm({ values: { start_date: '', end_date: '', customers: [], filterBy: undefined, }, }); filterModal.closeModal(); }; const getPaymentStatusBadgeColor = (notes: string): Color => { const normalizedValue = notes.toLowerCase(); if (normalizedValue === 'lunas') return 'primary'; if (normalizedValue.includes('belum')) return 'warning'; return 'neutral'; }; // ===== DATE CHANGE HANDLERS ===== const handleStartDateChange = (e: React.ChangeEvent) => { const value = e.target.value; formik.setFieldValue('start_date', value); if (value && formik.values.end_date) { if (new Date(formik.values.end_date) < new Date(value)) { setHasDateError(true); if (!dateErrorShown) { toast.error('Tanggal akhir tidak boleh masa lampau', { duration: Infinity, }); setDateErrorShown(true); } } else { setHasDateError(false); if (dateErrorShown) { toast.dismiss(); setDateErrorShown(false); } } } else { setHasDateError(false); } }; const handleEndDateChange = (e: React.ChangeEvent) => { const value = e.target.value; formik.setFieldValue('end_date', value); if (value && formik.values.start_date) { if (new Date(value) < new Date(formik.values.start_date)) { setHasDateError(true); if (!dateErrorShown) { toast.error('Tanggal akhir tidak boleh masa lampau', { duration: Infinity, }); setDateErrorShown(true); } return; } } setHasDateError(false); if (dateErrorShown) { toast.dismiss(); setDateErrorShown(false); } }; // ===== DATA FETCHING ===== const { data: customerPayment, isLoading } = useSWR< BaseApiResponse, AxiosError, SWRHttpKey >( `${FinanceApi.basePath}/customer-payment${getTableFilterQueryString()}`, httpClientFetcher ); const data: CustomerPaymentReport[] = isResponseSuccess(customerPayment) ? (customerPayment?.data as unknown as CustomerPaymentReport[]) || [] : []; const meta = isResponseSuccess(customerPayment) && customerPayment.meta ? customerPayment.meta : null; // ===== EXPORT DATA FETCHER ===== const customerPaymentExport = useCallback(async (): Promise< CustomerPaymentReport[] | null > => { const customer_ids = tableFilterState.customers.length > 0 ? tableFilterState.customers.map((o) => String(o.value)).join(',') : undefined; const filter_by = tableFilterState.filterBy?.value as | 'trans_date' | 'realization_date' | undefined; const response = await FinanceApi.getCustomerPaymentReport( customer_ids, filter_by, tableFilterState.start_date || undefined, tableFilterState.end_date || undefined, 1, 100 ); return isResponseSuccess(response) ? (response.data as unknown as CustomerPaymentReport[]) : null; }, [tableFilterState]); // ===== EXPORT HANDLERS ===== const handleExportExcelGeneral = useCallback(async () => { setIsExcelGeneralExportLoading(true); try { const customer_ids = tableFilterState.customers.length > 0 ? tableFilterState.customers.map((o) => String(o.value)).join(',') : undefined; await FinanceApi.exportCustomerPaymentToExcelGeneral( customer_ids, tableFilterState.filterBy?.value, tableFilterState.start_date || undefined, tableFilterState.end_date || undefined ); toast.success('Excel General berhasil dibuat dan diunduh.'); } catch { toast.error('Gagal membuat Excel General. Silakan coba lagi.'); } finally { setIsExcelGeneralExportLoading(false); } }, [tableFilterState]); const handleExportExcel = useCallback(async () => { setIsExcelExportLoading(true); try { const customer_ids = tableFilterState.customers.length > 0 ? tableFilterState.customers.map((o) => String(o.value)).join(',') : undefined; await FinanceApi.exportCustomerPaymentToExcelCustomerPerSheet( customer_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]); const handleExportPdf = useCallback(async () => { setIsPdfExportLoading(true); try { const allDataForExport = await customerPaymentExport(); if ( !allDataForExport || !Array.isArray(allDataForExport) || allDataForExport.length === 0 ) { toast.error('Tidak ada data untuk diekspor.'); return; } const customerName = tableFilterState.customers.length > 0 ? tableFilterState.customers.map((o) => o.label).join(', ') : 'Semua Customer'; await generateCustomerPaymentPDF({ data: allDataForExport, params: { customer_name: customerName, start_date: tableFilterState.start_date || undefined, end_date: tableFilterState.end_date || undefined, filter_by: tableFilterState.filterBy?.value as | 'trans_date' | 'realization_date' | undefined, }, }); toast.success('PDF berhasil dibuat dan diunduh.'); } catch { toast.error('Gagal membuat PDF. Silakan coba lagi.'); } finally { setIsPdfExportLoading(false); } }, [customerPaymentExport, tableFilterState]); // ===== TAB ACTIONS ===== useEffect(() => { setTabActions( tabId,
Export
} >
); }, [ tabId, setTabActions, tableFilterState, filterModal.openModal, isAnyExportLoading, handleExportExcel, handleExportExcelGeneral, handleExportPdf, isExcelExportLoading, isExcelGeneralExportLoading, isPdfExportLoading, ]); useEffect(() => { return () => clearTabActions(tabId); }, [tabId, clearTabActions]); const getTableColumns = ( summary: CustomerPaymentSummary ): ColumnDef[] => { const tableColumns: ColumnDef[] = [ { id: 'no', header: 'No', cell: (props) => props.row.index, footer: () =>
Total
, }, { id: 'trans_date', header: 'Tanggal Jual/Bayar', accessorKey: 'trans_date', enableSorting: false, cell: (props) => { const value = props.row.original.trans_date; return value ? formatDate(value, 'DD MMM YYYY') : '-'; }, }, { id: 'realization_date', header: 'Tanggal Realisasi', accessorKey: 'delivery_date', enableSorting: false, cell: (props) => { const value = props.row.original.delivery_date; return value ? formatDate(value, 'DD MMM YYYY') : '-'; }, }, { id: 'aging', header: 'Aging', accessorKey: 'aging_day', enableSorting: false, cell: (props) => { const value = props.row.original.aging_day; return (
{value !== null && value !== undefined ? `${formatNumber(value)} hari` : '-'}
); }, }, { id: 'reference', header: 'Referensi', accessorKey: 'reference', enableSorting: false, cell: (props) => { const value = props.row.original.reference; return value || '-'; }, }, { id: 'vehicle_plate', header: 'Nomor Polisi', accessorKey: 'vehicle_numbers', enableSorting: false, cell: (props) => { const value = props.row.original.vehicle_numbers; return Array.isArray(value) ? value.length > 0 ? value.join(', ') : '-' : '-'; }, }, { id: 'qty', header: 'Qty', accessorKey: 'qty', enableSorting: false, cell: (props) => { const value = props.row.original.qty; return
{formatNumber(value)}
; }, footer: () => (
{formatNumber(summary.total_qty) || '-'}
), }, { id: 'weight', header: 'Berat (Kg)', accessorKey: 'weight', enableSorting: false, cell: (props) => { const value = props.row.original.weight; return
{formatNumber(value)}
; }, footer: () => (
{formatNumber(summary.total_weight) || '-'}
), }, { id: 'average_weight', header: 'AVG', accessorKey: 'average_weight', enableSorting: false, cell: (props) => { const value = props.row.original.average_weight; return
{formatNumber(value)}
; }, footer: () => (
-
), }, { id: 'unit_price', header: 'Harga/Unit', accessorKey: 'unit_price', enableSorting: false, cell: (props) => { const value = props.row.original.unit_price; return
{formatCurrency(value)}
; }, footer: () => (
-
), }, { id: 'final_price', header: 'Harga Akhir', accessorKey: 'final_price', enableSorting: false, cell: (props) => { const value = props.row.original.final_price; return
{formatCurrency(value)}
; }, footer: () => (
{formatCurrency(summary.total_final_amount) || '-'}
), }, { id: 'total', header: 'Total', accessorKey: 'total_price', enableSorting: false, cell: (props) => { const value = props.row.original.total_price; return
{formatCurrency(value)}
; }, footer: () => (
{formatCurrency(summary.total_grand_amount) || '-'}
), }, { id: 'payment', header: 'Pembayaran', accessorKey: 'payment_amount', enableSorting: false, cell: (props) => { const value = props.row.original.payment_amount; return
{formatCurrency(value)}
; }, footer: () => (
{formatCurrency(summary.total_payment) || '-'}
), }, { id: 'accounts_receivable', header: 'Saldo Piutang', accessorKey: 'accounts_receivable', enableSorting: false, cell: (props) => { const value = props.row.original.accounts_receivable; return (
{formatCurrency(value)}
); }, footer: () => (
{formatCurrency(summary.total_accounts_receivable) || '-'}
), }, { id: 'notes', header: 'Keterangan', accessorKey: 'status', enableSorting: false, cell: (props) => { const value = props.row.original.status; if (!value) return '-'; return ( ); }, }, { id: 'pickup_info', header: 'Pengambilan', accessorKey: 'pickup_info', enableSorting: false, cell: (props) => { const value = props.row.original.pickup_info; return Array.isArray(value) ? value.length > 0 ? value.join(', ') : '-' : '-'; }, }, { id: 'sales_marketing', header: 'Sales/Marketing', accessorKey: 'sales_person', enableSorting: false, cell: (props) => { const value = props.row.original.sales_person; return value || '-'; }, }, ]; return tableColumns; }; return ( <>
{isLoading && (
)} {!isLoading && data.length === 0 && ( } title='Data Not Yet Available' subtitle='Please change your filters to get the data.' /> )} {!isLoading && data.length > 0 && meta && (
setPage(Math.max(1, tableFilterState.page - 1))} onNextPage={() => setPage( meta.total_pages && tableFilterState.page < meta.total_pages ? tableFilterState.page + 1 : tableFilterState.page ) } onPageChange={setPage} rowOptions={[10, 20, 50, 100]} onRowChange={setPageSize} />
)} {!isLoading && data.length > 0 && data.map((customerReport) => { const summary = customerReport.summary || { total_qty: 0, total_weight: 0, total_final_amount: 0, total_grand_amount: 0, total_payment: 0, total_accounts_receivable: 0, }; const tableColumns = getTableColumns(summary); return ( 0} className={{ containerClassName: 'w-full mb-0!', tableWrapperClassName: 'overflow-x-auto rounded-tr-none rounded-tl-none', tableClassName: 'w-full table-auto text-sm', headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', headerColumnClassName: 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', bodyRowClassName: 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', bodyColumnClassName: 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', tableFooterClassName: 'bg-gray-100 font-semibold border border-gray-200', footerRowClassName: 'border-t-2 border-gray-300', footerColumnClassName: 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', paginationClassName: 'hidden', }} renderCustomRow={(row) => { if (row.index === 0) { return ( ); } }} /> ); })} {!isLoading && data.length > 0 && meta && (
setPage(Math.max(1, tableFilterState.page - 1))} onNextPage={() => setPage( meta.total_pages && tableFilterState.page < meta.total_pages ? tableFilterState.page + 1 : tableFilterState.page ) } onPageChange={setPage} rowOptions={[10, 20, 50, 100]} onRowChange={setPageSize} />
)} {/* Filter Modal */} {/* Modal Header */}

Filter Data


formik.setFieldValue('customers', Array.isArray(val) ? val : []) } onInputChange={setCustomerInputValue} isLoading={isLoadingCustomers} isClearable onMenuScrollToBottom={loadMoreCustomers} className={{ wrapper: 'w-full' }} /> formik.setFieldValue( 'filterBy', !Array.isArray(val) ? (val ?? undefined) : undefined ) } className={{ wrapper: 'w-full' }} isClearable />
{/* Modal Footer */}
); }; export default CustomerPaymentTab;
{formatCurrency(row.original.accounts_receivable)}