mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
988 lines
31 KiB
TypeScript
988 lines
31 KiB
TypeScript
import Button from '@/components/Button';
|
|
import Card from '@/components/Card';
|
|
import Dropdown from '@/components/Dropdown';
|
|
import Pagination from '@/components/Pagination';
|
|
import DateInput from '@/components/input/DateInput';
|
|
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
|
import Modal, { useModal } from '@/components/Modal';
|
|
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 { generateDebtSupplierExcel } from '@/components/pages/report/finance/export/DebtSupplierExportXLSX';
|
|
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 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';
|
|
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
|
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';
|
|
|
|
const dueStatus: Record<string, Color> = {
|
|
'Sudah Jatuh Tempo': 'error',
|
|
'Belum Jatuh Tempo': 'success',
|
|
'Mendekati Jatuh Tempo': 'warning',
|
|
};
|
|
|
|
const paymentStatus: Record<string, Color> = {
|
|
'Belum Lunas': 'warning',
|
|
Lunas: 'primary',
|
|
Pembayaran: 'success',
|
|
};
|
|
|
|
const getPillBadge = (
|
|
statusText: string,
|
|
type: 'due' | 'payment' = 'payment'
|
|
) => {
|
|
// Get color based on type
|
|
const color =
|
|
type === 'due'
|
|
? dueStatus[statusText] || 'neutral'
|
|
: paymentStatus[statusText] || 'neutral';
|
|
|
|
return (
|
|
<StatusBadge
|
|
color={color as Color}
|
|
text={statusText}
|
|
className={{
|
|
badge: 'w-fit',
|
|
}}
|
|
/>
|
|
);
|
|
};
|
|
|
|
interface DebtSupplierTabProps {
|
|
tabId: string;
|
|
}
|
|
|
|
const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|
// ===== STATE MANAGEMENT =====
|
|
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
|
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
|
|
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
|
|
|
|
// ===== 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 {
|
|
setInputValue: setSupplierInputValue,
|
|
options: supplierOptions,
|
|
isLoadingOptions: isLoadingSupplierOptions,
|
|
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>({
|
|
initialValues: {
|
|
startDate: null,
|
|
endDate: null,
|
|
supplierIds: null,
|
|
filterBy: null,
|
|
},
|
|
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);
|
|
filterModal.closeModal();
|
|
},
|
|
});
|
|
|
|
handleFilterModalOpenRef.current = () => {
|
|
const restoredFilterBy =
|
|
dataTypeOptions.find((opt) => opt.value === filterParams.filter_by) ||
|
|
null;
|
|
|
|
const supplierIdList = filterParams.supplier_ids
|
|
? filterParams.supplier_ids.split(',')
|
|
: [];
|
|
const restoredSupplierIds = supplierOptions.filter((opt) =>
|
|
supplierIdList.includes(String(opt.value))
|
|
);
|
|
|
|
formik.setValues({
|
|
startDate: filterParams.start_date || null,
|
|
endDate: filterParams.end_date || null,
|
|
supplierIds: restoredSupplierIds.length > 0 ? restoredSupplierIds : null,
|
|
filterBy: restoredFilterBy,
|
|
});
|
|
filterModal.openModal();
|
|
};
|
|
|
|
// ===== 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: DebtSupplier[] = useMemo(
|
|
() =>
|
|
isResponseSuccess(debtSupplier)
|
|
? (debtSupplier?.data as unknown as DebtSupplier[]) || []
|
|
: [],
|
|
[debtSupplier]
|
|
);
|
|
|
|
const meta = useMemo(
|
|
() =>
|
|
isResponseSuccess(debtSupplier) && debtSupplier.meta
|
|
? debtSupplier.meta
|
|
: null,
|
|
[debtSupplier]
|
|
);
|
|
|
|
// ===== 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 response = await DebtSupplierApi.getDebtSupplierReport(
|
|
params.supplier_ids,
|
|
params.filter_by,
|
|
params.start_date,
|
|
params.end_date
|
|
);
|
|
|
|
return isResponseSuccess(response)
|
|
? (response.data as unknown as DebtSupplier[])
|
|
: null;
|
|
}, [
|
|
formik.values.supplierIds,
|
|
formik.values.startDate,
|
|
formik.values.endDate,
|
|
formik.values.filterBy,
|
|
]);
|
|
|
|
// ===== EXPORT HANDLERS =====
|
|
const handleExportExcel = useCallback(async () => {
|
|
setIsExcelExportLoading(true);
|
|
try {
|
|
const allDataForExport = await debtSupplierExport();
|
|
|
|
if (
|
|
!allDataForExport ||
|
|
!Array.isArray(allDataForExport) ||
|
|
allDataForExport.length === 0
|
|
) {
|
|
toast.error('Tidak ada data untuk diekspor.');
|
|
return;
|
|
}
|
|
|
|
generateDebtSupplierExcel({ data: allDataForExport });
|
|
toast.success('Excel berhasil dibuat dan diunduh.');
|
|
} catch {
|
|
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
|
} finally {
|
|
setIsExcelExportLoading(false);
|
|
}
|
|
}, [debtSupplierExport]);
|
|
|
|
const handleExportPdf = useCallback(async () => {
|
|
setIsPdfExportLoading(true);
|
|
try {
|
|
const allDataForExport = await debtSupplierExport();
|
|
|
|
if (
|
|
!allDataForExport ||
|
|
!Array.isArray(allDataForExport) ||
|
|
allDataForExport.length === 0
|
|
) {
|
|
toast.error('Tidak ada data untuk diekspor.');
|
|
return;
|
|
}
|
|
|
|
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,
|
|
},
|
|
});
|
|
toast.success('PDF berhasil dibuat dan diunduh.');
|
|
} catch {
|
|
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
|
} finally {
|
|
setIsPdfExportLoading(false);
|
|
}
|
|
}, [
|
|
debtSupplierExport,
|
|
formik.values.supplierIds,
|
|
formik.values.filterBy,
|
|
formik.values.startDate,
|
|
formik.values.endDate,
|
|
]);
|
|
|
|
// ===== 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()}
|
|
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]);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (dateErrorShown) {
|
|
toast.dismiss();
|
|
}
|
|
};
|
|
}, [dateErrorShown]);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (dateErrorShown) {
|
|
toast.dismiss();
|
|
setDateErrorShown(false);
|
|
}
|
|
};
|
|
}, [filterModal.open, dateErrorShown]);
|
|
|
|
const getTableColumns = (supplier?: DebtSupplier): ColumnDef<DebtRow>[] => [
|
|
{
|
|
id: 'no',
|
|
header: 'No',
|
|
enableSorting: false,
|
|
cell: (props) => props.row.index,
|
|
footer: () => 'Total',
|
|
},
|
|
{
|
|
id: 'pr_number',
|
|
header: 'Nomor PR',
|
|
accessorKey: 'pr_number',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.pr_number;
|
|
return value || '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'po_number',
|
|
header: 'Nomor PO',
|
|
accessorKey: 'po_number',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.po_number;
|
|
return value || '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'received_date',
|
|
header: 'Tanggal Terima/Bayar',
|
|
accessorKey: 'received_date',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.received_date;
|
|
return value
|
|
? value != '-'
|
|
? formatDate(value, 'DD MMM YYYY')
|
|
: '-'
|
|
: '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'po_date',
|
|
header: 'Tanggal PO',
|
|
accessorKey: 'po_date',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.po_date;
|
|
return value
|
|
? value != '-'
|
|
? formatDate(value, 'DD MMM YYYY')
|
|
: '-'
|
|
: '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'aging',
|
|
header: 'Aging',
|
|
accessorKey: 'aging',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.aging;
|
|
return <div className='text-center'>{formatNumber(value)} Hari</div>;
|
|
},
|
|
footer: () => {
|
|
const value = supplier?.total.aging;
|
|
return (
|
|
<div className='text-center'>{formatNumber(value || 0)} Hari</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: 'area',
|
|
header: 'Area',
|
|
accessorKey: 'area',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.area?.name;
|
|
return value || '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'warehouse',
|
|
header: 'Gudang',
|
|
accessorKey: 'warehouse',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.warehouse?.name;
|
|
return value || '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'due_date',
|
|
header: 'Jatuh Tempo',
|
|
accessorKey: 'due_date',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.due_date;
|
|
return value
|
|
? value != '-'
|
|
? formatDate(value, 'DD MMM YYYY')
|
|
: '-'
|
|
: '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'due_status',
|
|
header: 'Status Jatuh Tempo',
|
|
accessorKey: 'due_status',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.due_status;
|
|
return value ? (value != '-' ? getPillBadge(value, 'due') : '-') : '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'total_price',
|
|
header: 'Nominal Pembelian',
|
|
accessorKey: 'total_price',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.total_price;
|
|
return (
|
|
<div className={`text-right ${value < 0 ? 'text-red-500' : ''}`}>
|
|
{formatCurrency(value)}
|
|
</div>
|
|
);
|
|
},
|
|
footer: () => {
|
|
const value = supplier?.total.total_price;
|
|
return (
|
|
<div
|
|
className={`text-right ${value && value < 0 ? 'text-red-500' : ''}`}
|
|
>
|
|
{formatCurrency(value || 0)}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: 'payment_price',
|
|
header: 'Pembayaran',
|
|
accessorKey: 'payment_price',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.payment_price;
|
|
return (
|
|
<div className={`text-right ${value < 0 ? 'text-red-500' : ''}`}>
|
|
{formatCurrency(value)}
|
|
</div>
|
|
);
|
|
},
|
|
footer: () => {
|
|
const value = supplier?.total.payment_price;
|
|
return (
|
|
<div
|
|
className={`text-right ${value && value < 0 ? 'text-red-500' : ''}`}
|
|
>
|
|
{formatCurrency(value || 0)}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: 'balance',
|
|
header: 'Sisa Saldo Hutang',
|
|
accessorKey: 'balance',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.balance;
|
|
return (
|
|
<div className={`text-right ${value < 0 ? 'text-red-500' : ''}`}>
|
|
{formatCurrency(value)}
|
|
</div>
|
|
);
|
|
},
|
|
footer: () => {
|
|
const value = supplier?.total.debt_price;
|
|
return (
|
|
<div
|
|
className={`text-right ${value && value < 0 ? 'text-red-500' : ''}`}
|
|
>
|
|
{formatCurrency(value || 0)}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: 'status',
|
|
header: 'Status',
|
|
accessorKey: 'status',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.status;
|
|
return value
|
|
? value != '-'
|
|
? getPillBadge(value, 'payment')
|
|
: '-'
|
|
: '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'travel_number',
|
|
header: 'Nomor Perjalanan',
|
|
accessorKey: 'travel_number',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.travel_number;
|
|
return value || '-';
|
|
},
|
|
},
|
|
];
|
|
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.length === 0 && (
|
|
<DebtSupplierSkeleton
|
|
columns={getTableColumns()}
|
|
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 && meta && (
|
|
<div className='max-w-sm ml-auto'>
|
|
<Pagination
|
|
totalItems={meta.total_results || 0}
|
|
itemsPerPage={meta.limit || 0}
|
|
currentPage={meta.page || 0}
|
|
onPrevPage={() =>
|
|
setCurrentPage((curr) => (curr > 1 ? curr - 1 : curr))
|
|
}
|
|
onNextPage={() =>
|
|
setCurrentPage((curr) =>
|
|
meta.total_pages && curr < meta.total_pages ? curr + 1 : curr
|
|
)
|
|
}
|
|
onPageChange={(pageNumber) => setCurrentPage(pageNumber)}
|
|
rowOptions={[10, 20, 50, 100]}
|
|
onRowChange={setPageSize}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{!isLoading &&
|
|
data.length > 0 &&
|
|
data.map((supplierReport) => {
|
|
return (
|
|
<Card
|
|
key={supplierReport.supplier.id}
|
|
title={supplierReport.supplier.name}
|
|
className={{
|
|
wrapper: 'w-full rounded-lg border-none',
|
|
body: 'p-0',
|
|
title:
|
|
'px-2 py-1.5 font-normal text-sm bg-primary text-white',
|
|
collapsible: 'rounded-lg',
|
|
}}
|
|
variant='bordered'
|
|
collapsible={true}
|
|
>
|
|
<Table
|
|
data={[
|
|
{
|
|
balance: supplierReport.initial_balance,
|
|
} as DebtRow,
|
|
...supplierReport.rows,
|
|
]}
|
|
columns={getTableColumns(supplierReport)}
|
|
pageSize={supplierReport.rows.length + 1}
|
|
renderFooter={supplierReport.rows.length > 0}
|
|
className={{
|
|
containerClassName: 'w-full mb-0',
|
|
tableWrapperClassName:
|
|
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
|
headerColumnClassName: cn(
|
|
TABLE_DEFAULT_STYLING.headerColumnClassName,
|
|
'whitespace-nowrap'
|
|
),
|
|
bodyColumnClassName: cn(
|
|
TABLE_DEFAULT_STYLING.bodyColumnClassName,
|
|
'whitespace-nowrap'
|
|
),
|
|
footerRowClassName: cn(
|
|
TABLE_DEFAULT_STYLING.footerRowClassName,
|
|
'bg-white'
|
|
),
|
|
footerColumnClassName: cn(
|
|
TABLE_DEFAULT_STYLING.footerColumnClassName,
|
|
'whitespace-nowrap p-3'
|
|
),
|
|
paginationClassName: 'hidden',
|
|
}}
|
|
renderCustomRow={(row) => {
|
|
if (row.index == 0) {
|
|
return (
|
|
<tr
|
|
className={cn(TABLE_DEFAULT_STYLING.bodyRowClassName)}
|
|
key={row.index}
|
|
>
|
|
<td
|
|
className={cn(
|
|
TABLE_DEFAULT_STYLING.bodyColumnClassName
|
|
)}
|
|
colSpan={12}
|
|
></td>
|
|
<td
|
|
className={cn(
|
|
TABLE_DEFAULT_STYLING.bodyColumnClassName
|
|
)}
|
|
>
|
|
<div
|
|
className={`text-right ${row.original.balance < 0 ? 'text-red-500' : ''}`}
|
|
>
|
|
{formatCurrency(row.original.balance)}
|
|
</div>
|
|
</td>
|
|
<td
|
|
className={cn(
|
|
TABLE_DEFAULT_STYLING.bodyColumnClassName
|
|
)}
|
|
colSpan={2}
|
|
></td>
|
|
</tr>
|
|
);
|
|
}
|
|
}}
|
|
/>
|
|
</Card>
|
|
);
|
|
})}
|
|
|
|
{!isLoading && data.length > 0 && meta && (
|
|
<div className='mt-5 px-3'>
|
|
<Pagination
|
|
totalItems={meta.total_results || 0}
|
|
itemsPerPage={meta.limit || 0}
|
|
currentPage={meta.page || 0}
|
|
onPrevPage={() =>
|
|
setCurrentPage((curr) => (curr > 1 ? curr - 1 : curr))
|
|
}
|
|
onNextPage={() =>
|
|
setCurrentPage((curr) =>
|
|
meta.total_pages && curr < meta.total_pages ? curr + 1 : curr
|
|
)
|
|
}
|
|
onPageChange={(pageNumber) => setCurrentPage(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',
|
|
}}
|
|
>
|
|
<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>
|
|
</div>
|
|
|
|
{/* Modal Body */}
|
|
<div className='p-4 flex flex-col gap-1.5'>
|
|
<div>
|
|
<label className='block text-xs font-semibold text-base-content py-2'>
|
|
Tanggal
|
|
</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);
|
|
}
|
|
}}
|
|
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>
|
|
<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);
|
|
}
|
|
}}
|
|
className={{ wrapper: 'w-full' }}
|
|
isError={
|
|
(formik.touched.endDate && !!formik.errors.endDate) ||
|
|
hasDateError
|
|
}
|
|
errorMessage={formik.errors.endDate}
|
|
isNestedModal
|
|
/>
|
|
</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>
|
|
|
|
<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>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<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'
|
|
>
|
|
Reset Filter
|
|
</Button>
|
|
<Button
|
|
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
|
type='submit'
|
|
>
|
|
Apply Filter
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Modal>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default DebtSupplierTab;
|