feat(FE-350): create FinancesTable component

This commit is contained in:
ValdiANS
2025-12-10 11:12:55 +07:00
parent 3951f197e3
commit fa09e140c0
@@ -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<Finance, unknown>;
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<div className='w-full max-h-40 overflow-auto flex flex-col gap-1'>
<Button
href={`/finance/edit/${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='mdi:pencil-outline' width={16} height={16} />
Edit
</Button>
<Button
variant='ghost'
color='error'
className='justify-start text-sm'
// Implement delete handler later
>
<Icon icon='mdi:delete-outline' width={16} height={16} />
Delete
</Button>
</div>
</RowOptionsMenuWrapper>
);
};
const FinancesTable = () => {
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter<FinanceTableFilter>({
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<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
// Filter Selection States
const [selectedTransactionType, setSelectedTransactionType] =
useState<OptionType | null>(null);
const [selectedCustomer, setSelectedCustomer] = useState<OptionType | null>(
null
);
const [selectedSupplier, setSelectedSupplier] = useState<OptionType | null>(
null
);
const [selectedKandang, setSelectedKandang] = useState<OptionType | null>(
null
);
const [selectedBank, setSelectedBank] = useState<OptionType | null>(null);
const [selectedSortBy, setSelectedSortBy] = useState<OptionType | null>(null);
// APIs for SelectInputs
const {
options: customerOptions,
isLoadingOptions: isLoadingCustomerOptions,
setInputValue: setCustomerInputValue,
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
const {
options: supplierOptions,
isLoadingOptions: isLoadingSupplierOptions,
setInputValue: setSupplierInputValue,
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
const {
options: kandangOptions,
isLoadingOptions: isLoadingKandangOptions,
setInputValue: setKandangInputValue,
} = useSelect<Kandang>(KandangApi.basePath, 'id', 'name');
const {
options: bankOptions,
isLoadingOptions: isLoadingBankOptions,
setInputValue: setBankInputValue,
} = useSelect<Bank>(BankApi.basePath, 'id', 'name');
const financeColumns: ColumnDef<Finance>[] = [
{
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) => (
<span className='text-error'>
{formatCurrency(props.row.original.expense_amount)}
</span>
),
},
{
accessorKey: 'revenue_amount',
header: 'Pemasukan',
cell: (props) => (
<span className='text-success'>
{formatCurrency(props.row.original.revenue_amount)}
</span>
),
},
{
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 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu type='dropdown' props={props} />
</RowDropdownOptions>
)}
{currentPageSize <= 3 && (
<RowCollapseOptions>
<RowOptionsMenu type='collapse' props={props} />
</RowCollapseOptions>
)}
</>
);
},
},
];
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (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 (
<div className='w-full p-0 sm:p-4'>
<div className='flex flex-col gap-4 mb-4'>
{/* Row 1: Search */}
<div className='w-full'>
<DebouncedTextInput
name='search'
placeholder='Search'
value={tableFilterState.search}
onChange={searchChangeHandler}
className={{ wrapper: 'w-full' }}
/>
</div>
{/* Row 2: Transaction Type, Customer, Supplier, Mitra */}
<div className='grid grid-cols-12 gap-4'>
<SelectInput
label='Jenis Transaksi'
placeholder='Semua Jenis Transaksi'
options={TRANSACTION_TYPE_OPTIONS}
value={selectedTransactionType}
onChange={(val) =>
handleFilterChange(
val as OptionType,
setSelectedTransactionType,
'transactionType'
)
}
isClearable
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
/>
<SelectInput
label='Customer'
placeholder='Pilih Customer'
options={customerOptions}
isLoading={isLoadingCustomerOptions}
onInputChange={setCustomerInputValue}
value={selectedCustomer}
onChange={(val) =>
handleFilterChange(
val as OptionType,
setSelectedCustomer,
'customerId'
)
}
isClearable
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
/>
<SelectInput
label='Supplier'
placeholder='Pilih Supplier'
options={supplierOptions}
isLoading={isLoadingSupplierOptions}
onInputChange={setSupplierInputValue}
value={selectedSupplier}
onChange={(val) =>
handleFilterChange(
val as OptionType,
setSelectedSupplier,
'supplierId'
)
}
isClearable
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
/>
<SelectInput
label='Mitra'
placeholder='Pilih Kandang Mitra'
options={kandangOptions}
isLoading={isLoadingKandangOptions}
onInputChange={setKandangInputValue}
value={selectedKandang}
onChange={(val) =>
handleFilterChange(
val as OptionType,
setSelectedKandang,
'kandangId'
)
}
isClearable
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
/>
</div>
{/* Row 3: Bank, Sort By, Start Date, End Date */}
<div className='grid grid-cols-12 gap-4'>
<SelectInput
label='Bank'
placeholder='Pilih Bank'
options={bankOptions}
isLoading={isLoadingBankOptions}
onInputChange={setBankInputValue}
value={selectedBank}
onChange={(val) =>
handleFilterChange(val as OptionType, setSelectedBank, 'bankId')
}
isClearable
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
/>
<SelectInput
label='Urutkan Berdasarkan'
placeholder='Tanggal Pembayaran'
options={SORT_OPTIONS}
value={selectedSortBy}
onChange={(val) =>
handleFilterChange(val as OptionType, setSelectedSortBy, 'sortBy')
}
isClearable
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
/>
<DateInput
name='startDate'
label='Periode Tanggal'
placeholder='Pilih Tanggal Mulai'
value={tableFilterState.startDate}
onChange={(e) => updateFilter('startDate', e.target.value)}
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
/>
<DateInput
name='endDate'
label=' '
placeholder='Pilih Tanggal Selesai'
value={tableFilterState.endDate}
onChange={(e) => updateFilter('endDate', e.target.value)}
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
/>
</div>
{/* Row 4: Page Size, Filter Actions */}
<div className='flex flex-row justify-between items-center mt-2'>
<div className='w-20'>
{/* 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. */}
</div>
<div className='flex flex-row gap-2'>
<Button
onClick={() => {
/* Trigger search/filter explicitly if needed, but useEffect/onChange handles it */
}}
color='primary'
className='min-w-20'
>
Cari
</Button>
<Button
onClick={resetFilters}
variant='outline'
color='warning'
className='min-w-20'
>
Reset
</Button>
</div>
</div>
</div>
<Table<Finance>
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,
}),
}}
/>
</div>
);
};
export default FinancesTable;