mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
816 lines
26 KiB
TypeScript
816 lines
26 KiB
TypeScript
'use client';
|
|
|
|
import React, {
|
|
useState,
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
} 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 { formatCurrency, formatDate } from '@/lib/helper';
|
|
import { ReportExpense } from '@/types/api/report/report-expense';
|
|
import { ReportExpenseApi } from '@/services/api/report/expense-report';
|
|
import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper';
|
|
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.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 {
|
|
LocationApi,
|
|
NonstockApi,
|
|
SupplierApi,
|
|
} from '@/services/api/master-data';
|
|
import { Supplier } from '@/types/api/master-data/supplier';
|
|
import { Nonstock } from '@/types/api/master-data/nonstock';
|
|
import { ColumnDef, SortingState, Updater } from '@tanstack/react-table';
|
|
import { httpClient } from '@/services/http/client';
|
|
import { BaseApiResponse } from '@/types/api/api-general';
|
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
|
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
|
import { ProjectFlockKandangApi } from '@/services/api/production/project-flock-kandang';
|
|
|
|
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 [filterParams, setFilterParams] = useState<FilterParams>({});
|
|
|
|
// ===== PAGINATION STATE =====
|
|
const [page, setPage] = useState(1);
|
|
const [pageSize, setPageSize] = useState(10);
|
|
|
|
// ===== SORTING STATE =====
|
|
const [sortBy, setSortBy] = useState('');
|
|
const [orderBy, setOrderBy] = useState('');
|
|
|
|
const sorting: SortingState = sortBy
|
|
? [{ id: sortBy, desc: orderBy === 'desc' }]
|
|
: [];
|
|
|
|
const handleSortingChange = (updater: Updater<SortingState>) => {
|
|
const next = typeof updater === 'function' ? updater(sorting) : updater;
|
|
if (next.length > 0) {
|
|
setSortBy(next[0].id);
|
|
setOrderBy(next[0].desc ? 'desc' : 'asc');
|
|
} else {
|
|
setSortBy('');
|
|
setOrderBy('');
|
|
}
|
|
};
|
|
|
|
const handleFilterModalOpenRef = useRef(() => {});
|
|
|
|
const filterModal = useModal();
|
|
|
|
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();
|
|
setPage(1);
|
|
},
|
|
onReset: () => {
|
|
setFilterParams({});
|
|
setPage(1);
|
|
filterModal.closeModal();
|
|
},
|
|
});
|
|
|
|
handleFilterModalOpenRef.current = () => {
|
|
const restoredLocation = filterParams.location_id
|
|
? locationOptions.find(
|
|
(opt) => String(opt.value) === filterParams.location_id
|
|
) || {
|
|
value: filterParams.location_id,
|
|
label: filterParams.location_id,
|
|
}
|
|
: null;
|
|
const restoredSupplier = filterParams.supplier_id
|
|
? supplierOptions.find(
|
|
(opt) => String(opt.value) === filterParams.supplier_id
|
|
) || {
|
|
value: filterParams.supplier_id,
|
|
label: filterParams.supplier_id,
|
|
}
|
|
: null;
|
|
const restoredKandang = filterParams.kandang_id
|
|
? projectFlockKandangOptions.find(
|
|
(opt) => String(opt.value) === filterParams.kandang_id
|
|
) || { value: filterParams.kandang_id, label: filterParams.kandang_id }
|
|
: null;
|
|
const restoredNonstock = filterParams.nonstock_id
|
|
? nonstockOptions.find(
|
|
(opt) => String(opt.value) === filterParams.nonstock_id
|
|
) || {
|
|
value: filterParams.nonstock_id,
|
|
label: filterParams.nonstock_id,
|
|
}
|
|
: null;
|
|
const restoredCategory = filterParams.category
|
|
? categoryOptions.find((opt) => opt.value === filterParams.category) ||
|
|
null
|
|
: null;
|
|
|
|
formik.setValues({
|
|
location_id: restoredLocation,
|
|
supplier_id: restoredSupplier,
|
|
kandang_id: restoredKandang,
|
|
nonstock_id: restoredNonstock,
|
|
realization_date: filterParams.realization_date || null,
|
|
category: restoredCategory,
|
|
});
|
|
filterModal.openModal();
|
|
};
|
|
|
|
// ===== OPTIONS =====
|
|
const {
|
|
setInputValue: setLocationInputValue,
|
|
options: locationOptions,
|
|
isLoadingOptions: isLoadingLocations,
|
|
loadMore: loadMoreLocations,
|
|
} = useSelect<Location>(LocationApi.basePath, 'id', 'name', 'search');
|
|
|
|
const {
|
|
setInputValue: setSupplierInputValue,
|
|
options: supplierOptions,
|
|
isLoadingOptions: isLoadingSuppliers,
|
|
loadMore: loadMoreSuppliers,
|
|
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search');
|
|
|
|
const {
|
|
setInputValue: setProjectFlockKandangInputValue,
|
|
options: projectFlockKandangOptions,
|
|
isLoadingOptions: isLoadingProjectFlockKandangs,
|
|
loadMore: loadMoreProjectFlockKandangs,
|
|
} = useSelect<ProjectFlockKandang>(
|
|
ProjectFlockKandangApi.basePath,
|
|
'id',
|
|
'name_with_period',
|
|
'search',
|
|
formik.values.location_id?.value
|
|
? { location_id: String(formik.values.location_id.value) }
|
|
: undefined
|
|
);
|
|
|
|
const {
|
|
setInputValue: setNonstockInputValue,
|
|
options: nonstockOptions,
|
|
isLoadingOptions: isLoadingNonstocks,
|
|
loadMore: loadMoreNonstocks,
|
|
} = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name', 'search');
|
|
|
|
// ===== 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]
|
|
);
|
|
|
|
const buildReportExpenseQueryString = useCallback(
|
|
(extraParams?: Record<string, string>) => {
|
|
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('project_flock_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);
|
|
}
|
|
if (sortBy) params.append('sort_by', sortBy);
|
|
if (orderBy) params.append('sort_order', orderBy);
|
|
|
|
Object.entries(extraParams ?? {}).forEach(([key, value]) => {
|
|
params.set(key, value);
|
|
});
|
|
|
|
return params.toString();
|
|
},
|
|
[filterParams, sortBy, orderBy]
|
|
);
|
|
|
|
// ===== DATA FETCHING =====
|
|
const { data: reportExpenseResponse, isLoading } = useSWR(
|
|
() => {
|
|
const queryString = buildReportExpenseQueryString({
|
|
page: String(page),
|
|
limit: String(pageSize),
|
|
});
|
|
|
|
return [`${ReportExpenseApi.basePath}?${queryString}`];
|
|
},
|
|
([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 queryString = buildReportExpenseQueryString({
|
|
page: '1',
|
|
limit: '100',
|
|
});
|
|
|
|
const response = await httpClient<BaseApiResponse<ReportExpense[]>>(
|
|
`${ReportExpenseApi.basePath}?${queryString}`
|
|
);
|
|
|
|
return isResponseSuccess(response) ? response.data : null;
|
|
}, [buildReportExpenseQueryString]);
|
|
|
|
// ===== EXPORT HANDLERS =====
|
|
const handleExportExcel = useCallback(async () => {
|
|
setIsExcelExportLoading(true);
|
|
try {
|
|
await ReportExpenseApi.exportToExcel(buildReportExpenseQueryString());
|
|
} catch (error) {
|
|
toast.error(
|
|
await getErrorMessage(error, 'Gagal mengekspor data pengeluaran')
|
|
);
|
|
} finally {
|
|
setIsExcelExportLoading(false);
|
|
}
|
|
}, [buildReportExpenseQueryString]);
|
|
|
|
const handleExportPDF = useCallback(async () => {
|
|
setIsPdfExportLoading(true);
|
|
try {
|
|
const allData = await reportExpenseExport();
|
|
if (!allData || allData.length === 0) {
|
|
toast.error('Tidak ada data untuk diekspor.');
|
|
return;
|
|
}
|
|
|
|
await generateReportExpensePDF(allData);
|
|
|
|
toast.success('PDF berhasil dibuat dan diunduh.');
|
|
} catch {
|
|
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
|
} finally {
|
|
setIsPdfExportLoading(false);
|
|
}
|
|
}, [reportExpenseExport]);
|
|
|
|
// ===== 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}
|
|
onClick={() => handleFilterModalOpenRef.current()}
|
|
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>
|
|
}
|
|
>
|
|
<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>
|
|
);
|
|
}, [setTabActions]);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
clearTabActions(tabId);
|
|
};
|
|
}, [clearTabActions]);
|
|
|
|
return null;
|
|
};
|
|
}, [
|
|
tabId,
|
|
filterParams,
|
|
isAnyExportLoading,
|
|
handleExportExcel,
|
|
handleExportPDF,
|
|
isExcelExportLoading,
|
|
isPdfExportLoading,
|
|
]);
|
|
|
|
const TabActionsElement = useMemo(() => <TabActions />, [TabActions]);
|
|
|
|
// ===== TABLE COLUMNS DEFINITION =====
|
|
const columns = useMemo((): ColumnDef<ReportExpense>[] => {
|
|
return [
|
|
{
|
|
header: 'No',
|
|
enableSorting: false,
|
|
cell: (props) => (page - 1) * pageSize + props.row.index + 1,
|
|
},
|
|
{
|
|
header: 'No. PO',
|
|
accessorKey: 'po_number',
|
|
enableSorting: true,
|
|
},
|
|
{
|
|
header: 'No. Referensi',
|
|
accessorKey: 'reference_number',
|
|
enableSorting: true,
|
|
},
|
|
{
|
|
header: 'Tanggal Realisasi',
|
|
accessorKey: 'realization_date',
|
|
enableSorting: true,
|
|
cell: ({ row }) => {
|
|
return formatDate(row.original?.realization_date, 'DD MMM, YYYY');
|
|
},
|
|
},
|
|
{
|
|
header: 'Tanggal Transaksi',
|
|
accessorKey: 'transaction_date',
|
|
enableSorting: true,
|
|
cell: ({ row }) => {
|
|
return formatDate(row.original?.transaction_date, 'DD MMM, YYYY');
|
|
},
|
|
},
|
|
{
|
|
header: 'Kategori',
|
|
accessorKey: 'category',
|
|
enableSorting: true,
|
|
},
|
|
{
|
|
header: 'Produk',
|
|
accessorKey: 'product',
|
|
enableSorting: true,
|
|
accessorFn: (row) => row.pengajuan?.nonstock?.name,
|
|
},
|
|
{
|
|
header: 'Supplier',
|
|
accessorKey: 'supplier',
|
|
enableSorting: true,
|
|
accessorFn: (row) => row.supplier?.name,
|
|
},
|
|
{
|
|
header: 'Lokasi',
|
|
accessorKey: 'location',
|
|
enableSorting: true,
|
|
accessorFn: (row) => row.kandang?.location?.name,
|
|
},
|
|
{
|
|
header: 'Kandang',
|
|
accessorKey: 'kandang',
|
|
enableSorting: true,
|
|
accessorFn: (row) => row.kandang?.name,
|
|
},
|
|
{
|
|
header: 'Pengajuan',
|
|
columns: [
|
|
{
|
|
header: 'Qty',
|
|
accessorKey: 'qty_pengajuan',
|
|
cell: ({ row }) =>
|
|
row.original.pengajuan?.qty?.toLocaleString('id-ID') || '0',
|
|
},
|
|
{
|
|
header: 'Harga',
|
|
accessorKey: 'price_pengajuan',
|
|
cell: ({ row }) =>
|
|
formatCurrency(row.original.pengajuan?.price || 0),
|
|
},
|
|
{
|
|
header: 'Total',
|
|
accessorKey: 'total_pengajuan',
|
|
cell: ({ row }) => {
|
|
const total =
|
|
(row.original.pengajuan?.qty || 0) *
|
|
(row.original.pengajuan?.price || 0);
|
|
return formatCurrency(total);
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
header: 'Realisasi',
|
|
columns: [
|
|
{
|
|
header: 'Qty',
|
|
accessorKey: 'qty_realisasi',
|
|
cell: ({ row }) =>
|
|
row.original.realisasi?.qty?.toLocaleString('id-ID') || '0',
|
|
},
|
|
{
|
|
header: 'Harga',
|
|
accessorKey: 'price_realisasi',
|
|
cell: ({ row }) =>
|
|
formatCurrency(row.original.realisasi?.price || 0),
|
|
},
|
|
{
|
|
header: 'Total',
|
|
accessorKey: 'total_realisasi',
|
|
cell: ({ row }) => {
|
|
const total =
|
|
(row.original.realisasi?.qty || 0) *
|
|
(row.original.realisasi?.price || 0);
|
|
return formatCurrency(total);
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 'realization_status',
|
|
header: 'Status Pencairan',
|
|
cell: (props) => (
|
|
<RealizationStatusBadge
|
|
approval={props.row.original?.latest_approval}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
id: 'bop_status',
|
|
header: 'Status BOP',
|
|
cell: (props) => (
|
|
<ExpenseStatusBadge approval={props.row.original?.latest_approval} />
|
|
),
|
|
},
|
|
];
|
|
}, [page, pageSize]);
|
|
|
|
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'>
|
|
<span className='loading loading-spinner loading-xl' />
|
|
</div>
|
|
)}
|
|
|
|
{!isLoading && (!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.'
|
|
/>
|
|
)}
|
|
|
|
{!isLoading && data.length > 0 && (
|
|
<>
|
|
<Table
|
|
data={data}
|
|
columns={columns}
|
|
pageSize={pageSize}
|
|
page={meta?.page || 1}
|
|
totalItems={meta?.total_results || 0}
|
|
onPageChange={setPage}
|
|
onPageSizeChange={setPageSize}
|
|
sorting={sorting}
|
|
setSorting={handleSortingChange}
|
|
manualSorting
|
|
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={projectFlockKandangOptions}
|
|
isLoading={isLoadingProjectFlockKandangs}
|
|
value={kandangValue}
|
|
onChange={(val) => {
|
|
formik.setFieldValue('kandang_id', val);
|
|
}}
|
|
onInputChange={setProjectFlockKandangInputValue}
|
|
onMenuScrollToBottom={loadMoreProjectFlockKandangs}
|
|
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;
|