mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-23 23:05:46 +00:00
refactor(FE): Refactor expense report page to use tab-based layout
This commit is contained in:
@@ -0,0 +1,755 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { Icon } from '@iconify/react';
|
||||
import Button from '@/components/Button';
|
||||
import Dropdown from '@/components/dropdown/Dropdown';
|
||||
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import { useFormik } from 'formik';
|
||||
import {
|
||||
ReportExpenseFilterSchema,
|
||||
type ReportExpenseFilterValues,
|
||||
} from '@/components/pages/report/expense/filter/ReportExpenseFilter';
|
||||
import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge';
|
||||
import RealizationStatusBadge from '@/components/pages/expense/RealizationStatusBadge';
|
||||
import Table from '@/components/Table';
|
||||
import { cn, formatCurrency, formatDate } from '@/lib/helper';
|
||||
import { ReportExpense } from '@/types/api/report/report-expense';
|
||||
import { ReportExpenseApi } from '@/services/api/report';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useReportTabStore } from '@/stores/report/report-tab.store';
|
||||
import Modal, { useModal } from '@/components/Modal';
|
||||
import Pagination from '@/components/Pagination';
|
||||
import ReportExpenseSkeleton from '@/components/pages/report/expense/skeleton/ReportExpenseSkeleton';
|
||||
import { generateReportExpensePDF } from '../export/ReportExpenseExportPDF';
|
||||
import { generateReportExpenseExcel } from '../export/ReportExpenseExportXLSX';
|
||||
import toast from 'react-hot-toast';
|
||||
import {
|
||||
KandangApi,
|
||||
LocationApi,
|
||||
NonstockApi,
|
||||
SupplierApi,
|
||||
} from '@/services/api/master-data';
|
||||
import { Supplier } from '@/types/api/master-data/supplier';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { Nonstock } from '@/types/api/master-data/nonstock';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { httpClient } from '@/services/http/client';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
|
||||
interface ReportExpenseTabProps {
|
||||
tabId: string;
|
||||
}
|
||||
|
||||
interface FilterParams {
|
||||
location_id?: string;
|
||||
supplier_id?: string;
|
||||
kandang_id?: string;
|
||||
nonstock_id?: string;
|
||||
realization_date?: string;
|
||||
category?: string;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
||||
// ===== STATE MANAGEMENT =====
|
||||
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
||||
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
|
||||
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
|
||||
|
||||
// ===== SUBMISSION STATE =====
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
const [filterParams, setFilterParams] = useState<FilterParams>({});
|
||||
|
||||
// ===== PAGINATION STATE =====
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
const filterModal = useModal();
|
||||
|
||||
// ===== OPTIONS =====
|
||||
const {
|
||||
setInputValue: setLocationInputValue,
|
||||
options: locationOptions,
|
||||
isLoadingOptions: isLoadingLocations,
|
||||
loadMore: loadMoreLocations,
|
||||
} = useSelect<Kandang>(LocationApi.basePath, 'id', 'name', 'search');
|
||||
|
||||
const {
|
||||
setInputValue: setSupplierInputValue,
|
||||
options: supplierOptions,
|
||||
isLoadingOptions: isLoadingSuppliers,
|
||||
loadMore: loadMoreSuppliers,
|
||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search');
|
||||
|
||||
const {
|
||||
setInputValue: setKandangInputValue,
|
||||
options: kandangOptions,
|
||||
isLoadingOptions: isLoadingKandangs,
|
||||
loadMore: loadMoreKandangs,
|
||||
} = useSelect<Kandang>(KandangApi.basePath, 'id', 'name', 'search');
|
||||
|
||||
const {
|
||||
setInputValue: setNonstockInputValue,
|
||||
options: nonstockOptions,
|
||||
isLoadingOptions: isLoadingNonstocks,
|
||||
loadMore: loadMoreNonstocks,
|
||||
} = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name', 'search');
|
||||
|
||||
const categoryOptions = useMemo(
|
||||
() => [
|
||||
{ value: 'BOP', label: 'BOP' },
|
||||
{ value: 'NON-BOP', label: 'Non BOP' },
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
// ===== FORMIK SETUP =====
|
||||
const formik = useFormik<ReportExpenseFilterValues>({
|
||||
initialValues: {
|
||||
location_id: null,
|
||||
supplier_id: null,
|
||||
kandang_id: null,
|
||||
nonstock_id: null,
|
||||
realization_date: null,
|
||||
category: null,
|
||||
},
|
||||
validationSchema: ReportExpenseFilterSchema,
|
||||
onSubmit: (values) => {
|
||||
setFilterParams({
|
||||
location_id: values.location_id?.value
|
||||
? String(values.location_id.value)
|
||||
: undefined,
|
||||
supplier_id: values.supplier_id?.value
|
||||
? String(values.supplier_id.value)
|
||||
: undefined,
|
||||
kandang_id: values.kandang_id?.value
|
||||
? String(values.kandang_id.value)
|
||||
: undefined,
|
||||
nonstock_id: values.nonstock_id?.value
|
||||
? String(values.nonstock_id.value)
|
||||
: undefined,
|
||||
realization_date: values.realization_date || undefined,
|
||||
category: values.category?.value
|
||||
? String(values.category.value)
|
||||
: undefined,
|
||||
});
|
||||
filterModal.closeModal();
|
||||
setIsSubmitted(true);
|
||||
setPage(1);
|
||||
},
|
||||
onReset: () => {
|
||||
setFilterParams({});
|
||||
setIsSubmitted(false);
|
||||
setPage(1);
|
||||
},
|
||||
});
|
||||
|
||||
// ===== FILTER VALUES =====
|
||||
const locationValue = useMemo(
|
||||
() => formik.values.location_id,
|
||||
[formik.values.location_id]
|
||||
);
|
||||
const supplierValue = useMemo(
|
||||
() => formik.values.supplier_id,
|
||||
[formik.values.supplier_id]
|
||||
);
|
||||
const kandangValue = useMemo(
|
||||
() => formik.values.kandang_id,
|
||||
[formik.values.kandang_id]
|
||||
);
|
||||
const nonstockValue = useMemo(
|
||||
() => formik.values.nonstock_id,
|
||||
[formik.values.nonstock_id]
|
||||
);
|
||||
const categoryValue = useMemo(
|
||||
() => formik.values.category,
|
||||
[formik.values.category]
|
||||
);
|
||||
|
||||
// ===== ACTIVE FILTERS COUNT =====
|
||||
const activeFiltersCount = useMemo(() => {
|
||||
let count = 0;
|
||||
if (filterParams.location_id) count += 1;
|
||||
if (filterParams.supplier_id) count += 1;
|
||||
if (filterParams.kandang_id) count += 1;
|
||||
if (filterParams.nonstock_id) count += 1;
|
||||
if (filterParams.realization_date) count += 1;
|
||||
if (filterParams.category) count += 1;
|
||||
return count;
|
||||
}, [filterParams]);
|
||||
|
||||
const hasFilters = activeFiltersCount > 0;
|
||||
|
||||
// ===== DATA FETCHING =====
|
||||
const { data: reportExpenseResponse, isLoading } = useSWR(
|
||||
isSubmitted
|
||||
? () => {
|
||||
const params = new URLSearchParams();
|
||||
if (filterParams.location_id)
|
||||
params.append('location_id', filterParams.location_id);
|
||||
if (filterParams.supplier_id)
|
||||
params.append('supplier_id', filterParams.supplier_id);
|
||||
if (filterParams.kandang_id)
|
||||
params.append('kandang_id', filterParams.kandang_id);
|
||||
if (filterParams.nonstock_id)
|
||||
params.append('nonstock_id', filterParams.nonstock_id);
|
||||
if (filterParams.realization_date)
|
||||
params.append('realization_date', filterParams.realization_date);
|
||||
if (filterParams.category)
|
||||
params.append('category', filterParams.category);
|
||||
params.append('page', String(page));
|
||||
params.append('limit', String(pageSize));
|
||||
|
||||
return [`${ReportExpenseApi.basePath}?${params.toString()}`];
|
||||
}
|
||||
: null,
|
||||
([url]: string[]) => httpClient<BaseApiResponse<ReportExpense[]>>(url)
|
||||
);
|
||||
|
||||
const data: ReportExpense[] = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(reportExpenseResponse)
|
||||
? (reportExpenseResponse.data as ReportExpense[]) || []
|
||||
: [],
|
||||
[reportExpenseResponse]
|
||||
);
|
||||
|
||||
const meta = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(reportExpenseResponse) && reportExpenseResponse.meta
|
||||
? reportExpenseResponse.meta
|
||||
: null,
|
||||
[reportExpenseResponse]
|
||||
);
|
||||
|
||||
// ===== EXPORT DATA FETCHER =====
|
||||
const reportExpenseExport = useCallback(async (): Promise<
|
||||
ReportExpense[] | null
|
||||
> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filterParams.location_id)
|
||||
params.append('location_id', filterParams.location_id);
|
||||
if (filterParams.supplier_id)
|
||||
params.append('supplier_id', filterParams.supplier_id);
|
||||
if (filterParams.kandang_id)
|
||||
params.append('kandang_id', filterParams.kandang_id);
|
||||
if (filterParams.nonstock_id)
|
||||
params.append('nonstock_id', filterParams.nonstock_id);
|
||||
if (filterParams.realization_date)
|
||||
params.append('realization_date', filterParams.realization_date);
|
||||
if (filterParams.category) params.append('category', filterParams.category);
|
||||
params.append('limit', '100');
|
||||
params.append('page', '1');
|
||||
|
||||
const response = await httpClient<BaseApiResponse<ReportExpense[]>>(
|
||||
`${ReportExpenseApi.basePath}?${params.toString()}`
|
||||
);
|
||||
|
||||
return isResponseSuccess(response) ? response.data : null;
|
||||
}, [filterParams]);
|
||||
|
||||
// ===== EXPORT HANDLERS =====
|
||||
const handleExportExcel = useCallback(async () => {
|
||||
setIsExcelExportLoading(true);
|
||||
try {
|
||||
const allDataForExport = await reportExpenseExport();
|
||||
|
||||
if (!allDataForExport || allDataForExport.length === 0) {
|
||||
toast.error('Tidak ada data untuk diekspor.');
|
||||
return;
|
||||
}
|
||||
|
||||
await generateReportExpenseExcel(allDataForExport);
|
||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||
} catch {
|
||||
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
||||
} finally {
|
||||
setIsExcelExportLoading(false);
|
||||
}
|
||||
}, [reportExpenseExport]);
|
||||
|
||||
const handleExportPDF = useCallback(async () => {
|
||||
setIsPdfExportLoading(true);
|
||||
try {
|
||||
const allData = await reportExpenseExport();
|
||||
if (!allData || allData.length === 0) {
|
||||
toast.error('Tidak ada data untuk diekspor.');
|
||||
return;
|
||||
}
|
||||
|
||||
const pdfParams = {
|
||||
location_name: locationValue?.label,
|
||||
supplier_name: supplierValue?.label,
|
||||
realization_date: formik.values.realization_date || undefined,
|
||||
};
|
||||
|
||||
await generateReportExpensePDF(allData, pdfParams);
|
||||
|
||||
toast.success('PDF berhasil dibuat dan diunduh.');
|
||||
} catch {
|
||||
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
||||
} finally {
|
||||
setIsPdfExportLoading(false);
|
||||
}
|
||||
}, [
|
||||
reportExpenseExport,
|
||||
locationValue,
|
||||
supplierValue,
|
||||
kandangValue,
|
||||
nonstockValue,
|
||||
categoryValue,
|
||||
formik.values.realization_date,
|
||||
]);
|
||||
|
||||
// ===== REGISTER TAB ACTIONS TO STORE =====
|
||||
const setTabActions = useReportTabStore((state) => state.setTabActions);
|
||||
const clearTabActions = useReportTabStore((state) => state.clearTabActions);
|
||||
|
||||
useEffect(() => {
|
||||
setTabActions(
|
||||
tabId,
|
||||
<div className='flex flex-row gap-3'>
|
||||
<Button
|
||||
variant='outline'
|
||||
color='none'
|
||||
onClick={() => filterModal.openModal()}
|
||||
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>
|
||||
|
||||
<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>
|
||||
}
|
||||
>
|
||||
<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
|
||||
</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,
|
||||
hasFilters,
|
||||
activeFiltersCount,
|
||||
isAnyExportLoading,
|
||||
handleExportExcel,
|
||||
handleExportPDF,
|
||||
setTabActions,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearTabActions(tabId);
|
||||
};
|
||||
}, [tabId, clearTabActions]);
|
||||
|
||||
// ===== TABLE COLUMNS DEFINITION =====
|
||||
const columns = useMemo((): ColumnDef<ReportExpense>[] => {
|
||||
return [
|
||||
{
|
||||
header: 'No',
|
||||
cell: (props) => (page - 1) * pageSize + props.row.index + 1,
|
||||
},
|
||||
{
|
||||
header: 'No. PO',
|
||||
accessorKey: 'po_number',
|
||||
},
|
||||
{
|
||||
header: 'No. Referensi',
|
||||
accessorKey: 'reference_number',
|
||||
},
|
||||
{
|
||||
header: 'Tanggal Realisasi',
|
||||
accessorKey: 'realization_date',
|
||||
cell: ({ row }) => {
|
||||
return formatDate(row.original?.realization_date, 'DD MMM, YYYY');
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Tanggal Transaksi',
|
||||
accessorKey: 'transaction_date',
|
||||
cell: ({ row }) => {
|
||||
return formatDate(row.original?.transaction_date, 'DD MMM, YYYY');
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Kategori',
|
||||
accessorKey: 'category',
|
||||
},
|
||||
{
|
||||
header: 'Produk',
|
||||
accessorFn: (row) => row.pengajuan?.nonstock?.name,
|
||||
},
|
||||
{
|
||||
header: 'Supplier',
|
||||
accessorFn: (row) => row.supplier?.name,
|
||||
},
|
||||
{
|
||||
header: 'Lokasi',
|
||||
accessorFn: (row) => row.kandang?.location?.name,
|
||||
},
|
||||
{
|
||||
header: 'Kandang',
|
||||
accessorFn: (row) => row.kandang?.name,
|
||||
},
|
||||
{
|
||||
header: 'Pengajuan',
|
||||
columns: [
|
||||
{
|
||||
header: 'Qty',
|
||||
id: 'qty_pengajuan',
|
||||
accessorFn: (row) => row.pengajuan?.qty,
|
||||
cell: ({ row }) =>
|
||||
row.original.pengajuan?.qty?.toLocaleString('id-ID') || '0',
|
||||
},
|
||||
{
|
||||
header: 'Harga',
|
||||
id: 'harga_pengajuan',
|
||||
accessorFn: (row) => row.pengajuan?.price,
|
||||
cell: ({ row }) =>
|
||||
formatCurrency(row.original.pengajuan?.price || 0),
|
||||
},
|
||||
{
|
||||
header: 'Total',
|
||||
id: 'total_pengajuan',
|
||||
accessorFn: (row) =>
|
||||
(row.pengajuan?.qty || 0) * (row.pengajuan?.price || 0),
|
||||
cell: ({ row }) => {
|
||||
const total =
|
||||
(row.original.pengajuan?.qty || 0) *
|
||||
(row.original.pengajuan?.price || 0);
|
||||
return formatCurrency(total);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: 'Realisasi',
|
||||
columns: [
|
||||
{
|
||||
header: 'Qty',
|
||||
id: 'qty_realisasi',
|
||||
accessorFn: (row) => row.realisasi?.qty,
|
||||
cell: ({ row }) =>
|
||||
row.original.realisasi?.qty?.toLocaleString('id-ID') || '0',
|
||||
},
|
||||
{
|
||||
header: 'Harga',
|
||||
id: 'harga_realisasi',
|
||||
accessorFn: (row) => row.realisasi?.price,
|
||||
cell: ({ row }) =>
|
||||
formatCurrency(row.original.realisasi?.price || 0),
|
||||
},
|
||||
{
|
||||
header: 'Total',
|
||||
id: 'total_realisasi',
|
||||
accessorFn: (row) =>
|
||||
(row.realisasi?.qty || 0) * (row.realisasi?.price || 0),
|
||||
cell: ({ row }) => {
|
||||
const total =
|
||||
(row.original.realisasi?.qty || 0) *
|
||||
(row.original.realisasi?.price || 0);
|
||||
return formatCurrency(total);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: 'Status Pencairan',
|
||||
cell: (props) => (
|
||||
<RealizationStatusBadge
|
||||
approval={props.row.original?.latest_approval}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Status BOP',
|
||||
cell: (props) => (
|
||||
<ExpenseStatusBadge approval={props.row.original?.latest_approval} />
|
||||
),
|
||||
},
|
||||
];
|
||||
}, [page, pageSize]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
|
||||
{!isSubmitted ? (
|
||||
<ReportExpenseSkeleton
|
||||
columns={columns}
|
||||
icon={
|
||||
<Icon
|
||||
icon='heroicons:funnel'
|
||||
className='text-white'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
}
|
||||
title='No Filters Selected'
|
||||
subtitle='Please choose filters to narrow down your results and make your search easier.'
|
||||
/>
|
||||
) : isLoading ? (
|
||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
</div>
|
||||
) : !data || data.length === 0 ? (
|
||||
<ReportExpenseSkeleton
|
||||
columns={columns}
|
||||
icon={
|
||||
<Icon
|
||||
icon='heroicons:chart-bar'
|
||||
className='text-white'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
}
|
||||
title='Data Not Yet Available'
|
||||
subtitle='Please change your filters to get the data.'
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Table
|
||||
data={data}
|
||||
columns={columns}
|
||||
pageSize={pageSize}
|
||||
page={meta?.page || 1}
|
||||
totalItems={meta?.total_results || 0}
|
||||
onPageChange={setPage}
|
||||
onPageSizeChange={setPageSize}
|
||||
className={{
|
||||
containerClassName: 'w-full mb-0!',
|
||||
tableWrapperClassName: 'overflow-x-auto',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
||||
headerColumnClassName:
|
||||
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200 text-nowrap',
|
||||
bodyRowClassName:
|
||||
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
/>
|
||||
{meta && (
|
||||
<div className='max-w-sm ml-auto'>
|
||||
<Pagination
|
||||
totalItems={meta.total_results || 0}
|
||||
itemsPerPage={meta.limit || 0}
|
||||
currentPage={meta.page || 0}
|
||||
onPrevPage={() =>
|
||||
setPage((currPage) =>
|
||||
currPage > 1 ? currPage - 1 : currPage
|
||||
)
|
||||
}
|
||||
onNextPage={() =>
|
||||
setPage((currPage) =>
|
||||
meta && meta.total_pages && currPage < meta.total_pages
|
||||
? currPage + 1
|
||||
: currPage
|
||||
)
|
||||
}
|
||||
onPageChange={(pageNumber) => setPage(pageNumber)}
|
||||
rowOptions={[10, 20, 50, 100]}
|
||||
onRowChange={setPageSize}
|
||||
/>
|
||||
</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={formik.handleSubmit} onReset={formik.handleReset}>
|
||||
{/* Modal Body */}
|
||||
<div className='p-4 flex flex-col gap-3'>
|
||||
<SelectInput
|
||||
label='Lokasi'
|
||||
placeholder='Pilih Lokasi'
|
||||
options={locationOptions}
|
||||
isLoading={isLoadingLocations}
|
||||
value={locationValue}
|
||||
onChange={(val) => {
|
||||
formik.setFieldValue('location_id', val);
|
||||
formik.setFieldValue('kandang_id', null);
|
||||
}}
|
||||
onInputChange={setLocationInputValue}
|
||||
onMenuScrollToBottom={loadMoreLocations}
|
||||
isClearable
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label='Kandang'
|
||||
placeholder='Pilih Kandang'
|
||||
options={kandangOptions}
|
||||
isLoading={isLoadingKandangs}
|
||||
value={kandangValue}
|
||||
onChange={(val) => {
|
||||
formik.setFieldValue('kandang_id', val);
|
||||
}}
|
||||
onInputChange={setKandangInputValue}
|
||||
onMenuScrollToBottom={loadMoreKandangs}
|
||||
isClearable
|
||||
isDisabled={!formik.values.location_id}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label='Supplier'
|
||||
placeholder='Pilih Supplier'
|
||||
options={supplierOptions}
|
||||
isLoading={isLoadingSuppliers}
|
||||
value={supplierValue}
|
||||
onChange={(val) => {
|
||||
formik.setFieldValue('supplier_id', val);
|
||||
}}
|
||||
onInputChange={setSupplierInputValue}
|
||||
onMenuScrollToBottom={loadMoreSuppliers}
|
||||
isClearable
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label='Produk'
|
||||
placeholder='Pilih Produk'
|
||||
options={nonstockOptions}
|
||||
isLoading={isLoadingNonstocks}
|
||||
value={nonstockValue}
|
||||
onChange={(val) => {
|
||||
formik.setFieldValue('nonstock_id', val);
|
||||
}}
|
||||
onInputChange={setNonstockInputValue}
|
||||
onMenuScrollToBottom={loadMoreNonstocks}
|
||||
isClearable
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label='Kategori'
|
||||
placeholder='Pilih Kategori'
|
||||
options={categoryOptions}
|
||||
value={categoryValue}
|
||||
onChange={(val) => {
|
||||
formik.setFieldValue('category', val);
|
||||
}}
|
||||
isClearable
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
|
||||
<DateInput
|
||||
label='Tanggal Realisasi'
|
||||
name='realization_date'
|
||||
value={formik.values.realization_date || ''}
|
||||
onChange={(e) => {
|
||||
formik.setFieldValue(
|
||||
'realization_date',
|
||||
e.target.value || null
|
||||
);
|
||||
}}
|
||||
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'
|
||||
>
|
||||
Apply Filter
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReportExpenseTab;
|
||||
Reference in New Issue
Block a user