From fa09e140c00d91590b9a77036c6beb1b4fd690a7 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 10 Dec 2025 11:12:55 +0700 Subject: [PATCH] feat(FE-350): create FinancesTable component --- .../pages/finance/FinancesTable.tsx | 484 ++++++++++++++++++ 1 file changed, 484 insertions(+) create mode 100644 src/components/pages/finance/FinancesTable.tsx diff --git a/src/components/pages/finance/FinancesTable.tsx b/src/components/pages/finance/FinancesTable.tsx new file mode 100644 index 00000000..4cd6f571 --- /dev/null +++ b/src/components/pages/finance/FinancesTable.tsx @@ -0,0 +1,484 @@ +'use client'; + +import { ChangeEventHandler, useEffect, useState } from 'react'; +import useSWR from 'swr'; +import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; + +import { Icon } from '@iconify/react'; +import Table from '@/components/Table'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import Button from '@/components/Button'; +import RowDropdownOptions from '@/components/table/RowDropdownOptions'; +import RowCollapseOptions from '@/components/table/RowCollapseOptions'; +import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; +import DateInput from '@/components/input/DateInput'; +import { cn, formatCurrency, formatDate } from '@/lib/helper'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { FinanceApi } from '@/services/api/finance'; +import { Finance } from '@/types/api/finance'; +import { + BankApi, + CustomerApi, + KandangApi, + SupplierApi, +} from '@/services/api/master-data'; +import { Customer } from '@/types/api/master-data/customer'; +import { Supplier } from '@/types/api/master-data/supplier'; +import { Kandang } from '@/types/api/master-data/kandang'; +import { Bank } from '@/types/api/master-data/bank'; + +type FinanceTableFilter = { + search: string; + nameSort: string; + transactionType: string; + customerId: string; + supplierId: string; + kandangId: string; + bankId: string; + sortBy: string; + startDate: string; + endDate: string; +}; + +const TRANSACTION_TYPE_OPTIONS = [ + { value: 'REVENUE', label: 'Pemasukan' }, + { value: 'EXPENSE', label: 'Pengeluaran' }, +]; + +const SORT_OPTIONS = [ + { value: 'payment_date', label: 'Tanggal Pembayaran' }, + { value: 'created_date', label: 'Tanggal Dibuat' }, +]; + +const RowOptionsMenu = ({ + type = 'dropdown', + props, +}: { + type: 'dropdown' | 'collapse'; + props: CellContext; +}) => { + return ( + +
+ + +
+
+ ); +}; + +const FinancesTable = () => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { + search: '', + nameSort: '', + transactionType: '', + customerId: '', + supplierId: '', + kandangId: '', + bankId: '', + sortBy: '', + startDate: '', + endDate: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + nameSort: 'sort_name', + transactionType: 'transaction_type', + customerId: 'customer_id', + supplierId: 'supplier_id', + kandangId: 'kandang_id', + bankId: 'bank_id', + sortBy: 'sort_by', + startDate: 'start_date', + endDate: 'end_date', + }, + }); + + const { data: finances, isLoading: isLoadingFinances } = useSWR( + `${FinanceApi.basePath}${getTableFilterQueryString()}`, + FinanceApi.getAllFetcher + ); + + const [sorting, setSorting] = useState([]); + const [rowSelection, setRowSelection] = useState>({}); + + // Filter Selection States + const [selectedTransactionType, setSelectedTransactionType] = + useState(null); + const [selectedCustomer, setSelectedCustomer] = useState( + null + ); + const [selectedSupplier, setSelectedSupplier] = useState( + null + ); + const [selectedKandang, setSelectedKandang] = useState( + null + ); + const [selectedBank, setSelectedBank] = useState(null); + const [selectedSortBy, setSelectedSortBy] = useState(null); + + // APIs for SelectInputs + const { + options: customerOptions, + isLoadingOptions: isLoadingCustomerOptions, + setInputValue: setCustomerInputValue, + } = useSelect(CustomerApi.basePath, 'id', 'name'); + + const { + options: supplierOptions, + isLoadingOptions: isLoadingSupplierOptions, + setInputValue: setSupplierInputValue, + } = useSelect(SupplierApi.basePath, 'id', 'name'); + + const { + options: kandangOptions, + isLoadingOptions: isLoadingKandangOptions, + setInputValue: setKandangInputValue, + } = useSelect(KandangApi.basePath, 'id', 'name'); + + const { + options: bankOptions, + isLoadingOptions: isLoadingBankOptions, + setInputValue: setBankInputValue, + } = useSelect(BankApi.basePath, 'id', 'name'); + + const financeColumns: ColumnDef[] = [ + { + header: '#', + cell: (props) => props.row.index + 1, + }, + { + accessorKey: 'reference_number', + header: 'No Referensi', + }, + { + accessorKey: 'transaction_type', + header: 'Jenis Transaksi', + }, + { + accessorKey: 'customer_name', + header: 'Pelanggan', + }, + { + accessorKey: 'payment_date', + header: 'Tanggal Pembayaran', + cell: (props) => + formatDate(props.row.original.payment_date, 'DD MMM YYYY'), + }, + { + accessorKey: 'created_date', + header: 'Tanggal Dibuat', + cell: (props) => + formatDate(props.row.original.created_date, 'DD MMM YYYY'), + }, + { + accessorKey: 'payment_method', + header: 'Metode Pembayaran', + }, + { + accessorKey: 'bank_name', + header: 'Bank', + }, + { + accessorKey: 'expense_amount', + header: 'Pengeluaran', + cell: (props) => ( + + {formatCurrency(props.row.original.expense_amount)} + + ), + }, + { + accessorKey: 'revenue_amount', + header: 'Pemasukan', + cell: (props) => ( + + {formatCurrency(props.row.original.revenue_amount)} + + ), + }, + { + header: 'Aksi', + cell: (props) => { + 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 - 3; + + return ( + <> + {currentPageSize > 3 && ( + + + + )} + + {currentPageSize <= 3 && ( + + + + )} + + ); + }, + }, + ]; + + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; + + const handleFilterChange = ( + value: OptionType | null, + setter: (val: OptionType | null) => void, + filterKey: keyof FinanceTableFilter + ) => { + setter(value); + updateFilter(filterKey, value ? String(value.value) : ''); + }; + + const resetFilters = () => { + setSelectedTransactionType(null); + setSelectedCustomer(null); + setSelectedSupplier(null); + setSelectedKandang(null); + setSelectedBank(null); + setSelectedSortBy(null); + + updateFilter('search', ''); + updateFilter('transactionType', ''); + updateFilter('customerId', ''); + updateFilter('supplierId', ''); + updateFilter('kandangId', ''); + updateFilter('bankId', ''); + updateFilter('sortBy', ''); + updateFilter('startDate', ''); + updateFilter('endDate', ''); + }; + + // track sorting + useEffect(() => { + const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); + + if (!isNameSorted) { + updateFilter('nameSort', ''); + } else { + updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); + } + }, [sorting, updateFilter]); + + return ( +
+
+ {/* Row 1: Search */} +
+ +
+ + {/* Row 2: Transaction Type, Customer, Supplier, Mitra */} +
+ + handleFilterChange( + val as OptionType, + setSelectedTransactionType, + 'transactionType' + ) + } + isClearable + className={{ wrapper: 'col-span-12 sm:col-span-3' }} + /> + + handleFilterChange( + val as OptionType, + setSelectedCustomer, + 'customerId' + ) + } + isClearable + className={{ wrapper: 'col-span-12 sm:col-span-3' }} + /> + + handleFilterChange( + val as OptionType, + setSelectedSupplier, + 'supplierId' + ) + } + isClearable + className={{ wrapper: 'col-span-12 sm:col-span-3' }} + /> + + handleFilterChange( + val as OptionType, + setSelectedKandang, + 'kandangId' + ) + } + isClearable + className={{ wrapper: 'col-span-12 sm:col-span-3' }} + /> +
+ + {/* Row 3: Bank, Sort By, Start Date, End Date */} +
+ + handleFilterChange(val as OptionType, setSelectedBank, 'bankId') + } + isClearable + className={{ wrapper: 'col-span-12 sm:col-span-3' }} + /> + + handleFilterChange(val as OptionType, setSelectedSortBy, 'sortBy') + } + isClearable + className={{ wrapper: 'col-span-12 sm:col-span-3' }} + /> + updateFilter('startDate', e.target.value)} + className={{ wrapper: 'col-span-12 sm:col-span-3' }} + /> + updateFilter('endDate', e.target.value)} + className={{ wrapper: 'col-span-12 sm:col-span-3' }} + /> +
+ + {/* Row 4: Page Size, Filter Actions */} +
+
+ {/* Page size selector logic is inside Table usually, or we can add it here if needed, but Table component handles it via props. Let's keep it clean or add a rows selector if requested. Screenshot shows 'Baris 10' dropdown. */} + {/* Table component usually has a prop for rowOptions, but doesn't expose the selector UI outside unless we use TableRowSizeSelector. Let's just rely on Table's implicit size control or add it if strictly needed. The screenshot shows it outside. Implementing minimally for now. */} +
+
+ + +
+
+
+ + + data={isResponseSuccess(finances) ? finances?.data : []} + columns={financeColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={isResponseSuccess(finances) ? finances?.meta?.page : 0} + totalItems={ + isResponseSuccess(finances) ? finances?.meta?.total_results : 0 + } + onPageChange={setPage} + isLoading={isLoadingFinances} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn({ + 'w-full mb-20': + isResponseSuccess(finances) && finances?.data?.length === 0, + }), + }} + /> +
+ ); +}; + +export default FinancesTable;