mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-06-14 12:31:42 +00:00
1030 lines
34 KiB
TypeScript
1030 lines
34 KiB
TypeScript
'use client';
|
|
|
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
import {
|
|
CellContext,
|
|
ColumnDef,
|
|
SortingState,
|
|
Updater,
|
|
} 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 { getErrorMessage, 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 ButtonFilter from '@/components/helper/ButtonFilter';
|
|
import Dropdown from '@/components/dropdown/Dropdown';
|
|
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 {
|
|
state: tableFilterState,
|
|
updateFilter,
|
|
setPage,
|
|
setPageSize,
|
|
toQueryString: getTableFilterQueryString,
|
|
} = useTableFilter({
|
|
initial: {
|
|
search: '',
|
|
transactionTypes: '',
|
|
bankIds: '',
|
|
customerIds: '',
|
|
supplierIds: '',
|
|
sort_by: '',
|
|
orderBy: '',
|
|
startDate: '',
|
|
endDate: '',
|
|
bankNames: '',
|
|
customerNames: '',
|
|
supplierNames: '',
|
|
},
|
|
paramMap: {
|
|
page: 'page',
|
|
pageSize: 'limit',
|
|
transactionTypes: 'transaction_types',
|
|
bankIds: 'bank_ids',
|
|
customerIds: 'customer_ids',
|
|
supplierIds: 'supplier_ids',
|
|
sort_by: 'sort_by',
|
|
orderBy: 'sort_order',
|
|
startDate: 'start_date',
|
|
endDate: 'end_date',
|
|
},
|
|
excludeKeysFromUrl: ['bankNames', 'customerNames', 'supplierNames'],
|
|
persist: true,
|
|
storeName: 'finance-table',
|
|
});
|
|
|
|
// ===== 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 [isExportLoading, setIsExportLoading] = useState(false);
|
|
const [dateErrorShown, setDateErrorShown] = useState(false);
|
|
const [hasDateError, setHasDateError] = useState(false);
|
|
|
|
// ===== Formik for Filter =====
|
|
const filterFormik = useFormik<FinanceTableFilterValues>({
|
|
initialValues: {
|
|
search: tableFilterState.search || '',
|
|
transaction_types: '',
|
|
bank_ids: '',
|
|
customer_ids: '',
|
|
supplier_ids: '',
|
|
sort_by: '',
|
|
start_date: '',
|
|
end_date: '',
|
|
},
|
|
validationSchema: FinanceTableFilterSchema,
|
|
onSubmit: (values, { setSubmitting }) => {
|
|
updateFilter('search', values.search, true);
|
|
updateFilter('transactionTypes', values.transaction_types, true);
|
|
updateFilter('bankIds', values.bank_ids, true);
|
|
updateFilter('customerIds', values.customer_ids, true);
|
|
updateFilter('supplierIds', values.supplier_ids, true);
|
|
updateFilter('sort_by', values.sort_by, true);
|
|
updateFilter('startDate', values.start_date, true);
|
|
updateFilter('endDate', values.end_date, true);
|
|
// Save display names for restoration on modal reopen
|
|
const toNames = (val: OptionType | OptionType[] | null) =>
|
|
val
|
|
? (Array.isArray(val) ? val : [val])
|
|
.map((o) => String(o.label))
|
|
.join(',')
|
|
: '';
|
|
updateFilter('bankNames', toNames(selectedBank), true);
|
|
updateFilter('customerNames', toNames(selectedCustomerId), true);
|
|
updateFilter('supplierNames', toNames(selectedSupplierId), true);
|
|
filterModal.closeModal();
|
|
|
|
setSubmitting(false);
|
|
},
|
|
onReset: () => {
|
|
setSelectedTransactionType(null);
|
|
setSelectedBank(null);
|
|
setSelectedCustomerId(null);
|
|
setSelectedSupplierId(null);
|
|
setSelectedSortBy(null);
|
|
updateFilter('search', '', true);
|
|
updateFilter('transactionTypes', '', true);
|
|
updateFilter('bankIds', '', true);
|
|
updateFilter('customerIds', '', true);
|
|
updateFilter('supplierIds', '', true);
|
|
updateFilter('sort_by', '', true);
|
|
updateFilter('orderBy', '', true);
|
|
updateFilter('startDate', '', true);
|
|
updateFilter('endDate', '', true);
|
|
updateFilter('bankNames', '', true);
|
|
updateFilter('customerNames', '', true);
|
|
updateFilter('supplierNames', '', true);
|
|
filterModal.closeModal();
|
|
},
|
|
});
|
|
|
|
// ===== 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]);
|
|
|
|
// ===== Handler =====
|
|
const searchChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
updateFilter('search', e.target.value, true);
|
|
};
|
|
|
|
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 sorting: SortingState = tableFilterState.sort_by
|
|
? [
|
|
{
|
|
id: tableFilterState.sort_by,
|
|
desc: tableFilterState.orderBy === 'desc',
|
|
},
|
|
]
|
|
: [];
|
|
|
|
const handleSortingChange = (updater: Updater<SortingState>) => {
|
|
const next = typeof updater === 'function' ? updater(sorting) : updater;
|
|
if (next.length > 0) {
|
|
updateFilter('sort_by', next[0].id, true);
|
|
updateFilter('orderBy', next[0].desc ? 'desc' : 'asc', true);
|
|
} else {
|
|
updateFilter('sort_by', '', true);
|
|
updateFilter('orderBy', '', true);
|
|
}
|
|
};
|
|
|
|
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 = () => {
|
|
// Restore transaction types from stored comma-separated IDs
|
|
const txIds = tableFilterState.transactionTypes
|
|
? tableFilterState.transactionTypes.split(',')
|
|
: [];
|
|
const restoredTxTypes = FINANCE_TRANSACTION_TYPE_OPTIONS.filter((opt) =>
|
|
txIds.includes(String(opt.value))
|
|
);
|
|
setSelectedTransactionType(restoredTxTypes.length ? restoredTxTypes : null);
|
|
|
|
// Restore banks from stored IDs and names
|
|
const bankIdList = tableFilterState.bankIds
|
|
? tableFilterState.bankIds.split(',')
|
|
: [];
|
|
const bankNameList = tableFilterState.bankNames
|
|
? tableFilterState.bankNames.split(',')
|
|
: [];
|
|
const restoredBanks = bankIdList.map((id, i) => ({
|
|
value: id,
|
|
label: bankNameList[i] || id,
|
|
}));
|
|
setSelectedBank(restoredBanks.length ? restoredBanks : null);
|
|
|
|
// Restore customers from stored IDs and names
|
|
const customerIdList = tableFilterState.customerIds
|
|
? tableFilterState.customerIds.split(',')
|
|
: [];
|
|
const customerNameList = tableFilterState.customerNames
|
|
? tableFilterState.customerNames.split(',')
|
|
: [];
|
|
const restoredCustomers = customerIdList.map((id, i) => ({
|
|
value: id,
|
|
label: customerNameList[i] || id,
|
|
}));
|
|
setSelectedCustomerId(restoredCustomers.length ? restoredCustomers : null);
|
|
|
|
// Restore suppliers from stored IDs and names
|
|
const supplierIdList = tableFilterState.supplierIds
|
|
? tableFilterState.supplierIds.split(',')
|
|
: [];
|
|
const supplierNameList = tableFilterState.supplierNames
|
|
? tableFilterState.supplierNames.split(',')
|
|
: [];
|
|
const restoredSuppliers = supplierIdList.map((id, i) => ({
|
|
value: id,
|
|
label: supplierNameList[i] || id,
|
|
}));
|
|
setSelectedSupplierId(restoredSuppliers.length ? restoredSuppliers : null);
|
|
|
|
// Restore sort by
|
|
const restoredSortBy =
|
|
sortByOptions.find(
|
|
(opt) => String(opt.value) === tableFilterState.sort_by
|
|
) || null;
|
|
setSelectedSortBy(restoredSortBy);
|
|
|
|
// Restore formik values
|
|
filterFormik.setValues({
|
|
search: tableFilterState.search || '',
|
|
transaction_types: tableFilterState.transactionTypes || '',
|
|
bank_ids: tableFilterState.bankIds || '',
|
|
customer_ids: tableFilterState.customerIds || '',
|
|
supplier_ids: tableFilterState.supplierIds || '',
|
|
sort_by: tableFilterState.sort_by || '',
|
|
start_date: tableFilterState.startDate || '',
|
|
end_date: tableFilterState.endDate || '',
|
|
});
|
|
|
|
filterModal.openModal();
|
|
};
|
|
|
|
const exportToExcel = async () => {
|
|
setIsExportLoading(true);
|
|
try {
|
|
await FinanceApi.exportToExcel(getTableFilterQueryString());
|
|
toast.success('Excel berhasil dibuat dan diunduh.');
|
|
} catch (error) {
|
|
toast.error(
|
|
await getErrorMessage(error, 'Gagal mengekspor data finance.')
|
|
);
|
|
} finally {
|
|
setIsExportLoading(false);
|
|
}
|
|
};
|
|
|
|
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',
|
|
enableSorting: true,
|
|
},
|
|
{
|
|
header: 'References Number',
|
|
accessorKey: 'reference_number',
|
|
enableSorting: true,
|
|
cell: (props: CellContext<Finance, unknown>) => {
|
|
const value = props.row.original.reference_number;
|
|
return <span>{value ?? '-'}</span>;
|
|
},
|
|
},
|
|
{
|
|
header: 'Jenis Transaksi',
|
|
accessorKey: 'transaction_type',
|
|
enableSorting: true,
|
|
cell: (props: CellContext<Finance, unknown>) => {
|
|
const value = props.row.original.transaction_type
|
|
.split('_')
|
|
.join(' ');
|
|
return <span>{formatTitleCase(value)}</span>;
|
|
},
|
|
},
|
|
{
|
|
header: 'Pihak',
|
|
accessorKey: 'customer_name',
|
|
enableSorting: true,
|
|
cell: (props: CellContext<Finance, unknown>) => {
|
|
if (props.row.original.party?.id) {
|
|
return <span>{props.row.original.party?.name}</span>;
|
|
}
|
|
return <span>{'-'}</span>;
|
|
},
|
|
},
|
|
{
|
|
header: 'Tanggal Pembayaran',
|
|
accessorKey: 'payment_date',
|
|
enableSorting: true,
|
|
cell: (props) =>
|
|
formatDate(props.row.original.payment_date, 'DD MMM YYYY'),
|
|
},
|
|
{
|
|
header: 'Tanggal Dibuat',
|
|
accessorKey: 'created_at',
|
|
enableSorting: true,
|
|
cell: (props) =>
|
|
formatDate(props.row.original.created_at, 'DD MMM YYYY'),
|
|
},
|
|
{
|
|
header: 'Metode Pembayaran',
|
|
accessorKey: 'payment_method',
|
|
enableSorting: true,
|
|
cell: (props: CellContext<Finance, unknown>) => {
|
|
const value = props.row.original.payment_method.split('_').join(' ');
|
|
return <span>{formatTitleCase(value)}</span>;
|
|
},
|
|
},
|
|
{
|
|
header: 'Bank',
|
|
accessorKey: 'bank',
|
|
enableSorting: true,
|
|
cell: (props) =>
|
|
props.row.original.bank
|
|
? `${props.row.original.bank?.alias} - ${props.row.original.bank?.account_number} - ${props.row.original.bank?.owner}`
|
|
: '-',
|
|
},
|
|
{
|
|
header: 'Pengeluaran (Rp)',
|
|
accessorKey: 'expense_amount',
|
|
enableSorting: true,
|
|
cell: (props) =>
|
|
formatCurrency(Math.abs(props.row.original.expense_amount)),
|
|
},
|
|
{
|
|
header: 'Pemasukan (Rp)',
|
|
accessorKey: 'income_amount',
|
|
enableSorting: true,
|
|
cell: (props) =>
|
|
formatCurrency(Math.abs(props.row.original.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]);
|
|
|
|
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',
|
|
}}
|
|
/>
|
|
|
|
<ButtonFilter
|
|
values={tableFilterState}
|
|
excludeFields={[
|
|
'page',
|
|
'pageSize',
|
|
'search',
|
|
'orderBy',
|
|
'bankNames',
|
|
'customerNames',
|
|
'supplierNames',
|
|
]}
|
|
onClick={handleFilterModalOpen}
|
|
className='px-3 py-2.5'
|
|
/>
|
|
|
|
<Dropdown
|
|
align='end'
|
|
direction='bottom'
|
|
className={{
|
|
content:
|
|
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
|
|
}}
|
|
trigger={
|
|
<Button
|
|
variant='outline'
|
|
color='none'
|
|
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
|
>
|
|
<div className='flex flex-row items-center gap-1.5'>
|
|
<Icon
|
|
icon='heroicons:cloud-arrow-down'
|
|
width={20}
|
|
height={20}
|
|
/>
|
|
|
|
<span>Ekspor</span>
|
|
|
|
<div className='w-px self-stretch bg-base-content/10' />
|
|
|
|
<Icon
|
|
icon='heroicons:chevron-down'
|
|
width={14}
|
|
height={14}
|
|
/>
|
|
</div>
|
|
</Button>
|
|
}
|
|
>
|
|
<Button
|
|
variant='ghost'
|
|
color='none'
|
|
onClick={exportToExcel}
|
|
isLoading={isExportLoading}
|
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
|
>
|
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
|
Ekspor ke Excel
|
|
</Button>
|
|
</Dropdown>
|
|
</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}
|
|
sorting={sorting}
|
|
setSorting={handleSortingChange}
|
|
manualSorting
|
|
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='reset'
|
|
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'
|
|
>
|
|
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;
|