import { useEffect, useMemo, useRef, useState } from 'react'; import { CellContext } from '@tanstack/react-table'; import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { useFormik } from 'formik'; import Button from '@/components/Button'; import Card from '@/components/Card'; import DateInput from '@/components/input/DateInput'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import SelectInput, { OptionType, useSelect, } from '@/components/input/SelectInput'; import Table from '@/components/Table'; import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { Finance } from '@/types/api/finance/finance'; import { FINANCE_INITIAL_BALANCE_STATUS, FINANCE_INJECTION_STATUS, FINANCE_TRANSACTION_STATUS, FINANCE_TRANSACTION_TYPE_OPTIONS, } from '@/config/constant'; import { FinanceApi } from '@/services/api/finance'; import { isResponseSuccess } from '@/lib/api-helper'; import { BankApi, CustomerApi, SupplierApi } from '@/services/api/master-data'; import { Bank } from '@/types/api/master-data/bank'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import toast from 'react-hot-toast'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import RequirePermission from '@/components/helper/RequirePermission'; import { Icon } from '@iconify/react'; import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import { useUiStore } from '@/stores/ui/ui.store'; import { FinanceTableFilterSchema, FinanceTableFilterValues, } from './FinanceTableFilter.schema'; const RowOptionsMenu = ({ type = 'dropdown', props, deleteClickHandler, }: { type: 'dropdown' | 'collapse'; props: CellContext; deleteClickHandler: () => void; }) => { return ( {FINANCE_TRANSACTION_STATUS.includes( props.row.original.transaction_type ) && ( )} {FINANCE_INITIAL_BALANCE_STATUS.includes( props.row.original.transaction_type ) && ( )} {FINANCE_INJECTION_STATUS.includes( props.row.original.transaction_type ) && ( )} ); }; const FinanceTable = () => { const { searchValue, setSearchValue, resetSearchValue } = useUiStore(); const previousPathRef = useRef(null); const { state: tableFilterState, updateFilter, setPage, setPageSize, toQueryString: getTableFilterQueryString, } = useTableFilter({ initial: { search: searchValue, transactionTypes: '', bankIds: '', customerIds: '', supplierIds: '', sortBy: '', startDate: '', endDate: '', }, paramMap: { page: 'page', pageSize: 'limit', transactionTypes: 'transaction_types', bankIds: 'bank_ids', customerIds: 'customer_ids', supplierIds: 'supplier_ids', sortBy: 'sort_date', startDate: 'start_date', endDate: 'end_date', }, }); // ===== State ===== const deleteModal = useModal(); const [selectedTransactionType, setSelectedTransactionType] = useState< OptionType | OptionType[] | null >(null); const [selectedBank, setSelectedBank] = useState< OptionType | OptionType[] | null >(null); const [selectedCustomerId, setSelectedCustomerId] = useState< OptionType | OptionType[] | null >(null); const [selectedSupplierId, setSelectedSupplierId] = useState< OptionType | OptionType[] | null >(null); const [selectedSortBy, setSelectedSortBy] = useState(null); const [selectedFinance, setSelectedFinance] = useState(null); const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [dateErrorShown, setDateErrorShown] = useState(false); // ===== Formik for Filter ===== const filterFormik = useFormik({ initialValues: { search: searchValue, transaction_types: '', bank_ids: '', customer_ids: '', supplier_ids: '', sort_by: '', start_date: '', end_date: '', }, validationSchema: FinanceTableFilterSchema, enableReinitialize: true, onSubmit: (values) => { updateFilter('search', values.search); setSearchValue(values.search); updateFilter('transactionTypes', values.transaction_types); updateFilter('bankIds', values.bank_ids); updateFilter('customerIds', values.customer_ids); updateFilter('supplierIds', values.supplier_ids); updateFilter('sortBy', values.sort_by); updateFilter('startDate', values.start_date); updateFilter('endDate', values.end_date); }, }); // ===== Fetch Data ===== const { data: finances, isLoading, mutate: refreshFinances, } = useSWR( `${FinanceApi.basePath}/transactions${getTableFilterQueryString()}`, FinanceApi.getAllFetcher ); const { options: customerOptions, isLoadingOptions: customerIsLoadingOptions, setInputValue: customerInputValue, loadMore: customerLoadMore, } = useSelect(CustomerApi.basePath, 'id', 'name'); const { options: supplierOptions, isLoadingOptions: supplierIsLoadingOptions, setInputValue: supplierInputValue, loadMore: supplierLoadMore, } = useSelect(SupplierApi.basePath, 'id', 'name'); const sortByOptions = useMemo(() => { return [ { label: 'Tanggal Pembayaran', value: 'payment_date' }, { label: 'Tanggal Dibuat', value: 'created_at' }, ]; }, []); const { options: bankOptions, rawData: bankRawData, setInputValue: bankInputValue, loadMore: bankLoadMore, } = useSelect(BankApi.basePath, 'id', 'alias'); const bankSelectOptions = useMemo(() => { if (!isResponseSuccess(bankRawData)) return []; return bankOptions.map((bank) => { const bankData = bankRawData.data.find((data) => data.id === bank?.value); return { label: bankData ? `${bankData.alias} - ${bankData.account_number} - ${bankData.owner}` : '', value: bank?.value, }; }); }, [bankOptions, bankRawData]); // ===== Handler ===== const searchChangeHandler = (e: React.ChangeEvent) => { filterFormik.setFieldValue('search', e.target.value); }; const transactionTypeChangeHandler = ( val: OptionType | OptionType[] | null ) => { setSelectedTransactionType(val); filterFormik.setFieldValue( 'transaction_types', val ? Array.isArray(val) ? val.map((item) => item.value).join(',') : (val.value as string) : '' ); }; const bankChangeHandler = (val: OptionType | OptionType[] | null) => { setSelectedBank(val); filterFormik.setFieldValue( 'bank_ids', val ? Array.isArray(val) ? val.map((item) => item.value).join(',') : (val.value as string) : '' ); }; const customerIdChangeHandler = (val: OptionType | OptionType[] | null) => { setSelectedCustomerId(val); filterFormik.setFieldValue( 'customer_ids', val ? Array.isArray(val) ? val.map((item) => item.value).join(',') : (val.value as string) : '' ); }; const supplierIdChangeHandler = (val: OptionType | OptionType[] | null) => { setSelectedSupplierId(val); filterFormik.setFieldValue( 'supplier_ids', val ? Array.isArray(val) ? val.map((item) => item.value).join(',') : (val.value as string) : '' ); }; const sortByChangeHandler = (val: OptionType | OptionType[] | null) => { setSelectedSortBy(val as OptionType); filterFormik.setFieldValue( 'sort_by', val ? ((val as OptionType).value as string) : '' ); }; const startDateChangeHandler = (e: React.ChangeEvent) => { const value = e.target.value; const endDate = filterFormik.values.end_date; filterFormik.setFieldValue('start_date', value); if (value && endDate) { const startDate = new Date(value); const endDateObj = new Date(endDate); if (endDateObj < startDate) { filterFormik.setFieldError( 'end_date', 'Tanggal akhir tidak boleh masa lampau' ); if (!dateErrorShown) { toast.error('Tanggal akhir tidak boleh masa lampau', { duration: Infinity, }); setDateErrorShown(true); } } else { filterFormik.setFieldError('end_date', undefined); if (dateErrorShown) { toast.dismiss(); setDateErrorShown(false); } } } }; const endDateChangeHandler = (e: React.ChangeEvent) => { const value = e.target.value; const startDate = filterFormik.values.start_date; filterFormik.setFieldValue('end_date', value); if (value && startDate) { const startDateObj = new Date(startDate); const endDate = new Date(value); if (endDate < startDateObj) { filterFormik.setFieldError( 'end_date', 'Tanggal akhir tidak boleh masa lampau' ); if (!dateErrorShown) { toast.error('Tanggal akhir tidak boleh masa lampau', { duration: Infinity, }); setDateErrorShown(true); } return; } } filterFormik.setFieldError('end_date', undefined); if (dateErrorShown) { toast.dismiss(); setDateErrorShown(false); } }; const resetFilterHandler = () => { setSelectedTransactionType(null); setSelectedBank(null); setSelectedCustomerId(null); setSelectedSupplierId(null); setSelectedSortBy(null); filterFormik.resetForm(); updateFilter('search', ''); resetSearchValue(); updateFilter('transactionTypes', ''); updateFilter('bankIds', ''); updateFilter('customerIds', ''); updateFilter('supplierIds', ''); updateFilter('sortBy', ''); updateFilter('startDate', ''); updateFilter('endDate', ''); }; const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); await FinanceApi.delete(selectedFinance?.id as number); refreshFinances(); deleteModal.closeModal(); toast.success('Successfully delete Finance!'); setIsDeleteLoading(false); }; const columns = useMemo(() => { return [ { header: 'ID', accessorKey: 'payment_code', }, { header: 'References Number', accessorKey: 'reference_number', cell: (props: CellContext) => { const value = props.row.original.reference_number; return {value ?? '-'}; }, }, { header: 'Jenis Transaksi', accessorKey: 'transaction_type', cell: (props: CellContext) => { const value = props.row.original.transaction_type .split('_') .join(' '); return {formatTitleCase(value)}; }, }, { header: 'Pihak', accessorFn: (finance: Finance) => finance.party?.name, cell: (props: CellContext) => { if (props.row.original.party?.id) { return {props.row.original.party?.name}; } return {'-'}; }, }, { header: 'Tanggal', accessorFn: (finance: Finance) => formatDate(finance.payment_date, 'DD MMM YYYY'), }, { header: 'Metode Pembayaran', accessorKey: 'payment_method', cell: (props: CellContext) => { const value = props.row.original.payment_method.split('_').join(' '); return {formatTitleCase(value)}; }, }, { header: 'Bank', accessorFn: (finance: Finance) => finance.bank ? `${finance.bank?.alias} - ${finance.bank?.account_number} - ${finance.bank?.owner}` : '-', }, { header: 'Pengeluaran (Rp)', accessorFn: (finance: Finance) => formatCurrency(Math.abs(finance.expense_amount)), }, { header: 'Pemasukan (Rp)', accessorFn: (finance: Finance) => formatCurrency(Math.abs(finance.income_amount)), }, { header: 'Aksi', cell: (props: CellContext) => { const currentPageSize = props.table.getPaginationRowModel().rows.length; const currentPageRows = props.table.getPaginationRowModel().flatRows; const currentRowRelativeIndex = currentPageRows.findIndex((r) => r.id === props.row.id) + 1; const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; const deleteClickHandler = () => { setSelectedFinance(props.row.original); deleteModal.openModal(); }; return ( <> {currentPageSize > 2 && ( )} {currentPageSize <= 2 && ( )} ); }, }, ]; }, []); useEffect(() => { return () => { if (dateErrorShown) { toast.dismiss(); } }; }, [dateErrorShown]); useEffect(() => { previousPathRef.current = window.location.pathname; return () => { const currentPath = window.location.pathname; const isCurrentPathFinance = currentPath.includes('/finance'); const isPreviousPathFinance = previousPathRef.current?.includes('/finance'); if (isPreviousPathFinance && !isCurrentPathFinance) { resetSearchValue(); } if (dateErrorShown) { toast.dismiss(); setDateErrorShown(false); } }; }, [resetSearchValue, dateErrorShown]); return (
} >
data={isResponseSuccess(finances) ? finances.data : []} columns={columns} pageSize={tableFilterState.pageSize} page={tableFilterState.page} onPageChange={setPage} onPageSizeChange={setPageSize} totalItems={ isResponseSuccess(finances) ? finances.meta?.total_results : 0 } isLoading={isLoading} />
); }; export default FinanceTable;