mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
refactor(FE): Refactor ExpensesTable to use ExpensesFilterModal
This commit is contained in:
@@ -16,10 +16,6 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
|||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import SelectInput, {
|
|
||||||
OptionType,
|
|
||||||
useSelect,
|
|
||||||
} from '@/components/input/SelectInput';
|
|
||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
@@ -27,17 +23,15 @@ import RealizationStatusBadge from '@/components/pages/expense/RealizationStatus
|
|||||||
import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge';
|
import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge';
|
||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
import DateInput from '@/components/input/DateInput';
|
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
import ExpensesFilterModal from '@/components/pages/expense/filter/ExpensesFilterModal';
|
||||||
|
|
||||||
import { Expense } from '@/types/api/expense';
|
import { Expense } from '@/types/api/expense';
|
||||||
import { ExpenseApi } from '@/services/api/expense';
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
import { cn, formatCurrency, formatDate } from '@/lib/helper';
|
import { cn, formatCurrency, formatDate } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { LocationApi, SupplierApi } from '@/services/api/master-data';
|
|
||||||
import { Location } from '@/types/api/master-data/location';
|
|
||||||
import { Supplier } from '@/types/api/master-data/supplier';
|
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
@@ -179,6 +173,9 @@ const ExpensesTable = () => {
|
|||||||
const approveModal = useModal();
|
const approveModal = useModal();
|
||||||
const rejectModal = useModal();
|
const rejectModal = useModal();
|
||||||
|
|
||||||
|
// ===== FILTER MODAL STATE =====
|
||||||
|
const filterModal = useModal();
|
||||||
|
|
||||||
const [selectedExpense, setSelectedExpense] = useState<Expense | undefined>(
|
const [selectedExpense, setSelectedExpense] = useState<Expense | undefined>(
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
@@ -535,51 +532,32 @@ const ExpensesTable = () => {
|
|||||||
setIsRejectLoading(false);
|
setIsRejectLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
|
||||||
setInputValue: setLocationInputValue,
|
|
||||||
options: locationOptions,
|
|
||||||
isLoadingOptions: isLoadingLocationOptions,
|
|
||||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
|
||||||
|
|
||||||
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
|
||||||
setSelectedLocation(val as OptionType);
|
|
||||||
updateFilter(
|
|
||||||
'locationId',
|
|
||||||
val ? ((val as OptionType).value as string) : ''
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
|
||||||
setInputValue: setVendorInputValue,
|
|
||||||
options: vendorOptions,
|
|
||||||
isLoadingOptions: isLoadingVendorOptions,
|
|
||||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
|
||||||
|
|
||||||
const [selectedVendor, setSelectedVendor] = useState<OptionType | null>(null);
|
|
||||||
|
|
||||||
const vendorChangeHandler = (val: OptionType | OptionType[] | null) => {
|
|
||||||
setSelectedVendor(val as OptionType);
|
|
||||||
updateFilter('vendorId', val ? ((val as OptionType).value as string) : '');
|
|
||||||
};
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value);
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const transactionDateChangeHandler: ChangeEventHandler<HTMLInputElement> = (
|
// ===== FILTER MODAL HANDLERS =====
|
||||||
e
|
const handleFilterModalOpen = () => {
|
||||||
) => {
|
filterModal.openModal();
|
||||||
updateFilter('transactionDate', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const realizationDateChangeHandler: ChangeEventHandler<HTMLInputElement> = (
|
const handleFilterSubmit = (values: {
|
||||||
e
|
transaction_date?: string | null;
|
||||||
) => {
|
realization_date?: string | null;
|
||||||
updateFilter('realizationDate', e.target.value);
|
location_id?: string | null;
|
||||||
|
vendor_id?: string | null;
|
||||||
|
}) => {
|
||||||
|
updateFilter('transactionDate', values.transaction_date || '');
|
||||||
|
updateFilter('realizationDate', values.realization_date || '');
|
||||||
|
updateFilter('locationId', values.location_id || '');
|
||||||
|
updateFilter('vendorId', values.vendor_id || '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilterReset = () => {
|
||||||
|
updateFilter('transactionDate', '');
|
||||||
|
updateFilter('realizationDate', '');
|
||||||
|
updateFilter('locationId', '');
|
||||||
|
updateFilter('vendorId', '');
|
||||||
};
|
};
|
||||||
|
|
||||||
// track sorting
|
// track sorting
|
||||||
@@ -595,188 +573,176 @@ const ExpensesTable = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='w-full p-0 sm:p-4'>
|
<div className='w-full'>
|
||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
|
||||||
<div className='flex flex-col gap-2 mb-4'>
|
{/* Action Buttons */}
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-4'>
|
<div className='w-fit flex flex-row gap-3 flex-wrap'>
|
||||||
<div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'>
|
<RequirePermission permissions='lti.expense.create'>
|
||||||
<RequirePermission permissions='lti.expense.create'>
|
<Button
|
||||||
|
href='/expense/add'
|
||||||
|
color='primary'
|
||||||
|
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-xl shadow-button-soft'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:plus' width={20} height={20} />
|
||||||
|
Add Expense
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
{selectedRowIds.length > 0 && (
|
||||||
|
<>
|
||||||
|
<hr className='w-px h-full border-none bg-base-content/10 sm:block hidden' />
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.expense.approve.head_area'>
|
||||||
<Button
|
<Button
|
||||||
href='/expense/add'
|
variant='outline'
|
||||||
color='primary'
|
color='none'
|
||||||
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
onClick={bulkApproveClickHandler}
|
||||||
|
disabled={!isAllSelectedRowLatestApprovalOnHeadArea}
|
||||||
|
className='px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||||
>
|
>
|
||||||
<Icon icon='heroicons:plus' width={20} height={20} />
|
<Icon
|
||||||
Add Expense
|
icon='lucide-lab:farm'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className='text-success'
|
||||||
|
/>
|
||||||
|
Approve Head Area
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
|
|
||||||
{selectedRowIds.length > 0 && (
|
<RequirePermission permissions='lti.expense.approve.unit_vice_president'>
|
||||||
<>
|
<Button
|
||||||
<RequirePermission permissions='lti.expense.approve.head_area'>
|
variant='outline'
|
||||||
<Button
|
color='none'
|
||||||
variant='outline'
|
onClick={bulkApproveClickHandler}
|
||||||
color='info'
|
disabled={
|
||||||
onClick={bulkApproveClickHandler}
|
!isAllSelectedRowLatestApprovalOnUnitVicePresident
|
||||||
disabled={!isAllSelectedRowLatestApprovalOnHeadArea}
|
}
|
||||||
className='w-full sm:w-fit'
|
className='px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||||
>
|
>
|
||||||
<Icon icon='lucide-lab:farm' width={24} height={24} />
|
<Icon
|
||||||
Approve Head Area
|
icon='tdesign:money'
|
||||||
</Button>
|
width={20}
|
||||||
</RequirePermission>
|
height={20}
|
||||||
|
className='text-success'
|
||||||
|
/>
|
||||||
|
Approve Unit Vice President
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
<RequirePermission permissions='lti.expense.approve.unit_vice_president'>
|
<RequirePermission permissions='lti.expense.approve.finance'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='success'
|
color='none'
|
||||||
onClick={bulkApproveClickHandler}
|
onClick={bulkApproveClickHandler}
|
||||||
disabled={
|
disabled={!isAllSelectedRowLatestApprovalOnFinance}
|
||||||
!isAllSelectedRowLatestApprovalOnUnitVicePresident
|
className='px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||||
}
|
>
|
||||||
className='w-full sm:w-fit'
|
<Icon
|
||||||
>
|
icon='tdesign:money'
|
||||||
<Icon icon='tdesign:money' width={24} height={24} />
|
width={20}
|
||||||
Approve Unit Vice President
|
height={20}
|
||||||
</Button>
|
className='text-success'
|
||||||
</RequirePermission>
|
/>
|
||||||
|
Approve Finance
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
<RequirePermission permissions='lti.expense.approve.finance'>
|
<RequirePermission
|
||||||
<Button
|
permissions={[
|
||||||
variant='outline'
|
'lti.expense.approve.head_area',
|
||||||
color='success'
|
'lti.expense.approve.unit_vice_president',
|
||||||
onClick={bulkApproveClickHandler}
|
'lti.expense.approve.finance',
|
||||||
disabled={!isAllSelectedRowLatestApprovalOnFinance}
|
]}
|
||||||
className='w-full sm:w-fit'
|
>
|
||||||
>
|
<Button
|
||||||
<Icon icon='tdesign:money' width={24} height={24} />
|
variant='outline'
|
||||||
Approve Finance
|
color='none'
|
||||||
</Button>
|
onClick={bulkRejectClickHandler}
|
||||||
</RequirePermission>
|
disabled={
|
||||||
|
!isAllSelectedRowLatestApprovalOnHeadArea &&
|
||||||
|
!isAllSelectedRowLatestApprovalOnUnitVicePresident &&
|
||||||
|
!isAllSelectedRowLatestApprovalOnFinance
|
||||||
|
}
|
||||||
|
className='px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:close'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className='text-error'
|
||||||
|
/>
|
||||||
|
Reject
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<RequirePermission
|
{/* Search and Filter */}
|
||||||
permissions={[
|
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||||
'lti.expense.approve.head_area',
|
<DebouncedTextInput
|
||||||
'lti.expense.approve.unit_vice_president',
|
name='search'
|
||||||
'lti.expense.approve.finance',
|
placeholder='Search'
|
||||||
]}
|
value={tableFilterState.search ?? ''}
|
||||||
>
|
onChange={searchChangeHandler}
|
||||||
<Button
|
startAdornment={
|
||||||
variant='outline'
|
<Icon
|
||||||
color='error'
|
icon='heroicons:magnifying-glass'
|
||||||
onClick={bulkRejectClickHandler}
|
width={20}
|
||||||
disabled={
|
height={20}
|
||||||
!isAllSelectedRowLatestApprovalOnHeadArea &&
|
/>
|
||||||
!isAllSelectedRowLatestApprovalOnUnitVicePresident &&
|
}
|
||||||
!isAllSelectedRowLatestApprovalOnFinance
|
className={{
|
||||||
}
|
wrapper: 'w-full min-w-24 max-w-3xs',
|
||||||
className='w-full sm:w-fit'
|
inputWrapper: 'rounded-xl! shadow-button-soft',
|
||||||
>
|
input:
|
||||||
<Icon
|
'placeholder:font-semibold placeholder:text-base-content/50',
|
||||||
icon='material-symbols:close'
|
}}
|
||||||
width={24}
|
/>
|
||||||
height={24}
|
|
||||||
/>
|
|
||||||
Reject
|
|
||||||
</Button>
|
|
||||||
</RequirePermission>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='grid grid-cols-12 justify-end gap-2'>
|
<ButtonFilter
|
||||||
<DateInput
|
values={tableFilterState}
|
||||||
required
|
excludeFields={[
|
||||||
label='Tanggal Transaksi'
|
'page',
|
||||||
name='transaction_date'
|
'pageSize',
|
||||||
placeholder='Masukkan tanggal transaksi'
|
'search',
|
||||||
value={tableFilterState.transactionDate}
|
'nameSort',
|
||||||
onChange={transactionDateChangeHandler}
|
'userId',
|
||||||
className={{
|
]}
|
||||||
wrapper: 'col-span-12 sm:col-span-3',
|
onClick={handleFilterModalOpen}
|
||||||
}}
|
className='px-3 py-2.5'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DateInput
|
|
||||||
required
|
|
||||||
label='Tanggal Realisasi'
|
|
||||||
name='realization_date'
|
|
||||||
placeholder='Masukkan tanggal realisasi'
|
|
||||||
value={tableFilterState.realizationDate}
|
|
||||||
onChange={realizationDateChangeHandler}
|
|
||||||
className={{
|
|
||||||
wrapper: 'col-span-12 sm:col-span-3',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
label='Lokasi'
|
|
||||||
options={locationOptions}
|
|
||||||
isLoading={isLoadingLocationOptions}
|
|
||||||
value={selectedLocation}
|
|
||||||
onChange={locationChangeHandler}
|
|
||||||
onInputChange={setLocationInputValue}
|
|
||||||
isClearable
|
|
||||||
className={{
|
|
||||||
wrapper: 'col-span-12 sm:col-span-3',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
label='Vendor'
|
|
||||||
options={vendorOptions}
|
|
||||||
isLoading={isLoadingVendorOptions}
|
|
||||||
value={selectedVendor}
|
|
||||||
onChange={vendorChangeHandler}
|
|
||||||
onInputChange={setVendorInputValue}
|
|
||||||
isClearable
|
|
||||||
className={{
|
|
||||||
wrapper: 'col-span-12 sm:col-span-3',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DebouncedTextInput
|
|
||||||
name='search'
|
|
||||||
placeholder='Cari Biaya Operasional'
|
|
||||||
value={tableFilterState.search}
|
|
||||||
onChange={searchChangeHandler}
|
|
||||||
className={{ wrapper: 'col-span-12 max-w-52 justify-self-end' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Table<Expense>
|
{/* Table Section */}
|
||||||
data={isResponseSuccess(expenses) ? expenses?.data : []}
|
<div className='flex flex-col mb-4 -mx-4 px-4'>
|
||||||
columns={expensesColumns}
|
<Table<Expense>
|
||||||
pageSize={tableFilterState.pageSize}
|
data={isResponseSuccess(expenses) ? expenses?.data : []}
|
||||||
page={isResponseSuccess(expenses) ? expenses?.meta?.page : 0}
|
columns={expensesColumns}
|
||||||
totalItems={
|
pageSize={tableFilterState.pageSize}
|
||||||
isResponseSuccess(expenses) ? expenses?.meta?.total_results : 0
|
page={isResponseSuccess(expenses) ? expenses?.meta?.page : 0}
|
||||||
}
|
totalItems={
|
||||||
onPageChange={setPage}
|
isResponseSuccess(expenses) ? expenses?.meta?.total_results : 0
|
||||||
isLoading={isLoading}
|
}
|
||||||
sorting={sorting}
|
onPageChange={setPage}
|
||||||
setSorting={setSorting}
|
onPageSizeChange={setPageSize}
|
||||||
rowSelection={rowSelection}
|
isLoading={isLoading}
|
||||||
setRowSelection={setRowSelection}
|
sorting={sorting}
|
||||||
enableRowSelection={tableEnableRowSelectionHandler}
|
setSorting={setSorting}
|
||||||
className={{
|
rowSelection={rowSelection}
|
||||||
containerClassName: cn({
|
setRowSelection={setRowSelection}
|
||||||
'mb-20':
|
enableRowSelection={tableEnableRowSelectionHandler}
|
||||||
isResponseSuccess(expenses) && expenses?.data?.length === 0,
|
className={{
|
||||||
}),
|
containerClassName: cn('p-3 mb-0', {
|
||||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
'w-full':
|
||||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
isResponseSuccess(expenses) && expenses?.data?.length === 0,
|
||||||
headerRowClassName: 'border-b border-b-gray-200',
|
}),
|
||||||
headerColumnClassName:
|
headerColumnClassName: 'text-nowrap',
|
||||||
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
|
}}
|
||||||
bodyRowClassName: 'border-b border-b-gray-200',
|
/>
|
||||||
bodyColumnClassName:
|
</div>
|
||||||
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
@@ -831,6 +797,12 @@ const ExpensesTable = () => {
|
|||||||
onClick: confirmationModalRejectClickHandler,
|
onClick: confirmationModalRejectClickHandler,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ExpensesFilterModal
|
||||||
|
ref={filterModal.ref}
|
||||||
|
onSubmit={handleFilterSubmit}
|
||||||
|
onReset={handleFilterReset}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import * as yup from 'yup';
|
||||||
|
|
||||||
|
export type ExpensesFilterType = {
|
||||||
|
transaction_date: string | null;
|
||||||
|
realization_date: string | null;
|
||||||
|
location_id: string | null;
|
||||||
|
vendor_id: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ExpensesFilterSchema = yup.object({
|
||||||
|
transaction_date: yup.string().nullable(),
|
||||||
|
realization_date: yup
|
||||||
|
.string()
|
||||||
|
.nullable()
|
||||||
|
.test(
|
||||||
|
'is-greater-or-equal-transaction',
|
||||||
|
'Tanggal realisasi tidak boleh sebelum tanggal transaksi',
|
||||||
|
function (value) {
|
||||||
|
const { transaction_date } = this.parent;
|
||||||
|
if (!transaction_date || !value) return true;
|
||||||
|
return new Date(value) >= new Date(transaction_date);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
location_id: yup.string().nullable(),
|
||||||
|
vendor_id: yup.string().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ExpensesFilterValues = yup.InferType<typeof ExpensesFilterSchema>;
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { RefObject } from 'react';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import Modal from '@/components/Modal';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import DateInput from '@/components/input/DateInput';
|
||||||
|
import SelectInput from '@/components/input/SelectInput';
|
||||||
|
|
||||||
|
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||||
|
import { LocationApi, SupplierApi } from '@/services/api/master-data';
|
||||||
|
import { Location } from '@/types/api/master-data/location';
|
||||||
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
|
import {
|
||||||
|
ExpensesFilterSchema,
|
||||||
|
ExpensesFilterValues,
|
||||||
|
} from '@/components/pages/expense/filter/ExpensesFilter';
|
||||||
|
|
||||||
|
interface ExpensesFilterModalProps {
|
||||||
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
|
initialValues?: ExpensesFilterValues;
|
||||||
|
onSubmit?: (values: Partial<ExpensesFilterValues>) => void;
|
||||||
|
onReset?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExpensesFilterModal = ({
|
||||||
|
ref,
|
||||||
|
initialValues,
|
||||||
|
onSubmit,
|
||||||
|
onReset,
|
||||||
|
}: ExpensesFilterModalProps) => {
|
||||||
|
const closeModalHandler = () => {
|
||||||
|
ref.current?.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setLocationInputValue,
|
||||||
|
options: locationOptions,
|
||||||
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
|
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setVendorInputValue,
|
||||||
|
options: vendorOptions,
|
||||||
|
isLoadingOptions: isLoadingVendorOptions,
|
||||||
|
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
|
const formik = useFormik<ExpensesFilterValues>({
|
||||||
|
initialValues: initialValues || {
|
||||||
|
transaction_date: null,
|
||||||
|
realization_date: null,
|
||||||
|
location_id: null,
|
||||||
|
vendor_id: null,
|
||||||
|
},
|
||||||
|
validationSchema: ExpensesFilterSchema,
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
onSubmit?.(values);
|
||||||
|
closeModalHandler();
|
||||||
|
},
|
||||||
|
onReset: () => {
|
||||||
|
onReset?.();
|
||||||
|
closeModalHandler();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const locationValue = formik.values.location_id
|
||||||
|
? locationOptions.find(
|
||||||
|
(opt) => String(opt.value) === formik.values.location_id
|
||||||
|
) || null
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const vendorValue = formik.values.vendor_id
|
||||||
|
? vendorOptions.find(
|
||||||
|
(opt) => String(opt.value) === formik.values.vendor_id
|
||||||
|
) || null
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
const locationId =
|
||||||
|
val && !Array.isArray(val) ? (String(val.value) as string) : null;
|
||||||
|
formik.setFieldValue('location_id', locationId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const vendorChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
const vendorId =
|
||||||
|
val && !Array.isArray(val) ? (String(val.value) as string) : null;
|
||||||
|
formik.setFieldValue('vendor_id', vendorId);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
ref={ref}
|
||||||
|
className={{
|
||||||
|
modalBox: 'p-0 rounded-xl xl:max-w-4/12 max-w-sm',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
onReset={formik.handleReset}
|
||||||
|
className='w-full flex flex-col'
|
||||||
|
>
|
||||||
|
{/* Modal Header */}
|
||||||
|
<div className='p-4 flex items-center justify-between gap-2 border-b border-base-content/10'>
|
||||||
|
<div className='flex items-center gap-2 text-primary'>
|
||||||
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||||
|
<h3 className='text-sm font-medium'>Filter Data</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={closeModalHandler}
|
||||||
|
className='p-0 text-base-content/50 hover:text-base-content'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Body */}
|
||||||
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
|
<DateInput
|
||||||
|
label='Tanggal Transaksi'
|
||||||
|
name='transaction_date'
|
||||||
|
placeholder='Masukkan tanggal transaksi'
|
||||||
|
value={formik.values.transaction_date || ''}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={
|
||||||
|
formik.touched.transaction_date &&
|
||||||
|
!!formik.errors.transaction_date
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DateInput
|
||||||
|
label='Tanggal Realisasi'
|
||||||
|
name='realization_date'
|
||||||
|
placeholder='Masukkan tanggal realisasi'
|
||||||
|
value={formik.values.realization_date || ''}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={
|
||||||
|
formik.touched.realization_date &&
|
||||||
|
!!formik.errors.realization_date
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{formik.touched.realization_date &&
|
||||||
|
formik.errors.realization_date && (
|
||||||
|
<span className='text-xs text-error'>
|
||||||
|
{formik.errors.realization_date}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Lokasi'
|
||||||
|
placeholder='Pilih Lokasi'
|
||||||
|
options={locationOptions}
|
||||||
|
value={locationValue}
|
||||||
|
onChange={locationChangeHandler}
|
||||||
|
onInputChange={setLocationInputValue}
|
||||||
|
isLoading={isLoadingLocationOptions}
|
||||||
|
isClearable
|
||||||
|
isSearchable={true}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Vendor'
|
||||||
|
placeholder='Pilih Vendor'
|
||||||
|
options={vendorOptions}
|
||||||
|
value={vendorValue}
|
||||||
|
onChange={vendorChangeHandler}
|
||||||
|
onInputChange={setVendorInputValue}
|
||||||
|
isLoading={isLoadingVendorOptions}
|
||||||
|
isClearable
|
||||||
|
isSearchable={true}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Footer */}
|
||||||
|
<div className='p-4 flex justify-between gap-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={!formik.isValid || formik.isSubmitting}
|
||||||
|
>
|
||||||
|
Apply Filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpensesFilterModal;
|
||||||
Reference in New Issue
Block a user