mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 21:41:57 +00:00
refactor: optimize DebtSupplierTab with useTableFilter persistence pattern
Replace filterParams/currentPage/pageSize state with useTableFilter (persist:true), switch SWR to httpClientFetcher with explicit type, store OptionType[] directly for suppliers/filterBy, add formikResetHandler using resetFilter(), remove TabActions component anti-pattern and handleFilterModalOpenRef, pass filterModal.openModal directly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,23 +9,15 @@ import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||
import { SupplierApi } from '@/services/api/master-data';
|
||||
import {
|
||||
DebtRow,
|
||||
DebtSupplier,
|
||||
DebtSupplierFilter,
|
||||
} from '@/types/api/report/debt-supplier';
|
||||
import { DebtRow, DebtSupplier } from '@/types/api/report/debt-supplier';
|
||||
import { generateDebtSupplierPDF } from '@/components/pages/report/finance/export/DebtSupllierExportPDF';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import useSWR from 'swr';
|
||||
import { DebtSupplierApi } from '@/services/api/report/debt-supplier';
|
||||
import { useFormik } from 'formik';
|
||||
import {
|
||||
DebtSupplierFilterSchema,
|
||||
DebtSupplierFilterType,
|
||||
} from '@/components/pages/report/finance/filter/DebtSupplierFilter';
|
||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||
import { Color } from '@/types/theme';
|
||||
import { Supplier } from '@/types/api/master-data/supplier';
|
||||
@@ -34,6 +26,10 @@ import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
||||
import StatusBadge from '@/components/helper/StatusBadge';
|
||||
import DebtSupplierSkeleton from '@/components/pages/report/finance/skeleton/DebtSupplierSkeleton';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
const dueStatus: Record<string, Color> = {
|
||||
'Sudah Jatuh Tempo': 'error',
|
||||
@@ -51,7 +47,6 @@ const getPillBadge = (
|
||||
statusText: string,
|
||||
type: 'due' | 'payment' = 'payment'
|
||||
) => {
|
||||
// Get color based on type
|
||||
const color =
|
||||
type === 'due'
|
||||
? dueStatus[statusText] || 'neutral'
|
||||
@@ -68,6 +63,11 @@ const getPillBadge = (
|
||||
);
|
||||
};
|
||||
|
||||
const dataTypeOptions: OptionType<string>[] = [
|
||||
{ value: 'received_date', label: 'Tanggal Terima' },
|
||||
{ value: 'po_date', label: 'Tanggal PO' },
|
||||
];
|
||||
|
||||
interface DebtSupplierTabProps {
|
||||
tabId: string;
|
||||
}
|
||||
@@ -81,26 +81,45 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
||||
const isAnyExportLoading =
|
||||
isPdfExportLoading || isExcelExportLoading || isExcelGeneralExportLoading;
|
||||
|
||||
// ===== PAGINATION STATE =====
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
// ===== SUBMISSION STATE =====
|
||||
const [filterParams, setFilterParams] = useState<DebtSupplierFilter>({
|
||||
start_date: undefined,
|
||||
end_date: undefined,
|
||||
supplier_ids: undefined,
|
||||
filter_by: undefined,
|
||||
});
|
||||
|
||||
// ===== DATE ERROR STATE =====
|
||||
const [dateErrorShown, setDateErrorShown] = useState(false);
|
||||
const [hasDateError, setHasDateError] = useState(false);
|
||||
|
||||
const handleFilterModalOpenRef = useRef(() => {});
|
||||
|
||||
const filterModal = useModal();
|
||||
|
||||
const setTabActions = useTabActionsStore((state) => state.setTabActions);
|
||||
const clearTabActions = useTabActionsStore((state) => state.clearTabActions);
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
setPage,
|
||||
setPageSize,
|
||||
toQueryString: getTableFilterQueryString,
|
||||
reset: resetFilter,
|
||||
} = useTableFilter<{
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
suppliers: OptionType<number>[];
|
||||
filterBy?: OptionType<string>;
|
||||
}>({
|
||||
initial: {
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
suppliers: [],
|
||||
filterBy: undefined,
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
pageSize: 'limit',
|
||||
start_date: 'start_date',
|
||||
end_date: 'end_date',
|
||||
suppliers: 'supplier_ids',
|
||||
filterBy: 'filter_by',
|
||||
},
|
||||
persist: true,
|
||||
storeName: 'debt-supplier-report-table',
|
||||
});
|
||||
|
||||
const {
|
||||
setInputValue: setSupplierInputValue,
|
||||
options: supplierOptions,
|
||||
@@ -108,154 +127,149 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
||||
loadMore: loadMoreSuppliers,
|
||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
||||
|
||||
const dataTypeOptions = useMemo(
|
||||
() => [
|
||||
{ value: 'received_date', label: 'Tanggal Terima' },
|
||||
{ value: 'po_date', label: 'Tanggal PO' },
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
// ===== FORMIK SETUP =====
|
||||
const formik = useFormik<DebtSupplierFilterType>({
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
supplierIds: null,
|
||||
filterBy: null,
|
||||
start_date: tableFilterState.start_date,
|
||||
end_date: tableFilterState.end_date,
|
||||
suppliers: tableFilterState.suppliers,
|
||||
filterBy: tableFilterState.filterBy,
|
||||
},
|
||||
validationSchema: DebtSupplierFilterSchema,
|
||||
onSubmit: (values) => {
|
||||
setFilterParams({
|
||||
start_date: values.startDate?.toString() || undefined,
|
||||
end_date: values.endDate?.toString() || undefined,
|
||||
supplier_ids:
|
||||
values.supplierIds?.map((v) => String(v.value)).join(',') ||
|
||||
undefined,
|
||||
filter_by: values.filterBy?.value?.toString() || undefined,
|
||||
});
|
||||
filterModal.closeModal();
|
||||
setCurrentPage(1);
|
||||
},
|
||||
onReset: () => {
|
||||
setFilterParams({
|
||||
start_date: undefined,
|
||||
end_date: undefined,
|
||||
supplier_ids: undefined,
|
||||
filter_by: undefined,
|
||||
});
|
||||
setCurrentPage(1);
|
||||
updateFilter('start_date', values.start_date, true);
|
||||
updateFilter('end_date', values.end_date, true);
|
||||
updateFilter('suppliers', values.suppliers, true);
|
||||
updateFilter('filterBy', values.filterBy, true);
|
||||
filterModal.closeModal();
|
||||
},
|
||||
});
|
||||
|
||||
handleFilterModalOpenRef.current = () => {
|
||||
const restoredFilterBy =
|
||||
dataTypeOptions.find((opt) => opt.value === filterParams.filter_by) ||
|
||||
null;
|
||||
const formikResetHandler = () => {
|
||||
resetFilter();
|
||||
|
||||
const supplierIdList = filterParams.supplier_ids
|
||||
? filterParams.supplier_ids.split(',')
|
||||
: [];
|
||||
const restoredSupplierIds = supplierOptions.filter((opt) =>
|
||||
supplierIdList.includes(String(opt.value))
|
||||
);
|
||||
setHasDateError(false);
|
||||
if (dateErrorShown) {
|
||||
toast.dismiss();
|
||||
setDateErrorShown(false);
|
||||
}
|
||||
|
||||
formik.setValues({
|
||||
startDate: filterParams.start_date || null,
|
||||
endDate: filterParams.end_date || null,
|
||||
supplierIds: restoredSupplierIds.length > 0 ? restoredSupplierIds : null,
|
||||
filterBy: restoredFilterBy,
|
||||
formik.resetForm({
|
||||
values: {
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
suppliers: [],
|
||||
filterBy: undefined,
|
||||
},
|
||||
});
|
||||
filterModal.openModal();
|
||||
|
||||
filterModal.closeModal();
|
||||
};
|
||||
|
||||
// ===== DATE CHANGE HANDLERS =====
|
||||
const handleStartDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
formik.setFieldValue('start_date', value);
|
||||
|
||||
if (value && formik.values.end_date) {
|
||||
if (new Date(formik.values.end_date) < new Date(value)) {
|
||||
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 handleEndDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
formik.setFieldValue('end_date', value);
|
||||
|
||||
if (value && formik.values.start_date) {
|
||||
if (new Date(value) < new Date(formik.values.start_date)) {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// ===== DATA FETCHING =====
|
||||
const { data: debtSupplier, isLoading } = useSWR(
|
||||
() => {
|
||||
const params = {
|
||||
supplier_ids: filterParams.supplier_ids,
|
||||
filter_by: filterParams.filter_by,
|
||||
start_date: filterParams.start_date,
|
||||
end_date: filterParams.end_date,
|
||||
page: currentPage,
|
||||
limit: pageSize,
|
||||
};
|
||||
|
||||
return ['debt-supplier-report', params];
|
||||
},
|
||||
([, params]) =>
|
||||
DebtSupplierApi.getDebtSupplierReport(
|
||||
params.supplier_ids,
|
||||
params.filter_by,
|
||||
params.start_date,
|
||||
params.end_date,
|
||||
params.page,
|
||||
params.limit
|
||||
)
|
||||
const { data: debtSupplierResponse, isLoading } = useSWR<
|
||||
BaseApiResponse<DebtSupplier[]>,
|
||||
AxiosError<BaseApiResponse>,
|
||||
SWRHttpKey
|
||||
>(
|
||||
`${DebtSupplierApi.basePath}/debt-supplier${getTableFilterQueryString()}`,
|
||||
httpClientFetcher
|
||||
);
|
||||
|
||||
const data: DebtSupplier[] = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(debtSupplier)
|
||||
? (debtSupplier?.data as unknown as DebtSupplier[]) || []
|
||||
: [],
|
||||
[debtSupplier]
|
||||
);
|
||||
const data: DebtSupplier[] = isResponseSuccess(debtSupplierResponse)
|
||||
? ((debtSupplierResponse?.data as unknown as DebtSupplier[]) ?? [])
|
||||
: [];
|
||||
|
||||
const meta = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(debtSupplier) && debtSupplier.meta
|
||||
? debtSupplier.meta
|
||||
: null,
|
||||
[debtSupplier]
|
||||
);
|
||||
const meta =
|
||||
isResponseSuccess(debtSupplierResponse) && debtSupplierResponse.meta
|
||||
? debtSupplierResponse.meta
|
||||
: null;
|
||||
|
||||
// ===== EXPORT DATA FETCHER =====
|
||||
const debtSupplierExport = useCallback(async (): Promise<
|
||||
DebtSupplier[] | null
|
||||
> => {
|
||||
const params = {
|
||||
supplier_ids:
|
||||
formik.values.supplierIds && formik.values.supplierIds.length > 0
|
||||
? formik.values.supplierIds.map((v) => String(v.value)).join(',')
|
||||
: undefined,
|
||||
filter_by: formik.values.filterBy?.value?.toString() || undefined,
|
||||
start_date: formik.values.startDate || undefined,
|
||||
end_date: formik.values.endDate || undefined,
|
||||
date_type: formik.values.filterBy
|
||||
? formik.values.filterBy.value
|
||||
: undefined,
|
||||
limit: 100,
|
||||
page: 1,
|
||||
};
|
||||
const supplier_ids =
|
||||
tableFilterState.suppliers.length > 0
|
||||
? tableFilterState.suppliers.map((o) => String(o.value)).join(',')
|
||||
: undefined;
|
||||
|
||||
const response = await DebtSupplierApi.getDebtSupplierReport(
|
||||
params.supplier_ids,
|
||||
params.filter_by,
|
||||
params.start_date,
|
||||
params.end_date
|
||||
supplier_ids,
|
||||
tableFilterState.filterBy?.value,
|
||||
tableFilterState.start_date || undefined,
|
||||
tableFilterState.end_date || undefined,
|
||||
1,
|
||||
100
|
||||
);
|
||||
|
||||
return isResponseSuccess(response)
|
||||
? (response.data as unknown as DebtSupplier[])
|
||||
: null;
|
||||
}, [
|
||||
formik.values.supplierIds,
|
||||
formik.values.startDate,
|
||||
formik.values.endDate,
|
||||
formik.values.filterBy,
|
||||
]);
|
||||
}, [tableFilterState]);
|
||||
|
||||
// ===== EXPORT HANDLERS =====
|
||||
const handleExportExcel = useCallback(async () => {
|
||||
setIsExcelExportLoading(true);
|
||||
try {
|
||||
const supplier_ids =
|
||||
tableFilterState.suppliers.length > 0
|
||||
? tableFilterState.suppliers.map((o) => String(o.value)).join(',')
|
||||
: undefined;
|
||||
await DebtSupplierApi.exportToExcelSupplierPerSheet(
|
||||
filterParams.supplier_ids,
|
||||
filterParams.filter_by,
|
||||
filterParams.start_date,
|
||||
filterParams.end_date
|
||||
supplier_ids,
|
||||
tableFilterState.filterBy?.value,
|
||||
tableFilterState.start_date || undefined,
|
||||
tableFilterState.end_date || undefined
|
||||
);
|
||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||
} catch {
|
||||
@@ -263,7 +277,30 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
||||
} finally {
|
||||
setIsExcelExportLoading(false);
|
||||
}
|
||||
}, [filterParams]);
|
||||
}, [tableFilterState]);
|
||||
|
||||
const handleExportExcelGeneral = useCallback(async () => {
|
||||
setIsExcelGeneralExportLoading(true);
|
||||
try {
|
||||
const supplier_ids =
|
||||
tableFilterState.suppliers.length > 0
|
||||
? tableFilterState.suppliers.map((o) => String(o.value)).join(',')
|
||||
: undefined;
|
||||
|
||||
await DebtSupplierApi.exportToExcelGeneral(
|
||||
supplier_ids,
|
||||
tableFilterState.filterBy?.value,
|
||||
tableFilterState.start_date || undefined,
|
||||
tableFilterState.end_date || undefined
|
||||
);
|
||||
|
||||
toast.success('Excel General berhasil dibuat dan diunduh.');
|
||||
} catch {
|
||||
toast.error('Gagal membuat Excel General. Silakan coba lagi.');
|
||||
} finally {
|
||||
setIsExcelGeneralExportLoading(false);
|
||||
}
|
||||
}, [tableFilterState]);
|
||||
|
||||
const handleExportPdf = useCallback(async () => {
|
||||
setIsPdfExportLoading(true);
|
||||
@@ -279,15 +316,18 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const supplierName =
|
||||
tableFilterState.suppliers.length > 0
|
||||
? tableFilterState.suppliers.map((o) => o.label).join(', ')
|
||||
: undefined;
|
||||
|
||||
await generateDebtSupplierPDF({
|
||||
data: allDataForExport,
|
||||
params: {
|
||||
supplier_name: formik.values.supplierIds
|
||||
?.map((v) => v.label)
|
||||
.join(', '),
|
||||
filter_by: formik.values.filterBy?.label,
|
||||
start_date: formik.values.startDate || undefined,
|
||||
end_date: formik.values.endDate || undefined,
|
||||
supplier_name: supplierName,
|
||||
filter_by: tableFilterState.filterBy?.label,
|
||||
start_date: tableFilterState.start_date || undefined,
|
||||
end_date: tableFilterState.end_date || undefined,
|
||||
},
|
||||
});
|
||||
toast.success('PDF berhasil dibuat dan diunduh.');
|
||||
@@ -296,131 +336,91 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
||||
} finally {
|
||||
setIsPdfExportLoading(false);
|
||||
}
|
||||
}, [
|
||||
debtSupplierExport,
|
||||
formik.values.supplierIds,
|
||||
formik.values.filterBy,
|
||||
formik.values.startDate,
|
||||
formik.values.endDate,
|
||||
]);
|
||||
}, [debtSupplierExport, tableFilterState]);
|
||||
|
||||
const handleExportExcelGeneral = useCallback(async () => {
|
||||
setIsExcelGeneralExportLoading(true);
|
||||
try {
|
||||
await DebtSupplierApi.exportToExcelGeneral(
|
||||
filterParams.supplier_ids,
|
||||
filterParams.filter_by,
|
||||
filterParams.start_date,
|
||||
filterParams.end_date
|
||||
);
|
||||
toast.success('Excel General berhasil dibuat dan diunduh.');
|
||||
} catch {
|
||||
toast.error('Gagal membuat Excel General. Silakan coba lagi.');
|
||||
} finally {
|
||||
setIsExcelGeneralExportLoading(false);
|
||||
}
|
||||
}, [filterParams]);
|
||||
// ===== TAB ACTIONS =====
|
||||
useEffect(() => {
|
||||
setTabActions(
|
||||
tabId,
|
||||
<div className='flex flex-row gap-3'>
|
||||
<ButtonFilter
|
||||
values={{
|
||||
start_date: tableFilterState.start_date,
|
||||
end_date: tableFilterState.end_date,
|
||||
suppliers: tableFilterState.suppliers,
|
||||
filterBy: tableFilterState.filterBy,
|
||||
}}
|
||||
fieldGroups={[['start_date', 'end_date']]}
|
||||
onClick={filterModal.openModal}
|
||||
variant='outline'
|
||||
className='px-3 py-2.5'
|
||||
/>
|
||||
|
||||
// ===== TAB ACTIONS COMPONENT =====
|
||||
const TabActions = useMemo(() => {
|
||||
return function TabActionsComponent() {
|
||||
const setTabActions = useTabActionsStore((state) => state.setTabActions);
|
||||
const clearTabActions = useTabActionsStore(
|
||||
(state) => state.clearTabActions
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTabActions(
|
||||
tabId,
|
||||
<div className='flex flex-row gap-3'>
|
||||
<ButtonFilter
|
||||
values={filterParams}
|
||||
fieldGroups={[['start_date', 'end_date']]}
|
||||
onClick={() => handleFilterModalOpenRef.current()}
|
||||
<Dropdown
|
||||
align='end'
|
||||
direction='bottom'
|
||||
className={{
|
||||
content:
|
||||
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
|
||||
}}
|
||||
trigger={
|
||||
<Button
|
||||
variant='outline'
|
||||
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'
|
||||
isLoading={isAnyExportLoading}
|
||||
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>Export</span>
|
||||
|
||||
<div className='w-px self-stretch bg-base-content/10' />
|
||||
|
||||
<Icon
|
||||
icon='heroicons:chevron-down'
|
||||
width={14}
|
||||
height={14}
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
}
|
||||
color='none'
|
||||
isLoading={isAnyExportLoading}
|
||||
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||
>
|
||||
<Button
|
||||
variant='ghost'
|
||||
color='none'
|
||||
onClick={handleExportExcel}
|
||||
isLoading={isExcelExportLoading}
|
||||
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} />
|
||||
Export to Excel - Supplier Per Sheet
|
||||
</Button>
|
||||
<Button
|
||||
variant='ghost'
|
||||
color='none'
|
||||
onClick={handleExportExcelGeneral}
|
||||
isLoading={isExcelGeneralExportLoading}
|
||||
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} />
|
||||
Export to Excel - General
|
||||
</Button>
|
||||
<Button
|
||||
variant='ghost'
|
||||
color='none'
|
||||
onClick={handleExportPdf}
|
||||
isLoading={isPdfExportLoading}
|
||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||
>
|
||||
<Icon icon='heroicons:document' width={20} height={20} />
|
||||
Export to PDF
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}, [setTabActions]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearTabActions(tabId);
|
||||
};
|
||||
}, [clearTabActions]);
|
||||
|
||||
return null;
|
||||
};
|
||||
<div className='flex flex-row items-center gap-1.5'>
|
||||
<Icon
|
||||
icon='heroicons:cloud-arrow-down'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
<span>Export</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={handleExportExcel}
|
||||
isLoading={isExcelExportLoading}
|
||||
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} />
|
||||
Export to Excel - Supplier Per Sheet
|
||||
</Button>
|
||||
<Button
|
||||
variant='ghost'
|
||||
color='none'
|
||||
onClick={handleExportExcelGeneral}
|
||||
isLoading={isExcelGeneralExportLoading}
|
||||
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} />
|
||||
Export to Excel - General
|
||||
</Button>
|
||||
<Button
|
||||
variant='ghost'
|
||||
color='none'
|
||||
onClick={handleExportPdf}
|
||||
isLoading={isPdfExportLoading}
|
||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||
>
|
||||
<Icon icon='heroicons:document' width={20} height={20} />
|
||||
Export to PDF
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}, [
|
||||
tabId,
|
||||
filterParams,
|
||||
setTabActions,
|
||||
tableFilterState,
|
||||
filterModal.openModal,
|
||||
isAnyExportLoading,
|
||||
handleExportExcel,
|
||||
handleExportExcelGeneral,
|
||||
@@ -430,24 +430,9 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
||||
isPdfExportLoading,
|
||||
]);
|
||||
|
||||
const TabActionsElement = useMemo(() => <TabActions />, [TabActions]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (dateErrorShown) {
|
||||
toast.dismiss();
|
||||
}
|
||||
};
|
||||
}, [dateErrorShown]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (dateErrorShown) {
|
||||
toast.dismiss();
|
||||
setDateErrorShown(false);
|
||||
}
|
||||
};
|
||||
}, [filterModal.open, dateErrorShown]);
|
||||
return () => clearTabActions(tabId);
|
||||
}, [tabId, clearTabActions]);
|
||||
|
||||
const getTableColumns = (supplier?: DebtSupplier): ColumnDef<DebtRow>[] => [
|
||||
{
|
||||
@@ -662,9 +647,9 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{TabActionsElement}
|
||||
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
|
||||
{isLoading && (
|
||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||
@@ -693,16 +678,16 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
||||
<Pagination
|
||||
totalItems={meta.total_results || 0}
|
||||
itemsPerPage={meta.limit || 0}
|
||||
currentPage={meta.page || 0}
|
||||
onPrevPage={() =>
|
||||
setCurrentPage((curr) => (curr > 1 ? curr - 1 : curr))
|
||||
}
|
||||
currentPage={tableFilterState.page}
|
||||
onPrevPage={() => setPage(Math.max(1, tableFilterState.page - 1))}
|
||||
onNextPage={() =>
|
||||
setCurrentPage((curr) =>
|
||||
meta.total_pages && curr < meta.total_pages ? curr + 1 : curr
|
||||
setPage(
|
||||
meta.total_pages && tableFilterState.page < meta.total_pages
|
||||
? tableFilterState.page + 1
|
||||
: tableFilterState.page
|
||||
)
|
||||
}
|
||||
onPageChange={(pageNumber) => setCurrentPage(pageNumber)}
|
||||
onPageChange={setPage}
|
||||
rowOptions={[10, 20, 50, 100]}
|
||||
onRowChange={setPageSize}
|
||||
/>
|
||||
@@ -802,16 +787,16 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
||||
<Pagination
|
||||
totalItems={meta.total_results || 0}
|
||||
itemsPerPage={meta.limit || 0}
|
||||
currentPage={meta.page || 0}
|
||||
onPrevPage={() =>
|
||||
setCurrentPage((curr) => (curr > 1 ? curr - 1 : curr))
|
||||
}
|
||||
currentPage={tableFilterState.page}
|
||||
onPrevPage={() => setPage(Math.max(1, tableFilterState.page - 1))}
|
||||
onNextPage={() =>
|
||||
setCurrentPage((curr) =>
|
||||
meta.total_pages && curr < meta.total_pages ? curr + 1 : curr
|
||||
setPage(
|
||||
meta.total_pages && tableFilterState.page < meta.total_pages
|
||||
? tableFilterState.page + 1
|
||||
: tableFilterState.page
|
||||
)
|
||||
}
|
||||
onPageChange={(pageNumber) => setCurrentPage(pageNumber)}
|
||||
onPageChange={setPage}
|
||||
rowOptions={[10, 20, 50, 100]}
|
||||
onRowChange={setPageSize}
|
||||
/>
|
||||
@@ -827,23 +812,23 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
||||
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
||||
}}
|
||||
>
|
||||
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||
{/* 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'
|
||||
type='button'
|
||||
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>
|
||||
{/* 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'
|
||||
type='button'
|
||||
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={formik.handleSubmit} onReset={formikResetHandler}>
|
||||
{/* Modal Body */}
|
||||
<div className='p-4 flex flex-col gap-1.5'>
|
||||
<div>
|
||||
@@ -852,153 +837,68 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
||||
</label>
|
||||
<div className='flex flex-row gap-1.5 items-center justify-between'>
|
||||
<DateInput
|
||||
name='startDate'
|
||||
value={formik.values.startDate || ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
formik.setFieldValue('startDate', value || null);
|
||||
|
||||
if (value && formik.values.endDate) {
|
||||
const startDate = new Date(value);
|
||||
const endDateObj = new Date(formik.values.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);
|
||||
}
|
||||
}}
|
||||
name='start_date'
|
||||
value={formik.values.start_date || ''}
|
||||
onChange={handleStartDateChange}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
isError={
|
||||
formik.touched.startDate && !!formik.errors.startDate
|
||||
}
|
||||
errorMessage={formik.errors.startDate}
|
||||
isNestedModal
|
||||
/>
|
||||
<hr className='w-full max-w-3 h-px border-base-content/10'></hr>
|
||||
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
||||
<DateInput
|
||||
name='endDate'
|
||||
value={formik.values.endDate || ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
formik.setFieldValue('endDate', value || null);
|
||||
|
||||
if (value && formik.values.startDate) {
|
||||
const startDateObj = new Date(formik.values.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);
|
||||
}
|
||||
}}
|
||||
name='end_date'
|
||||
value={formik.values.end_date || ''}
|
||||
onChange={handleEndDateChange}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
isError={
|
||||
(formik.touched.endDate && !!formik.errors.endDate) ||
|
||||
hasDateError
|
||||
}
|
||||
errorMessage={formik.errors.endDate}
|
||||
isNestedModal
|
||||
isError={hasDateError}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<SelectInputCheckbox
|
||||
label='Supplier'
|
||||
placeholder='Pilih Supplier'
|
||||
isMulti
|
||||
options={supplierOptions}
|
||||
value={
|
||||
(formik.values.supplierIds as
|
||||
| { value: number; label: string }
|
||||
| { value: number; label: string }[]
|
||||
| null
|
||||
| undefined) || []
|
||||
}
|
||||
onChange={(val) => {
|
||||
formik.setFieldValue(
|
||||
'supplierIds',
|
||||
Array.isArray(val) ? val : val ? [val] : null
|
||||
);
|
||||
}}
|
||||
onInputChange={setSupplierInputValue}
|
||||
onMenuScrollToBottom={loadMoreSuppliers}
|
||||
isLoading={isLoadingSupplierOptions}
|
||||
isClearable
|
||||
className={{ wrapper: 'w-full' }}
|
||||
isError={
|
||||
formik.touched.supplierIds && !!formik.errors.supplierIds
|
||||
}
|
||||
errorMessage={formik.errors.supplierIds as string}
|
||||
/>
|
||||
</div>
|
||||
<SelectInputCheckbox
|
||||
label='Supplier'
|
||||
placeholder='Pilih Supplier'
|
||||
options={supplierOptions}
|
||||
value={formik.values.suppliers}
|
||||
onChange={(val) =>
|
||||
formik.setFieldValue('suppliers', Array.isArray(val) ? val : [])
|
||||
}
|
||||
onInputChange={setSupplierInputValue}
|
||||
onMenuScrollToBottom={loadMoreSuppliers}
|
||||
isLoading={isLoadingSupplierOptions}
|
||||
isClearable
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<SelectInputRadio
|
||||
label='Filter Berdasarkan'
|
||||
placeholder='Pilih Filter Berdasarkan'
|
||||
options={dataTypeOptions}
|
||||
value={
|
||||
(formik.values.filterBy as
|
||||
| { value: string; label: string }
|
||||
| { value: string; label: string }[]
|
||||
| null
|
||||
| undefined) || null
|
||||
}
|
||||
onChange={(val) => {
|
||||
formik.setFieldValue(
|
||||
'filterBy',
|
||||
val ? (val as OptionType) : null
|
||||
);
|
||||
}}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
isClearable
|
||||
isError={formik.touched.filterBy && !!formik.errors.filterBy}
|
||||
errorMessage={formik.errors.filterBy as string}
|
||||
/>
|
||||
</div>
|
||||
<SelectInputRadio
|
||||
label='Filter Berdasarkan'
|
||||
placeholder='Pilih Filter Berdasarkan'
|
||||
options={dataTypeOptions}
|
||||
value={formik.values.filterBy ?? null}
|
||||
onChange={(val) =>
|
||||
formik.setFieldValue(
|
||||
'filterBy',
|
||||
!Array.isArray(val) ? (val ?? undefined) : undefined
|
||||
)
|
||||
}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
isClearable
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
{/* Modal Footer */}
|
||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||
<Button
|
||||
variant='soft'
|
||||
color='none'
|
||||
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'
|
||||
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
|
||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||
type='submit'
|
||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||
disabled={hasDateError}
|
||||
>
|
||||
Apply Filter
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user