mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat(FE-350): create FinancesTable component
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user