'use client'; import React, { useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { CellContext, ColumnDef } 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 { 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 { useUiStore } from '@/stores/ui/ui.store'; 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 { 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', }, }); // ===== 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 [dateErrorShown, setDateErrorShown] = useState(false); const [hasDateError, setHasDateError] = 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); filterModal.closeModal(); }, onReset: () => { updateFilter('search', ''); resetSearchValue(); updateFilter('transactionTypes', ''); updateFilter('bankIds', ''); updateFilter('customerIds', ''); updateFilter('supplierIds', ''); updateFilter('sortBy', ''); updateFilter('startDate', ''); updateFilter('endDate', ''); }, }); // ===== 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]); // ===== ACTIVE FILTERS COUNT ===== const activeFiltersCount = useMemo(() => { let count = 0; if (tableFilterState.transactionTypes) count += 1; if (tableFilterState.bankIds) count += 1; if (tableFilterState.customerIds) count += 1; if (tableFilterState.supplierIds) count += 1; if (tableFilterState.sortBy) count += 1; if (tableFilterState.startDate) count += 1; if (tableFilterState.endDate) count += 1; return count; }, [ tableFilterState.transactionTypes, tableFilterState.bankIds, tableFilterState.customerIds, tableFilterState.supplierIds, tableFilterState.sortBy, tableFilterState.startDate, tableFilterState.endDate, ]); const hasFilters = activeFiltersCount > 0; // ===== Handler ===== const searchChangeHandler = useCallback( (e: React.ChangeEvent) => { updateFilter('search', e.target.value); setSearchValue(e.target.value); setPage(1); }, [updateFilter, setSearchValue, setPage] ); 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) { 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 = () => { filterModal.openModal(); filterFormik.validateForm(); }; 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: ColumnDef[] = useMemo( () => [ { 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 ( ); }, }, ], [deleteModal] ); 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 ( <>
{/* 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', }} />
{/* 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} 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;