feat(FE): adding report debt supplier report with temporary data types and dummy data

This commit is contained in:
randy-ar
2026-01-11 00:16:12 +07:00
parent a012707bae
commit cdfb59a70b
8 changed files with 1322 additions and 0 deletions
@@ -2,6 +2,7 @@
import Tabs from '@/components/Tabs'; import Tabs from '@/components/Tabs';
import CustomerPaymentTab from '@/components/pages/report/finance/tab/CustomerPaymentTab'; import CustomerPaymentTab from '@/components/pages/report/finance/tab/CustomerPaymentTab';
import DebtSupplier from '@/components/pages/report/finance/tab/DebtSupplier';
const FinanceTabs = () => { const FinanceTabs = () => {
const tabs = [ const tabs = [
@@ -11,6 +12,12 @@ const FinanceTabs = () => {
content: <CustomerPaymentTab />, content: <CustomerPaymentTab />,
}, },
{
id: '2',
label: 'Rekapitulasi Hutang Ke Supplier',
content: <DebtSupplier />,
},
]; ];
return ( return (
@@ -0,0 +1,363 @@
'use client';
import {
Page,
Text,
View,
Document,
StyleSheet,
Font,
pdf,
} from '@react-pdf/renderer';
import { formatDate, formatCurrency, formatNumber } from '@/lib/helper';
import { DebtSupplier } from '@/types/api/report/debt-supplier';
Font.register({
family: 'Helvetica',
src: 'helvetica',
});
const pdfStyles = StyleSheet.create({
page: {
fontSize: 10,
fontFamily: 'Helvetica',
padding: 20,
backgroundColor: '#FFFFFF',
},
titleSection: {
marginBottom: 10,
},
mainTitle: {
fontSize: 14,
fontWeight: 'bold',
marginBottom: 5,
color: '#1f74bf',
},
supplierTitle: {
fontSize: 12,
fontWeight: 'bold',
marginBottom: 8,
color: '#1f74bf',
},
supplierInfo: {
fontSize: 9,
marginBottom: 5,
color: '#333333',
},
table: {
borderWidth: 1,
borderColor: '#000000',
marginBottom: 15,
},
tableRow: {
flexDirection: 'row',
},
tableHeader: {
backgroundColor: '#F5F5F5',
},
tableCell: {
flex: 1,
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
padding: 4,
fontSize: 7,
textAlign: 'left',
},
tableCellNo: {
flex: 0.5,
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
padding: 4,
fontSize: 7,
textAlign: 'center',
},
tableCellLast: {
flex: 1,
padding: 4,
fontSize: 7,
},
tableCellHeader: {
flex: 1,
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
padding: 4,
fontSize: 7,
fontWeight: 'bold',
backgroundColor: '#F5F5F5',
borderBottomWidth: 1,
borderBottomColor: '#000000',
borderBottomStyle: 'solid',
paddingVertical: 12,
textAlign: 'center',
},
tableCellHeaderRight: {
flex: 1,
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
padding: 4,
fontSize: 7,
fontWeight: 'bold',
backgroundColor: '#F5F5F5',
textAlign: 'right',
borderBottomWidth: 1,
borderBottomColor: '#000000',
borderBottomStyle: 'solid',
paddingVertical: 12,
},
tableCellRight: {
flex: 1,
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
padding: 4,
fontSize: 7,
textAlign: 'right',
},
tableCellCenter: {
flex: 1,
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
padding: 4,
fontSize: 7,
textAlign: 'center',
},
tableBorderBottom: {
borderBottomWidth: 1,
borderBottomColor: '#000000',
borderBottomStyle: 'solid',
},
summaryRow: {
backgroundColor: '#F0F0F0',
fontWeight: 'bold',
},
});
interface DebtSupplierExportPDFParams {
data: DebtSupplier[];
}
const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
return (
<Document>
{params.data.map((supplierReport, supplierIndex) => (
<Page
key={supplierIndex}
size='A4'
orientation='landscape'
style={pdfStyles.page}
>
{/* Title and Supplier Info */}
<View style={pdfStyles.titleSection}>
<Text style={pdfStyles.mainTitle}>
Laporan &gt; Hutang Supplier
</Text>
<Text style={pdfStyles.supplierTitle}>
{supplierReport.supplier.name}
</Text>
</View>
{/* Table */}
<View style={pdfStyles.table}>
{/* Table Header */}
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
<View style={[pdfStyles.tableCellHeader, { flex: 0.5 }]}>
<Text>No</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>No. PR</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>No. PO</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>Tgl PR</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>Tgl PO</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 0.8 }]}>
<Text>Aging</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>Area</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>Gudang</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>Tgl Jatuh Tempo</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>Status JT</Text>
</View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
<Text>Total Harga</Text>
</View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
<Text>Pembayaran</Text>
</View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
<Text>Hutang</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>Status</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>No. Perjalanan</Text>
</View>
</View>
{/* Table Body */}
{supplierReport.rows.map((item, index) => (
<View
key={index}
style={[
pdfStyles.tableRow,
index < supplierReport.rows.length - 1
? pdfStyles.tableBorderBottom
: {},
]}
>
<View style={[pdfStyles.tableCellNo, { flex: 0.5 }]}>
<Text>{index + 1}</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text>{item.pr_number || '-'}</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text>{item.po_number || '-'}</Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
<Text>
{item.pr_date ? formatDate(item.pr_date, 'DD MMM YY') : '-'}
</Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
<Text>
{item.po_date ? formatDate(item.po_date, 'DD MMM YY') : '-'}
</Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 0.8 }]}>
<Text>{formatNumber(item.aging)} Hari</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text>{item.area?.name || '-'}</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text>{item.warehouse?.name || '-'}</Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
<Text>
{item.due_date
? formatDate(item.due_date, 'DD MMM YY')
: '-'}
</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text>{item.due_status || '-'}</Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text>{formatCurrency(item.total_price)}</Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text>{formatCurrency(item.payment_price)}</Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text>{formatCurrency(item.debt_price)}</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text>{item.status || '-'}</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text>{item.travel_number || '-'}</Text>
</View>
</View>
))}
{/* Summary Row */}
{supplierReport.total && (
<View style={[pdfStyles.tableRow, pdfStyles.summaryRow]}>
<View style={[pdfStyles.tableCellNo, { flex: 0.5 }]}>
<Text>Total</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 0.8 }]}>
<Text>{formatNumber(supplierReport.total.aging)} Hari</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text>
{formatCurrency(supplierReport.total.total_price)}
</Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text>
{formatCurrency(supplierReport.total.payment_price)}
</Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text>{formatCurrency(supplierReport.total.debt_price)}</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellLast, { flex: 1 }]}>
<Text></Text>
</View>
</View>
)}
</View>
</Page>
))}
</Document>
);
};
export const generateDebtSupplierPDF = async (
params: DebtSupplierExportPDFParams
): Promise<void> => {
const PDFDocument = createPDFDocument(params);
try {
const blob = await pdf(PDFDocument).toBlob();
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `laporan-hutang-supplier-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.pdf`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (error) {
throw error;
}
};
@@ -0,0 +1,101 @@
'use client';
import * as XLSX from 'xlsx';
import { formatDate, formatCurrency, formatNumber } from '@/lib/helper';
import { DebtSupplier } from '@/types/api/report/debt-supplier';
interface DebtSupplierExportExcelParams {
data: DebtSupplier[];
}
export const generateDebtSupplierExcel = (
params: DebtSupplierExportExcelParams
): void => {
if (!params.data || params.data.length === 0) {
return;
}
const workbook = XLSX.utils.book_new();
params.data.forEach((supplierReport) => {
const supplierData = supplierReport.rows;
const supplierName = supplierReport.supplier.name || 'Unknown Supplier';
const excelData: { [key: string]: string | number }[] = supplierData.map(
(item, index) => ({
No: index + 1,
'Nomor PR': item.pr_number || '',
'Nomor PO': item.po_number || '',
'Tanggal PR': item.pr_date
? formatDate(item.pr_date, 'DD MMM YYYY')
: '',
'Tanggal PO': item.po_date
? formatDate(item.po_date, 'DD MMM YYYY')
: '',
'Aging (Hari)': formatNumber(item.aging || 0),
Area: item.area?.name || '',
Gudang: item.warehouse?.name || '',
'Tanggal Jatuh Tempo': item.due_date
? formatDate(item.due_date, 'DD MMM YYYY')
: '',
'Status Jatuh Tempo': item.due_status || '',
'Total Harga': formatCurrency(item.total_price || 0),
'Harga Pembayaran': formatCurrency(item.payment_price || 0),
'Harga Hutang': formatCurrency(item.debt_price || 0),
Status: item.status || '',
'Nomor Perjalanan': item.travel_number || '',
})
);
if (supplierReport.total) {
excelData.push({
No: 'Total',
'Nomor PR': '',
'Nomor PO': '',
'Tanggal PR': '',
'Tanggal PO': '',
'Aging (Hari)': formatNumber(supplierReport.total.aging || 0),
Area: '',
Gudang: '',
'Tanggal Jatuh Tempo': '',
'Status Jatuh Tempo': '',
'Total Harga': formatCurrency(supplierReport.total.total_price || 0),
'Harga Pembayaran': formatCurrency(
supplierReport.total.payment_price || 0
),
'Harga Hutang': formatCurrency(supplierReport.total.debt_price || 0),
Status: '',
'Nomor Perjalanan': '',
});
}
const worksheet = XLSX.utils.json_to_sheet(excelData);
const colWidths = [
{ wch: 5 }, // No
{ wch: 15 }, // Nomor PR
{ wch: 15 }, // Nomor PO
{ wch: 15 }, // Tanggal PR
{ wch: 15 }, // Tanggal PO
{ wch: 12 }, // Aging
{ wch: 15 }, // Area
{ wch: 15 }, // Gudang
{ wch: 18 }, // Tanggal Jatuh Tempo
{ wch: 18 }, // Status Jatuh Tempo
{ wch: 15 }, // Total Harga
{ wch: 15 }, // Harga Pembayaran
{ wch: 15 }, // Harga Hutang
{ wch: 12 }, // Status
{ wch: 15 }, // Nomor Perjalanan
];
worksheet['!cols'] = colWidths;
const sheetName =
supplierName.length > 31 ? supplierName.substring(0, 31) : supplierName;
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
});
const filename = `laporan-hutang-supplier-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`;
XLSX.writeFile(workbook, filename);
};
@@ -0,0 +1,587 @@
import Button from '@/components/Button';
import Card from '@/components/Card';
import Dropdown from '@/components/Dropdown';
import DateInput from '@/components/input/DateInput';
import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import Menu from '@/components/menu/Menu';
import MenuItem from '@/components/menu/MenuItem';
import Modal, { useModal } from '@/components/Modal';
import Table from '@/components/Table';
import { isResponseSuccess } from '@/lib/api-helper';
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
import { SupplierApi } from '@/services/api/master-data';
import { FinanceApi } from '@/services/api/report/finance-report';
import { DebtRow, DebtSupplier } 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, useMemo, useState } from 'react';
import toast from 'react-hot-toast';
import useSWR from 'swr';
const DebtSupplierTab = () => {
// ===== 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 [isSubmitted, setIsSubmitted] = useState(false);
// ===== FILTER STATE =====
const [filterSupplier, setFilterSupplier] = useState<OptionType[]>([]);
const [filterStartDate, setFilterStartDate] = useState('');
const [filterEndDate, setFilterEndDate] = useState('');
const [filterErrors, setFilterErrors] = useState<Record<string, string>>({});
const filterModal = useModal();
const { options: supplierOptions, isLoadingOptions: isLoadingSuppliers } =
useSelect(SupplierApi.basePath, 'id', 'name', '', {
limit: 'limit',
});
const dataTypeOptions = useMemo(
() => [
{ value: 'do_date', label: 'Tanggal Terima' },
{ value: 'po_date', label: 'Tanggal PO' },
],
[]
);
// ===== FILTER HANDLERS =====
const handleResetFilters = useCallback(() => {
setIsSubmitted(false);
setFilterSupplier([]);
setFilterStartDate('');
setFilterEndDate('');
setFilterErrors({});
}, []);
const handleApplyFilters = useCallback(() => {
const errors: Record<string, string> = {};
if (!filterStartDate) {
errors.start_date = 'Tanggal mulai wajib diisi';
}
if (!filterEndDate) {
errors.end_date = 'Tanggal akhir wajib diisi';
}
setFilterErrors(errors);
if (Object.keys(errors).length === 0) {
setIsSubmitted(true);
setCurrentPage(1);
filterModal.closeModal();
}
}, [filterModal, filterStartDate, filterEndDate]);
// ===== DATA FETCHING =====
// const { data: debtSupplier, isLoading } = useSWR(
// isSubmitted
// ? () => {
// const params = {
// supplier_id:
// filterSupplier.length > 0
// ? filterSupplier.map((v) => String(v.value)).join(',')
// : undefined,
// filter_by: 'do_date' as const,
// start_date: filterStartDate || undefined,
// end_date: filterEndDate || undefined,
// page: currentPage,
// limit: pageSize,
// };
// return ['debt-supplier-report', params];
// }
// : null,
// ([, params]) =>
// FinanceApi.getDebtSupplierReport(
// params.supplier_id,
// params.filter_by,
// params.start_date,
// params.end_date,
// params.page,
// params.limit
// )
// );
const { data: debtSupplier, isLoading } = useSWR(FinanceApi.basePath, () =>
FinanceApi.getDebtSupplierReport()
);
const data: DebtSupplier[] = useMemo(
() =>
isResponseSuccess(debtSupplier)
? (debtSupplier?.data as unknown as DebtSupplier[]) || []
: [],
[debtSupplier]
);
const meta =
isResponseSuccess(debtSupplier) && debtSupplier?.meta
? debtSupplier.meta
: null;
// ===== EXPORT DATA FETCHER =====
const debtSupplierExport = useCallback(async (): Promise<
DebtSupplier[] | null
> => {
const params = {
supplier_id:
filterSupplier.length > 0
? filterSupplier.map((v) => String(v.value)).join(',')
: undefined,
filter_by: 'do_date' as const,
start_date: filterStartDate || undefined,
end_date: filterEndDate || undefined,
limit: 100,
page: 1,
};
const response = await FinanceApi.getDebtSupplierReport(
params.supplier_id,
params.filter_by,
params.start_date,
params.end_date,
params.page,
params.limit
);
return isResponseSuccess(response)
? (response.data as unknown as DebtSupplier[])
: null;
}, [filterSupplier, filterStartDate, filterEndDate]);
// ===== 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 });
toast.success('PDF berhasil dibuat dan diunduh.');
} catch {
toast.error('Gagal membuat PDF. Silakan coba lagi.');
} finally {
setIsPdfExportLoading(false);
}
}, [debtSupplierExport]);
// ===== PAGINATION HANDLERS =====
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
const handleRowChange = (pageSize: number) => {
setPageSize(pageSize);
};
const handleNextPage = () => {
if (meta && currentPage < meta.total_pages) {
setCurrentPage(currentPage + 1);
}
};
const handlePrevPage = () => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
}
};
const getTableColumns = (supplier: DebtSupplier): ColumnDef<DebtRow>[] => [
{
id: 'no',
header: 'No',
cell: (props) => props.row.index + 1,
},
{
id: 'pr_number',
header: 'Nomor PR',
accessorKey: 'pr_number',
cell: (props) => {
const value = props.row.original.pr_number;
return value || '-';
},
},
{
id: 'po_number',
header: 'Nomor PO',
accessorKey: 'po_number',
cell: (props) => {
const value = props.row.original.po_number;
return value || '-';
},
},
{
id: 'pr_date',
header: 'Tanggal PR',
accessorKey: 'pr_date',
cell: (props) => {
const value = props.row.original.pr_date;
return formatDate(value, 'DD MMM YYYY');
},
},
{
id: 'po_date',
header: 'Tanggal PO',
accessorKey: 'po_date',
cell: (props) => {
const value = props.row.original.po_date;
return formatDate(value, 'DD MMM YYYY');
},
},
{
id: 'aging',
header: 'Aging',
accessorKey: 'aging',
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)} Hari</div>;
},
},
{
id: 'area',
header: 'Area',
accessorKey: 'area',
cell: (props) => {
const value = props.row.original.area?.name;
return value || '-';
},
},
{
id: 'warehouse',
header: 'Gudang',
accessorKey: 'warehouse',
cell: (props) => {
const value = props.row.original.warehouse?.name;
return value || '-';
},
},
{
id: 'due_date',
header: 'Tanggal Jatuh Tempo',
accessorKey: 'due_date',
cell: (props) => {
const value = props.row.original.due_date;
return formatDate(value, 'DD MMM YYYY');
},
},
{
id: 'due_status',
header: 'Status Jatuh Tempo',
accessorKey: 'due_status',
cell: (props) => {
const value = props.row.original.due_status;
return value || '-';
},
},
{
id: 'total_price',
header: 'Total Harga',
accessorKey: 'total_price',
cell: (props) => {
const value = props.row.original.total_price;
return <div className='text-right'>{formatCurrency(value)}</div>;
},
footer: () => {
const value = supplier.total.total_price;
return <div className='text-right'>{formatCurrency(value)}</div>;
},
},
{
id: 'payment_price',
header: 'Harga Pembayaran',
accessorKey: 'payment_price',
cell: (props) => {
const value = props.row.original.payment_price;
return <div className='text-right'>{formatCurrency(value)}</div>;
},
footer: () => {
const value = supplier.total.payment_price;
return <div className='text-right'>{formatCurrency(value)}</div>;
},
},
{
id: 'debt_price',
header: 'Harga Hutang',
accessorKey: 'debt_price',
cell: (props) => {
const value = props.row.original.debt_price;
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 < 0 ? 'text-red-500' : ''}`}>
{formatCurrency(value)}
</div>
);
},
},
{
id: 'status',
header: 'Status',
accessorKey: 'status',
cell: (props) => {
const value = props.row.original.status;
return value || '-';
},
},
{
id: 'travel_number',
header: 'Nomor Perjalanan',
accessorKey: 'travel_number',
cell: (props) => {
const value = props.row.original.travel_number;
return value || '-';
},
},
];
return (
<>
<div className='w-full p-0 sm:p-4 flex flex-col gap-4'>
<Card
subtitle='Laporan > Kontrol Pembayaran Customer'
className={{ wrapper: 'w-full', body: 'p-1!' }}
>
<div className='mb-4 flex justify-end gap-2 [&_button]:px-4'>
<Button variant='outline' onClick={filterModal.openModal}>
<Icon icon='heroicons:funnel' width={18} height={18} />
Filter
</Button>
<Dropdown
trigger={
<Button variant='outline' isLoading={isAnyExportLoading}>
<Icon
icon='heroicons:cloud-arrow-down'
width={18}
height={18}
/>
Export
</Button>
}
align='end'
>
<Menu>
<MenuItem title='Excel' onClick={handleExportExcel} />
<MenuItem title='PDF' onClick={handleExportPdf} />
</Menu>
</Dropdown>
</div>
</Card>
{/* {!isSubmitted ? (
<div className='mt-6 text-center text-gray-500'>
Silakan klik tombol Filter untuk mengatur filter dan menampilkan
data.
</div>
) : isLoading ? (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
) : data.length === 0 ? (
<div className='mt-6 text-center text-gray-500'>
Tidak ada data yang dapat ditampilkan...
</div>
) : ( */}
{data.map((supplierReport) => {
return (
<Card
key={supplierReport.supplier.id}
title={supplierReport.supplier.name}
className={{ wrapper: 'w-full' }}
variant='bordered'
collapsible={true}
>
<Table
data={supplierReport.rows}
columns={getTableColumns(supplierReport)}
pageSize={10}
renderFooter={supplierReport.rows.length > 0}
className={{
containerClassName: 'w-full',
tableWrapperClassName: 'overflow-x-auto mt-4',
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',
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',
tableFooterClassName:
'bg-gray-100 font-semibold border border-gray-200',
footerRowClassName: 'border-t-2 border-gray-300',
footerColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
paginationClassName: 'hidden',
}}
/>
</Card>
);
})}
</div>
{/* Filter Modal */}
<Modal
ref={filterModal.ref}
className={{
modal: 'p-0',
modalBox: 'p-0 rounded-2xl xl:max-w-4/12 max-w-sm',
}}
>
<div className='space-y-6'>
{/* Modal Header */}
<div className='flex items-center justify-between gap-2 py-3 border-b border-gray-300 px-4'>
<div className='flex items-center gap-2 text-primary'>
<Icon icon='heroicons:funnel' width={20} height={20} />
<h3 className='font-semibold'>Filter Data</h3>
</div>
<Button
variant='link'
onClick={filterModal.closeModal}
className='text-gray-500 hover:text-gray-700 transition-colors cursor-pointer'
>
<Icon icon='heroicons:x-mark' width={20} height={20} />
</Button>
</div>
<div className='space-y-4 px-4'>
<div className='grid grid-cols-1 sm:grid-cols-2 sm:gap-4'>
<div>
<DateInput
label='Tanggal'
name='start_date'
value={filterStartDate}
onChange={(e) => {
setFilterStartDate(e.target.value);
setFilterErrors((prev) => ({ ...prev, start_date: '' }));
}}
className={{ wrapper: 'w-full' }}
/>
{filterErrors.start_date && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.start_date}
</p>
)}
</div>
<div className='mt-auto'>
<DateInput
name='end_date'
value={filterEndDate}
onChange={(e) => {
setFilterEndDate(e.target.value);
setFilterErrors((prev) => ({ ...prev, end_date: '' }));
}}
className={{ wrapper: 'w-full' }}
/>
{filterErrors.end_date && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.end_date}
</p>
)}
</div>
</div>
<div>
<SelectInput
label='Supplier'
placeholder='Pilih Supplier'
isMulti
options={supplierOptions}
value={filterSupplier}
onChange={(val) => {
setFilterSupplier(
Array.isArray(val) ? val : val ? [val] : []
);
}}
isLoading={isLoadingSuppliers}
isClearable
className={{ wrapper: 'w-full' }}
/>
</div>
<div>
<SelectInput
label='Filter Berdasarkan'
placeholder='Pilih Filter Berdasarkan'
options={dataTypeOptions}
value={dataTypeOptions[0]}
isDisabled={true}
className={{ wrapper: 'w-full' }}
/>
</div>
</div>
{/* Action Buttons */}
<div className='flex justify-between gap-4 py-4 mt-8 border-t border-gray-300 bg-gray-100'>
<Button
variant='soft'
className='ms-4 min-w-36 rounded-lg'
onClick={handleResetFilters}
>
Reset Filter
</Button>
<Button
className='me-4 min-w-36 rounded-lg'
onClick={handleApplyFilters}
>
Apply Filter
</Button>
</div>
</div>
</Modal>
</>
);
};
export default DebtSupplierTab;
+168
View File
@@ -0,0 +1,168 @@
[
{
"supplier": {
"id": 1,
"name": "INDOVETRACO MAKMUR ABADI (IMA)"
},
"rows": [
{
"pr_number": "PR-MBU-02145",
"po_number": "PO-MBU-02145",
"pr_date": "2025-11-03",
"po_date": "2025-11-05",
"aging": 68,
"area": {
"id": 101,
"name": "Banten 1"
},
"warehouse": {
"id": 201,
"name": "Gudang Area Banten"
},
"due_date": "2025-12-03",
"due_status": "Sudah Jatuh Tempo",
"total_price": 8658000,
"payment_price": 0,
"debt_price": -8658000,
"status": "Belum Lunas",
"travel_number": "-"
}
],
"total": {
"aging": 68,
"total_price": 8658000,
"payment_price": 0,
"debt_price": -8658000
}
},
{
"supplier": {
"id": 2,
"name": "MANDIRI BERLIAN UNGGAS (MANBU)"
},
"rows": [
{
"pr_number": "PR-MBU-01980",
"po_number": "PO-MBU-01980",
"pr_date": "2025-08-20",
"po_date": "2025-09-18",
"aging": 143,
"area": {
"id": 101,
"name": "Banten 1"
},
"warehouse": {
"id": 202,
"name": "GUDANG CIANGSANA 5 (P16)"
},
"due_date": "2025-08-21",
"due_status": "Sudah Jatuh Tempo",
"total_price": 266700000,
"payment_price": 0,
"debt_price": -267245000,
"status": "Belum Lunas",
"travel_number": "-"
},
{
"pr_number": "PR-MBU-01981",
"po_number": "PO-MBU-01981",
"pr_date": "2025-08-21",
"po_date": "2025-09-18",
"aging": 142,
"area": {
"id": 102,
"name": "Priangan Timur 2"
},
"warehouse": {
"id": 203,
"name": "GUDANG SINGAPARNA 1 P.7"
},
"due_date": "2025-08-22",
"due_status": "Sudah Jatuh Tempo",
"total_price": 157480000,
"payment_price": 0,
"debt_price": -424725000,
"status": "Belum Lunas",
"travel_number": "-"
}
],
"total": {
"aging": 143,
"total_price": 424180000,
"payment_price": 0,
"debt_price": -692465000
}
},
{
"supplier": {
"id": 3,
"name": "SUMBER PROTEIN JAYA"
},
"rows": [
{
"pr_number": "PR-SPJ-00551",
"po_number": "PO-SPJ-00551",
"pr_date": "2025-12-01",
"po_date": "2025-12-02",
"aging": 39,
"area": {
"id": 103,
"name": "Jawa Tengah"
},
"warehouse": {
"id": 204,
"name": "Gudang Solo"
},
"due_date": "2026-01-01",
"due_status": "Mendekati Jatuh Tempo",
"total_price": 45000000,
"payment_price": 15000000,
"debt_price": -30000000,
"status": "Belum Lunas",
"travel_number": "SJ-001/XII"
}
],
"total": {
"aging": 39,
"total_price": 45000000,
"payment_price": 15000000,
"debt_price": -30000000
}
},
{
"supplier": {
"id": 4,
"name": "CHAROEN POKPHAND INDONESIA"
},
"rows": [
{
"pr_number": "PR-CPI-0992",
"po_number": "PO-CPI-0992",
"pr_date": "2025-11-15",
"po_date": "2025-11-16",
"aging": 56,
"area": {
"id": 104,
"name": "Jawa Timur"
},
"warehouse": {
"id": 205,
"name": "Gudang Surabaya"
},
"due_date": "2025-12-15",
"due_status": "Sudah Jatuh Tempo",
"total_price": 125000000,
"payment_price": 0,
"debt_price": -125000000,
"status": "Belum Lunas",
"travel_number": "-"
}
],
"total": {
"aging": 56,
"total_price": 125000000,
"payment_price": 0,
"debt_price": -125000000
}
}
]
+35
View File
@@ -0,0 +1,35 @@
/**
* Dummy data for DebtSupplier[]
* Generated from: debt.supllier.dummy.json
*
* This file is auto-generated. Do not edit manually.
*/
import { DebtSupplier } from '../../types/api/report/debt-supplier';
import { BaseApiResponse } from '@/types/api/api-general';
import dummyData from './debt.supllier.dummy.json';
/**
* Get dummy DebtSupplier[] data
* @returns Promise with BaseApiResponse containing DebtSupplier[]
*/
export async function getDebtSupllierDummy(): Promise<
BaseApiResponse<DebtSupplier[]>
> {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
code: 200,
status: 'success',
message: 'Data retrieved successfully',
meta: {
total_pages: 1,
page: 1,
limit: 10,
total_results: dummyData.length,
},
data: dummyData as unknown as DebtSupplier[],
});
}, 500);
});
}
+27
View File
@@ -1,6 +1,8 @@
import { getDebtSupllierDummy } from '@/dummy/report/debt.supllier.dummy';
import { BaseApiService } from '@/services/api/base'; import { BaseApiService } from '@/services/api/base';
import { BaseApiResponse } from '@/types/api/api-general'; import { BaseApiResponse } from '@/types/api/api-general';
import { CustomerPaymentReport } from '@/types/api/report/customer-payment'; import { CustomerPaymentReport } from '@/types/api/report/customer-payment';
import { DebtSupplier } from '@/types/api/report/debt-supplier';
export class FinanceApiService extends BaseApiService< export class FinanceApiService extends BaseApiService<
CustomerPaymentReport, CustomerPaymentReport,
@@ -36,6 +38,31 @@ export class FinanceApiService extends BaseApiService<
} }
); );
} }
async getDebtSupplierReport(
supplier_id?: string,
filter_by?: 'do_date',
start_date?: string,
end_date?: string,
page?: number,
limit?: number
): Promise<BaseApiResponse<DebtSupplier[]> | undefined> {
return (await getDebtSupllierDummy()) as BaseApiResponse<DebtSupplier[]>;
return await this.customRequest<BaseApiResponse<DebtSupplier[]>>(
`debt-supplier`,
{
method: 'GET',
params: {
supplier_id: supplier_id,
filter_by: filter_by,
start_date: start_date,
end_date: end_date,
page: page,
limit: limit,
},
}
);
}
} }
export const FinanceApi = new FinanceApiService('reports'); export const FinanceApi = new FinanceApiService('reports');
+34
View File
@@ -0,0 +1,34 @@
import { BaseMetadata } from '@/types/api/api-general';
import { Area } from '@/types/api/master-data/area';
import { Supplier } from '@/types/api/master-data/supplier';
import { Warehouse } from '@/types/api/master-data/warehouse';
export type DebtSupplier = BaseMetadata & {
supplier: Supplier;
rows: DebtRow[];
total: DebtTotal;
};
export type DebtRow = {
pr_number: string;
po_number: string;
pr_date: string;
po_date: string;
aging: number;
area: Area;
warehouse: Warehouse;
due_date: string;
due_status: string;
total_price: number;
payment_price: number;
debt_price: number;
status: string;
travel_number: string;
};
export type DebtTotal = {
aging: number;
total_price: number;
payment_price: number;
debt_price: number;
};