fix(FE): fixing report finance filter ui

This commit is contained in:
randy-ar
2026-01-29 20:59:16 +07:00
parent 5509f52464
commit e980320d00
5 changed files with 253 additions and 195 deletions
+20 -7
View File
@@ -280,7 +280,7 @@ const DateInput = ({
ref={calendarModal.ref}
className={{
modal: 'rounded',
modalBox: `!max-w-max min-h-${isRange ? '124' : '110'} flex flex-col`,
modalBox: `max-w-max flex flex-col`,
}}
closeOnBackdrop
>
@@ -296,7 +296,11 @@ const DateInput = ({
endMonth={maxDate ?? new Date(new Date().getFullYear() + 5, 11)}
selected={selectedRange as DateRange}
onSelect={handleSelectRange}
footer={<div className='text-center mt-3'>{displayValue}</div>}
footer={
<div className='text-center py-2 text-base-content/65 font-semibold text-xs'>
{displayValue}
</div>
}
disabled={
[
minDate ? { before: minDate } : undefined,
@@ -326,17 +330,26 @@ const DateInput = ({
)}
<div className='mt-auto flex flex-col gap-2'>
{isRange && (
<small className='text-secondary'>
Tekan dua kali untuk memilih tanggal awal
<small className='text-base-content/65'>
Tekan dua kali untuk reset tanggal awal
</small>
)}
<div className='flex h-full justify-end items-end gap-2'>
<Button type='button' color='warning' onClick={handleResetDate}>
<div className='flex h-full justify-end items-end gap-1.5 mt-3'>
<Button
type='button'
color='none'
className='bg-transparent hover:bg-base-content/10 border-none text-base text-base-content/65 px-3'
onClick={handleResetDate}
>
Reset
</Button>
{isRange && (
<Button type='button' onClick={handleSaveDate}>
<Button
type='button'
className='rounded-lg px-3 py-2 text-white'
onClick={handleSaveDate}
>
Simpan
</Button>
)}
@@ -463,6 +463,7 @@ const DashboardProduction = () => {
Boolean(formik.errors.endDate) &&
Boolean(formik.touched.endDate)
}
isRange
/>
</div>
</div>
@@ -19,7 +19,7 @@ const FinanceTabs = () => {
{
id: '2',
label: 'Kontrol Pembayaran Customer',
content: <CustomerPaymentTab />,
content: <CustomerPaymentTab tabId={'2'} />,
},
];
@@ -1,4 +1,4 @@
import { useState, useMemo, useCallback } from 'react';
import { useState, useMemo, useCallback, useEffect } from 'react';
import useSWR from 'swr';
import { Icon } from '@iconify/react';
import Card from '@/components/Card';
@@ -11,7 +11,7 @@ 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 } from '@/lib/helper';
import { formatCurrency, formatDate, formatNumber, cn } from '@/lib/helper';
import {
CustomerPaymentReport,
CustomerPaymentSummary,
@@ -26,8 +26,13 @@ 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';
const CustomerPaymentTab = () => {
interface CustomerPaymentTabProps {
tabId: string;
}
const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
// ===== STATE MANAGEMENT =====
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
@@ -111,6 +116,10 @@ const CustomerPaymentTab = () => {
};
// ===== FILTER HANDLERS =====
const handleFilterModalOpen = useCallback(() => {
filterModal.openModal();
}, [filterModal]);
const handleResetFilters = useCallback(() => {
setIsSubmitted(false);
setFilterCustomer([]);
@@ -298,6 +307,92 @@ const CustomerPaymentTab = () => {
}
}, [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]>[] => {
@@ -552,181 +647,10 @@ const CustomerPaymentTab = () => {
};
return (
<div className='w-full p-0 sm:p-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}
className={
hasFilters
? 'bg-linear-to-b from-[#0069E0]/40 to-white text-[#0069E0] rounded-lg'
: 'rounded-lg'
}
>
<Icon icon='heroicons:funnel' width={18} height={18} />
Filter
{hasFilters && (
<Badge
variant='default'
className={{
badge:
'rounded-lg px-1.5 py-2.5 text-xs font-semibold bg-error text-white',
}}
>
{activeFiltersCount}
</Badge>
)}
</Button>
<Dropdown
trigger={
<Button
variant='outline'
isLoading={isAnyExportLoading}
className='rounded-lg'
>
<Icon
icon='heroicons:cloud-arrow-down'
width={18}
height={18}
/>
Export
</Button>
}
align='end'
>
<Menu className={'w-full'}>
<MenuItem title='Excel' onClick={handleExportExcel} />
<MenuItem title='PDF' onClick={handleExportPdf} />
</Menu>
</Dropdown>
</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);
}}
className={{ wrapper: 'w-full' }}
/>
</div>
<div>
<DateInput
label=' '
name='end_date'
value={filterEndDate}
onChange={(e) => {
setFilterEndDate(e.target.value);
}}
className={{ wrapper: 'w-full' }}
/>
</div>
</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' }}
/>
</div>
{/* 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> */}
{/* TODO: Uncomment when BE is ready */}
{/* <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>
<>
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
{!isSubmitted ? (
<div className='mt-6 text-center text-gray-500'>
<div className='mt-6 text-center text-base-content/50'>
Silakan klik tombol Filter untuk mengatur filter dan menampilkan
data.
</div>
@@ -735,7 +659,7 @@ const CustomerPaymentTab = () => {
<span className='loading loading-spinner loading-xl' />
</div>
) : data.length === 0 ? (
<div className='mt-6 text-center text-gray-500'>
<div className='mt-6 text-center text-base-content/50'>
Tidak ada data yang dapat ditampilkan...
</div>
) : (
@@ -757,15 +681,17 @@ const CustomerPaymentTab = () => {
title={customerReport.customer.name}
subtitle={`(${customerReport.customer.address})`}
className={{
wrapper: 'w-full rounded-2xl',
wrapper: 'w-full rounded-lg border-none',
body: 'p-0',
title:
'py-1.5 px-3 bg-primary text-white text-lg font-normal',
'px-2 py-1.5 font-normal text-sm bg-primary text-white',
subtitle:
'px-3 pb-1 bg-primary text-white text-sm font-normal',
'px-2 pb-1.5 bg-primary text-white text-xs font-normal',
collapsible: 'rounded-lg',
}}
variant='bordered'
collapsible={true}
defaultCollapsed={true}
>
<Table
data={[
@@ -779,7 +705,8 @@ const CustomerPaymentTab = () => {
renderFooter={customerReport.rows.length > 0}
className={{
containerClassName: 'w-full mb-0!',
tableWrapperClassName: 'overflow-x-auto',
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:
@@ -799,8 +726,8 @@ const CustomerPaymentTab = () => {
if (row.index === 0) {
return (
<tr
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'
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'
@@ -830,8 +757,123 @@ const CustomerPaymentTab = () => {
);
})
)}
</Card>
</div>
</div>
{/* Filter Modal */}
<Modal
ref={filterModal.ref}
className={{
modal: 'p-0',
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
}}
>
{/* Modal Header */}
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
<div className='flex items-center gap-2 text-primary'>
<Icon icon='heroicons:funnel' width={20} height={20} />
<h3 className='font-medium text-sm'>Filter Data</h3>
</div>
<Button
variant='link'
onClick={filterModal.closeModal}
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
>
<Icon icon='heroicons:x-mark' width={20} height={20} />
</Button>
</div>
<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={(e) => {
setFilterStartDate(e.target.value);
}}
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={(e) => {
setFilterEndDate(e.target.value);
}}
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> */}
{/* TODO: Uncomment when BE is ready */}
{/* <div>
<SelectInput
label='Filter Berdasarkan'
placeholder='Pilih Filter Berdasarkan'
options={dataTypeOptions}
value={dataTypeOptions[0]}
isDisabled={true}
className={{ wrapper: 'w-full' }}
/>
</div> */}
{/* 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}
>
Apply Filter
</Button>
</div>
</Modal>
</>
);
};
@@ -686,6 +686,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
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
@@ -697,6 +698,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
className={{ wrapper: 'w-full' }}
isError={formik.touched.endDate && !!formik.errors.endDate}
errorMessage={formik.errors.endDate}
isNestedModal
/>
</div>
</div>
@@ -759,7 +761,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
</div>
{/* Action Buttons */}
<div className='flex justify-between items-center gap-4 p-4 border-t border-gray-300 bg-gray-100'>
<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'
@@ -769,7 +771,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
Reset Filter
</Button>
<Button
className='min-w-40 text-sm rounded-lg py-3 text-white'
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
type='submit'
>
Apply Filter