'use client'; import React, { useEffect, useMemo, useState } from 'react'; import { CellContext, ColumnDef, SortingState, Updater, } from '@tanstack/react-table'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; import { useFormik } from 'formik'; import { cn, formatCurrency, formatDate, formatTitleCase } from '@/lib/helper'; import Button from '@/components/Button'; 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 { 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 { getErrorMessage, isResponseSuccess } from '@/lib/api-helper'; import { BankApi, CustomerApi, SupplierApi } from '@/services/api/master-data'; import { Bank } from '@/types/api/master-data/bank'; import Modal, { useModal } from '@/components/Modal'; import PopoverButton from '@/components/popover/PopoverButton'; import PopoverContent from '@/components/popover/PopoverContent'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import toast from 'react-hot-toast'; import RequirePermission from '@/components/helper/RequirePermission'; import ButtonFilter from '@/components/helper/ButtonFilter'; import Dropdown from '@/components/dropdown/Dropdown'; import { FinanceTableFilterSchema, FinanceTableFilterValues, } from '@/components/pages/finance/filter/FinanceFilter'; import FinanceTableSkeleton from '@/components/pages/finance/skeleton/FinanceTableSkeleton'; const RowOptionsMenu = ({ popoverPosition = 'bottom', props, deleteClickHandler, }: { popoverPosition: 'bottom' | 'top'; props: CellContext; deleteClickHandler: () => void; }) => { const popoverId = `finance#${props.row.original.id}`; const popoverAnchorName = `--anchor-finance#${props.row.original.id}`; const closePopover = () => { const popover = document.getElementById(popoverId) as | HTMLDivElement | undefined; popover?.hidePopover?.(); }; 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 { state: tableFilterState, updateFilter, setPage, setPageSize, toQueryString: getTableFilterQueryString, } = useTableFilter({ initial: { search: '', transactionTypes: '', bankIds: '', customerIds: '', supplierIds: '', sort_by: '', orderBy: '', startDate: '', endDate: '', bankNames: '', customerNames: '', supplierNames: '', }, paramMap: { page: 'page', pageSize: 'limit', transactionTypes: 'transaction_types', bankIds: 'bank_ids', customerIds: 'customer_ids', supplierIds: 'supplier_ids', sort_by: 'sort_by', orderBy: 'sort_order', startDate: 'start_date', endDate: 'end_date', }, excludeKeysFromUrl: ['bankNames', 'customerNames', 'supplierNames'], persist: true, storeName: 'finance-table', }); // ===== FILTER MODAL STATE ===== const filterModal = useModal(); // ===== 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 [isExportLoading, setIsExportLoading] = useState(false); const [dateErrorShown, setDateErrorShown] = useState(false); const [hasDateError, setHasDateError] = useState(false); // ===== Formik for Filter ===== const filterFormik = useFormik({ initialValues: { search: tableFilterState.search || '', transaction_types: '', bank_ids: '', customer_ids: '', supplier_ids: '', sort_by: '', start_date: '', end_date: '', }, validationSchema: FinanceTableFilterSchema, onSubmit: (values, { setSubmitting }) => { updateFilter('search', values.search, true); updateFilter('transactionTypes', values.transaction_types, true); updateFilter('bankIds', values.bank_ids, true); updateFilter('customerIds', values.customer_ids, true); updateFilter('supplierIds', values.supplier_ids, true); updateFilter('sort_by', values.sort_by, true); updateFilter('startDate', values.start_date, true); updateFilter('endDate', values.end_date, true); // Save display names for restoration on modal reopen const toNames = (val: OptionType | OptionType[] | null) => val ? (Array.isArray(val) ? val : [val]) .map((o) => String(o.label)) .join(',') : ''; updateFilter('bankNames', toNames(selectedBank), true); updateFilter('customerNames', toNames(selectedCustomerId), true); updateFilter('supplierNames', toNames(selectedSupplierId), true); filterModal.closeModal(); setSubmitting(false); }, onReset: () => { setSelectedTransactionType(null); setSelectedBank(null); setSelectedCustomerId(null); setSelectedSupplierId(null); setSelectedSortBy(null); updateFilter('search', '', true); updateFilter('transactionTypes', '', true); updateFilter('bankIds', '', true); updateFilter('customerIds', '', true); updateFilter('supplierIds', '', true); updateFilter('sort_by', '', true); updateFilter('orderBy', '', true); updateFilter('startDate', '', true); updateFilter('endDate', '', true); updateFilter('bankNames', '', true); updateFilter('customerNames', '', true); updateFilter('supplierNames', '', true); filterModal.closeModal(); }, }); // ===== 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) => { updateFilter('search', e.target.value, true); }; 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 sorting: SortingState = tableFilterState.sort_by ? [ { id: tableFilterState.sort_by, desc: tableFilterState.orderBy === '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('orderBy', next[0].desc ? 'desc' : 'asc', true); } else { updateFilter('sort_by', '', true); updateFilter('orderBy', '', true); } }; 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) { 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 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) { 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); } }; const handleFilterModalOpen = () => { // Restore transaction types from stored comma-separated IDs const txIds = tableFilterState.transactionTypes ? tableFilterState.transactionTypes.split(',') : []; const restoredTxTypes = FINANCE_TRANSACTION_TYPE_OPTIONS.filter((opt) => txIds.includes(String(opt.value)) ); setSelectedTransactionType(restoredTxTypes.length ? restoredTxTypes : null); // Restore banks from stored IDs and names const bankIdList = tableFilterState.bankIds ? tableFilterState.bankIds.split(',') : []; const bankNameList = tableFilterState.bankNames ? tableFilterState.bankNames.split(',') : []; const restoredBanks = bankIdList.map((id, i) => ({ value: id, label: bankNameList[i] || id, })); setSelectedBank(restoredBanks.length ? restoredBanks : null); // Restore customers from stored IDs and names const customerIdList = tableFilterState.customerIds ? tableFilterState.customerIds.split(',') : []; const customerNameList = tableFilterState.customerNames ? tableFilterState.customerNames.split(',') : []; const restoredCustomers = customerIdList.map((id, i) => ({ value: id, label: customerNameList[i] || id, })); setSelectedCustomerId(restoredCustomers.length ? restoredCustomers : null); // Restore suppliers from stored IDs and names const supplierIdList = tableFilterState.supplierIds ? tableFilterState.supplierIds.split(',') : []; const supplierNameList = tableFilterState.supplierNames ? tableFilterState.supplierNames.split(',') : []; const restoredSuppliers = supplierIdList.map((id, i) => ({ value: id, label: supplierNameList[i] || id, })); setSelectedSupplierId(restoredSuppliers.length ? restoredSuppliers : null); // Restore sort by const restoredSortBy = sortByOptions.find( (opt) => String(opt.value) === tableFilterState.sort_by ) || null; setSelectedSortBy(restoredSortBy); // Restore formik values filterFormik.setValues({ search: tableFilterState.search || '', transaction_types: tableFilterState.transactionTypes || '', bank_ids: tableFilterState.bankIds || '', customer_ids: tableFilterState.customerIds || '', supplier_ids: tableFilterState.supplierIds || '', sort_by: tableFilterState.sort_by || '', start_date: tableFilterState.startDate || '', end_date: tableFilterState.endDate || '', }); filterModal.openModal(); }; const exportToExcel = async () => { setIsExportLoading(true); try { await FinanceApi.exportToExcel(getTableFilterQueryString()); toast.success('Excel berhasil dibuat dan diunduh.'); } catch (error) { toast.error( await getErrorMessage(error, 'Gagal mengekspor data finance.') ); } finally { setIsExportLoading(false); } }; const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); await FinanceApi.delete(selectedFinance?.id as number); refreshFinances(); deleteModal.closeModal(); toast.success('Successfully delete Finance!'); setIsDeleteLoading(false); }; const columns: ColumnDef[] = useMemo( () => [ { header: 'ID', accessorKey: 'payment_code', enableSorting: true, }, { header: 'References Number', accessorKey: 'reference_number', enableSorting: true, cell: (props: CellContext) => { const value = props.row.original.reference_number; return {value ?? '-'}; }, }, { header: 'Jenis Transaksi', accessorKey: 'transaction_type', enableSorting: true, cell: (props: CellContext) => { const value = props.row.original.transaction_type .split('_') .join(' '); return {formatTitleCase(value)}; }, }, { header: 'Pihak', accessorKey: 'customer_name', enableSorting: true, cell: (props: CellContext) => { if (props.row.original.party?.id) { return {props.row.original.party?.name}; } return {'-'}; }, }, { header: 'Tanggal Pembayaran', accessorKey: 'payment_date', enableSorting: true, cell: (props) => formatDate(props.row.original.payment_date, 'DD MMM YYYY'), }, { header: 'Tanggal Dibuat', accessorKey: 'created_at', enableSorting: true, cell: (props) => formatDate(props.row.original.created_at, 'DD MMM YYYY'), }, { header: 'Metode Pembayaran', accessorKey: 'payment_method', enableSorting: true, cell: (props: CellContext) => { const value = props.row.original.payment_method.split('_').join(' '); return {formatTitleCase(value)}; }, }, { header: 'Bank', accessorKey: 'bank', enableSorting: true, cell: (props) => props.row.original.bank ? `${props.row.original.bank?.alias} - ${props.row.original.bank?.account_number} - ${props.row.original.bank?.owner}` : '-', }, { header: 'Pengeluaran (Rp)', accessorKey: 'expense_amount', enableSorting: true, cell: (props) => formatCurrency(Math.abs(props.row.original.expense_amount)), }, { header: 'Pemasukan (Rp)', accessorKey: 'income_amount', enableSorting: true, cell: (props) => formatCurrency(Math.abs(props.row.original.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 ( ); }, }, ], [deleteModal] ); useEffect(() => { return () => { if (dateErrorShown) { toast.dismiss(); } }; }, [dateErrorShown]); return ( <>
{/* Header Section */}
{/* Action Buttons */}
{/* Search and Filter */}
} className={{ wrapper: 'w-full min-w-24 max-w-3xs', inputWrapper: 'rounded-xl! shadow-button-soft', input: 'placeholder:font-semibold placeholder:text-base-content/50', }} />
Ekspor
} >
{/* Table Section */}
{isLoading ? (
) : !isResponseSuccess(finances) || finances.data?.length === 0 ? (
} />
) : ( data={isResponseSuccess(finances) ? finances.data : []} columns={columns} pageSize={tableFilterState.pageSize} page={tableFilterState.page} totalItems={ isResponseSuccess(finances) ? finances.meta?.total_results : 0 } onPageChange={setPage} onPageSizeChange={setPageSize} isLoading={isLoading} sorting={sorting} setSorting={handleSortingChange} manualSorting className={{ containerClassName: cn('p-3 mb-0'), headerColumnClassName: 'text-nowrap', }} /> )}
{/* Filter Modal */} {/* Modal Header */}

Filter Data

Tanggal

{}} closeMenuOnSelect={false} isClearable isMulti className={{ wrapper: 'w-full' }} />
{/* Modal Footer */}
); }; export default FinanceTable;