diff --git a/src/app/report/finance/page.tsx b/src/app/report/finance/page.tsx new file mode 100644 index 00000000..ae2e85e0 --- /dev/null +++ b/src/app/report/finance/page.tsx @@ -0,0 +1,7 @@ +import FinanceTabs from '@/components/pages/report/finance/FinanceTabs'; + +const Finance = () => { + return ; +}; + +export default Finance; diff --git a/src/components/pages/report/finance/FinanceTabs.tsx b/src/components/pages/report/finance/FinanceTabs.tsx new file mode 100644 index 00000000..7a970c76 --- /dev/null +++ b/src/components/pages/report/finance/FinanceTabs.tsx @@ -0,0 +1,23 @@ +'use client'; + +import Tabs from '@/components/Tabs'; +import CustomerPaymentTab from '@/components/pages/report/finance/tab/CustomerPaymentTab'; + +const FinanceTabs = () => { + const tabs = [ + { + id: '1', + label: 'Kontrol Pembayaran Customer', + + content: , + }, + ]; + + return ( +
+ +
+ ); +}; + +export default FinanceTabs; diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx new file mode 100644 index 00000000..e224d0f0 --- /dev/null +++ b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx @@ -0,0 +1,425 @@ +'use client'; + +import { + Page, + Text, + View, + Document, + StyleSheet, + Font, + pdf, +} from '@react-pdf/renderer'; + +import { formatDate, formatCurrency, formatNumber } from '@/lib/helper'; +import { CustomerPaymentReport } from '@/types/api/report/customer-payment'; + +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 CustomerPaymentExportPDFParams { + data: CustomerPaymentReport[]; +} + +const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { + return ( + + {params.data.map((customerReport, customerIndex) => ( + + {/* Title and Customer Info */} + + + Laporan > Kontrol Pembayaran Customer + + + {customerReport.customer.name} + + + {customerReport.customer_address || ''} + + + NPWP: {customerReport.customer_npwp || '-'} + + {customerReport.summary && ( + + Total Saldo Piutang:{' '} + {formatCurrency( + customerReport.summary.total_accounts_receivable + )} + + )} + + + {/* Table */} + + {/* Table Header */} + + + No + + + Tgl DO/Bayar + + + Tgl Realisasi + + + Aging + + + Referensi + + + No. Polisi + + + Qty + + + Berat (Kg) + + + AVG + + + Harga Awal + + + CN + + + Harga Akhir + + + PPN (%) + + + Total + + + Pembayaran + + + Saldo Piutang + + + Ket + + + Pengambilan + + + Sales + + + + {/* Table Body */} + {customerReport.rows.map((item, index) => ( + + + {index + 1} + + + + {item.do_date ? formatDate(item.do_date, 'DD MMM YY') : '-'} + + + + + {item.realization_date + ? formatDate(item.realization_date, 'DD MMM YY') + : '-'} + + + + {formatNumber(item.aging)} hari + + + {item.reference || '-'} + + + {item.vehicle_plate || '-'} + + + {formatNumber(item.qty)} + + + {formatNumber(item.weight)} + + + {formatNumber(item.average_weight)} + + + {formatCurrency(item.price)} + + + {formatCurrency(item.credit_note)} + + + {formatCurrency(item.final_price)} + + + {formatNumber(item.ppn)}% + + + {formatCurrency(item.total)} + + + {formatCurrency(item.payment)} + + + {formatCurrency(item.accounts_receivable)} + + + {item.notes || '-'} + + + {item.pickup_info || '-'} + + + {item.sales_marketing || '-'} + + + ))} + + {/* Summary Row */} + {customerReport.summary && ( + + + Total + + + + + + + + + + + + + + + + + + {formatNumber(customerReport.summary.total_qty)} + + + + {formatNumber(customerReport.summary.total_weight)} + + + + + + + + {formatCurrency( + customerReport.summary.total_initial_amount + )} + + + + + {formatCurrency(customerReport.summary.total_credit_note)} + + + + + {formatCurrency(customerReport.summary.total_final_amount)} + + + + + + + + {formatCurrency(customerReport.summary.total_grand_amount)} + + + + + {formatCurrency(customerReport.summary.total_payment)} + + + + + {formatCurrency( + customerReport.summary.total_accounts_receivable + )} + + + + + + + + + + + + + )} + + + ))} + + ); +}; + +export const generateCustomerPaymentPDF = async ( + params: CustomerPaymentExportPDFParams +): Promise => { + 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-kontrol-pembayaran-customer-${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; + } +}; diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx new file mode 100644 index 00000000..3cc4d67a --- /dev/null +++ b/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx @@ -0,0 +1,115 @@ +'use client'; + +import * as XLSX from 'xlsx'; +import { formatDate, formatCurrency, formatNumber } from '@/lib/helper'; +import { CustomerPaymentReport } from '@/types/api/report/customer-payment'; + +interface CustomerPaymentExportExcelParams { + data: CustomerPaymentReport[]; +} + +export const generateCustomerPaymentExcel = ( + params: CustomerPaymentExportExcelParams +): void => { + if (!params.data || params.data.length === 0) { + return; + } + + const workbook = XLSX.utils.book_new(); + + params.data.forEach((customerReport) => { + const customerData = customerReport.rows; + const customerName = customerReport.customer.name || 'Unknown Customer'; + + const excelData: { [key: string]: string | number }[] = customerData.map( + (item, index) => ({ + No: index + 1, + 'Tanggal DO/Bayar': item.do_date + ? formatDate(item.do_date, 'DD MMM YYYY') + : '', + 'Tanggal Realisasi': item.realization_date + ? formatDate(item.realization_date, 'DD MMM YYYY') + : '', + Aging: formatNumber(item.aging || 0), + Referensi: item.reference || '', + 'Nomor Polisi': item.vehicle_plate || '', + 'Ekor/Qty': formatNumber(item.qty || 0), + 'Berat (Kg)': formatNumber(item.weight || 0), + AVG: formatNumber(item.average_weight || 0), + 'Harga Awal': formatCurrency(item.price || 0), + CN: formatCurrency(item.credit_note || 0), + 'Harga Akhir': formatCurrency(item.final_price || 0), + 'PPN (%)': formatNumber(item.ppn || 0), + Total: formatCurrency(item.total || 0), + Pembayaran: formatCurrency(item.payment || 0), + 'Saldo Piutang': formatCurrency(item.accounts_receivable || 0), + Keterangan: item.notes || '', + Pengambilan: item.pickup_info || '', + 'Sales/Marketing': item.sales_marketing || '', + }) + ); + + if (customerReport.summary) { + excelData.push({ + No: 'Total', + 'Tanggal DO/Bayar': '', + 'Tanggal Realisasi': '', + Aging: '', + Referensi: '', + 'Nomor Polisi': '', + 'Ekor/Qty': formatNumber(customerReport.summary.total_qty || 0), + 'Berat (Kg)': formatNumber(customerReport.summary.total_weight || 0), + AVG: '', + 'Harga Awal': formatCurrency( + customerReport.summary.total_initial_amount || 0 + ), + CN: formatCurrency(customerReport.summary.total_credit_note || 0), + 'Harga Akhir': formatCurrency( + customerReport.summary.total_final_amount || 0 + ), + 'PPN (%)': '', + Total: formatCurrency(customerReport.summary.total_grand_amount || 0), + Pembayaran: formatCurrency(customerReport.summary.total_payment || 0), + 'Saldo Piutang': formatCurrency( + customerReport.summary.total_accounts_receivable || 0 + ), + Keterangan: '', + Pengambilan: '', + 'Sales/Marketing': '', + }); + } + + const worksheet = XLSX.utils.json_to_sheet(excelData); + + const colWidths = [ + { wch: 5 }, // No + { wch: 15 }, // Tanggal DO/Bayar + { wch: 15 }, // Tanggal Realisasi + { wch: 8 }, // Aging + { wch: 12 }, // Referensi + { wch: 15 }, // Nomor Polisi + { wch: 10 }, // Ekor/Qty + { wch: 12 }, // Berat + { wch: 10 }, // AVG + { wch: 15 }, // Harga Awal + { wch: 10 }, // CN + { wch: 15 }, // Harga Akhir + { wch: 10 }, // PPN + { wch: 15 }, // Total + { wch: 15 }, // Pembayaran + { wch: 15 }, // Saldo Piutang + { wch: 20 }, // Keterangan + { wch: 15 }, // Pengambilan + { wch: 20 }, // Sales/Marketing + ]; + worksheet['!cols'] = colWidths; + + const sheetName = + customerName.length > 31 ? customerName.substring(0, 31) : customerName; + XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); + }); + + const filename = `laporan-kontrol-pembayaran-customer-dicetak-pada-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; + + XLSX.writeFile(workbook, filename); +}; diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx new file mode 100644 index 00000000..f57f7335 --- /dev/null +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -0,0 +1,717 @@ +import { useState, useMemo, useCallback } from 'react'; +import useSWR from 'swr'; +import { Icon } from '@iconify/react'; +import Card from '@/components/Card'; +import SelectInput, { + useSelect, + OptionType, +} from '@/components/input/SelectInput'; +import DateInput from '@/components/input/DateInput'; +import { CustomerApi } from '@/services/api/master-data'; +import { FinanceApi } from '@/services/api/report/finance-report'; +import Table from '@/components/Table'; +import { ColumnDef } from '@tanstack/react-table'; +import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; +import { + CustomerPaymentReport, + CustomerPaymentSummary, +} from '@/types/api/report/customer-payment'; +import { isResponseSuccess } from '@/lib/api-helper'; +import Pagination from '@/components/Pagination'; +import Button from '@/components/Button'; +import Dropdown from '@/components/Dropdown'; +import MenuItem from '@/components/menu/MenuItem'; +import Menu from '@/components/menu/Menu'; +import Modal from '@/components/Modal'; +import { useModal } from '@/components/Modal'; +import toast from 'react-hot-toast'; +import { generateCustomerPaymentExcel } from '@/components/pages/report/finance/export/CustomerPaymentExportXLSX'; +import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/export/CustomerPaymentExportPDF'; + +const CustomerPaymentTab = () => { + // ===== 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 [filterCustomer, setFilterCustomer] = useState([]); + const [filterSales, setFilterSales] = useState([]); + const [filterStartDate, setFilterStartDate] = useState(''); + const [filterEndDate, setFilterEndDate] = useState(''); + const [filterErrors, setFilterErrors] = useState>({}); + + const filterModal = useModal(); + + const { options: customerOptions, isLoadingOptions: isLoadingCustomers } = + useSelect(CustomerApi.basePath, 'id', 'name', 'search'); + + const salesOptions = useMemo( + () => [ + { value: 'Sales A', label: 'Sales A' }, + { value: 'Sales B', label: 'Sales B' }, + { value: 'Sales C', label: 'Sales C' }, + // TODO: Fetch sales options from API + ], + [] + ); + + const dataTypeOptions = useMemo( + () => [{ value: 'do_date', label: 'Tanggal Jual' }], + [] + ); + + // ===== FILTER HANDLERS ===== + const handleResetFilters = useCallback(() => { + setIsSubmitted(false); + setFilterCustomer([]); + setFilterSales([]); + setFilterStartDate(''); + setFilterEndDate(''); + setFilterErrors({}); + }, []); + + const handleApplyFilters = useCallback(() => { + const errors: Record = {}; + + 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: customerPayment, isLoading } = useSWR( + isSubmitted + ? () => { + const params = { + customer_id: + filterCustomer.length > 0 + ? filterCustomer.map((v) => String(v.value)).join(',') + : undefined, + sales: + filterSales.length > 0 + ? filterSales.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 ['customer-payment-report', params]; + } + : null, + ([, params]) => + FinanceApi.getCustomerPaymentReport( + params.customer_id, + params.sales, + params.filter_by, + params.start_date, + params.end_date, + params.page, + params.limit + ) + ); + + const data: CustomerPaymentReport[] = useMemo( + () => + isResponseSuccess(customerPayment) + ? (customerPayment?.data as unknown as CustomerPaymentReport[]) || [] + : [], + [customerPayment] + ); + + const meta = + isResponseSuccess(customerPayment) && customerPayment?.meta + ? customerPayment.meta + : null; + + // ===== EXPORT DATA FETCHER ===== + const customerPaymentExport = useCallback(async (): Promise< + CustomerPaymentReport[] | null + > => { + const params = { + customer_id: + filterCustomer.length > 0 + ? filterCustomer.map((v) => String(v.value)).join(',') + : undefined, + sales: + filterSales.length > 0 + ? filterSales.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.getCustomerPaymentReport( + params.customer_id, + params.sales, + params.filter_by, + params.start_date, + params.end_date, + params.page, + params.limit + ); + + return isResponseSuccess(response) + ? (response.data as unknown as CustomerPaymentReport[]) + : null; + }, [filterCustomer, filterSales, filterStartDate, filterEndDate]); + + // ===== EXPORT HANDLERS ===== + const handleExportExcel = useCallback(async () => { + setIsExcelExportLoading(true); + try { + const allDataForExport = await customerPaymentExport(); + + if ( + !allDataForExport || + !Array.isArray(allDataForExport) || + allDataForExport.length === 0 + ) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + generateCustomerPaymentExcel({ data: allDataForExport }); + toast.success('Excel berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat Excel. Silakan coba lagi.'); + } finally { + setIsExcelExportLoading(false); + } + }, [customerPaymentExport]); + + const handleExportPdf = useCallback(async () => { + setIsPdfExportLoading(true); + try { + const allDataForExport = await customerPaymentExport(); + + if ( + !allDataForExport || + !Array.isArray(allDataForExport) || + allDataForExport.length === 0 + ) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + await generateCustomerPaymentPDF({ data: allDataForExport }); + toast.success('PDF berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat PDF. Silakan coba lagi.'); + } finally { + setIsPdfExportLoading(false); + } + }, [customerPaymentExport]); + + // ===== 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 = ( + summary: CustomerPaymentSummary + ): ColumnDef[] => { + const tableColumns: ColumnDef[] = [ + { + id: 'no', + header: 'No', + cell: (props) => props.row.index + 1, + footer: () =>
Total
, + }, + { + id: 'do_date_or_payment_date', + header: 'Tanggal DO/Bayar', + accessorKey: 'do_date', + cell: (props) => { + const value = props.row.original.do_date; + return formatDate(value, 'DD MMM YYYY'); + }, + }, + { + id: 'realization_date', + header: 'Tanggal Realisasi', + accessorKey: 'realization_date', + cell: (props) => { + const value = props.row.original.realization_date; + return formatDate(value, 'DD MMM YYYY'); + }, + }, + { + id: 'aging', + header: 'Aging', + accessorKey: 'aging', + cell: (props) => { + const value = props.row.original.aging; + return
{formatNumber(value)} hari
; + }, + }, + { + id: 'reference', + header: 'Referensi', + accessorKey: 'reference', + cell: (props) => { + const value = props.row.original.reference; + return value || '-'; + }, + }, + { + id: 'vehicle_plate', + header: 'Nomor Polisi', + accessorKey: 'vehicle_plate', + cell: (props) => { + const value = props.row.original.vehicle_plate; + return value || '-'; + }, + }, + { + id: 'qty', + header: 'Ekor/Qty', + accessorKey: 'qty', + cell: (props) => { + const value = props.row.original.qty; + return
{formatNumber(value)}
; + }, + footer: () => ( +
+ {formatNumber(summary.total_qty) || '-'} +
+ ), + }, + { + id: 'weight', + header: 'Berat (Kg)', + accessorKey: 'weight', + cell: (props) => { + const value = props.row.original.weight; + return
{formatNumber(value)}
; + }, + footer: () => ( +
+ {formatNumber(summary.total_weight) || '-'} +
+ ), + }, + { + id: 'average_weight', + header: 'AVG', + accessorKey: 'average_weight', + cell: (props) => { + const value = props.row.original.average_weight; + return
{formatNumber(value)}
; + }, + footer: () => ( +
-
+ ), + }, + { + id: 'price', + header: 'Harga Awal', + accessorKey: 'price', + cell: (props) => { + const value = props.row.original.price; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(summary.total_initial_amount) || '-'} +
+ ), + }, + { + id: 'credit_note', + header: 'CN', + accessorKey: 'credit_note', + cell: (props) => { + const value = props.row.original.credit_note; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(summary.total_credit_note) || '-'} +
+ ), + }, + { + id: 'final_price', + header: 'Harga Akhir', + accessorKey: 'final_price', + cell: (props) => { + const value = props.row.original.final_price; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(summary.total_final_amount) || '-'} +
+ ), + }, + { + id: 'ppn', + header: 'PPN (%)', + accessorKey: 'ppn', + cell: (props) => { + const value = props.row.original.ppn; + return
{formatNumber(value)}%
; + }, + footer: () => ( +
-
+ ), + }, + { + id: 'total', + header: 'Total', + accessorKey: 'total', + cell: (props) => { + const value = props.row.original.total; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(summary.total_grand_amount) || '-'} +
+ ), + }, + { + id: 'payment', + header: 'Pembayaran', + accessorKey: 'payment', + cell: (props) => { + const value = props.row.original.payment; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(summary.total_payment) || '-'} +
+ ), + }, + { + id: 'accounts_receivable', + header: 'Saldo Piutang', + accessorKey: 'accounts_receivable', + cell: (props) => { + const value = props.row.original.accounts_receivable; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(summary.total_accounts_receivable) || '-'} +
+ ), + }, + { + id: 'notes', + header: 'Keterangan', + accessorKey: 'notes', + cell: (props) => { + const value = props.row.original.notes; + return value || '-'; + }, + }, + { + id: 'pickup_info', + header: 'Pengambilan', + accessorKey: 'pickup_info', + cell: (props) => { + const value = props.row.original.pickup_info; + return value || '-'; + }, + }, + { + id: 'sales_marketing', + header: 'Sales/Marketing', + accessorKey: 'sales_marketing', + cell: (props) => { + const value = props.row.original.sales_marketing; + return value || '-'; + }, + }, + ]; + return tableColumns; + }; + + return ( +
+ +
+ + + + + Export + + } + align='end' + > + + + + + +
+ + {/* Filter Modal */} + +
+ {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+
+ { + setFilterStartDate(e.target.value); + setFilterErrors((prev) => ({ ...prev, start_date: '' })); + }} + className={{ wrapper: 'w-full' }} + /> + {filterErrors.start_date && ( +

+ {filterErrors.start_date} +

+ )} +
+ +
+ { + setFilterEndDate(e.target.value); + setFilterErrors((prev) => ({ ...prev, end_date: '' })); + }} + className={{ wrapper: 'w-full' }} + /> + {filterErrors.end_date && ( +

+ {filterErrors.end_date} +

+ )} +
+
+ +
+ { + setFilterCustomer( + Array.isArray(val) ? val : val ? [val] : [] + ); + }} + isLoading={isLoadingCustomers} + isClearable + className={{ wrapper: 'w-full' }} + /> +
+ +
+ { + setFilterSales(Array.isArray(val) ? val : val ? [val] : []); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> +
+ +
+ +
+
+ + {/* Action Buttons */} +
+ + +
+
+
+ + {!isSubmitted ? ( +
+ Silakan klik tombol Filter untuk mengatur filter dan menampilkan + data. +
+ ) : isLoading ? ( +
+ +
+ ) : data.length === 0 ? ( +
+ Tidak ada data yang dapat ditampilkan... +
+ ) : ( + data.map((customerReport) => { + const summary = customerReport.summary || { + total_qty: 0, + total_weight: 0, + total_initial_amount: 0, + total_credit_note: 0, + total_final_amount: 0, + total_ppn: 0, + total_grand_amount: 0, + total_payment: 0, + total_accounts_receivable: 0, + }; + + const totalAccountsReceivable = summary.total_accounts_receivable; + const tableColumns = getTableColumns(summary); + + return ( + + 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', + }} + /> + + ); + }) + )} + + {meta && data.length > 0 && ( +
+ +
+ )} + + ); +}; + +export default CustomerPaymentTab; diff --git a/src/config/constant.ts b/src/config/constant.ts index f7f2255e..77e210a2 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -127,6 +127,10 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ link: '/report', icon: 'mdi:chart-box-outline', submenu: [ + { + text: 'Keuangan', + link: '/report/finance', + }, { text: 'Logistik & Persediaan', link: '/report/logistic-stock', diff --git a/src/config/route-permission.ts b/src/config/route-permission.ts index ca720f28..10a66f8c 100644 --- a/src/config/route-permission.ts +++ b/src/config/route-permission.ts @@ -117,6 +117,7 @@ export const ROUTE_PERMISSIONS: Record = { '/report/expense/': ['lti.repport.expense.list'], '/report/marketing/': ['lti.repport.delivery.list'], '/report/production-result/': ['lti.repport.production_result.list'], + '/report/finance/': ['lti.repport.finance.list'], // Inventory '/inventory/adjustment/': ['lti.inventory.list'], diff --git a/src/services/api/report/finance-report.ts b/src/services/api/report/finance-report.ts new file mode 100644 index 00000000..e0acb6b0 --- /dev/null +++ b/src/services/api/report/finance-report.ts @@ -0,0 +1,45 @@ +import { BaseApiService } from '@/services/api/base'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { CustomerPaymentReport } from '@/types/api/report/customer-payment'; + +export class FinanceApiService extends BaseApiService< + CustomerPaymentReport, + unknown, + unknown +> { + constructor(basePath: string) { + super(basePath); + } + + async getCustomerPaymentReport( + customer_id?: string, + sales?: string, + filter_by?: 'do_date', + start_date?: string, + end_date?: string, + page?: number, + limit?: number + ): Promise | undefined> { + return await this.customRequest>( + `customer-payment`, + { + method: 'GET', + params: { + customer_id: customer_id, + sales: sales, + 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( +// 'http://localhost:4010/api/reports/finance' +// ); diff --git a/src/types/api/report/customer-payment.d.ts b/src/types/api/report/customer-payment.d.ts new file mode 100644 index 00000000..776d640d --- /dev/null +++ b/src/types/api/report/customer-payment.d.ts @@ -0,0 +1,47 @@ +import { BaseMetadata } from '@/types/api/api-general'; +import { BaseCustomer } from '@/types/api/master-data/customer'; +import { BaseProduct } from '@/types/api/master-data/product'; + +export type CustomerPaymentRow = { + no: number; + do_date: string; + payment_date: string; + realization_date: string; + aging: number; + reference: string; + vehicle_plate: string; + qty: number; + weight: number; + average_weight: number; + price: number; + credit_note: number; + final_price: number; + ppn: number; + total: number; + payment: number; + accounts_receivable: number; + notes: string; + pickup_info: string; + sales_marketing: string; + product?: BaseProduct; +}; + +export type CustomerPaymentSummary = { + total_qty: number; + total_weight: number; + total_initial_amount: number; + total_credit_note: number; + total_final_amount: number; + total_ppn: number; + total_grand_amount: number; + total_payment: number; + total_accounts_receivable: number; +}; + +export type CustomerPaymentReport = BaseMetadata & { + customer: BaseCustomer; + customer_npwp: string; + customer_address: string; + rows: CustomerPaymentRow[]; + summary: CustomerPaymentSummary; +};