mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 21:41:57 +00:00
923 lines
30 KiB
TypeScript
923 lines
30 KiB
TypeScript
'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<Finance, unknown>;
|
|
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 (
|
|
<div className='relative'>
|
|
<PopoverButton
|
|
tabIndex={0}
|
|
variant='ghost'
|
|
color='none'
|
|
popoverTarget={popoverId}
|
|
anchorName={popoverAnchorName}
|
|
>
|
|
<Icon icon='material-symbols:more-vert' width={16} height={16} />
|
|
</PopoverButton>
|
|
|
|
<PopoverContent
|
|
id={popoverId}
|
|
anchorName={popoverAnchorName}
|
|
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
|
|
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
|
|
>
|
|
<div className='flex flex-col bg-base-100 rounded-xl'>
|
|
<RequirePermission
|
|
permissions={[
|
|
'lti.finance.transactions.detail',
|
|
'lti.finance.initial_balances.detail',
|
|
'lti.finance.injections.detail',
|
|
'lti.finance.payments.detail',
|
|
]}
|
|
>
|
|
<Button
|
|
href={`/finance/detail?financeId=${props.row.original.id}`}
|
|
variant='ghost'
|
|
color='none'
|
|
className='p-3 justify-start text-sm font-semibold w-full'
|
|
onClick={closePopover}
|
|
>
|
|
<Icon icon='heroicons:eye' width={20} height={20} />
|
|
Detail
|
|
</Button>
|
|
</RequirePermission>
|
|
|
|
{FINANCE_TRANSACTION_STATUS.includes(
|
|
props.row.original.transaction_type
|
|
) && (
|
|
<RequirePermission permissions='lti.finance.payments.update'>
|
|
<Button
|
|
href={`/finance/detail/edit?financeId=${props.row.original.id}`}
|
|
variant='ghost'
|
|
color='none'
|
|
className='p-3 justify-start text-sm font-semibold w-full'
|
|
onClick={closePopover}
|
|
>
|
|
<Icon icon='mdi:pencil-outline' width={20} height={20} />
|
|
Edit
|
|
</Button>
|
|
</RequirePermission>
|
|
)}
|
|
|
|
{FINANCE_INITIAL_BALANCE_STATUS.includes(
|
|
props.row.original.transaction_type
|
|
) && (
|
|
<RequirePermission permissions='lti.finance.initial_balances.update'>
|
|
<Button
|
|
href={`/finance/detail/edit/initial-balance?financeId=${props.row.original.id}`}
|
|
variant='ghost'
|
|
color='none'
|
|
className='p-3 justify-start text-sm font-semibold w-full'
|
|
onClick={closePopover}
|
|
>
|
|
<Icon icon='mdi:pencil-outline' width={20} height={20} />
|
|
Edit
|
|
</Button>
|
|
</RequirePermission>
|
|
)}
|
|
|
|
{FINANCE_INJECTION_STATUS.includes(
|
|
props.row.original.transaction_type
|
|
) && (
|
|
<RequirePermission permissions='lti.finance.injections.update'>
|
|
<Button
|
|
href={`/finance/detail/edit/injection?financeId=${props.row.original.id}`}
|
|
variant='ghost'
|
|
color='none'
|
|
className='p-3 justify-start text-sm font-semibold w-full'
|
|
onClick={closePopover}
|
|
>
|
|
<Icon icon='mdi:pencil-outline' width={20} height={20} />
|
|
Edit
|
|
</Button>
|
|
</RequirePermission>
|
|
)}
|
|
|
|
<RequirePermission permissions='lti.finance.transactions.delete'>
|
|
<Button
|
|
onClick={() => {
|
|
deleteClickHandler();
|
|
closePopover();
|
|
}}
|
|
variant='ghost'
|
|
color='error'
|
|
className='p-3 justify-start text-sm font-semibold w-full focus-visible:text-error-content hover:text-error-content'
|
|
>
|
|
<Icon icon='mdi:delete-outline' width={20} height={20} />
|
|
Delete
|
|
</Button>
|
|
</RequirePermission>
|
|
</div>
|
|
</PopoverContent>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const FinanceTable = () => {
|
|
const { searchValue, setSearchValue, resetSearchValue } = useUiStore();
|
|
const previousPathRef = useRef<string | null>(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<OptionType | null>(null);
|
|
const [selectedFinance, setSelectedFinance] = useState<Finance | null>(null);
|
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
|
const [dateErrorShown, setDateErrorShown] = useState(false);
|
|
const [hasDateError, setHasDateError] = useState(false);
|
|
|
|
// ===== Formik for Filter =====
|
|
const filterFormik = useFormik<FinanceTableFilterValues>({
|
|
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<Bank>(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<HTMLInputElement>) => {
|
|
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<HTMLInputElement>) => {
|
|
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<HTMLInputElement>) => {
|
|
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<Finance>[] = useMemo(
|
|
() => [
|
|
{
|
|
header: 'ID',
|
|
accessorKey: 'payment_code',
|
|
},
|
|
{
|
|
header: 'References Number',
|
|
accessorKey: 'reference_number',
|
|
cell: (props: CellContext<Finance, unknown>) => {
|
|
const value = props.row.original.reference_number;
|
|
return <span>{value ?? '-'}</span>;
|
|
},
|
|
},
|
|
{
|
|
header: 'Jenis Transaksi',
|
|
accessorKey: 'transaction_type',
|
|
cell: (props: CellContext<Finance, unknown>) => {
|
|
const value = props.row.original.transaction_type
|
|
.split('_')
|
|
.join(' ');
|
|
return <span>{formatTitleCase(value)}</span>;
|
|
},
|
|
},
|
|
{
|
|
header: 'Pihak',
|
|
accessorFn: (finance: Finance) => finance.party?.name,
|
|
cell: (props: CellContext<Finance, unknown>) => {
|
|
if (props.row.original.party?.id) {
|
|
return <span>{props.row.original.party?.name}</span>;
|
|
}
|
|
return <span>{'-'}</span>;
|
|
},
|
|
},
|
|
{
|
|
header: 'Tanggal',
|
|
accessorFn: (finance: Finance) =>
|
|
formatDate(finance.payment_date, 'DD MMM YYYY'),
|
|
},
|
|
{
|
|
header: 'Metode Pembayaran',
|
|
accessorKey: 'payment_method',
|
|
cell: (props: CellContext<Finance, unknown>) => {
|
|
const value = props.row.original.payment_method.split('_').join(' ');
|
|
return <span>{formatTitleCase(value)}</span>;
|
|
},
|
|
},
|
|
{
|
|
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<Finance, unknown>) => {
|
|
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 (
|
|
<RowOptionsMenu
|
|
props={props}
|
|
deleteClickHandler={deleteClickHandler}
|
|
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
|
/>
|
|
);
|
|
},
|
|
},
|
|
],
|
|
[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 (
|
|
<>
|
|
<div className='w-full'>
|
|
{/* Header Section */}
|
|
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
|
|
{/* Action Buttons */}
|
|
<div className='w-fit flex flex-row gap-3 flex-wrap'>
|
|
<RequirePermission permissions='lti.finance.injections.create'>
|
|
<Button
|
|
href='/finance/add/injection'
|
|
color='warning'
|
|
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
|
>
|
|
<Icon icon='mdi:bank-transfer-in' width={20} height={20} />
|
|
Add Injection (Saldo Bank)
|
|
</Button>
|
|
</RequirePermission>
|
|
<RequirePermission permissions='lti.finance.initial_balances.create'>
|
|
<Button
|
|
href='/finance/add/initial-balance'
|
|
color='info'
|
|
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
|
>
|
|
<Icon icon='mdi:cash-register' width={20} height={20} />
|
|
Add Initial Balance
|
|
</Button>
|
|
</RequirePermission>
|
|
<RequirePermission permissions='lti.finance.payments.create'>
|
|
<Button
|
|
href='/finance/add'
|
|
color='primary'
|
|
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
|
>
|
|
<Icon icon='heroicons:plus' width={20} height={20} />
|
|
Add Finance
|
|
</Button>
|
|
</RequirePermission>
|
|
</div>
|
|
|
|
{/* Search and Filter */}
|
|
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
|
<DebouncedTextInput
|
|
name='search'
|
|
placeholder='Search'
|
|
value={tableFilterState.search ?? ''}
|
|
onChange={searchChangeHandler}
|
|
startAdornment={
|
|
<Icon
|
|
icon='heroicons:magnifying-glass'
|
|
width={20}
|
|
height={20}
|
|
/>
|
|
}
|
|
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',
|
|
}}
|
|
/>
|
|
|
|
<Button
|
|
variant='outline'
|
|
color='none'
|
|
onClick={handleFilterModalOpen}
|
|
className={cn(
|
|
'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all',
|
|
{
|
|
'border-primary-gradient text-primary': hasFilters,
|
|
}
|
|
)}
|
|
>
|
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
|
Filter
|
|
{hasFilters && (
|
|
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
|
|
{activeFiltersCount}
|
|
</span>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Table Section */}
|
|
<div className='flex flex-col mb-4'>
|
|
{isLoading ? (
|
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
|
<span className='loading loading-spinner loading-xl' />
|
|
</div>
|
|
) : !isResponseSuccess(finances) || finances.data?.length === 0 ? (
|
|
<div className='p-3'>
|
|
<FinanceTableSkeleton
|
|
columns={columns}
|
|
icon={
|
|
<Icon
|
|
icon='heroicons:document-text'
|
|
className='text-white'
|
|
width={20}
|
|
height={20}
|
|
/>
|
|
}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<Table<Finance>
|
|
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',
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filter Modal */}
|
|
<Modal
|
|
ref={filterModal.ref}
|
|
className={{
|
|
modal: 'p-0',
|
|
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
|
}}
|
|
>
|
|
{/* Modal Header */}
|
|
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
|
<div className='flex items-center gap-2 text-primary'>
|
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
|
<h3 className='font-medium text-sm'>Filter Data</h3>
|
|
</div>
|
|
<Button
|
|
variant='link'
|
|
onClick={filterModal.closeModal}
|
|
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
|
>
|
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
|
</Button>
|
|
</div>
|
|
<form
|
|
onSubmit={filterFormik.handleSubmit}
|
|
onReset={filterFormik.handleReset}
|
|
>
|
|
<div className='p-4 flex flex-col gap-1.5'>
|
|
<div className='flex flex-col'>
|
|
<span className='py-2 text-xs font-semibold'>Tanggal</span>
|
|
<div className='flex flex-row items-center gap-1.5'>
|
|
<DateInput
|
|
name='start_date'
|
|
placeholder='Periode Tanggal Awal'
|
|
value={filterFormik.values.start_date}
|
|
errorMessage={filterFormik.errors.start_date}
|
|
onChange={startDateChangeHandler}
|
|
isError={
|
|
filterFormik.touched.start_date &&
|
|
Boolean(filterFormik.errors.start_date)
|
|
}
|
|
/>
|
|
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
|
<DateInput
|
|
name='end_date'
|
|
placeholder='Periode Tanggal Akhir'
|
|
value={filterFormik.values.end_date}
|
|
errorMessage={filterFormik.errors.end_date}
|
|
onChange={endDateChangeHandler}
|
|
isError={
|
|
(filterFormik.touched.end_date &&
|
|
Boolean(filterFormik.errors.end_date)) ||
|
|
hasDateError
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<SelectInput
|
|
options={FINANCE_TRANSACTION_TYPE_OPTIONS}
|
|
label='Jenis Transaksi'
|
|
placeholder='Pilih Jenis Transaksi'
|
|
value={selectedTransactionType}
|
|
onChange={transactionTypeChangeHandler}
|
|
onInputChange={() => {}}
|
|
closeMenuOnSelect={false}
|
|
isClearable
|
|
isMulti
|
|
className={{ wrapper: 'w-full' }}
|
|
/>
|
|
<SelectInput
|
|
options={customerOptions}
|
|
label='Customer'
|
|
placeholder='Pilih Customer'
|
|
value={selectedCustomerId}
|
|
onChange={customerIdChangeHandler}
|
|
onInputChange={customerInputValue}
|
|
onMenuScrollToBottom={customerLoadMore}
|
|
isLoading={customerIsLoadingOptions}
|
|
closeMenuOnSelect={false}
|
|
isClearable
|
|
isMulti
|
|
className={{ wrapper: 'w-full' }}
|
|
/>
|
|
<SelectInput
|
|
options={supplierOptions}
|
|
label='Supplier'
|
|
placeholder='Pilih Supplier'
|
|
value={selectedSupplierId}
|
|
onChange={supplierIdChangeHandler}
|
|
onInputChange={supplierInputValue}
|
|
onMenuScrollToBottom={supplierLoadMore}
|
|
isLoading={supplierIsLoadingOptions}
|
|
closeMenuOnSelect={false}
|
|
isClearable
|
|
isMulti
|
|
className={{ wrapper: 'w-full' }}
|
|
/>
|
|
<SelectInput
|
|
options={bankSelectOptions}
|
|
label='Bank'
|
|
placeholder='Pilih Bank'
|
|
value={selectedBank}
|
|
onChange={bankChangeHandler}
|
|
onInputChange={bankInputValue}
|
|
onMenuScrollToBottom={bankLoadMore}
|
|
closeMenuOnSelect={false}
|
|
isClearable
|
|
isMulti
|
|
className={{ wrapper: 'w-full' }}
|
|
/>
|
|
<SelectInput
|
|
options={sortByOptions}
|
|
label='Urutkan Berdasarkan'
|
|
placeholder='Pilih Urutan'
|
|
value={selectedSortBy}
|
|
onChange={sortByChangeHandler}
|
|
isClearable
|
|
className={{ wrapper: 'w-full' }}
|
|
/>
|
|
</div>
|
|
|
|
{/* Modal Footer */}
|
|
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
|
<Button
|
|
type='button'
|
|
variant='soft'
|
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
|
onClick={() => {
|
|
filterFormik.resetForm();
|
|
setSelectedTransactionType(null);
|
|
setSelectedBank(null);
|
|
setSelectedCustomerId(null);
|
|
setSelectedSupplierId(null);
|
|
setSelectedSortBy(null);
|
|
resetFilterHandler();
|
|
filterModal.closeModal();
|
|
}}
|
|
>
|
|
Reset Filter
|
|
</Button>
|
|
<Button
|
|
type='submit'
|
|
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
|
disabled={!filterFormik.isValid || filterFormik.isSubmitting}
|
|
>
|
|
Apply Filter
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Modal>
|
|
|
|
<ConfirmationModal
|
|
ref={deleteModal.ref}
|
|
type='error'
|
|
text={`Apakah anda yakin ingin menghapus data Finance ini (${selectedFinance?.payment_code})?`}
|
|
secondaryButton={{
|
|
text: 'Tidak',
|
|
}}
|
|
primaryButton={{
|
|
text: 'Ya',
|
|
color: 'error',
|
|
isLoading: isDeleteLoading,
|
|
onClick: confirmationModalDeleteClickHandler,
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default FinanceTable;
|