mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-24 07:15:44 +00:00
1029 lines
32 KiB
TypeScript
1029 lines
32 KiB
TypeScript
import { useState, useMemo, useCallback, useEffect } from 'react';
|
|
import useSWR from 'swr';
|
|
import { Icon } from '@iconify/react';
|
|
import Card from '@/components/Card';
|
|
import Badge from '@/components/Badge';
|
|
import { useSelect } from '@/components/input/SelectInput';
|
|
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
|
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
|
import DateInput from '@/components/input/DateInput';
|
|
import { CustomerApi } from '@/services/api/master-data';
|
|
import { FinanceApi } from '@/services/api/report/finance-report';
|
|
// import { UserApi } from '@/services/api/user';
|
|
import Table from '@/components/Table';
|
|
import { ColumnDef } from '@tanstack/react-table';
|
|
import { formatCurrency, formatDate, formatNumber, cn } from '@/lib/helper';
|
|
import {
|
|
CustomerPaymentReport,
|
|
CustomerPaymentSummary,
|
|
} from '@/types/api/report/customer-payment';
|
|
import { isResponseSuccess } from '@/lib/api-helper';
|
|
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';
|
|
import { useFinanceTabStore } from '@/stores/finance-tab/finance-tab.store';
|
|
import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton';
|
|
|
|
interface CustomerPaymentTabProps {
|
|
tabId: string;
|
|
}
|
|
|
|
const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|
// ===== 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] = useState(10);
|
|
|
|
// ===== SUBMISSION STATE =====
|
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
|
|
|
// ===== FILTER STATE =====
|
|
const [appliedFilterCustomer, setAppliedFilterCustomer] = useState<
|
|
typeof customerOptions
|
|
>([]);
|
|
// TODO: Uncomment when BE is ready
|
|
// const [appliedFilterSales, setAppliedFilterSales] = useState<
|
|
// typeof salesOptions
|
|
// >([]);
|
|
const [appliedFilterByType, setAppliedFilterByType] = useState<
|
|
(typeof dataTypeOptions)[0] | null
|
|
>(null);
|
|
const [appliedFilterStartDate, setAppliedFilterStartDate] = useState('');
|
|
const [appliedFilterEndDate, setAppliedFilterEndDate] = useState('');
|
|
const [dateErrorShown, setDateErrorShown] = useState(false);
|
|
const [hasDateError, setHasDateError] = useState(false);
|
|
|
|
const [filterCustomer, setFilterCustomer] = useState<typeof customerOptions>(
|
|
[]
|
|
);
|
|
// TODO: Uncomment when BE is ready
|
|
// const [filterSales, setFilterSales] = useState<typeof salesOptions>([]);
|
|
const [filterStartDate, setFilterStartDate] = useState('');
|
|
const [filterEndDate, setFilterEndDate] = useState('');
|
|
|
|
const filterModal = useModal();
|
|
|
|
const dataTypeOptions = useMemo(
|
|
() => [
|
|
{ value: 'trans_date', label: 'Tanggal Jual/Bayar' },
|
|
{ value: 'realization_date', label: 'Tanggal Realisasi' },
|
|
],
|
|
[]
|
|
);
|
|
|
|
const [filterByType, setFilterByType] = useState<
|
|
(typeof dataTypeOptions)[0] | null
|
|
>(null);
|
|
|
|
const {
|
|
options: customerOptions,
|
|
setInputValue: setCustomerInputValue,
|
|
isLoadingOptions: isLoadingCustomers,
|
|
loadMore: loadMoreCustomers,
|
|
} = useSelect(CustomerApi.basePath, 'id', 'name', 'search');
|
|
|
|
// TODO: Uncomment when BE is ready
|
|
// const {
|
|
// options: salesOptions,
|
|
// setInputValue: setSalesInputValue,
|
|
// isLoadingOptions: isLoadingSales,
|
|
// loadMore: loadMoreSales,
|
|
// hasMore: hasMoreSales,
|
|
// } = useSelect(UserApi.basePath, 'id', 'name', 'search');
|
|
|
|
const getPaymentStatusColor = (notes: string) => {
|
|
const normalizedValue = notes.toLowerCase();
|
|
|
|
if (normalizedValue === 'lunas') {
|
|
return 'bg-info/10 text-black border-info';
|
|
}
|
|
|
|
if (normalizedValue.includes('belum')) {
|
|
return 'bg-warning/10 text-black border-warning';
|
|
}
|
|
|
|
return 'bg-gray-100 text-black border-gray-300';
|
|
};
|
|
|
|
const getPaymentStatusIndicatorColor = (notes: string) => {
|
|
const normalizedValue = notes.toLowerCase();
|
|
|
|
if (normalizedValue === 'lunas') {
|
|
return 'bg-info';
|
|
}
|
|
|
|
if (normalizedValue.includes('belum')) {
|
|
return 'bg-warning';
|
|
}
|
|
|
|
return 'bg-gray-400';
|
|
};
|
|
|
|
const getPaymentStatusText = (notes: string) => {
|
|
return notes
|
|
.toLowerCase()
|
|
.split(' ')
|
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
.join(' ');
|
|
};
|
|
|
|
// ===== FILTER HANDLERS =====
|
|
const handleFilterModalOpen = useCallback(() => {
|
|
setFilterCustomer(appliedFilterCustomer);
|
|
// setFilterSales(appliedFilterSales);
|
|
setFilterByType(appliedFilterByType);
|
|
setFilterStartDate(appliedFilterStartDate);
|
|
setFilterEndDate(appliedFilterEndDate);
|
|
filterModal.openModal();
|
|
}, [
|
|
filterModal,
|
|
appliedFilterCustomer,
|
|
appliedFilterByType,
|
|
appliedFilterStartDate,
|
|
appliedFilterEndDate,
|
|
]);
|
|
|
|
const handleResetFilters = useCallback(() => {
|
|
setIsSubmitted(false);
|
|
setFilterCustomer([]);
|
|
setFilterByType(null);
|
|
setFilterStartDate('');
|
|
setFilterEndDate('');
|
|
setAppliedFilterCustomer([]);
|
|
setAppliedFilterByType(null);
|
|
setAppliedFilterStartDate('');
|
|
setAppliedFilterEndDate('');
|
|
setHasDateError(false);
|
|
if (dateErrorShown) {
|
|
toast.dismiss();
|
|
setDateErrorShown(false);
|
|
}
|
|
}, [dateErrorShown]);
|
|
|
|
const handleApplyFilters = useCallback(() => {
|
|
setAppliedFilterCustomer(filterCustomer);
|
|
setAppliedFilterByType(filterByType);
|
|
setAppliedFilterStartDate(filterStartDate);
|
|
setAppliedFilterEndDate(filterEndDate);
|
|
setIsSubmitted(true);
|
|
setCurrentPage(1);
|
|
filterModal.closeModal();
|
|
}, [
|
|
filterModal,
|
|
filterCustomer,
|
|
filterByType,
|
|
filterStartDate,
|
|
filterEndDate,
|
|
]);
|
|
|
|
const handleStartDateChange = useCallback(
|
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const value = e.target.value;
|
|
setFilterStartDate(value);
|
|
|
|
if (value && filterEndDate) {
|
|
const startDate = new Date(value);
|
|
const endDateObj = new Date(filterEndDate);
|
|
|
|
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);
|
|
}
|
|
},
|
|
[filterEndDate, dateErrorShown]
|
|
);
|
|
|
|
const handleEndDateChange = useCallback(
|
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const value = e.target.value;
|
|
setFilterEndDate(value);
|
|
|
|
if (value && filterStartDate) {
|
|
const startDateObj = new Date(filterStartDate);
|
|
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);
|
|
}
|
|
},
|
|
[filterStartDate, dateErrorShown]
|
|
);
|
|
|
|
// ===== ACTIVE FILTERS COUNT =====
|
|
const activeFiltersCount = useMemo(() => {
|
|
let count = 0;
|
|
|
|
// Date filter (start_date + end_date = 1 filter)
|
|
if (appliedFilterStartDate || appliedFilterEndDate) {
|
|
count += 1;
|
|
}
|
|
|
|
// Customer filter
|
|
if (appliedFilterCustomer.length > 0) {
|
|
count += 1;
|
|
}
|
|
|
|
// Filter by type filter (hanya dihitung jika ada nilai yang dipilih)
|
|
if (appliedFilterByType) {
|
|
count += 1;
|
|
}
|
|
|
|
// TODO: Uncomment when BE is ready
|
|
// // Sales filter
|
|
// if (appliedFilterSales.length > 0) {
|
|
// count += 1;
|
|
// }
|
|
|
|
return count;
|
|
}, [
|
|
appliedFilterStartDate,
|
|
appliedFilterEndDate,
|
|
appliedFilterCustomer,
|
|
appliedFilterByType,
|
|
]);
|
|
|
|
const hasFilters = activeFiltersCount > 0;
|
|
|
|
// ===== DATA FETCHING =====
|
|
const { data: customerPayment, isLoading } = useSWR(
|
|
isSubmitted
|
|
? () => {
|
|
const params = {
|
|
customer_ids:
|
|
appliedFilterCustomer.length > 0
|
|
? appliedFilterCustomer.map((v) => String(v.value)).join(',')
|
|
: undefined,
|
|
// TODO: Uncomment when BE is ready
|
|
// sales_id:
|
|
// appliedFilterSales.length > 0
|
|
// ? appliedFilterSales.map((v) => String(v.value)).join(',')
|
|
// : undefined,
|
|
filter_by: appliedFilterByType?.value as
|
|
| 'trans_date'
|
|
| 'realization_date'
|
|
| undefined,
|
|
start_date: appliedFilterStartDate || undefined,
|
|
end_date: appliedFilterEndDate || undefined,
|
|
page: currentPage,
|
|
limit: pageSize,
|
|
};
|
|
|
|
return ['customer-payment-report', params];
|
|
}
|
|
: null,
|
|
([, params]) =>
|
|
FinanceApi.getCustomerPaymentReport(
|
|
params.customer_ids,
|
|
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]
|
|
);
|
|
|
|
// ===== EXPORT DATA FETCHER =====
|
|
const customerPaymentExport = useCallback(async (): Promise<
|
|
CustomerPaymentReport[] | null
|
|
> => {
|
|
const params = {
|
|
customer_ids:
|
|
appliedFilterCustomer.length > 0
|
|
? appliedFilterCustomer.map((v) => String(v.value)).join(',')
|
|
: undefined,
|
|
// TODO: Uncomment when BE is ready
|
|
// sales_id:
|
|
// appliedFilterSales.length > 0
|
|
// ? appliedFilterSales.map((v) => String(v.value)).join(',')
|
|
// : undefined,
|
|
filter_by: appliedFilterByType?.value as
|
|
| 'trans_date'
|
|
| 'realization_date'
|
|
| undefined,
|
|
start_date: appliedFilterStartDate || undefined,
|
|
end_date: appliedFilterEndDate || undefined,
|
|
limit: 100,
|
|
page: 1,
|
|
};
|
|
|
|
const response = await FinanceApi.getCustomerPaymentReport(
|
|
params.customer_ids,
|
|
params.filter_by,
|
|
params.start_date,
|
|
params.end_date,
|
|
params.page,
|
|
params.limit
|
|
);
|
|
|
|
return isResponseSuccess(response)
|
|
? (response.data as unknown as CustomerPaymentReport[])
|
|
: null;
|
|
}, [
|
|
appliedFilterCustomer,
|
|
// appliedFilterSales,
|
|
appliedFilterStartDate,
|
|
appliedFilterEndDate,
|
|
appliedFilterByType,
|
|
]);
|
|
|
|
// ===== 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;
|
|
}
|
|
|
|
await 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,
|
|
params: {
|
|
customer_name:
|
|
appliedFilterCustomer.length > 0
|
|
? appliedFilterCustomer.map((c) => c.label).join(', ')
|
|
: undefined,
|
|
// TODO: Uncomment when BE is ready
|
|
// sales:
|
|
// appliedFilterSales.length > 0
|
|
// ? appliedFilterSales.map((s) => s.label).join(', ')
|
|
// : undefined,
|
|
start_date: appliedFilterStartDate || undefined,
|
|
end_date: appliedFilterEndDate || undefined,
|
|
filter_by: appliedFilterByType?.value as
|
|
| 'trans_date'
|
|
| 'realization_date'
|
|
| undefined,
|
|
},
|
|
});
|
|
toast.success('PDF berhasil dibuat dan diunduh.');
|
|
} catch {
|
|
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
|
} finally {
|
|
setIsPdfExportLoading(false);
|
|
}
|
|
}, [customerPaymentExport]);
|
|
|
|
// ===== REGISTER TAB ACTIONS TO STORE =====
|
|
const setTabActions = useFinanceTabStore((state) => state.setTabActions);
|
|
const clearTabActions = useFinanceTabStore((state) => state.clearTabActions);
|
|
|
|
useEffect(() => {
|
|
setTabActions(
|
|
tabId,
|
|
<div className='flex flex-row gap-3'>
|
|
<Button
|
|
variant='outline'
|
|
color='none'
|
|
onClick={handleFilterModalOpen}
|
|
className={cn(
|
|
'px-3 py-2.5',
|
|
'rounded-lg! font-semibold text-sm gap-1.5',
|
|
'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft',
|
|
hasFilters && 'border-primary-gradient text-primary'
|
|
)}
|
|
>
|
|
<Icon icon='heroicons:funnel' width={18} height={18} />
|
|
Filter
|
|
{hasFilters && (
|
|
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
|
|
{activeFiltersCount}
|
|
</span>
|
|
)}
|
|
</Button>
|
|
|
|
<Dropdown
|
|
trigger={
|
|
<Button
|
|
variant='outline'
|
|
color='none'
|
|
isLoading={isAnyExportLoading}
|
|
className={cn(
|
|
'px-3 py-2.5',
|
|
'rounded-lg font-semibold text-sm gap-1.5',
|
|
'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft'
|
|
)}
|
|
>
|
|
<Icon icon='heroicons:cloud-arrow-down' width={20} height={20} />
|
|
Export
|
|
<div className='w-6.5 h-5 flex items-center justify-center border-l border-base-content/10'>
|
|
<Icon width={14} height={14} icon='heroicons:chevron-down' />
|
|
</div>
|
|
</Button>
|
|
}
|
|
align='end'
|
|
className={{
|
|
content:
|
|
'mt-1 p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg',
|
|
}}
|
|
>
|
|
<Menu className='p-0 w-full'>
|
|
<MenuItem
|
|
className='text-sm p-3'
|
|
title='Excel'
|
|
onClick={handleExportExcel}
|
|
/>
|
|
<MenuItem
|
|
className='text-sm p-3'
|
|
title='PDF'
|
|
onClick={handleExportPdf}
|
|
/>
|
|
</Menu>
|
|
</Dropdown>
|
|
</div>
|
|
);
|
|
}, [
|
|
tabId,
|
|
hasFilters,
|
|
activeFiltersCount,
|
|
isAnyExportLoading,
|
|
handleExportExcel,
|
|
handleExportPdf,
|
|
filterModal.open,
|
|
setTabActions,
|
|
]);
|
|
|
|
// Cleanup on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
clearTabActions(tabId);
|
|
};
|
|
}, [tabId, clearTabActions]);
|
|
|
|
const getTableColumns = (
|
|
summary: CustomerPaymentSummary
|
|
): ColumnDef<CustomerPaymentReport['rows'][0]>[] => {
|
|
const tableColumns: ColumnDef<CustomerPaymentReport['rows'][0]>[] = [
|
|
{
|
|
id: 'no',
|
|
header: 'No',
|
|
cell: (props) => props.row.index,
|
|
footer: () => <div className='font-semibold text-gray-900'>Total</div>,
|
|
},
|
|
{
|
|
id: 'trans_date',
|
|
header: 'Tanggal Jual/Bayar',
|
|
accessorKey: 'trans_date',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.trans_date;
|
|
return value ? formatDate(value, 'DD MMM YYYY') : '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'realization_date',
|
|
header: 'Tanggal Realisasi',
|
|
accessorKey: 'delivery_date',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.delivery_date;
|
|
return value ? formatDate(value, 'DD MMM YYYY') : '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'aging',
|
|
header: 'Aging',
|
|
accessorKey: 'aging_day',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.aging_day;
|
|
return (
|
|
<div className='text-center'>
|
|
{value !== null && value !== undefined
|
|
? `${formatNumber(value)} hari`
|
|
: '-'}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: 'reference',
|
|
header: 'Referensi',
|
|
accessorKey: 'reference',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.reference;
|
|
return value || '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'vehicle_plate',
|
|
header: 'Nomor Polisi',
|
|
accessorKey: 'vehicle_numbers',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.vehicle_numbers;
|
|
return Array.isArray(value)
|
|
? value.length > 0
|
|
? value.join(', ')
|
|
: '-'
|
|
: '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'qty',
|
|
header: 'Qty',
|
|
accessorKey: 'qty',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.qty;
|
|
return <div className='text-right'>{formatNumber(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatNumber(summary.total_qty) || '-'}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'weight',
|
|
header: 'Berat (Kg)',
|
|
accessorKey: 'weight',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.weight;
|
|
return <div className='text-right'>{formatNumber(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatNumber(summary.total_weight) || '-'}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'average_weight',
|
|
header: 'AVG',
|
|
accessorKey: 'average_weight',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.average_weight;
|
|
return <div className='text-right'>{formatNumber(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>-</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'unit_price',
|
|
header: 'Harga/Unit',
|
|
accessorKey: 'unit_price',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.unit_price;
|
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>-</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'final_price',
|
|
header: 'Harga Akhir',
|
|
accessorKey: 'final_price',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.final_price;
|
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatCurrency(summary.total_final_amount) || '-'}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'total',
|
|
header: 'Total',
|
|
accessorKey: 'total_price',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.total_price;
|
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatCurrency(summary.total_grand_amount) || '-'}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'payment',
|
|
header: 'Pembayaran',
|
|
accessorKey: 'payment_amount',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.payment_amount;
|
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatCurrency(summary.total_payment) || '-'}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'accounts_receivable',
|
|
header: 'Saldo Piutang',
|
|
accessorKey: 'accounts_receivable',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.accounts_receivable;
|
|
return (
|
|
<div
|
|
className={`text-right font-semibold ${
|
|
value < 0 ? 'text-error' : ''
|
|
}`}
|
|
>
|
|
{formatCurrency(value)}
|
|
</div>
|
|
);
|
|
},
|
|
footer: () => (
|
|
<div
|
|
className={`text-right font-semibold ${
|
|
summary.total_accounts_receivable < 0 ? 'text-error' : ''
|
|
}`}
|
|
>
|
|
{formatCurrency(summary.total_accounts_receivable) || '-'}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'notes',
|
|
header: 'Keterangan',
|
|
accessorKey: 'status',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.status;
|
|
|
|
if (!value) {
|
|
return '-';
|
|
}
|
|
|
|
return (
|
|
<Badge
|
|
statusIndicator={true}
|
|
size='sm'
|
|
variant='soft'
|
|
className={{
|
|
badge: `py-2.5 px-2 font-thin text-xs border border-gray-200 rounded-xl justify-start ${getPaymentStatusColor(value)}`,
|
|
status: getPaymentStatusIndicatorColor(value),
|
|
}}
|
|
>
|
|
{getPaymentStatusText(value)}
|
|
</Badge>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: 'pickup_info',
|
|
header: 'Pengambilan',
|
|
accessorKey: 'pickup_info',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.pickup_info;
|
|
return Array.isArray(value)
|
|
? value.length > 0
|
|
? value.join(', ')
|
|
: '-'
|
|
: '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'sales_marketing',
|
|
header: 'Sales/Marketing',
|
|
accessorKey: 'sales_person',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.sales_person;
|
|
return value || '-';
|
|
},
|
|
},
|
|
];
|
|
return tableColumns;
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
|
|
{!isSubmitted ? (
|
|
<CustomerSupplierSkeleton
|
|
columns={getTableColumns({} as CustomerPaymentSummary)}
|
|
icon={
|
|
<Icon
|
|
icon='heroicons:funnel'
|
|
className='text-white'
|
|
width={20}
|
|
height={20}
|
|
/>
|
|
}
|
|
title='No Filters Selected'
|
|
subtitle='Please choose filters to narrow down your results and make your search easier.'
|
|
/>
|
|
) : isLoading ? (
|
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
|
<span className='loading loading-spinner loading-xl' />
|
|
</div>
|
|
) : data.length === 0 ? (
|
|
<CustomerSupplierSkeleton
|
|
columns={getTableColumns({} as CustomerPaymentSummary)}
|
|
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.'
|
|
/>
|
|
) : (
|
|
data.map((customerReport) => {
|
|
const summary = customerReport.summary || {
|
|
total_qty: 0,
|
|
total_weight: 0,
|
|
total_final_amount: 0,
|
|
total_grand_amount: 0,
|
|
total_payment: 0,
|
|
total_accounts_receivable: 0,
|
|
};
|
|
|
|
const tableColumns = getTableColumns(summary);
|
|
|
|
return (
|
|
<Card
|
|
key={customerReport.customer.id}
|
|
title={customerReport.customer.name}
|
|
subtitle={`(${customerReport.customer.address})`}
|
|
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',
|
|
subtitle:
|
|
'px-2 pb-1.5 bg-primary text-white text-xs font-normal',
|
|
collapsible: 'rounded-lg',
|
|
}}
|
|
variant='bordered'
|
|
collapsible={true}
|
|
defaultCollapsed={true}
|
|
>
|
|
<Table
|
|
data={[
|
|
{
|
|
accounts_receivable: customerReport.initial_balance,
|
|
} as CustomerPaymentReport['rows'][0],
|
|
...customerReport.rows,
|
|
]}
|
|
columns={tableColumns}
|
|
pageSize={customerReport.rows.length + 1}
|
|
renderFooter={customerReport.rows.length > 0}
|
|
className={{
|
|
containerClassName: 'w-full mb-0!',
|
|
tableWrapperClassName:
|
|
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
|
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',
|
|
}}
|
|
renderCustomRow={(row) => {
|
|
if (row.index === 0) {
|
|
return (
|
|
<tr
|
|
key={row.index}
|
|
className='hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200'
|
|
>
|
|
<td
|
|
className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap'
|
|
colSpan={13}
|
|
></td>
|
|
<td className='px-4 py-3 text-xs whitespace-nowrap'>
|
|
<div
|
|
className={`text-right ${
|
|
row.original.accounts_receivable < 0
|
|
? 'text-error'
|
|
: ''
|
|
}`}
|
|
>
|
|
{formatCurrency(row.original.accounts_receivable)}
|
|
</div>
|
|
</td>
|
|
<td
|
|
className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap'
|
|
colSpan={4}
|
|
></td>
|
|
</tr>
|
|
);
|
|
}
|
|
}}
|
|
/>
|
|
</Card>
|
|
);
|
|
})
|
|
)}
|
|
</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>
|
|
<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='start_date'
|
|
value={filterStartDate}
|
|
onChange={handleStartDateChange}
|
|
className={{ wrapper: 'w-full' }}
|
|
isNestedModal
|
|
/>
|
|
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
|
|
|
<DateInput
|
|
name='end_date'
|
|
value={filterEndDate}
|
|
onChange={handleEndDateChange}
|
|
className={{ wrapper: 'w-full' }}
|
|
isNestedModal
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<SelectInputCheckbox
|
|
label='Customer'
|
|
placeholder='Pilih Customer'
|
|
options={customerOptions}
|
|
value={filterCustomer}
|
|
onChange={(val) => {
|
|
setFilterCustomer(Array.isArray(val) ? val : val ? [val] : []);
|
|
}}
|
|
onInputChange={setCustomerInputValue}
|
|
isLoading={isLoadingCustomers}
|
|
isClearable
|
|
onMenuScrollToBottom={loadMoreCustomers}
|
|
className={{ wrapper: 'w-full' }}
|
|
/>
|
|
|
|
{/* TODO: Uncomment when BE is ready */}
|
|
{/* <div>
|
|
<SelectInputCheckbox
|
|
label='Sales'
|
|
placeholder='Pilih Sales'
|
|
options={salesOptions}
|
|
value={filterSales}
|
|
onChange={(val) => {
|
|
setFilterSales(Array.isArray(val) ? val : val ? [val] : []);
|
|
}}
|
|
onInputChange={setSalesInputValue}
|
|
isLoading={isLoadingSales}
|
|
isClearable
|
|
onMenuScrollToBottom={loadMoreSales}
|
|
className={{ wrapper: 'w-full' }}
|
|
/>
|
|
</div> */}
|
|
|
|
<SelectInputRadio
|
|
label='Filter Berdasarkan'
|
|
placeholder='Pilih Filter Berdasarkan'
|
|
options={dataTypeOptions}
|
|
value={filterByType}
|
|
onChange={(val) => {
|
|
if (val && !Array.isArray(val)) {
|
|
setFilterByType(val);
|
|
}
|
|
}}
|
|
className={{ wrapper: 'w-full' }}
|
|
/>
|
|
|
|
{/* Action Buttons */}
|
|
</div>
|
|
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
|
<Button
|
|
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'
|
|
onClick={handleResetFilters}
|
|
>
|
|
Reset Filter
|
|
</Button>
|
|
<Button
|
|
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
|
onClick={handleApplyFilters}
|
|
disabled={hasDateError}
|
|
>
|
|
Apply Filter
|
|
</Button>
|
|
</div>
|
|
</Modal>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default CustomerPaymentTab;
|