diff --git a/src/components/pages/report/finance/tab/BalanceMonitoringTab.tsx b/src/components/pages/report/finance/tab/BalanceMonitoringTab.tsx index 134f27df..b2920008 100644 --- a/src/components/pages/report/finance/tab/BalanceMonitoringTab.tsx +++ b/src/components/pages/report/finance/tab/BalanceMonitoringTab.tsx @@ -1,21 +1,22 @@ 'use client'; -import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; +import { useState, useMemo, useEffect } from 'react'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; import { useFormik } from 'formik'; import toast from 'react-hot-toast'; -import { ColumnDef, SortingState, Updater } from '@tanstack/react-table'; +import { ColumnDef } from '@tanstack/react-table'; +import { AxiosError } from 'axios'; import { FinanceApi } from '@/services/api/report/finance-report'; import { CustomerApi } from '@/services/api/master-data'; import { UserApi } from '@/services/api/user'; -import SelectInput, { - useSelect, - OptionType, -} from '@/components/input/SelectInput'; +import { useSelect, OptionType } from '@/components/input/SelectInput'; +import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; +import { BaseApiResponse } from '@/types/api/api-general'; +import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; -import Dropdown from '@/components/Dropdown'; import ButtonFilter from '@/components/helper/ButtonFilter'; import { formatCurrency, formatNumber } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; @@ -24,7 +25,6 @@ import { CustomerPaymentRow } from '@/types/api/report/customer-payment'; import Modal, { useModal } from '@/components/Modal'; import Button from '@/components/Button'; import DateInput from '@/components/input/DateInput'; -import Pagination from '@/components/Pagination'; import Table from '@/components/Table'; import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton'; @@ -32,12 +32,15 @@ interface BalanceMonitoringTabProps { tabId: string; } +const filterByOptions: OptionType[] = [ + { label: 'Tanggal Penjualan (SO Date)', value: 'sold_at' }, + { label: 'Tanggal Realisasi (Delivery Date)', value: 'realized_at' }, +]; + const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { - const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); const [hasDateError, setHasDateError] = useState(false); const [dateErrorShown, setDateErrorShown] = useState(false); - const handleFilterModalOpenRef = useRef(() => {}); const filterModal = useModal(); const setTabActions = useTabActionsStore((state) => state.setTabActions); @@ -48,20 +51,23 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { updateFilter, setPage, setPageSize, - toQueryString, + toQueryString: getTableFilterQueryString, + reset: resetFilter, } = useTableFilter<{ start_date: string; end_date: string; - customerFilter?: OptionType; - salesFilter?: OptionType; + customers: OptionType[]; + salesPersons: OptionType[]; + filterBy?: OptionType; sort_by: string; order_by: string; }>({ initial: { start_date: '', end_date: '', - customerFilter: undefined, - salesFilter: undefined, + customers: [], + salesPersons: [], + filterBy: undefined, sort_by: '', order_by: '', }, @@ -70,8 +76,9 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { pageSize: 'limit', start_date: 'start_date', end_date: 'end_date', - customerFilter: 'customer_id', - salesFilter: 'sales_id', + customers: 'customer_ids', + salesPersons: 'sales_ids', + filterBy: 'filter_by', sort_by: 'sort_by', order_by: 'sort_order', }, @@ -79,31 +86,25 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { storeName: 'balance-monitoring-table', }); - // Keep a stable ref so handleExportPDF doesn't need toQueryString as a dep - const toQueryStringRef = useRef(toQueryString); - useEffect(() => { - toQueryStringRef.current = toQueryString; - }); + // const sorting: SortingState = tableFilterState.sort_by + // ? [ + // { + // id: tableFilterState.sort_by, + // desc: tableFilterState.order_by === 'desc', + // }, + // ] + // : []; - const sorting: SortingState = tableFilterState.sort_by - ? [ - { - id: tableFilterState.sort_by, - desc: tableFilterState.order_by === 'desc', - }, - ] - : []; - - const handleSortingChange = (updater: Updater) => { - const next = typeof updater === 'function' ? updater(sorting) : updater; - if (next.length > 0) { - updateFilter('sort_by', next[0].id, true); - updateFilter('order_by', next[0].desc ? 'desc' : 'asc', true); - } else { - updateFilter('sort_by', '', true); - updateFilter('order_by', '', true); - } - }; + // const handleSortingChange = (updater: Updater) => { + // const next = typeof updater === 'function' ? updater(sorting) : updater; + // if (next.length > 0) { + // updateFilter('sort_by', next[0].id, true); + // updateFilter('order_by', next[0].desc ? 'desc' : 'asc', true); + // } else { + // updateFilter('sort_by', '', true); + // updateFilter('order_by', '', true); + // } + // }; const { options: customerOptions, @@ -123,33 +124,40 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { initialValues: { start_date: tableFilterState.start_date, end_date: tableFilterState.end_date, - customerFilter: tableFilterState.customerFilter, - salesFilter: tableFilterState.salesFilter, + customers: tableFilterState.customers, + salesPersons: tableFilterState.salesPersons, + filterBy: tableFilterState.filterBy, }, - enableReinitialize: true, onSubmit: (values) => { updateFilter('start_date', values.start_date, true); updateFilter('end_date', values.end_date, true); - updateFilter('customerFilter', values.customerFilter, true); - updateFilter('salesFilter', values.salesFilter, true); - filterModal.closeModal(); - }, - onReset: () => { - updateFilter('start_date', '', true); - updateFilter('end_date', '', true); - updateFilter('customerFilter', undefined, true); - updateFilter('salesFilter', undefined, true); - setHasDateError(false); - if (dateErrorShown) { - toast.dismiss(); - setDateErrorShown(false); - } + updateFilter('customers', values.customers, true); + updateFilter('salesPersons', values.salesPersons, true); + updateFilter('filterBy', values.filterBy, true); filterModal.closeModal(); }, }); - handleFilterModalOpenRef.current = () => { - filterModal.openModal(); + const formikResetHandler = () => { + resetFilter(); + + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + + formik.resetForm({ + values: { + start_date: '', + end_date: '', + customers: [], + salesPersons: [], + filterBy: undefined, + }, + }); + + filterModal.closeModal(); }; const handleStartDateChange = (e: React.ChangeEvent) => { @@ -201,41 +209,26 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { } }; - const queryString = toQueryString(); - - const { data: response, isLoading } = useSWR(queryString, (qs) => - FinanceApi.getBalanceMonitoringReport( - Object.fromEntries(new URLSearchParams(qs)) as Parameters< - typeof FinanceApi.getBalanceMonitoringReport - >[0] - ) + const { data: balanceMonitoringsResponse, isLoading } = useSWR< + BaseApiResponse, + AxiosError, + SWRHttpKey + >( + `${FinanceApi.basePath}/balance-monitoring${getTableFilterQueryString()}`, + httpClientFetcher ); - const data: BalanceMonitoringRow[] = useMemo( - () => - isResponseSuccess(response) - ? ((response.data as BalanceMonitoringRow[]) ?? []) - : [], - [response] - ); + const balanceMonitorings: BalanceMonitoringRow[] = isResponseSuccess( + balanceMonitoringsResponse + ) + ? ((balanceMonitoringsResponse.data as BalanceMonitoringRow[]) ?? []) + : []; - const meta = useMemo( - () => (isResponseSuccess(response) && response.meta ? response.meta : null), - [response] - ); - - // Stable — uses ref so toQueryString is always current without being a dep - const handleExportPDF = useCallback(async () => { - setIsPdfExportLoading(true); - try { - await FinanceApi.exportBalanceMonitoringToPDF(toQueryStringRef.current()); - toast.success('PDF berhasil dibuat dan diunduh.'); - } catch { - toast.error('Gagal membuat PDF. Silakan coba lagi.'); - } finally { - setIsPdfExportLoading(false); - } - }, []); + const meta = + isResponseSuccess(balanceMonitoringsResponse) && + balanceMonitoringsResponse.meta + ? balanceMonitoringsResponse.meta + : null; // Inject tab actions directly — no nested component, no remount cycle useEffect(() => { @@ -246,85 +239,39 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { values={{ start_date: tableFilterState.start_date, end_date: tableFilterState.end_date, - customerFilter: tableFilterState.customerFilter, - salesFilter: tableFilterState.salesFilter, + customers: tableFilterState.customers, + salesPersons: tableFilterState.salesPersons, + filterBy: tableFilterState.filterBy, }} fieldGroups={[['start_date', 'end_date']]} - onClick={() => handleFilterModalOpenRef.current()} + onClick={filterModal.openModal} variant='outline' className='px-3 py-2.5' /> - - -
- - Export -
- -
- - } - > - -
); - }, [ - tabId, - setTabActions, - isPdfExportLoading, - handleExportPDF, - tableFilterState.start_date, - tableFilterState.end_date, - tableFilterState.customerFilter, - tableFilterState.salesFilter, - ]); + }, [tabId, setTabActions, tableFilterState, filterModal.openModal]); useEffect(() => { return () => clearTabActions(tabId); }, [tabId, clearTabActions]); - const page = meta?.page || tableFilterState.page; - const pageSize = meta?.limit || tableFilterState.pageSize; - const columns = useMemo( (): ColumnDef[] => [ { header: 'No', enableSorting: false, - cell: (props) => (page - 1) * pageSize + props.row.index + 1, + cell: (props) => + (tableFilterState.page - 1) * tableFilterState.pageSize + + props.row.index + + 1, }, { header: 'Customer', - accessorKey: 'customer_name', + accessorKey: 'customer.name', enableSorting: true, id: 'customer_name', + cell: ({ row }) => row.original.customer.name, }, { header: 'Saldo Awal', @@ -342,34 +289,34 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { columns: [ { header: 'Ekor', - accessorKey: 'penjualan_ayam_ekor', + accessorKey: 'penjualan_ayam.ekor', id: 'penjualan_ayam_ekor', enableSorting: true, cell: ({ row }) => (
- {formatNumber(row.original.penjualan_ayam_ekor)} + {formatNumber(row.original.penjualan_ayam.ekor)}
), }, { header: 'Kg', - accessorKey: 'penjualan_ayam_kg', + accessorKey: 'penjualan_ayam.kg', id: 'penjualan_ayam_kg', enableSorting: true, cell: ({ row }) => (
- {formatNumber(row.original.penjualan_ayam_kg)} + {formatNumber(row.original.penjualan_ayam.kg)}
), }, { header: 'Nominal', - accessorKey: 'penjualan_ayam_nominal', + accessorKey: 'penjualan_ayam.nominal', id: 'penjualan_ayam_nominal', enableSorting: true, cell: ({ row }) => (
- {formatCurrency(row.original.penjualan_ayam_nominal)} + {formatCurrency(row.original.penjualan_ayam.nominal)}
), }, @@ -379,35 +326,35 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { header: 'Penjualan Telur', columns: [ { - header: 'Kuantitas', - accessorKey: 'penjualan_telur_kuantitas', - id: 'penjualan_telur_kuantitas', + header: 'Butir', + accessorKey: 'penjualan_telur.butir', + id: 'penjualan_telur_butir', enableSorting: true, cell: ({ row }) => (
- {formatNumber(row.original.penjualan_telur_kuantitas)} + {formatNumber(row.original.penjualan_telur.butir)}
), }, { header: 'Kg', - accessorKey: 'penjualan_telur_kg', + accessorKey: 'penjualan_telur.kg', id: 'penjualan_telur_kg', enableSorting: true, cell: ({ row }) => (
- {formatNumber(row.original.penjualan_telur_kg)} + {formatNumber(row.original.penjualan_telur.kg)}
), }, { header: 'Nominal', - accessorKey: 'penjualan_telur_nominal', + accessorKey: 'penjualan_telur.nominal', id: 'penjualan_telur_nominal', enableSorting: true, cell: ({ row }) => (
- {formatCurrency(row.original.penjualan_telur_nominal)} + {formatCurrency(row.original.penjualan_telur.nominal)}
), }, @@ -415,12 +362,12 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { }, { header: 'Penjualan Trading', - accessorKey: 'penjualan_trading', + accessorKey: 'penjualan_trading.nominal', id: 'penjualan_trading', enableSorting: true, cell: ({ row }) => (
- {formatCurrency(row.original.penjualan_trading)} + {formatCurrency(row.original.penjualan_trading.nominal)}
), }, @@ -471,7 +418,7 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { ), }, ], - [page, pageSize] + [tableFilterState.page, tableFilterState.pageSize] ); return ( @@ -483,7 +430,7 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { )} - {!isLoading && data.length === 0 && ( + {!isLoading && balanceMonitorings.length === 0 && ( []} icon={ @@ -499,20 +446,20 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { /> )} - {!isLoading && data.length > 0 && ( + {!isLoading && balanceMonitorings.length > 0 && ( <>
{ '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', - paginationClassName: 'hidden', }} /> - - {meta && ( -
- setPage(Math.max(1, (meta.page || 1) - 1))} - onNextPage={() => - setPage( - meta.total_pages && (meta.page || 1) < meta.total_pages - ? (meta.page || 1) + 1 - : meta.page || 1 - ) - } - onPageChange={setPage} - rowOptions={[10, 20, 50, 100]} - onRowChange={setPageSize} - /> -
- )} )} @@ -576,7 +501,7 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { - +
- - formik.setFieldValue( - 'customerFilter', - val as OptionType | null - ) + formik.setFieldValue('customers', Array.isArray(val) ? val : []) } onInputChange={setCustomerInput} isLoading={isLoadingCustomers} @@ -620,15 +542,15 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { className={{ wrapper: 'w-full' }} /> - formik.setFieldValue( - 'salesFilter', - val as OptionType | null + 'salesPersons', + Array.isArray(val) ? val : [] ) } onInputChange={setSalesInput} @@ -637,6 +559,21 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { onMenuScrollToBottom={loadMoreSales} className={{ wrapper: 'w-full' }} /> + + + formik.setFieldValue( + 'filterBy', + !Array.isArray(val) ? (val ?? undefined) : undefined + ) + } + isClearable + className={{ wrapper: 'w-full' }} + /> {/* Modal Footer */}