From a99a399f094940e3cad7745099bf5d15027a19af Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 19 May 2026 14:44:12 +0700 Subject: [PATCH 01/27] fix: show kandang label even if its not loaded yet in kandang options --- .../daily-checklist/DailyChecklistContent.tsx | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx b/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx index 80c13f01..0f86f7e9 100644 --- a/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx +++ b/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx @@ -184,6 +184,11 @@ export function DailyChecklistContent() { const [emptyKandangEndDateError, setEmptyKandangEndDateError] = useState(''); + const [preloadedKandang, setPreloadedKandang] = useState<{ + id: string; + name: string; + } | null>(null); + const [existingDocuments, setExistingDocuments] = useState([]); const [documents, setDocuments] = useState([]); const [deletedDocumentIds, setDeletedDocumentIds] = useState([]); @@ -228,7 +233,11 @@ export function DailyChecklistContent() { const rawDate = data.date || ''; setDate(rawDate.length > 10 ? rawDate.slice(0, 10) : rawDate); skipKandangClearRef.current = true; - setKandangId(String(data.kandang?.id || '')); + const loadedKandangId = String(data.kandang?.id || ''); + setKandangId(loadedKandangId); + if (data.kandang?.name) { + setPreloadedKandang({ id: loadedKandangId, name: data.kandang.name }); + } const isEmptyKandang = !!data.empty_kandang || data.category === 'empty_kandang'; @@ -1162,9 +1171,17 @@ export function DailyChecklistContent() { - {kandangOptions.map((kandang) => ( + {preloadedKandang && + !kandangOptions.some( + (k) => String(k.value) === preloadedKandang.id + ) && ( + + {preloadedKandang.name} + + )} + {kandangOptions.map((kandang, kandangIdx) => ( {kandang.label} From 146192a5b3304b8e30225dc960b155c00bd287c0 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 19 May 2026 16:06:41 +0700 Subject: [PATCH 02/27] feat: create exportToExcelGeneral method --- src/services/api/report/debt-supplier.ts | 40 ++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/services/api/report/debt-supplier.ts b/src/services/api/report/debt-supplier.ts index dad46d18..8198cc57 100644 --- a/src/services/api/report/debt-supplier.ts +++ b/src/services/api/report/debt-supplier.ts @@ -1,4 +1,6 @@ import { BaseApiService } from '@/services/api/base'; +import { httpClient } from '@/services/http/client'; +import { formatDate } from '@/lib/helper'; import { BaseApiResponse } from '@/types/api/api-general'; import { DebtSupplier } from '@/types/api/report/debt-supplier'; @@ -11,6 +13,44 @@ export class DebtSupplierApiService extends BaseApiService< super(basePath); } + async exportToExcelGeneral( + supplier_ids?: string, + filter_by?: string, + start_date?: string, + end_date?: string + ) { + const params = new URLSearchParams(); + + if (supplier_ids) params.set('supplier_ids', supplier_ids); + if (filter_by) params.set('filter_by', filter_by); + if (start_date) params.set('start_date', start_date); + if (end_date) params.set('end_date', end_date); + params.set('export', 'excel-all'); + params.set('page', '1'); + params.set('limit', '99999999999'); + + const queryString = `?${params.toString()}`; + + const res = await httpClient( + `${this.basePath.replace(/\/$/, '')}/debt-supplier${queryString}`, + { + method: 'GET', + responseType: 'blob', + } + ); + + const url = window.URL.createObjectURL(new Blob([res])); + const link = document.createElement('a'); + link.href = url; + + const fileName = `laporan-hutang-supplier-general-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`; + link.setAttribute('download', fileName); + + document.body.appendChild(link); + link.click(); + link.remove(); + } + async getDebtSupplierReport( supplier_ids?: string, filter_by?: string, From ce4f50c92ab8110bda07695a8836912b0331537f Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 19 May 2026 16:06:54 +0700 Subject: [PATCH 03/27] feat: create Export to Excel - General button --- .../report/finance/tab/DebtSupplierTab.tsx | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index 174ff1e2..be2271f2 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -77,7 +77,10 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { // ===== STATE MANAGEMENT ===== const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); - const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; + const [isExcelGeneralExportLoading, setIsExcelGeneralExportLoading] = + useState(false); + const isAnyExportLoading = + isPdfExportLoading || isExcelExportLoading || isExcelGeneralExportLoading; // ===== PAGINATION STATE ===== const [currentPage, setCurrentPage] = useState(1); @@ -308,6 +311,23 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { formik.values.endDate, ]); + const handleExportExcelGeneral = useCallback(async () => { + setIsExcelGeneralExportLoading(true); + try { + await DebtSupplierApi.exportToExcelGeneral( + filterParams.supplier_ids, + filterParams.filter_by, + filterParams.start_date, + filterParams.end_date + ); + toast.success('Excel General berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat Excel General. Silakan coba lagi.'); + } finally { + setIsExcelGeneralExportLoading(false); + } + }, [filterParams]); + // ===== TAB ACTIONS COMPONENT ===== const TabActions = useMemo(() => { return function TabActionsComponent() { @@ -370,7 +390,17 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap' > - Export to Excel + Export to Excel - Supplier Per Sheet + + + } + > + + + + ); + }, [ + tabId, + setTabActions, + isPdfExportLoading, + handleExportPDF, + tableFilterState.start_date, + tableFilterState.end_date, + tableFilterState.customerFilter, + tableFilterState.salesFilter, + ]); + + useEffect(() => { + return () => clearTabActions(tabId); + }, [tabId, clearTabActions]); + + const page = meta?.page || tableFilterState.page; + const pageSize = meta?.limit || tableFilterState.pageSize; + + const columns = useMemo( + (): ColumnDef[] => [ + { + header: 'No', + enableSorting: false, + cell: (props) => (page - 1) * pageSize + props.row.index + 1, + }, + { + header: 'Customer', + accessorKey: 'customer_name', + enableSorting: true, + id: 'customer_name', + }, + { + header: 'Saldo Awal', + accessorKey: 'saldo_awal', + id: 'saldo_awal', + enableSorting: true, + cell: ({ row }) => ( +
+ {formatCurrency(row.original.saldo_awal)} +
+ ), + }, + { + header: 'Penjualan Ayam', + columns: [ + { + header: 'Ekor', + accessorKey: 'penjualan_ayam_ekor', + id: 'penjualan_ayam_ekor', + enableSorting: true, + cell: ({ row }) => ( +
+ {formatNumber(row.original.penjualan_ayam_ekor)} +
+ ), + }, + { + header: 'Kg', + accessorKey: 'penjualan_ayam_kg', + id: 'penjualan_ayam_kg', + enableSorting: true, + cell: ({ row }) => ( +
+ {formatNumber(row.original.penjualan_ayam_kg)} +
+ ), + }, + { + header: 'Nominal', + accessorKey: 'penjualan_ayam_nominal', + id: 'penjualan_ayam_nominal', + enableSorting: true, + cell: ({ row }) => ( +
+ {formatCurrency(row.original.penjualan_ayam_nominal)} +
+ ), + }, + ], + }, + { + header: 'Penjualan Telur', + columns: [ + { + header: 'Kuantitas', + accessorKey: 'penjualan_telur_kuantitas', + id: 'penjualan_telur_kuantitas', + enableSorting: true, + cell: ({ row }) => ( +
+ {formatNumber(row.original.penjualan_telur_kuantitas)} +
+ ), + }, + { + header: 'Kg', + accessorKey: 'penjualan_telur_kg', + id: 'penjualan_telur_kg', + enableSorting: true, + cell: ({ row }) => ( +
+ {formatNumber(row.original.penjualan_telur_kg)} +
+ ), + }, + { + header: 'Nominal', + accessorKey: 'penjualan_telur_nominal', + id: 'penjualan_telur_nominal', + enableSorting: true, + cell: ({ row }) => ( +
+ {formatCurrency(row.original.penjualan_telur_nominal)} +
+ ), + }, + ], + }, + { + header: 'Penjualan Trading', + accessorKey: 'penjualan_trading', + id: 'penjualan_trading', + enableSorting: true, + cell: ({ row }) => ( +
+ {formatCurrency(row.original.penjualan_trading)} +
+ ), + }, + { + header: 'Pembayaran', + accessorKey: 'pembayaran', + id: 'pembayaran', + enableSorting: true, + cell: ({ row }) => ( +
+ {formatCurrency(row.original.pembayaran)} +
+ ), + }, + { + header: 'Aging', + accessorKey: 'aging', + id: 'aging', + enableSorting: true, + cell: ({ row }) => ( +
+ {formatNumber(row.original.aging)} hari +
+ ), + }, + { + header: 'Aging Rata-Rata', + accessorKey: 'aging_rata_rata', + id: 'aging_rata_rata', + enableSorting: true, + cell: ({ row }) => ( +
+ {formatNumber(row.original.aging_rata_rata)} hari +
+ ), + }, + { + header: 'Saldo Akhir', + accessorKey: 'saldo_akhir', + id: 'saldo_akhir', + enableSorting: true, + cell: ({ row }) => ( +
+ {formatCurrency(row.original.saldo_akhir)} +
+ ), + }, + ], + [page, pageSize] + ); + + return ( + <> +
+ {isLoading && ( +
+ +
+ )} + + {!isLoading && data.length === 0 && ( + []} + icon={ + + } + title='Data Not Yet Available' + subtitle='Please change your filters to get the data.' + /> + )} + + {!isLoading && data.length > 0 && ( + <> +
+ + + + {meta && ( +
+ setPage(Math.max(1, (meta.page || 1) - 1))} + onNextPage={() => + setPage( + meta.total_pages && (meta.page || 1) < meta.total_pages + ? (meta.page || 1) + 1 + : meta.page || 1 + ) + } + onPageChange={setPage} + rowOptions={[10, 20, 50, 100]} + onRowChange={setPageSize} + /> +
+ )} + + )} + + + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+ +
+
+
+ +
+ +
+ +
+
+ + + formik.setFieldValue( + 'customerFilter', + val as OptionType | null + ) + } + onInputChange={setCustomerInput} + isLoading={isLoadingCustomers} + isClearable + onMenuScrollToBottom={loadMoreCustomers} + className={{ wrapper: 'w-full' }} + /> + + + formik.setFieldValue( + 'salesFilter', + val as OptionType | null + ) + } + onInputChange={setSalesInput} + isLoading={isLoadingSales} + isClearable + onMenuScrollToBottom={loadMoreSales} + className={{ wrapper: 'w-full' }} + /> +
+ + {/* Modal Footer */} +
+ + +
+ +
+ + ); +}; + +export default BalanceMonitoringTab; From 8df5af0124d5eac9e7bafd9526ec45dfea46a874 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 19 May 2026 17:30:38 +0700 Subject: [PATCH 07/27] feat: create getBalanceMonitoringReport method --- src/services/api/report/finance-report.ts | 253 ++++++++++++++++++++++ 1 file changed, 253 insertions(+) diff --git a/src/services/api/report/finance-report.ts b/src/services/api/report/finance-report.ts index 95a85b85..e5ec9b64 100644 --- a/src/services/api/report/finance-report.ts +++ b/src/services/api/report/finance-report.ts @@ -1,6 +1,9 @@ import { BaseApiService } from '@/services/api/base'; import { BaseApiResponse } from '@/types/api/api-general'; import { CustomerPaymentReport } from '@/types/api/report/customer-payment'; +import { httpClient } from '@/services/http/client'; +import { formatDate } from '@/lib/helper'; +import { BalanceMonitoringRow } from '@/types/api/report/balance-monitoring'; export class FinanceApiService extends BaseApiService< CustomerPaymentReport, @@ -11,6 +14,256 @@ export class FinanceApiService extends BaseApiService< super(basePath); } + async getBalanceMonitoringReport(params: { + start_date?: string; + end_date?: string; + customer_id?: string; + sales_id?: string; + sort_by?: string; + sort_order?: string; + page?: number; + limit?: number; + }): Promise | undefined> { + // TODO: Remove dummy data when BE is ready + const dummyRows: BalanceMonitoringRow[] = [ + { + customer_id: 1, + customer_name: 'PT Maju Bersama', + saldo_awal: 12500000, + penjualan_ayam_ekor: 450, + penjualan_ayam_kg: 1125.5, + penjualan_ayam_nominal: 22510000, + penjualan_telur_kuantitas: 300, + penjualan_telur_kg: 900, + penjualan_telur_nominal: 4500000, + penjualan_trading: 3000000, + pembayaran: 30000000, + aging: 15, + aging_rata_rata: 12, + saldo_akhir: 12510000, + }, + { + customer_id: 2, + customer_name: 'CV Sumber Rezeki', + saldo_awal: 8750000, + penjualan_ayam_ekor: 320, + penjualan_ayam_kg: 800, + penjualan_ayam_nominal: 16000000, + penjualan_telur_kuantitas: 150, + penjualan_telur_kg: 450, + penjualan_telur_nominal: 2250000, + penjualan_trading: 0, + pembayaran: 20000000, + aging: 7, + aging_rata_rata: 8, + saldo_akhir: 7000000, + }, + { + customer_id: 3, + customer_name: 'UD Karya Mandiri', + saldo_awal: 5000000, + penjualan_ayam_ekor: 600, + penjualan_ayam_kg: 1500, + penjualan_ayam_nominal: 30000000, + penjualan_telur_kuantitas: 0, + penjualan_telur_kg: 0, + penjualan_telur_nominal: 0, + penjualan_trading: 1500000, + pembayaran: 25000000, + aging: 30, + aging_rata_rata: 22, + saldo_akhir: 11500000, + }, + { + customer_id: 4, + customer_name: 'PT Sejahtera Abadi', + saldo_awal: 20000000, + penjualan_ayam_ekor: 100, + penjualan_ayam_kg: 250, + penjualan_ayam_nominal: 5000000, + penjualan_telur_kuantitas: 500, + penjualan_telur_kg: 1500, + penjualan_telur_nominal: 7500000, + penjualan_trading: 2000000, + pembayaran: 40000000, + aging: 45, + aging_rata_rata: 38, + saldo_akhir: -5500000, + }, + { + customer_id: 5, + customer_name: 'CV Berkah Jaya', + saldo_awal: 3200000, + penjualan_ayam_ekor: 200, + penjualan_ayam_kg: 500, + penjualan_ayam_nominal: 10000000, + penjualan_telur_kuantitas: 200, + penjualan_telur_kg: 600, + penjualan_telur_nominal: 3000000, + penjualan_trading: 500000, + pembayaran: 15000000, + aging: 10, + aging_rata_rata: 9, + saldo_akhir: 1700000, + }, + { + customer_id: 6, + customer_name: 'PT Harapan Makmur', + saldo_awal: 17000000, + penjualan_ayam_ekor: 780, + penjualan_ayam_kg: 1950, + penjualan_ayam_nominal: 39000000, + penjualan_telur_kuantitas: 400, + penjualan_telur_kg: 1200, + penjualan_telur_nominal: 6000000, + penjualan_trading: 4500000, + pembayaran: 50000000, + aging: 20, + aging_rata_rata: 17, + saldo_akhir: 16500000, + }, + { + customer_id: 7, + customer_name: 'UD Rejeki Lancar', + saldo_awal: 2000000, + penjualan_ayam_ekor: 50, + penjualan_ayam_kg: 125, + penjualan_ayam_nominal: 2500000, + penjualan_telur_kuantitas: 80, + penjualan_telur_kg: 240, + penjualan_telur_nominal: 1200000, + penjualan_trading: 0, + pembayaran: 5000000, + aging: 5, + aging_rata_rata: 6, + saldo_akhir: 700000, + }, + { + customer_id: 8, + customer_name: 'CV Putra Unggul', + saldo_awal: 9500000, + penjualan_ayam_ekor: 410, + penjualan_ayam_kg: 1025, + penjualan_ayam_nominal: 20500000, + penjualan_telur_kuantitas: 250, + penjualan_telur_kg: 750, + penjualan_telur_nominal: 3750000, + penjualan_trading: 1000000, + pembayaran: 28000000, + aging: 18, + aging_rata_rata: 15, + saldo_akhir: 6750000, + }, + { + customer_id: 9, + customer_name: 'PT Duta Poultry', + saldo_awal: 35000000, + penjualan_ayam_ekor: 1200, + penjualan_ayam_kg: 3000, + penjualan_ayam_nominal: 60000000, + penjualan_telur_kuantitas: 800, + penjualan_telur_kg: 2400, + penjualan_telur_nominal: 12000000, + penjualan_trading: 8000000, + pembayaran: 70000000, + aging: 60, + aging_rata_rata: 50, + saldo_akhir: -15000000, + }, + { + customer_id: 10, + customer_name: 'UD Sari Ayam', + saldo_awal: 6800000, + penjualan_ayam_ekor: 350, + penjualan_ayam_kg: 875, + penjualan_ayam_nominal: 17500000, + penjualan_telur_kuantitas: 0, + penjualan_telur_kg: 0, + penjualan_telur_nominal: 0, + penjualan_trading: 2500000, + pembayaran: 22000000, + aging: 12, + aging_rata_rata: 11, + saldo_akhir: 4800000, + }, + { + customer_id: 11, + customer_name: 'CV Nusa Ternak', + saldo_awal: 14200000, + penjualan_ayam_ekor: 530, + penjualan_ayam_kg: 1325, + penjualan_ayam_nominal: 26500000, + penjualan_telur_kuantitas: 350, + penjualan_telur_kg: 1050, + penjualan_telur_nominal: 5250000, + penjualan_trading: 3500000, + pembayaran: 35000000, + aging: 25, + aging_rata_rata: 20, + saldo_akhir: 14450000, + }, + { + customer_id: 12, + customer_name: 'PT Agro Sentosa', + saldo_awal: 4100000, + penjualan_ayam_ekor: 180, + penjualan_ayam_kg: 450, + penjualan_ayam_nominal: 9000000, + penjualan_telur_kuantitas: 120, + penjualan_telur_kg: 360, + penjualan_telur_nominal: 1800000, + penjualan_trading: 750000, + pembayaran: 12000000, + aging: 8, + aging_rata_rata: 9, + saldo_akhir: 3650000, + }, + ]; + + const page = Number(params.page) || 1; + const limit = Number(params.limit) || 10; + + return { + status: 'success', + message: 'Data retrieved successfully', + data: dummyRows, + meta: { + page, + limit, + total_results: dummyRows.length, + total_pages: 1, + }, + } as BaseApiResponse; + + // return await this.customRequest>( + // 'balance-monitoring', + // { method: 'GET', params } + // ); + } + + async exportBalanceMonitoringToPDF(initialQueryString: string) { + const params = new URLSearchParams(initialQueryString); + params.set('export', 'pdf'); + params.set('page', '1'); + params.set('limit', '99999999999'); + + const res = await httpClient( + `${this.basePath}/balance-monitoring?${params.toString()}`, + { method: 'GET', responseType: 'blob' } + ); + + const url = window.URL.createObjectURL(new Blob([res])); + const link = document.createElement('a'); + link.href = url; + link.setAttribute( + 'download', + `monitoring-saldo-${formatDate(Date.now(), 'DD-MM-YYYY')}.pdf` + ); + document.body.appendChild(link); + link.click(); + link.remove(); + } + async getCustomerPaymentReport( customer_ids?: string, // TODO: Uncomment when BE is ready From f76b5b981c93cd3198094bb9619a83e7306cc1fb Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 19 May 2026 17:31:00 +0700 Subject: [PATCH 08/27] feat: create balance-monitoring type --- src/types/api/report/balance-monitoring.d.ts | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/types/api/report/balance-monitoring.d.ts diff --git a/src/types/api/report/balance-monitoring.d.ts b/src/types/api/report/balance-monitoring.d.ts new file mode 100644 index 00000000..9c7933dd --- /dev/null +++ b/src/types/api/report/balance-monitoring.d.ts @@ -0,0 +1,23 @@ +export type BalanceMonitoringRow = { + customer_id: number; + customer_name: string; + saldo_awal: number; + penjualan_ayam_ekor: number; + penjualan_ayam_kg: number; + penjualan_ayam_nominal: number; + penjualan_telur_kuantitas: number; + penjualan_telur_kg: number; + penjualan_telur_nominal: number; + penjualan_trading: number; + pembayaran: number; + aging: number; + aging_rata_rata: number; + saldo_akhir: number; +}; + +export type BalanceMonitoringMeta = { + page: number; + limit: number; + total_results: number; + total_pages: number; +}; From 94d623d79321d09d16ba21adb5d52e34fa878b8e Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 19 May 2026 17:31:18 +0700 Subject: [PATCH 09/27] feat: update gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 9e1fadc8..44fee0bc 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,6 @@ next-env.d.ts # rtk rtk.exe + +# local specs +/local-specs \ No newline at end of file From a8c02243a4c9fc4e50c50f00522d3ff80ba13c61 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 20 May 2026 11:13:50 +0700 Subject: [PATCH 10/27] feat: implement export general and server-side export --- .../report/finance/tab/CustomerPaymentTab.tsx | 58 +++++++++++---- .../report/finance/tab/DebtSupplierTab.tsx | 21 ++---- src/services/api/report/debt-supplier.ts | 38 ++++++++++ src/services/api/report/finance-report.ts | 74 +++++++++++++++++++ 4 files changed, 161 insertions(+), 30 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 55cf08f3..e9c20053 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -31,7 +31,6 @@ import { CustomerPaymentFilterSchema, CustomerPaymentFilterType, } from '@/components/pages/report/finance/filter/CustomerPaymentFilter'; -import { generateCustomerPaymentExcel } from '@/components/pages/report/finance/export/CustomerPaymentExportXLSX'; import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/export/CustomerPaymentExportPDF'; import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton'; @@ -55,7 +54,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // ===== STATE MANAGEMENT ===== const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); - const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; + const [isExcelGeneralExportLoading, setIsExcelGeneralExportLoading] = + useState(false); + const isAnyExportLoading = + isPdfExportLoading || isExcelExportLoading || isExcelGeneralExportLoading; // ===== PAGINATION STATE ===== const [currentPage, setCurrentPage] = useState(1); @@ -294,28 +296,39 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { }, [filterParams]); // ===== EXPORT HANDLERS ===== + const handleExportExcelGeneral = useCallback(async () => { + setIsExcelGeneralExportLoading(true); + try { + await FinanceApi.exportCustomerPaymentToExcelGeneral( + filterParams.customer_ids, + filterParams.filter_by, + filterParams.start_date, + filterParams.end_date + ); + toast.success('Excel General berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat Excel General. Silakan coba lagi.'); + } finally { + setIsExcelGeneralExportLoading(false); + } + }, [filterParams]); + 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 }); + await FinanceApi.exportCustomerPaymentToExcelCustomerPerSheet( + filterParams.customer_ids, + filterParams.filter_by, + filterParams.start_date, + filterParams.end_date + ); toast.success('Excel berhasil dibuat dan diunduh.'); } catch { toast.error('Gagal membuat Excel. Silakan coba lagi.'); } finally { setIsExcelExportLoading(false); } - }, [customerPaymentExport]); + }, [filterParams]); const handleExportPdf = useCallback(async () => { setIsPdfExportLoading(true); @@ -422,8 +435,19 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap' > - Export to Excel + Export to Excel - Customer Per Sheet + + - } - > - - ); - }, [ - tabId, - setTabActions, - isPdfExportLoading, - handleExportPDF, - tableFilterState.start_date, - tableFilterState.end_date, - tableFilterState.customerFilter, - tableFilterState.salesFilter, - ]); + }, [tabId, setTabActions, tableFilterState, filterModal.openModal]); useEffect(() => { return () => clearTabActions(tabId); }, [tabId, clearTabActions]); - const page = meta?.page || tableFilterState.page; - const pageSize = meta?.limit || tableFilterState.pageSize; - const columns = useMemo( (): ColumnDef[] => [ { header: 'No', enableSorting: false, - cell: (props) => (page - 1) * pageSize + props.row.index + 1, + cell: (props) => + (tableFilterState.page - 1) * tableFilterState.pageSize + + props.row.index + + 1, }, { header: 'Customer', - accessorKey: 'customer_name', + accessorKey: 'customer.name', enableSorting: true, id: 'customer_name', + cell: ({ row }) => row.original.customer.name, }, { header: 'Saldo Awal', @@ -342,34 +289,34 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { columns: [ { header: 'Ekor', - accessorKey: 'penjualan_ayam_ekor', + accessorKey: 'penjualan_ayam.ekor', id: 'penjualan_ayam_ekor', enableSorting: true, cell: ({ row }) => (
- {formatNumber(row.original.penjualan_ayam_ekor)} + {formatNumber(row.original.penjualan_ayam.ekor)}
), }, { header: 'Kg', - accessorKey: 'penjualan_ayam_kg', + accessorKey: 'penjualan_ayam.kg', id: 'penjualan_ayam_kg', enableSorting: true, cell: ({ row }) => (
- {formatNumber(row.original.penjualan_ayam_kg)} + {formatNumber(row.original.penjualan_ayam.kg)}
), }, { header: 'Nominal', - accessorKey: 'penjualan_ayam_nominal', + accessorKey: 'penjualan_ayam.nominal', id: 'penjualan_ayam_nominal', enableSorting: true, cell: ({ row }) => (
- {formatCurrency(row.original.penjualan_ayam_nominal)} + {formatCurrency(row.original.penjualan_ayam.nominal)}
), }, @@ -379,35 +326,35 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { header: 'Penjualan Telur', columns: [ { - header: 'Kuantitas', - accessorKey: 'penjualan_telur_kuantitas', - id: 'penjualan_telur_kuantitas', + header: 'Butir', + accessorKey: 'penjualan_telur.butir', + id: 'penjualan_telur_butir', enableSorting: true, cell: ({ row }) => (
- {formatNumber(row.original.penjualan_telur_kuantitas)} + {formatNumber(row.original.penjualan_telur.butir)}
), }, { header: 'Kg', - accessorKey: 'penjualan_telur_kg', + accessorKey: 'penjualan_telur.kg', id: 'penjualan_telur_kg', enableSorting: true, cell: ({ row }) => (
- {formatNumber(row.original.penjualan_telur_kg)} + {formatNumber(row.original.penjualan_telur.kg)}
), }, { header: 'Nominal', - accessorKey: 'penjualan_telur_nominal', + accessorKey: 'penjualan_telur.nominal', id: 'penjualan_telur_nominal', enableSorting: true, cell: ({ row }) => (
- {formatCurrency(row.original.penjualan_telur_nominal)} + {formatCurrency(row.original.penjualan_telur.nominal)}
), }, @@ -415,12 +362,12 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { }, { header: 'Penjualan Trading', - accessorKey: 'penjualan_trading', + accessorKey: 'penjualan_trading.nominal', id: 'penjualan_trading', enableSorting: true, cell: ({ row }) => (
- {formatCurrency(row.original.penjualan_trading)} + {formatCurrency(row.original.penjualan_trading.nominal)}
), }, @@ -471,7 +418,7 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { ), }, ], - [page, pageSize] + [tableFilterState.page, tableFilterState.pageSize] ); return ( @@ -483,7 +430,7 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { )} - {!isLoading && data.length === 0 && ( + {!isLoading && balanceMonitorings.length === 0 && ( []} icon={ @@ -499,20 +446,20 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { /> )} - {!isLoading && data.length > 0 && ( + {!isLoading && balanceMonitorings.length > 0 && ( <>
{ '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', - paginationClassName: 'hidden', }} /> - - {meta && ( -
- setPage(Math.max(1, (meta.page || 1) - 1))} - onNextPage={() => - setPage( - meta.total_pages && (meta.page || 1) < meta.total_pages - ? (meta.page || 1) + 1 - : meta.page || 1 - ) - } - onPageChange={setPage} - rowOptions={[10, 20, 50, 100]} - onRowChange={setPageSize} - /> -
- )} )} @@ -576,7 +501,7 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { - +
- - formik.setFieldValue( - 'customerFilter', - val as OptionType | null - ) + formik.setFieldValue('customers', Array.isArray(val) ? val : []) } onInputChange={setCustomerInput} isLoading={isLoadingCustomers} @@ -620,15 +542,15 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { className={{ wrapper: 'w-full' }} /> - formik.setFieldValue( - 'salesFilter', - val as OptionType | null + 'salesPersons', + Array.isArray(val) ? val : [] ) } onInputChange={setSalesInput} @@ -637,6 +559,21 @@ const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => { onMenuScrollToBottom={loadMoreSales} className={{ wrapper: 'w-full' }} /> + + + formik.setFieldValue( + 'filterBy', + !Array.isArray(val) ? (val ?? undefined) : undefined + ) + } + isClearable + className={{ wrapper: 'w-full' }} + /> {/* Modal Footer */} From b3b60018bb9e3acd4a2483dca708371bf2e00d6b Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 20 May 2026 16:10:10 +0700 Subject: [PATCH 13/27] refactor: optimize CustomerPaymentTab with useTableFilter persistence pattern Replace filterParams/currentPage/pageSize state with useTableFilter (persist:true), switch SWR to httpClientFetcher with explicit type, store OptionType[] directly for customers/filterBy, add formikResetHandler using resetFilter(), remove enableReinitialize and handleFilterModalOpenRef, pass filterModal.openModal directly. Co-Authored-By: Claude Sonnet 4.6 --- .../report/finance/tab/CustomerPaymentTab.tsx | 644 ++++++++---------- 1 file changed, 280 insertions(+), 364 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index e9c20053..e786c9a5 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -1,14 +1,17 @@ -import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; +import { AxiosError } from 'axios'; import Card from '@/components/Card'; import StatusBadge from '@/components/helper/StatusBadge'; -import { useSelect } from '@/components/input/SelectInput'; +import { useSelect, OptionType } 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 { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; +import { BaseApiResponse } from '@/types/api/api-general'; import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; import { @@ -27,28 +30,22 @@ import Dropdown from '@/components/Dropdown'; import Modal, { useModal } from '@/components/Modal'; import toast from 'react-hot-toast'; import { useFormik } from 'formik'; -import { - CustomerPaymentFilterSchema, - CustomerPaymentFilterType, -} from '@/components/pages/report/finance/filter/CustomerPaymentFilter'; import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/export/CustomerPaymentExportPDF'; import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton'; -import { OptionType } from '@/components/table/TableRowSizeSelector'; import { Color } from '@/types/theme'; import ButtonFilter from '@/components/helper/ButtonFilter'; import Pagination from '@/components/Pagination'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; interface CustomerPaymentTabProps { tabId: string; } -interface FilterParams { - customer_ids?: string; - start_date?: string; - end_date?: string; - filter_by?: string; -} +const dataTypeOptions: OptionType[] = [ + { value: 'trans_date', label: 'Tanggal Jual/Bayar' }, + { value: 'realization_date', label: 'Tanggal Realisasi' }, +]; const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // ===== STATE MANAGEMENT ===== @@ -59,26 +56,44 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading || isExcelGeneralExportLoading; - // ===== PAGINATION STATE ===== - const [currentPage, setCurrentPage] = useState(1); - const [pageSize, setPageSize] = useState(10); - - // ===== SUBMISSION STATE ===== - const [filterParams, setFilterParams] = useState({}); const [dateErrorShown, setDateErrorShown] = useState(false); const [hasDateError, setHasDateError] = useState(false); - const handleFilterModalOpenRef = useRef(() => {}); - const filterModal = useModal(); - const dataTypeOptions = useMemo( - () => [ - { value: 'trans_date', label: 'Tanggal Jual/Bayar' }, - { value: 'realization_date', label: 'Tanggal Realisasi' }, - ], - [] - ); + const setTabActions = useTabActionsStore((state) => state.setTabActions); + const clearTabActions = useTabActionsStore((state) => state.clearTabActions); + + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + reset: resetFilter, + } = useTableFilter<{ + start_date: string; + end_date: string; + customers: OptionType[]; + filterBy?: OptionType; + }>({ + initial: { + start_date: '', + end_date: '', + customers: [], + filterBy: undefined, + }, + paramMap: { + page: 'page', + pageSize: 'limit', + start_date: 'start_date', + end_date: 'end_date', + customers: 'customer_ids', + filterBy: 'filter_by', + }, + persist: true, + storeName: 'customer-payment-report-table', + }); const { options: customerOptions, @@ -88,222 +103,159 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { } = useSelect(CustomerApi.basePath, 'id', 'name', 'search'); // ===== FORMIK SETUP ===== - const formik = useFormik({ + const formik = useFormik({ initialValues: { - start_date: null, - end_date: null, - customer_ids: null, - filter_by: null, + start_date: tableFilterState.start_date, + end_date: tableFilterState.end_date, + customers: tableFilterState.customers, + filterBy: tableFilterState.filterBy, }, - validationSchema: CustomerPaymentFilterSchema, - onSubmit: (values, { setSubmitting }) => { - setFilterParams({ - start_date: values.start_date || undefined, - end_date: values.end_date || undefined, - customer_ids: values.customer_ids || undefined, - filter_by: values.filter_by || undefined, - }); - filterModal.closeModal(); - setCurrentPage(1); - setSubmitting(false); - }, - onReset: () => { - setFilterParams({}); - setCurrentPage(1); - setHasDateError(false); - if (dateErrorShown) { - toast.dismiss(); - setDateErrorShown(false); - } + onSubmit: (values) => { + updateFilter('start_date', values.start_date, true); + updateFilter('end_date', values.end_date, true); + updateFilter('customers', values.customers, true); + updateFilter('filterBy', values.filterBy, true); filterModal.closeModal(); }, }); - handleFilterModalOpenRef.current = () => { - formik.setValues({ - start_date: filterParams.start_date || null, - end_date: filterParams.end_date || null, - customer_ids: filterParams.customer_ids || null, - filter_by: filterParams.filter_by || null, + const formikResetHandler = () => { + resetFilter(); + + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + + formik.resetForm({ + values: { + start_date: '', + end_date: '', + customers: [], + filterBy: undefined, + }, }); - filterModal.openModal(); + + filterModal.closeModal(); }; const getPaymentStatusBadgeColor = (notes: string): Color => { const normalizedValue = notes.toLowerCase(); - - if (normalizedValue === 'lunas') { - return 'primary'; - } - - if (normalizedValue.includes('belum')) { - return 'warning'; - } - + if (normalizedValue === 'lunas') return 'primary'; + if (normalizedValue.includes('belum')) return 'warning'; return 'neutral'; }; // ===== DATE CHANGE HANDLERS ===== - const handleStartDateChange = useCallback( - (e: React.ChangeEvent) => { - const value = e.target.value; - formik.setFieldValue('start_date', value || null); + const handleStartDateChange = (e: React.ChangeEvent) => { + const value = e.target.value; + formik.setFieldValue('start_date', value); - if (value && formik.values.end_date) { - const startDate = new Date(value); - const endDateObj = new Date(formik.values.end_date); - - 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); - } + if (value && formik.values.end_date) { + if (new Date(formik.values.end_date) < new Date(value)) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); } } else { setHasDateError(false); - } - }, - [formik, dateErrorShown] - ); - - const handleEndDateChange = useCallback( - (e: React.ChangeEvent) => { - const value = e.target.value; - formik.setFieldValue('end_date', value || null); - - if (value && formik.values.start_date) { - const startDateObj = new Date(formik.values.start_date); - 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; + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); } } - + } else { setHasDateError(false); - if (dateErrorShown) { - toast.dismiss(); - setDateErrorShown(false); + } + }; + + const handleEndDateChange = (e: React.ChangeEvent) => { + const value = e.target.value; + formik.setFieldValue('end_date', value); + + if (value && formik.values.start_date) { + if (new Date(value) < new Date(formik.values.start_date)) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + return; } - }, - [formik, dateErrorShown] - ); + } - // ===== FILTER HELPERS ===== - const customerIdsValue = useMemo(() => { - if (!formik.values.customer_ids) return []; - return customerOptions.filter((opt) => - formik.values.customer_ids?.split(',').includes(String(opt.value)) - ); - }, [formik.values.customer_ids, customerOptions]); - - const filterByValue = useMemo(() => { - if (!formik.values.filter_by) return null; - return ( - dataTypeOptions.find((opt) => opt.value === formik.values.filter_by) || - null - ); - }, [formik.values.filter_by, dataTypeOptions]); + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }; // ===== DATA FETCHING ===== - const { data: customerPayment, isLoading } = useSWR( - () => { - const params = { - customer_ids: filterParams.customer_ids, - filter_by: filterParams.filter_by as - | 'trans_date' - | 'realization_date' - | undefined, - start_date: filterParams.start_date, - end_date: filterParams.end_date, - page: currentPage, - limit: pageSize, - }; - - return ['customer-payment-report', params]; - }, - ([, params]) => - FinanceApi.getCustomerPaymentReport( - params.customer_ids, - params.filter_by, - params.start_date, - params.end_date, - params.page, - params.limit - ) + const { data: customerPayment, isLoading } = useSWR< + BaseApiResponse, + AxiosError, + SWRHttpKey + >( + `${FinanceApi.basePath}/customer-payment${getTableFilterQueryString()}`, + httpClientFetcher ); - const data: CustomerPaymentReport[] = useMemo( - () => - isResponseSuccess(customerPayment) - ? (customerPayment?.data as unknown as CustomerPaymentReport[]) || [] - : [], - [customerPayment] - ); + const data: CustomerPaymentReport[] = isResponseSuccess(customerPayment) + ? (customerPayment?.data as unknown as CustomerPaymentReport[]) || [] + : []; - const meta = useMemo( - () => - isResponseSuccess(customerPayment) && customerPayment.meta - ? customerPayment.meta - : null, - [customerPayment] - ); + const meta = + isResponseSuccess(customerPayment) && customerPayment.meta + ? customerPayment.meta + : null; // ===== EXPORT DATA FETCHER ===== const customerPaymentExport = useCallback(async (): Promise< CustomerPaymentReport[] | null > => { - const params = { - customer_ids: filterParams.customer_ids, - filter_by: filterParams.filter_by as - | 'trans_date' - | 'realization_date' - | undefined, - start_date: filterParams.start_date, - end_date: filterParams.end_date, - limit: 100, - page: 1, - }; + const customer_ids = + tableFilterState.customers.length > 0 + ? tableFilterState.customers.map((o) => String(o.value)).join(',') + : undefined; + const filter_by = tableFilterState.filterBy?.value as + | 'trans_date' + | 'realization_date' + | undefined; const response = await FinanceApi.getCustomerPaymentReport( - params.customer_ids, - params.filter_by, - params.start_date, - params.end_date, - params.page, - params.limit + customer_ids, + filter_by, + tableFilterState.start_date || undefined, + tableFilterState.end_date || undefined, + 1, + 100 ); return isResponseSuccess(response) ? (response.data as unknown as CustomerPaymentReport[]) : null; - }, [filterParams]); + }, [tableFilterState]); // ===== EXPORT HANDLERS ===== const handleExportExcelGeneral = useCallback(async () => { setIsExcelGeneralExportLoading(true); try { + const customer_ids = + tableFilterState.customers.length > 0 + ? tableFilterState.customers.map((o) => String(o.value)).join(',') + : undefined; await FinanceApi.exportCustomerPaymentToExcelGeneral( - filterParams.customer_ids, - filterParams.filter_by, - filterParams.start_date, - filterParams.end_date + customer_ids, + tableFilterState.filterBy?.value, + tableFilterState.start_date || undefined, + tableFilterState.end_date || undefined ); toast.success('Excel General berhasil dibuat dan diunduh.'); } catch { @@ -311,16 +263,20 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { } finally { setIsExcelGeneralExportLoading(false); } - }, [filterParams]); + }, [tableFilterState]); const handleExportExcel = useCallback(async () => { setIsExcelExportLoading(true); try { + const customer_ids = + tableFilterState.customers.length > 0 + ? tableFilterState.customers.map((o) => String(o.value)).join(',') + : undefined; await FinanceApi.exportCustomerPaymentToExcelCustomerPerSheet( - filterParams.customer_ids, - filterParams.filter_by, - filterParams.start_date, - filterParams.end_date + customer_ids, + tableFilterState.filterBy?.value, + tableFilterState.start_date || undefined, + tableFilterState.end_date || undefined ); toast.success('Excel berhasil dibuat dan diunduh.'); } catch { @@ -328,7 +284,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { } finally { setIsExcelExportLoading(false); } - }, [filterParams]); + }, [tableFilterState]); const handleExportPdf = useCallback(async () => { setIsPdfExportLoading(true); @@ -344,22 +300,18 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { return; } - const customerName = filterParams.customer_ids - ? customerOptions - .filter((opt) => - filterParams.customer_ids?.split(',').includes(String(opt.value)) - ) - .map((opt) => opt.label) - .join(', ') || 'Semua Customer' - : 'Semua Customer'; + const customerName = + tableFilterState.customers.length > 0 + ? tableFilterState.customers.map((o) => o.label).join(', ') + : 'Semua Customer'; await generateCustomerPaymentPDF({ data: allDataForExport, params: { customer_name: customerName, - start_date: filterParams.start_date, - end_date: filterParams.end_date, - filter_by: filterParams.filter_by as + start_date: tableFilterState.start_date || undefined, + end_date: tableFilterState.end_date || undefined, + filter_by: tableFilterState.filterBy?.value as | 'trans_date' | 'realization_date' | undefined, @@ -371,119 +323,103 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { } finally { setIsPdfExportLoading(false); } - }, [customerPaymentExport, filterParams, customerOptions]); + }, [customerPaymentExport, tableFilterState]); - // ===== TAB ACTIONS COMPONENT ===== - const TabActions = useMemo(() => { - return function TabActionsComponent() { - const setTabActions = useTabActionsStore((state) => state.setTabActions); - const clearTabActions = useTabActionsStore( - (state) => state.clearTabActions - ); + // ===== TAB ACTIONS ===== + useEffect(() => { + setTabActions( + tabId, +
+ - useEffect(() => { - setTabActions( - tabId, -
- handleFilterModalOpenRef.current()} + - - -
- - - Export - -
- - -
- - } + color='none' + isLoading={isAnyExportLoading} + className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft' > - - - - - -
- ); - }, [setTabActions]); - - useEffect(() => { - return () => { - clearTabActions(tabId); - }; - }, [clearTabActions]); - - return null; - }; +
+ + Export +
+ +
+ + } + > + + + + +
+ ); }, [ tabId, + setTabActions, + tableFilterState, + filterModal.openModal, isAnyExportLoading, - handleExportExcelGeneral, handleExportExcel, + handleExportExcelGeneral, handleExportPdf, - isExcelGeneralExportLoading, isExcelExportLoading, + isExcelGeneralExportLoading, isPdfExportLoading, - filterParams, ]); - const TabActionsElement = useMemo(() => , [TabActions]); + useEffect(() => { + return () => clearTabActions(tabId); + }, [tabId, clearTabActions]); const getTableColumns = ( summary: CustomerPaymentSummary @@ -690,11 +626,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { enableSorting: false, cell: (props) => { const value = props.row.original.status; - - if (!value) { - return '-'; - } - + if (!value) return '-'; return ( { return ( <> - {TabActionsElement}
{isLoading && (
@@ -762,16 +693,16 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { - setCurrentPage((curr) => (curr > 1 ? curr - 1 : curr)) - } + currentPage={tableFilterState.page} + onPrevPage={() => setPage(Math.max(1, tableFilterState.page - 1))} onNextPage={() => - setCurrentPage((curr) => - meta.total_pages && curr < meta.total_pages ? curr + 1 : curr + setPage( + meta.total_pages && tableFilterState.page < meta.total_pages + ? tableFilterState.page + 1 + : tableFilterState.page ) } - onPageChange={(pageNumber) => setCurrentPage(pageNumber)} + onPageChange={setPage} rowOptions={[10, 20, 50, 100]} onRowChange={setPageSize} /> @@ -878,16 +809,16 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { - setCurrentPage((curr) => (curr > 1 ? curr - 1 : curr)) - } + currentPage={tableFilterState.page} + onPrevPage={() => setPage(Math.max(1, tableFilterState.page - 1))} onNextPage={() => - setCurrentPage((curr) => - meta.total_pages && curr < meta.total_pages ? curr + 1 : curr + setPage( + meta.total_pages && tableFilterState.page < meta.total_pages + ? tableFilterState.page + 1 + : tableFilterState.page ) } - onPageChange={(pageNumber) => setCurrentPage(pageNumber)} + onPageChange={setPage} rowOptions={[10, 20, 50, 100]} onRowChange={setPageSize} /> @@ -917,7 +848,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
- +
@@ -958,15 +878,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { label='Customer' placeholder='Pilih Customer' options={customerOptions} - value={customerIdsValue} - onChange={(val) => { - formik.setFieldValue( - 'customer_ids', - Array.isArray(val) && val.length > 0 - ? val.map((v: OptionType) => String(v.value)).join(',') - : null - ); - }} + value={formik.values.customers} + onChange={(val) => + formik.setFieldValue('customers', Array.isArray(val) ? val : []) + } onInputChange={setCustomerInputValue} isLoading={isLoadingCustomers} isClearable @@ -978,14 +893,15 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { label='Filter Berdasarkan' placeholder='Pilih Filter Berdasarkan' options={dataTypeOptions} - value={filterByValue} - onChange={(val) => { - if (!Array.isArray(val)) { - formik.setFieldValue('filter_by', val?.value || null); - } - }} + value={formik.values.filterBy ?? null} + onChange={(val) => + formik.setFieldValue( + 'filterBy', + !Array.isArray(val) ? (val ?? undefined) : undefined + ) + } className={{ wrapper: 'w-full' }} - isClearable={true} + isClearable />
@@ -1001,7 +917,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { From d60877d391cc1a43a6c843529460190f74ae8f29 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 20 May 2026 16:10:19 +0700 Subject: [PATCH 14/27] refactor: optimize DebtSupplierTab with useTableFilter persistence pattern Replace filterParams/currentPage/pageSize state with useTableFilter (persist:true), switch SWR to httpClientFetcher with explicit type, store OptionType[] directly for suppliers/filterBy, add formikResetHandler using resetFilter(), remove TabActions component anti-pattern and handleFilterModalOpenRef, pass filterModal.openModal directly. Co-Authored-By: Claude Sonnet 4.6 --- .../report/finance/tab/DebtSupplierTab.tsx | 776 ++++++++---------- 1 file changed, 338 insertions(+), 438 deletions(-) diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index 95bed1f2..78d27f2a 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -9,23 +9,15 @@ import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table'; import { isResponseSuccess } from '@/lib/api-helper'; import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { SupplierApi } from '@/services/api/master-data'; -import { - DebtRow, - DebtSupplier, - DebtSupplierFilter, -} from '@/types/api/report/debt-supplier'; +import { DebtRow, DebtSupplier } from '@/types/api/report/debt-supplier'; import { generateDebtSupplierPDF } from '@/components/pages/report/finance/export/DebtSupllierExportPDF'; import { Icon } from '@iconify/react'; import { ColumnDef } from '@tanstack/react-table'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import toast from 'react-hot-toast'; import useSWR from 'swr'; import { DebtSupplierApi } from '@/services/api/report/debt-supplier'; import { useFormik } from 'formik'; -import { - DebtSupplierFilterSchema, - DebtSupplierFilterType, -} from '@/components/pages/report/finance/filter/DebtSupplierFilter'; import ButtonFilter from '@/components/helper/ButtonFilter'; import { Color } from '@/types/theme'; import { Supplier } from '@/types/api/master-data/supplier'; @@ -34,6 +26,10 @@ import SelectInputRadio from '@/components/input/SelectInputRadio'; import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import StatusBadge from '@/components/helper/StatusBadge'; import DebtSupplierSkeleton from '@/components/pages/report/finance/skeleton/DebtSupplierSkeleton'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { AxiosError } from 'axios'; const dueStatus: Record = { 'Sudah Jatuh Tempo': 'error', @@ -51,7 +47,6 @@ const getPillBadge = ( statusText: string, type: 'due' | 'payment' = 'payment' ) => { - // Get color based on type const color = type === 'due' ? dueStatus[statusText] || 'neutral' @@ -68,6 +63,11 @@ const getPillBadge = ( ); }; +const dataTypeOptions: OptionType[] = [ + { value: 'received_date', label: 'Tanggal Terima' }, + { value: 'po_date', label: 'Tanggal PO' }, +]; + interface DebtSupplierTabProps { tabId: string; } @@ -81,26 +81,45 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading || isExcelGeneralExportLoading; - // ===== PAGINATION STATE ===== - const [currentPage, setCurrentPage] = useState(1); - const [pageSize, setPageSize] = useState(10); - - // ===== SUBMISSION STATE ===== - const [filterParams, setFilterParams] = useState({ - start_date: undefined, - end_date: undefined, - supplier_ids: undefined, - filter_by: undefined, - }); - - // ===== DATE ERROR STATE ===== const [dateErrorShown, setDateErrorShown] = useState(false); const [hasDateError, setHasDateError] = useState(false); - const handleFilterModalOpenRef = useRef(() => {}); - const filterModal = useModal(); + const setTabActions = useTabActionsStore((state) => state.setTabActions); + const clearTabActions = useTabActionsStore((state) => state.clearTabActions); + + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + reset: resetFilter, + } = useTableFilter<{ + start_date: string; + end_date: string; + suppliers: OptionType[]; + filterBy?: OptionType; + }>({ + initial: { + start_date: '', + end_date: '', + suppliers: [], + filterBy: undefined, + }, + paramMap: { + page: 'page', + pageSize: 'limit', + start_date: 'start_date', + end_date: 'end_date', + suppliers: 'supplier_ids', + filterBy: 'filter_by', + }, + persist: true, + storeName: 'debt-supplier-report-table', + }); + const { setInputValue: setSupplierInputValue, options: supplierOptions, @@ -108,154 +127,149 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { loadMore: loadMoreSuppliers, } = useSelect(SupplierApi.basePath, 'id', 'name'); - const dataTypeOptions = useMemo( - () => [ - { value: 'received_date', label: 'Tanggal Terima' }, - { value: 'po_date', label: 'Tanggal PO' }, - ], - [] - ); - // ===== FORMIK SETUP ===== - const formik = useFormik({ + const formik = useFormik({ initialValues: { - startDate: null, - endDate: null, - supplierIds: null, - filterBy: null, + start_date: tableFilterState.start_date, + end_date: tableFilterState.end_date, + suppliers: tableFilterState.suppliers, + filterBy: tableFilterState.filterBy, }, - validationSchema: DebtSupplierFilterSchema, onSubmit: (values) => { - setFilterParams({ - start_date: values.startDate?.toString() || undefined, - end_date: values.endDate?.toString() || undefined, - supplier_ids: - values.supplierIds?.map((v) => String(v.value)).join(',') || - undefined, - filter_by: values.filterBy?.value?.toString() || undefined, - }); - filterModal.closeModal(); - setCurrentPage(1); - }, - onReset: () => { - setFilterParams({ - start_date: undefined, - end_date: undefined, - supplier_ids: undefined, - filter_by: undefined, - }); - setCurrentPage(1); + updateFilter('start_date', values.start_date, true); + updateFilter('end_date', values.end_date, true); + updateFilter('suppliers', values.suppliers, true); + updateFilter('filterBy', values.filterBy, true); filterModal.closeModal(); }, }); - handleFilterModalOpenRef.current = () => { - const restoredFilterBy = - dataTypeOptions.find((opt) => opt.value === filterParams.filter_by) || - null; + const formikResetHandler = () => { + resetFilter(); - const supplierIdList = filterParams.supplier_ids - ? filterParams.supplier_ids.split(',') - : []; - const restoredSupplierIds = supplierOptions.filter((opt) => - supplierIdList.includes(String(opt.value)) - ); + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } - formik.setValues({ - startDate: filterParams.start_date || null, - endDate: filterParams.end_date || null, - supplierIds: restoredSupplierIds.length > 0 ? restoredSupplierIds : null, - filterBy: restoredFilterBy, + formik.resetForm({ + values: { + start_date: '', + end_date: '', + suppliers: [], + filterBy: undefined, + }, }); - filterModal.openModal(); + + filterModal.closeModal(); + }; + + // ===== DATE CHANGE HANDLERS ===== + const handleStartDateChange = (e: React.ChangeEvent) => { + const value = e.target.value; + formik.setFieldValue('start_date', value); + + if (value && formik.values.end_date) { + if (new Date(formik.values.end_date) < new Date(value)) { + 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); + } + }; + + const handleEndDateChange = (e: React.ChangeEvent) => { + const value = e.target.value; + formik.setFieldValue('end_date', value); + + if (value && formik.values.start_date) { + if (new Date(value) < new Date(formik.values.start_date)) { + 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); + } }; // ===== DATA FETCHING ===== - const { data: debtSupplier, isLoading } = useSWR( - () => { - const params = { - supplier_ids: filterParams.supplier_ids, - filter_by: filterParams.filter_by, - start_date: filterParams.start_date, - end_date: filterParams.end_date, - page: currentPage, - limit: pageSize, - }; - - return ['debt-supplier-report', params]; - }, - ([, params]) => - DebtSupplierApi.getDebtSupplierReport( - params.supplier_ids, - params.filter_by, - params.start_date, - params.end_date, - params.page, - params.limit - ) + const { data: debtSupplierResponse, isLoading } = useSWR< + BaseApiResponse, + AxiosError, + SWRHttpKey + >( + `${DebtSupplierApi.basePath}/debt-supplier${getTableFilterQueryString()}`, + httpClientFetcher ); - const data: DebtSupplier[] = useMemo( - () => - isResponseSuccess(debtSupplier) - ? (debtSupplier?.data as unknown as DebtSupplier[]) || [] - : [], - [debtSupplier] - ); + const data: DebtSupplier[] = isResponseSuccess(debtSupplierResponse) + ? ((debtSupplierResponse?.data as unknown as DebtSupplier[]) ?? []) + : []; - const meta = useMemo( - () => - isResponseSuccess(debtSupplier) && debtSupplier.meta - ? debtSupplier.meta - : null, - [debtSupplier] - ); + const meta = + isResponseSuccess(debtSupplierResponse) && debtSupplierResponse.meta + ? debtSupplierResponse.meta + : null; // ===== EXPORT DATA FETCHER ===== const debtSupplierExport = useCallback(async (): Promise< DebtSupplier[] | null > => { - const params = { - supplier_ids: - formik.values.supplierIds && formik.values.supplierIds.length > 0 - ? formik.values.supplierIds.map((v) => String(v.value)).join(',') - : undefined, - filter_by: formik.values.filterBy?.value?.toString() || undefined, - start_date: formik.values.startDate || undefined, - end_date: formik.values.endDate || undefined, - date_type: formik.values.filterBy - ? formik.values.filterBy.value - : undefined, - limit: 100, - page: 1, - }; + const supplier_ids = + tableFilterState.suppliers.length > 0 + ? tableFilterState.suppliers.map((o) => String(o.value)).join(',') + : undefined; const response = await DebtSupplierApi.getDebtSupplierReport( - params.supplier_ids, - params.filter_by, - params.start_date, - params.end_date + supplier_ids, + tableFilterState.filterBy?.value, + tableFilterState.start_date || undefined, + tableFilterState.end_date || undefined, + 1, + 100 ); return isResponseSuccess(response) ? (response.data as unknown as DebtSupplier[]) : null; - }, [ - formik.values.supplierIds, - formik.values.startDate, - formik.values.endDate, - formik.values.filterBy, - ]); + }, [tableFilterState]); // ===== EXPORT HANDLERS ===== const handleExportExcel = useCallback(async () => { setIsExcelExportLoading(true); try { + const supplier_ids = + tableFilterState.suppliers.length > 0 + ? tableFilterState.suppliers.map((o) => String(o.value)).join(',') + : undefined; await DebtSupplierApi.exportToExcelSupplierPerSheet( - filterParams.supplier_ids, - filterParams.filter_by, - filterParams.start_date, - filterParams.end_date + supplier_ids, + tableFilterState.filterBy?.value, + tableFilterState.start_date || undefined, + tableFilterState.end_date || undefined ); toast.success('Excel berhasil dibuat dan diunduh.'); } catch { @@ -263,7 +277,30 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { } finally { setIsExcelExportLoading(false); } - }, [filterParams]); + }, [tableFilterState]); + + const handleExportExcelGeneral = useCallback(async () => { + setIsExcelGeneralExportLoading(true); + try { + const supplier_ids = + tableFilterState.suppliers.length > 0 + ? tableFilterState.suppliers.map((o) => String(o.value)).join(',') + : undefined; + + await DebtSupplierApi.exportToExcelGeneral( + supplier_ids, + tableFilterState.filterBy?.value, + tableFilterState.start_date || undefined, + tableFilterState.end_date || undefined + ); + + toast.success('Excel General berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat Excel General. Silakan coba lagi.'); + } finally { + setIsExcelGeneralExportLoading(false); + } + }, [tableFilterState]); const handleExportPdf = useCallback(async () => { setIsPdfExportLoading(true); @@ -279,15 +316,18 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { return; } + const supplierName = + tableFilterState.suppliers.length > 0 + ? tableFilterState.suppliers.map((o) => o.label).join(', ') + : undefined; + await generateDebtSupplierPDF({ data: allDataForExport, params: { - supplier_name: formik.values.supplierIds - ?.map((v) => v.label) - .join(', '), - filter_by: formik.values.filterBy?.label, - start_date: formik.values.startDate || undefined, - end_date: formik.values.endDate || undefined, + supplier_name: supplierName, + filter_by: tableFilterState.filterBy?.label, + start_date: tableFilterState.start_date || undefined, + end_date: tableFilterState.end_date || undefined, }, }); toast.success('PDF berhasil dibuat dan diunduh.'); @@ -296,131 +336,91 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { } finally { setIsPdfExportLoading(false); } - }, [ - debtSupplierExport, - formik.values.supplierIds, - formik.values.filterBy, - formik.values.startDate, - formik.values.endDate, - ]); + }, [debtSupplierExport, tableFilterState]); - const handleExportExcelGeneral = useCallback(async () => { - setIsExcelGeneralExportLoading(true); - try { - await DebtSupplierApi.exportToExcelGeneral( - filterParams.supplier_ids, - filterParams.filter_by, - filterParams.start_date, - filterParams.end_date - ); - toast.success('Excel General berhasil dibuat dan diunduh.'); - } catch { - toast.error('Gagal membuat Excel General. Silakan coba lagi.'); - } finally { - setIsExcelGeneralExportLoading(false); - } - }, [filterParams]); + // ===== TAB ACTIONS ===== + useEffect(() => { + setTabActions( + tabId, +
+ - // ===== TAB ACTIONS COMPONENT ===== - const TabActions = useMemo(() => { - return function TabActionsComponent() { - const setTabActions = useTabActionsStore((state) => state.setTabActions); - const clearTabActions = useTabActionsStore( - (state) => state.clearTabActions - ); - - useEffect(() => { - setTabActions( - tabId, -
- handleFilterModalOpenRef.current()} + - - -
- - - Export - -
- - -
- - } + color='none' + isLoading={isAnyExportLoading} + className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft' > - - - - -
- ); - }, [setTabActions]); - - useEffect(() => { - return () => { - clearTabActions(tabId); - }; - }, [clearTabActions]); - - return null; - }; +
+ + Export +
+ +
+ + } + > + + + + +
+ ); }, [ tabId, - filterParams, + setTabActions, + tableFilterState, + filterModal.openModal, isAnyExportLoading, handleExportExcel, handleExportExcelGeneral, @@ -430,24 +430,9 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { isPdfExportLoading, ]); - const TabActionsElement = useMemo(() => , [TabActions]); - useEffect(() => { - return () => { - if (dateErrorShown) { - toast.dismiss(); - } - }; - }, [dateErrorShown]); - - useEffect(() => { - return () => { - if (dateErrorShown) { - toast.dismiss(); - setDateErrorShown(false); - } - }; - }, [filterModal.open, dateErrorShown]); + return () => clearTabActions(tabId); + }, [tabId, clearTabActions]); const getTableColumns = (supplier?: DebtSupplier): ColumnDef[] => [ { @@ -662,9 +647,9 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { }, }, ]; + return ( <> - {TabActionsElement}
{isLoading && (
@@ -693,16 +678,16 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { - setCurrentPage((curr) => (curr > 1 ? curr - 1 : curr)) - } + currentPage={tableFilterState.page} + onPrevPage={() => setPage(Math.max(1, tableFilterState.page - 1))} onNextPage={() => - setCurrentPage((curr) => - meta.total_pages && curr < meta.total_pages ? curr + 1 : curr + setPage( + meta.total_pages && tableFilterState.page < meta.total_pages + ? tableFilterState.page + 1 + : tableFilterState.page ) } - onPageChange={(pageNumber) => setCurrentPage(pageNumber)} + onPageChange={setPage} rowOptions={[10, 20, 50, 100]} onRowChange={setPageSize} /> @@ -802,16 +787,16 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { - setCurrentPage((curr) => (curr > 1 ? curr - 1 : curr)) - } + currentPage={tableFilterState.page} + onPrevPage={() => setPage(Math.max(1, tableFilterState.page - 1))} onNextPage={() => - setCurrentPage((curr) => - meta.total_pages && curr < meta.total_pages ? curr + 1 : curr + setPage( + meta.total_pages && tableFilterState.page < meta.total_pages + ? tableFilterState.page + 1 + : tableFilterState.page ) } - onPageChange={(pageNumber) => setCurrentPage(pageNumber)} + onPageChange={setPage} rowOptions={[10, 20, 50, 100]} onRowChange={setPageSize} /> @@ -827,23 +812,23 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm', }} > - - {/* Modal Header */} -
-
- -

Filter Data

-
- + {/* Modal Header */} +
+
+ +

Filter Data

+ +
+ {/* Modal Body */}
@@ -852,153 +837,68 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
{ - const value = e.target.value; - formik.setFieldValue('startDate', value || null); - - if (value && formik.values.endDate) { - const startDate = new Date(value); - const endDateObj = new Date(formik.values.endDate); - - if (endDateObj < startDate) { - setHasDateError(true); - if (!dateErrorShown) { - toast.error('Tanggal akhir tidak boleh masa lampau', { - duration: Infinity, - }); - setDateErrorShown(true); - } - } else { - setHasDateError(false); - if (dateErrorShown) { - toast.dismiss(); - setDateErrorShown(false); - } - } - } else { - setHasDateError(false); - } - }} + name='start_date' + value={formik.values.start_date || ''} + onChange={handleStartDateChange} className={{ wrapper: 'w-full' }} - isError={ - formik.touched.startDate && !!formik.errors.startDate - } - errorMessage={formik.errors.startDate} isNestedModal /> -
+
{ - const value = e.target.value; - formik.setFieldValue('endDate', value || null); - - if (value && formik.values.startDate) { - const startDateObj = new Date(formik.values.startDate); - const endDate = new Date(value); - - if (endDate < startDateObj) { - setHasDateError(true); - if (!dateErrorShown) { - toast.error('Tanggal akhir tidak boleh masa lampau', { - duration: Infinity, - }); - setDateErrorShown(true); - } - return; - } - } - - setHasDateError(false); - if (dateErrorShown) { - toast.dismiss(); - setDateErrorShown(false); - } - }} + name='end_date' + value={formik.values.end_date || ''} + onChange={handleEndDateChange} className={{ wrapper: 'w-full' }} - isError={ - (formik.touched.endDate && !!formik.errors.endDate) || - hasDateError - } - errorMessage={formik.errors.endDate} isNestedModal + isError={hasDateError} />
-
- { - formik.setFieldValue( - 'supplierIds', - Array.isArray(val) ? val : val ? [val] : null - ); - }} - onInputChange={setSupplierInputValue} - onMenuScrollToBottom={loadMoreSuppliers} - isLoading={isLoadingSupplierOptions} - isClearable - className={{ wrapper: 'w-full' }} - isError={ - formik.touched.supplierIds && !!formik.errors.supplierIds - } - errorMessage={formik.errors.supplierIds as string} - /> -
+ + formik.setFieldValue('suppliers', Array.isArray(val) ? val : []) + } + onInputChange={setSupplierInputValue} + onMenuScrollToBottom={loadMoreSuppliers} + isLoading={isLoadingSupplierOptions} + isClearable + className={{ wrapper: 'w-full' }} + /> -
- { - formik.setFieldValue( - 'filterBy', - val ? (val as OptionType) : null - ); - }} - className={{ wrapper: 'w-full' }} - isClearable - isError={formik.touched.filterBy && !!formik.errors.filterBy} - errorMessage={formik.errors.filterBy as string} - /> -
+ + formik.setFieldValue( + 'filterBy', + !Array.isArray(val) ? (val ?? undefined) : undefined + ) + } + className={{ wrapper: 'w-full' }} + isClearable + />
- {/* Action Buttons */} + {/* Modal Footer */}
From 3d37fb2ecb7bfb4394a331eff37f76be73f0cc2b Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 20 May 2026 16:10:36 +0700 Subject: [PATCH 15/27] fix: remove dummy data --- src/services/api/report/finance-report.ts | 245 +--------------------- 1 file changed, 6 insertions(+), 239 deletions(-) diff --git a/src/services/api/report/finance-report.ts b/src/services/api/report/finance-report.ts index a4a5197b..132e3063 100644 --- a/src/services/api/report/finance-report.ts +++ b/src/services/api/report/finance-report.ts @@ -89,251 +89,18 @@ export class FinanceApiService extends BaseApiService< async getBalanceMonitoringReport(params: { start_date?: string; end_date?: string; - customer_id?: string; - sales_id?: string; + customer_ids?: string; + sales_ids?: string; + filter_by?: string; sort_by?: string; sort_order?: string; page?: number; limit?: number; }): Promise | undefined> { - // TODO: Remove dummy data when BE is ready - const dummyRows: BalanceMonitoringRow[] = [ - { - customer_id: 1, - customer_name: 'PT Maju Bersama', - saldo_awal: 12500000, - penjualan_ayam_ekor: 450, - penjualan_ayam_kg: 1125.5, - penjualan_ayam_nominal: 22510000, - penjualan_telur_kuantitas: 300, - penjualan_telur_kg: 900, - penjualan_telur_nominal: 4500000, - penjualan_trading: 3000000, - pembayaran: 30000000, - aging: 15, - aging_rata_rata: 12, - saldo_akhir: 12510000, - }, - { - customer_id: 2, - customer_name: 'CV Sumber Rezeki', - saldo_awal: 8750000, - penjualan_ayam_ekor: 320, - penjualan_ayam_kg: 800, - penjualan_ayam_nominal: 16000000, - penjualan_telur_kuantitas: 150, - penjualan_telur_kg: 450, - penjualan_telur_nominal: 2250000, - penjualan_trading: 0, - pembayaran: 20000000, - aging: 7, - aging_rata_rata: 8, - saldo_akhir: 7000000, - }, - { - customer_id: 3, - customer_name: 'UD Karya Mandiri', - saldo_awal: 5000000, - penjualan_ayam_ekor: 600, - penjualan_ayam_kg: 1500, - penjualan_ayam_nominal: 30000000, - penjualan_telur_kuantitas: 0, - penjualan_telur_kg: 0, - penjualan_telur_nominal: 0, - penjualan_trading: 1500000, - pembayaran: 25000000, - aging: 30, - aging_rata_rata: 22, - saldo_akhir: 11500000, - }, - { - customer_id: 4, - customer_name: 'PT Sejahtera Abadi', - saldo_awal: 20000000, - penjualan_ayam_ekor: 100, - penjualan_ayam_kg: 250, - penjualan_ayam_nominal: 5000000, - penjualan_telur_kuantitas: 500, - penjualan_telur_kg: 1500, - penjualan_telur_nominal: 7500000, - penjualan_trading: 2000000, - pembayaran: 40000000, - aging: 45, - aging_rata_rata: 38, - saldo_akhir: -5500000, - }, - { - customer_id: 5, - customer_name: 'CV Berkah Jaya', - saldo_awal: 3200000, - penjualan_ayam_ekor: 200, - penjualan_ayam_kg: 500, - penjualan_ayam_nominal: 10000000, - penjualan_telur_kuantitas: 200, - penjualan_telur_kg: 600, - penjualan_telur_nominal: 3000000, - penjualan_trading: 500000, - pembayaran: 15000000, - aging: 10, - aging_rata_rata: 9, - saldo_akhir: 1700000, - }, - { - customer_id: 6, - customer_name: 'PT Harapan Makmur', - saldo_awal: 17000000, - penjualan_ayam_ekor: 780, - penjualan_ayam_kg: 1950, - penjualan_ayam_nominal: 39000000, - penjualan_telur_kuantitas: 400, - penjualan_telur_kg: 1200, - penjualan_telur_nominal: 6000000, - penjualan_trading: 4500000, - pembayaran: 50000000, - aging: 20, - aging_rata_rata: 17, - saldo_akhir: 16500000, - }, - { - customer_id: 7, - customer_name: 'UD Rejeki Lancar', - saldo_awal: 2000000, - penjualan_ayam_ekor: 50, - penjualan_ayam_kg: 125, - penjualan_ayam_nominal: 2500000, - penjualan_telur_kuantitas: 80, - penjualan_telur_kg: 240, - penjualan_telur_nominal: 1200000, - penjualan_trading: 0, - pembayaran: 5000000, - aging: 5, - aging_rata_rata: 6, - saldo_akhir: 700000, - }, - { - customer_id: 8, - customer_name: 'CV Putra Unggul', - saldo_awal: 9500000, - penjualan_ayam_ekor: 410, - penjualan_ayam_kg: 1025, - penjualan_ayam_nominal: 20500000, - penjualan_telur_kuantitas: 250, - penjualan_telur_kg: 750, - penjualan_telur_nominal: 3750000, - penjualan_trading: 1000000, - pembayaran: 28000000, - aging: 18, - aging_rata_rata: 15, - saldo_akhir: 6750000, - }, - { - customer_id: 9, - customer_name: 'PT Duta Poultry', - saldo_awal: 35000000, - penjualan_ayam_ekor: 1200, - penjualan_ayam_kg: 3000, - penjualan_ayam_nominal: 60000000, - penjualan_telur_kuantitas: 800, - penjualan_telur_kg: 2400, - penjualan_telur_nominal: 12000000, - penjualan_trading: 8000000, - pembayaran: 70000000, - aging: 60, - aging_rata_rata: 50, - saldo_akhir: -15000000, - }, - { - customer_id: 10, - customer_name: 'UD Sari Ayam', - saldo_awal: 6800000, - penjualan_ayam_ekor: 350, - penjualan_ayam_kg: 875, - penjualan_ayam_nominal: 17500000, - penjualan_telur_kuantitas: 0, - penjualan_telur_kg: 0, - penjualan_telur_nominal: 0, - penjualan_trading: 2500000, - pembayaran: 22000000, - aging: 12, - aging_rata_rata: 11, - saldo_akhir: 4800000, - }, - { - customer_id: 11, - customer_name: 'CV Nusa Ternak', - saldo_awal: 14200000, - penjualan_ayam_ekor: 530, - penjualan_ayam_kg: 1325, - penjualan_ayam_nominal: 26500000, - penjualan_telur_kuantitas: 350, - penjualan_telur_kg: 1050, - penjualan_telur_nominal: 5250000, - penjualan_trading: 3500000, - pembayaran: 35000000, - aging: 25, - aging_rata_rata: 20, - saldo_akhir: 14450000, - }, - { - customer_id: 12, - customer_name: 'PT Agro Sentosa', - saldo_awal: 4100000, - penjualan_ayam_ekor: 180, - penjualan_ayam_kg: 450, - penjualan_ayam_nominal: 9000000, - penjualan_telur_kuantitas: 120, - penjualan_telur_kg: 360, - penjualan_telur_nominal: 1800000, - penjualan_trading: 750000, - pembayaran: 12000000, - aging: 8, - aging_rata_rata: 9, - saldo_akhir: 3650000, - }, - ]; - - const page = Number(params.page) || 1; - const limit = Number(params.limit) || 10; - - return { - status: 'success', - message: 'Data retrieved successfully', - data: dummyRows, - meta: { - page, - limit, - total_results: dummyRows.length, - total_pages: 1, - }, - } as BaseApiResponse; - - // return await this.customRequest>( - // 'balance-monitoring', - // { method: 'GET', params } - // ); - } - - async exportBalanceMonitoringToPDF(initialQueryString: string) { - const params = new URLSearchParams(initialQueryString); - params.set('export', 'pdf'); - params.set('page', '1'); - params.set('limit', '99999999999'); - - const res = await httpClient( - `${this.basePath}/balance-monitoring?${params.toString()}`, - { method: 'GET', responseType: 'blob' } + return await this.customRequest>( + 'balance-monitoring', + { method: 'GET', params } ); - - const url = window.URL.createObjectURL(new Blob([res])); - const link = document.createElement('a'); - link.href = url; - link.setAttribute( - 'download', - `monitoring-saldo-${formatDate(Date.now(), 'DD-MM-YYYY')}.pdf` - ); - document.body.appendChild(link); - link.click(); - link.remove(); } async getCustomerPaymentReport( From 8d014a8fea7d17415b180eaf9877279a31aa031f Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 20 May 2026 16:14:37 +0700 Subject: [PATCH 16/27] fix: adjust BalanceMonitoringRow type --- src/types/api/report/balance-monitoring.d.ts | 34 +++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/types/api/report/balance-monitoring.d.ts b/src/types/api/report/balance-monitoring.d.ts index 9c7933dd..a2309314 100644 --- a/src/types/api/report/balance-monitoring.d.ts +++ b/src/types/api/report/balance-monitoring.d.ts @@ -1,23 +1,25 @@ +import { Customer } from '@/services/api/master-data'; + export type BalanceMonitoringRow = { - customer_id: number; - customer_name: string; + customer: Customer; saldo_awal: number; - penjualan_ayam_ekor: number; - penjualan_ayam_kg: number; - penjualan_ayam_nominal: number; - penjualan_telur_kuantitas: number; - penjualan_telur_kg: number; - penjualan_telur_nominal: number; - penjualan_trading: number; + penjualan_ayam: { + ekor: number; + kg: number; + nominal: number; + }; + penjualan_telur: { + butir: number; + kg: number; + nominal: number; + }; + penjualan_trading: { + qty: number; + kg: number; + nominal: number; + }; pembayaran: number; aging: number; aging_rata_rata: number; saldo_akhir: number; }; - -export type BalanceMonitoringMeta = { - page: number; - limit: number; - total_results: number; - total_pages: number; -}; From 9abb8b0b583eae176b09810bc46bcfc2ed0e5ec5 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 20 May 2026 16:34:53 +0700 Subject: [PATCH 17/27] feat: add hide field in TabItem type --- src/components/Tabs.tsx | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/components/Tabs.tsx b/src/components/Tabs.tsx index 52047d8b..6e6956d6 100644 --- a/src/components/Tabs.tsx +++ b/src/components/Tabs.tsx @@ -6,6 +6,7 @@ export interface TabItem { label: ReactNode; content?: ReactNode; disabled?: boolean; + hide?: boolean; } export interface TabsProps @@ -122,17 +123,19 @@ const Tabs = ({ >
- {tabs.map(({ id, label, disabled }) => ( - - ))} + {tabs.map(({ id, label, disabled, hide }) => + hide ? null : ( + + ) + )}
{sideContent && sideContent}
From c4827bb81055170ef29d154fbb4b4c253eb86618 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 20 May 2026 16:35:26 +0700 Subject: [PATCH 18/27] feat: implement Query Param Tab Navigation --- .../pages/report/finance/FinanceTabs.tsx | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/components/pages/report/finance/FinanceTabs.tsx b/src/components/pages/report/finance/FinanceTabs.tsx index 29abe5e9..0604cb48 100644 --- a/src/components/pages/report/finance/FinanceTabs.tsx +++ b/src/components/pages/report/finance/FinanceTabs.tsx @@ -1,31 +1,47 @@ 'use client'; -import { useState } from 'react'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import Tabs from '@/components/Tabs'; import CustomerPaymentTab from '@/components/pages/report/finance/tab/CustomerPaymentTab'; import DebtSupplierTab from '@/components/pages/report/finance/tab/DebtSupplierTab'; import BalanceMonitoringTab from '@/components/pages/report/finance/tab/BalanceMonitoringTab'; import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; +const VALID_TAB_IDS = [ + 'debt-supplier', + 'customer-payment', + 'balance-monitoring', +]; + const FinanceTabs = () => { - const [activeTabId, setActiveTabId] = useState('1'); + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const tabParam = searchParams.get('tab') ?? 'debt-supplier'; + const activeTabId = VALID_TAB_IDS.includes(tabParam) + ? tabParam + : 'debt-supplier'; const tabActions = useTabActionsStore((state) => state.tabActions); + const handleTabChange = (tabId: string) => { + router.push(`${pathname}?tab=${tabId}`); + }; + const tabs = [ { - id: '1', + id: 'debt-supplier', label: 'Rekapitulasi Hutang Ke Supplier', - content: , + content: , }, { - id: '2', + id: 'customer-payment', label: 'Kontrol Pembayaran Customer', - content: , + content: , }, { - id: '3', + id: 'balance-monitoring', label: 'Monitoring Saldo', - content: , + content: , }, ]; @@ -35,7 +51,7 @@ const FinanceTabs = () => { tabs={tabs} variant='boxed' activeTabId={activeTabId} - onTabChange={setActiveTabId} + onTabChange={handleTabChange} className={{ tabHeaderWrapper: 'justify-between items-center p-3 border-b border-base-content/10', From ef56f87e45149e19b84cd70a9c5245e69ffac3d4 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 20 May 2026 16:35:43 +0700 Subject: [PATCH 19/27] feat: create report finance layout file --- src/app/report/finance/layout.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/app/report/finance/layout.tsx diff --git a/src/app/report/finance/layout.tsx b/src/app/report/finance/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/report/finance/layout.tsx @@ -0,0 +1,11 @@ +import SuspenseHelper from '@/components/helper/SuspenseHelper'; + +const Layout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return {children}; +}; + +export default Layout; From 027668a1bf56a94b85f55a31387501854e40365b Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 21 May 2026 14:43:08 +0700 Subject: [PATCH 20/27] feat: add export to excel feature --- src/components/pages/finance/FinanceTable.tsx | 63 ++++++++++++++++++- src/services/api/finance.ts | 25 ++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/src/components/pages/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx index 211af1f7..a4cb9cbd 100644 --- a/src/components/pages/finance/FinanceTable.tsx +++ b/src/components/pages/finance/FinanceTable.tsx @@ -29,7 +29,7 @@ import { FINANCE_TRANSACTION_TYPE_OPTIONS, } from '@/config/constant'; import { FinanceApi } from '@/services/api/finance'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper'; import { BankApi, CustomerApi, SupplierApi } from '@/services/api/master-data'; import { Bank } from '@/types/api/master-data/bank'; import Modal, { useModal } from '@/components/Modal'; @@ -39,6 +39,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal'; import toast from 'react-hot-toast'; import RequirePermission from '@/components/helper/RequirePermission'; import ButtonFilter from '@/components/helper/ButtonFilter'; +import Dropdown from '@/components/dropdown/Dropdown'; import { FinanceTableFilterSchema, FinanceTableFilterValues, @@ -233,6 +234,7 @@ const FinanceTable = () => { const [selectedSortBy, setSelectedSortBy] = useState(null); const [selectedFinance, setSelectedFinance] = useState(null); const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const [isExportLoading, setIsExportLoading] = useState(false); const [dateErrorShown, setDateErrorShown] = useState(false); const [hasDateError, setHasDateError] = useState(false); @@ -552,6 +554,20 @@ const FinanceTable = () => { filterModal.openModal(); }; + const exportToExcel = async () => { + setIsExportLoading(true); + try { + await FinanceApi.exportToExcel(getTableFilterQueryString()); + toast.success('Excel berhasil dibuat dan diunduh.'); + } catch (error) { + toast.error( + await getErrorMessage(error, 'Gagal mengekspor data finance.') + ); + } finally { + setIsExportLoading(false); + } + }; + const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); @@ -759,6 +775,51 @@ const FinanceTable = () => { onClick={handleFilterModalOpen} className='px-3 py-2.5' /> + + +
+ + + Ekspor + +
+ + +
+ + } + > + +
diff --git a/src/services/api/finance.ts b/src/services/api/finance.ts index f9ba367f..291551f3 100644 --- a/src/services/api/finance.ts +++ b/src/services/api/finance.ts @@ -2,6 +2,7 @@ import axios from 'axios'; import { BaseApiService } from '@/services/api/base'; import { BaseApiResponse } from '@/types/api/api-general'; import { httpClient, httpClientFetcher } from '@/services/http/client'; +import { formatDate } from '@/lib/helper'; import { CreateFinancePayment, CreateInitialBalance, @@ -174,6 +175,30 @@ export class FinanceApiService extends BaseApiService< } } + async exportToExcel(initialQueryString: string) { + const params = new URLSearchParams(initialQueryString); + + params.set('export', 'excel'); + params.set('page', '1'); + params.set('limit', '99999999999'); + + const res = await httpClient( + `${this.basePath}/transactions?${params.toString()}`, + { method: 'GET', responseType: 'blob' } + ); + + const url = window.URL.createObjectURL(new Blob([res])); + const link = document.createElement('a'); + link.href = url; + link.setAttribute( + 'download', + `finance-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx` + ); + document.body.appendChild(link); + link.click(); + link.remove(); + } + async delete(id: number) { try { const deletePath = `${this.basePath}/transactions/${id}`; From a4e5116beff006994191e439887a76b08e28369f Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 21 May 2026 14:44:07 +0700 Subject: [PATCH 21/27] feat: add start_date, end_date, and filter_by input --- .../pages/purchase/PurchaseFilterModal.tsx | 116 +++++++++++++++++- 1 file changed, 115 insertions(+), 1 deletion(-) diff --git a/src/components/pages/purchase/PurchaseFilterModal.tsx b/src/components/pages/purchase/PurchaseFilterModal.tsx index 856f2074..55db1aea 100644 --- a/src/components/pages/purchase/PurchaseFilterModal.tsx +++ b/src/components/pages/purchase/PurchaseFilterModal.tsx @@ -10,6 +10,7 @@ import Button from '@/components/Button'; import DateInput from '@/components/input/DateInput'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInput from '@/components/input/SelectInput'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; import { OptionType, useSelect } from '@/components/input/SelectInput'; import { PurchaseFilter } from '@/types/api/purchase/purchase'; @@ -24,10 +25,20 @@ import { ProjectFlockApi } from '@/services/api/production'; import { ProjectFlock } from '@/types/api/production/project-flock'; import { isResponseSuccess } from '@/lib/api-helper'; +const filterByOptions: OptionType[] = [ + { value: 'po_date', label: 'Tanggal PO' }, + { value: 'received_date', label: 'Tanggal Terima' }, + { value: 'due_date', label: 'Tanggal Jatuh Tempo' }, + { value: 'created_at', label: 'Tanggal Dibuat' }, +]; + interface PurchaseFilterModalProps { ref: RefObject; initialValues?: { poDate: string; + start_date: string; + end_date: string; + filterBy: OptionType | undefined; category: OptionType[]; status: OptionType[]; supplier: OptionType | null; @@ -51,6 +62,7 @@ const PurchaseFilterModal = ({ }, [ref]); // ===== DATE ERROR STATE ===== + const [hasDateError, setHasDateError] = useState(false); const [dateErrorShown, setDateErrorShown] = useState(false); // ===== CLEANUP TOAST ON UNMOUNT ===== @@ -139,6 +151,9 @@ const PurchaseFilterModal = ({ const formik = useFormik<{ poDate: string; + start_date: string; + end_date: string; + filterBy: OptionType | undefined; category: { label: string; value: number }[]; status: { label: string; value: string }[]; supplier: OptionType | null; @@ -150,6 +165,9 @@ const PurchaseFilterModal = ({ // enableReinitialize: true, initialValues: initialValues || { poDate: '', + start_date: '', + end_date: '', + filterBy: undefined, category: [], status: [], supplier: null, @@ -230,9 +248,17 @@ const PurchaseFilterModal = ({ }; const formikResetHandler = useCallback(() => { + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } resetForm({ values: { poDate: '', + start_date: '', + end_date: '', + filterBy: undefined, category: [], status: [], supplier: null, @@ -246,7 +272,56 @@ const PurchaseFilterModal = ({ setSelectedLocationId(''); onReset?.(); closeModalHandler(); - }, [resetForm, onReset, closeModalHandler]); + }, [resetForm, onReset, closeModalHandler, dateErrorShown]); + + const handleStartDateChange = (e: React.ChangeEvent) => { + const value = e.target.value; + formik.setFieldValue('start_date', value); + + if (value && formik.values.end_date) { + if (new Date(formik.values.end_date) < new Date(value)) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh sebelum tanggal mulai', { + duration: Infinity, + }); + setDateErrorShown(true); + } + } else { + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + } + } else { + setHasDateError(false); + } + }; + + const handleEndDateChange = (e: React.ChangeEvent) => { + const value = e.target.value; + formik.setFieldValue('end_date', value); + + if (value && formik.values.start_date) { + if (new Date(value) < new Date(formik.values.start_date)) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh sebelum tanggal mulai', { + duration: Infinity, + }); + setDateErrorShown(true); + } + return; + } + } + + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }; const formikSubmitHandler = useCallback(async () => { await submitForm(); @@ -287,6 +362,44 @@ const PurchaseFilterModal = ({ {/* Modal Body */}
+
+ +
+ +
+ +
+
+ + + formik.setFieldValue( + 'filterBy', + !Array.isArray(val) ? (val ?? undefined) : undefined + ) + } + isClearable + /> + Apply Filter From 80e0bd5a8ee6eb9125687b279025f1b73914f975 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 21 May 2026 14:44:20 +0700 Subject: [PATCH 22/27] fix: update table columns --- .../pages/purchase/PurchaseTable.tsx | 262 ++++++++++++++++-- 1 file changed, 235 insertions(+), 27 deletions(-) diff --git a/src/components/pages/purchase/PurchaseTable.tsx b/src/components/pages/purchase/PurchaseTable.tsx index fdd2f9be..1e2da838 100644 --- a/src/components/pages/purchase/PurchaseTable.tsx +++ b/src/components/pages/purchase/PurchaseTable.tsx @@ -28,7 +28,7 @@ import PurchaseFilterModal from '@/components/pages/purchase/PurchaseFilterModal import Dropdown from '@/components/dropdown/Dropdown'; import { OptionType } from '@/components/input/SelectInput'; -import { cn, formatDate } from '@/lib/helper'; +import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; @@ -41,6 +41,9 @@ type PurchaseTableFilters = { search: string; sort_by: string; order_by: string; + start_date: string; + end_date: string; + filter_by: string; po_date: string; approval_status: string; product_category_id: string; @@ -177,6 +180,9 @@ const PurchaseTable = () => { search: '', sort_by: '', order_by: '', + start_date: '', + end_date: '', + filter_by: '', po_date: '', approval_status: '', product_category_id: '', @@ -197,6 +203,9 @@ const PurchaseTable = () => { pageSize: 'limit', sort_by: 'sort_by', order_by: 'sort_order', + start_date: 'start_date', + end_date: 'end_date', + filter_by: 'filter_by', po_date: 'po_date', approval_status: 'approval_status', product_category_id: 'product_category_id', @@ -297,36 +306,11 @@ const PurchaseTable = () => { ); }, }, - { - accessorKey: 'supplier', - header: 'Vendor', - cell: (props) => props.row.original.supplier.name, - }, { accessorKey: 'requester_name', header: 'Nama Pengaju', cell: (props) => props.row.original.requester_name || '-', }, - { - accessorKey: 'products', - header: 'Produk', - cell: (props) => { - const products = props.row.original.products; - if (!products || products.length === 0) return '-'; - return ( -
    - {products.map((product, index) => ( -
  • {product.name}
  • - ))} -
- ); - }, - }, - { - accessorKey: 'location', - header: 'Lokasi', - cell: (props) => props.row.original.location?.name || '-', - }, { accessorKey: 'po_date', header: 'Tgl. PO', @@ -364,6 +348,202 @@ const PurchaseTable = () => { return `${diffDays} hari`; }, }, + { + accessorKey: 'supplier', + header: 'Vendor', + cell: (props) => props.row.original.supplier.name, + }, + { + accessorKey: 'location', + header: 'Lokasi', + cell: (props) => props.row.original.location?.name || '-', + }, + { + accessorKey: 'warehouse', + header: 'Gudang', + enableSorting: false, + cell: (props) => { + const items = props.row.original.items; + if (!items || items.length === 0) return '-'; + return ( +
    + {items.map((item, index) => ( +
  • {item.warehouse?.name ?? '-'}
  • + ))} +
+ ); + }, + }, + { + accessorKey: 'products', + header: 'Produk', + cell: (props) => { + const products = props.row.original.products; + if (!products || products.length === 0) return '-'; + return ( +
    + {products.map((product, index) => ( +
  • {product.name}
  • + ))} +
+ ); + }, + }, + { + accessorKey: 'total_qty', + header: 'Kuantitas', + enableSorting: false, + cell: (props) => { + const items = props.row.original.items; + if (!items || items.length === 0) return '-'; + return ( +
    + {items.map((item, index) => ( +
  • {formatNumber(item.total_qty ?? 0)}
  • + ))} +
+ ); + }, + }, + { + accessorKey: 'uom', + header: 'Satuan', + enableSorting: false, + cell: (props) => { + const items = props.row.original.items; + if (!items || items.length === 0) return '-'; + return ( +
    + {items.map((item, index) => ( +
  • {item.product?.uom?.name ?? '-'}
  • + ))} +
+ ); + }, + }, + { + accessorKey: 'price', + header: 'Harga', + enableSorting: false, + cell: (props) => { + const items = props.row.original.items; + if (!items || items.length === 0) return '-'; + return ( +
    + {items.map((item, index) => ( +
  • {formatCurrency(item.price ?? 0)}
  • + ))} +
+ ); + }, + }, + { + accessorKey: 'total_price', + header: 'Total Harga', + enableSorting: false, + cell: (props) => { + const items = props.row.original.items; + if (!items || items.length === 0) return '-'; + return ( +
    + {items.map((item, index) => ( +
  • {formatCurrency(item.total_price ?? 0)}
  • + ))} +
+ ); + }, + }, + { + accessorKey: 'products_total', + header: 'Total Harga Produk', + cell: (props) => formatCurrency(props.row.original.products_total ?? 0), + }, + { + accessorKey: 'expedition_vendor', + header: 'Vendor Ekspedisi', + enableSorting: false, + cell: (props) => { + const items = props.row.original.items; + if (!items || items.length === 0) return '-'; + return ( +
    + {items.map((item, index) => ( +
  • {item.expedition_vendor?.name ?? '-'}
  • + ))} +
+ ); + }, + }, + { + accessorKey: 'expedition_qty', + header: 'Qty Ekspedisi', + enableSorting: false, + cell: (props) => { + const items = props.row.original.items; + if (!items || items.length === 0) return '-'; + return ( +
    + {items.map((item, index) => ( +
  • + {item.expedition_qty != null + ? formatNumber(item.expedition_qty) + : '-'} +
  • + ))} +
+ ); + }, + }, + { + accessorKey: 'transport_per_item', + header: 'Harga Ekspedisi', + enableSorting: false, + cell: (props) => { + const items = props.row.original.items; + if (!items || items.length === 0) return '-'; + return ( +
    + {items.map((item, index) => ( +
  • + {item.transport_per_item != null + ? formatCurrency(item.transport_per_item) + : '-'} +
  • + ))} +
+ ); + }, + }, + { + accessorKey: 'item_expedition_total', + header: 'Total Ekspedisi', + enableSorting: false, + cell: (props) => { + const items = props.row.original.items; + if (!items || items.length === 0) return '-'; + return ( +
    + {items.map((item, index) => ( +
  • + {item.expedition_total != null + ? formatCurrency(item.expedition_total) + : '-'} +
  • + ))} +
+ ); + }, + }, + { + accessorKey: 'expedition_total', + header: 'Total Ekspedisi Semua Produk', + cell: (props) => formatCurrency(props.row.original.expedition_total ?? 0), + }, + { + accessorKey: 'grand_total_all', + header: 'Grand Total All', + cell: (props) => formatCurrency(props.row.original.grand_total_all ?? 0), + }, { accessorKey: 'status', header: 'Status Approval', @@ -410,6 +590,11 @@ const PurchaseTable = () => { ); }, }, + { + accessorKey: 'notes', + header: 'Notes', + cell: (props) => props.row.original.notes || '-', + }, { accessorKey: 'created_at', header: 'Tanggal Dibuat', @@ -476,6 +661,9 @@ const PurchaseTable = () => { const filterSubmitHandler = (values: PurchaseFilter) => { setFilters({ + start_date: values.start_date || '', + end_date: values.end_date || '', + filter_by: values.filterBy?.value || '', po_date: values.poDate, product_category_id: values.category.join(','), product_category_name: @@ -500,6 +688,9 @@ const PurchaseTable = () => { const filterResetHandler = () => { setFilters({ + start_date: '', + end_date: '', + filter_by: '', po_date: '', product_category_id: '', product_category_name: '', @@ -518,6 +709,13 @@ const PurchaseTable = () => { }; const purchaseFilterInitialValues = useMemo(() => { + const filterByLabelMap: Record = { + po_date: 'Tanggal PO', + received_date: 'Tanggal Terima', + due_date: 'Tanggal Jatuh Tempo', + created_at: 'Tanggal Dibuat', + }; + const categoryIds = tableFilterState.product_category_id ? tableFilterState.product_category_id .split(',') @@ -539,6 +737,16 @@ const PurchaseTable = () => { return { poDate: tableFilterState.po_date, + start_date: tableFilterState.start_date, + end_date: tableFilterState.end_date, + filterBy: tableFilterState.filter_by + ? { + value: tableFilterState.filter_by, + label: + filterByLabelMap[tableFilterState.filter_by] || + tableFilterState.filter_by, + } + : undefined, category: categoryIds.map((value, index) => ({ value: Number(value), label: categoryLabels[index] || value, @@ -706,7 +914,7 @@ const PurchaseTable = () => { 'project_flock_name', 'project_flock_kandang_name', ]} - fieldGroups={[['startDate', 'endDate']]} + fieldGroups={[['start_date', 'end_date']]} onClick={filterModal.openModal} className='px-3 py-2.5' /> From 585918cc28e95af2776483912370ce99bcf509d7 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 21 May 2026 14:44:28 +0700 Subject: [PATCH 23/27] fix: update purchase type --- src/types/api/purchase/purchase.d.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/types/api/purchase/purchase.d.ts b/src/types/api/purchase/purchase.d.ts index 98e76aab..d790e895 100644 --- a/src/types/api/purchase/purchase.d.ts +++ b/src/types/api/purchase/purchase.d.ts @@ -57,6 +57,8 @@ export type PurchaseItem = { alias?: string; category?: string; } | null; + expedition_qty?: number; + expedition_total?: number; }; export type BasePurchase = { @@ -81,6 +83,9 @@ export type BasePurchase = { po_expedition?: { id: number; refrence: string }[]; created_user?: CreatedUser; products?: PurchaseItemProduct[]; + products_total?: number; + expedition_total?: number; + grand_total_all?: number; }; export type Purchase = BaseMetadata & BasePurchase; @@ -149,6 +154,9 @@ export type UpdatePurchaseRequestPayload = CreatePurchaseRequestPayload; export type PurchaseFilter = { poDate: string; + start_date?: string; + end_date?: string; + filterBy?: { label: string; value: string }; category: string[]; category_labels?: { label: string; value: number }[]; status: string[]; From 07dd2d26be0d58ef8e4e0bf42780a2fdcd674815 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Fri, 22 May 2026 10:17:04 +0700 Subject: [PATCH 24/27] fix: cache product stock --- .../inventory/movement/form/MovementForm.tsx | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index 1907d498..51d35857 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -56,6 +56,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { const [productQtyErrorShown, setProductQtyErrorShown] = useState(false); const [deliveryQtyErrorShown, setDeliveryQtyErrorShown] = useState(false); const [isInitialized, setIsInitialized] = useState(false); + const productStockCacheRef = useRef< + Map + >(new Map()); // ===== FORM HANDLERS ===== const createMovementHandler = useCallback( @@ -337,6 +340,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { prevSourceWarehouseId !== currentSourceWarehouseId && prevSourceWarehouseId !== null ) { + productStockCacheRef.current = new Map(); formik.setFieldValue('products', [ { product: null, @@ -399,6 +403,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { : []; }, [productWarehouses]); + useEffect(() => { + productWarehouseOptions.forEach((pw) => { + productStockCacheRef.current.set(pw.product_id, { + quantity: pw.quantity, + transfer_available_qty: pw.transfer_available_qty, + }); + }); + }, [productWarehouseOptions]); + // ===== HELPER FUNCTIONS ===== const isRepeaterInputError = ( arrayName: T, @@ -840,15 +853,12 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { const getAvailableStock = useCallback( (productId: number) => { if (type === 'detail') return 0; - const productWarehouse = productWarehouseOptions.find( + const live = productWarehouseOptions.find( (pw) => pw.product_id === productId ); - - return ( - productWarehouse?.transfer_available_qty ?? - productWarehouse?.quantity ?? - 0 - ); + if (live) return live.transfer_available_qty ?? live.quantity ?? 0; + const cached = productStockCacheRef.current.get(productId); + return cached?.transfer_available_qty ?? cached?.quantity ?? 0; }, [productWarehouseOptions, type] ); @@ -856,20 +866,25 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { const getTotalStock = useCallback( (productId: number) => { if (type === 'detail') return 0; - const productWarehouse = productWarehouseOptions.find( + const live = productWarehouseOptions.find( (pw) => pw.product_id === productId ); - return productWarehouse?.quantity ?? 0; + if (live) return live.quantity ?? 0; + return productStockCacheRef.current.get(productId)?.quantity ?? 0; }, [productWarehouseOptions, type] ); const hasAvailableQty = useCallback( (productId: number) => { - const productWarehouse = productWarehouseOptions.find( + const live = productWarehouseOptions.find( (pw) => pw.product_id === productId ); - return productWarehouse?.transfer_available_qty !== undefined; + if (live) return live.transfer_available_qty !== undefined; + return ( + productStockCacheRef.current.get(productId)?.transfer_available_qty !== + undefined + ); }, [productWarehouseOptions] ); From b5a0614218bb8f7390e4a2aacb697976b1dd1685 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Fri, 22 May 2026 11:13:52 +0700 Subject: [PATCH 25/27] feat: implement url query param tab navigation --- src/app/report/expense/layout.tsx | 11 ++++++++ .../report/expense/ReportExpenseTabs.tsx | 26 ++++++++++++++----- 2 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 src/app/report/expense/layout.tsx diff --git a/src/app/report/expense/layout.tsx b/src/app/report/expense/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/report/expense/layout.tsx @@ -0,0 +1,11 @@ +import SuspenseHelper from '@/components/helper/SuspenseHelper'; + +const Layout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return {children}; +}; + +export default Layout; diff --git a/src/components/pages/report/expense/ReportExpenseTabs.tsx b/src/components/pages/report/expense/ReportExpenseTabs.tsx index 045e7a99..f700a0bb 100644 --- a/src/components/pages/report/expense/ReportExpenseTabs.tsx +++ b/src/components/pages/report/expense/ReportExpenseTabs.tsx @@ -1,26 +1,38 @@ 'use client'; -import { useState } from 'react'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import Tabs from '@/components/Tabs'; import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import ReportExpenseTab from '@/components/pages/report/expense/tab/ReportExpenseTab'; import ReportDepreciationTab from '@/components/pages/report/expense/tab/ReportDepreciationTab'; +const VALID_TAB_IDS = ['operational-expense', 'depreciation']; + const ReportExpenseTabs = () => { - const [activeTabId, setActiveTabId] = useState('1'); + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const tabParam = searchParams.get('tab') ?? 'operational-expense'; + const activeTabId = VALID_TAB_IDS.includes(tabParam) + ? tabParam + : 'operational-expense'; const tabActions = useTabActionsStore((state) => state.tabActions); + const handleTabChange = (tabId: string) => { + router.push(`${pathname}?tab=${tabId}`); + }; + const tabs = [ { - id: '1', + id: 'operational-expense', label: 'Laporan Biaya Operasional', - content: , + content: , }, { - id: '2', + id: 'depreciation', label: 'Laporan Depresiasi', - content: , + content: , }, ]; @@ -30,7 +42,7 @@ const ReportExpenseTabs = () => { tabs={tabs} variant='boxed' activeTabId={activeTabId} - onTabChange={setActiveTabId} + onTabChange={handleTabChange} className={{ tabHeaderWrapper: 'justify-between items-center p-3 border-b border-base-content/10', From 05138dbb6f1669e5b2f3ebbb70af85f244a38e82 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Fri, 22 May 2026 11:14:16 +0700 Subject: [PATCH 26/27] feat: implement table filter state persist --- .../tab/ReportDepreciationFilterModal.tsx | 137 +++++++----------- .../expense/tab/ReportDepreciationTab.tsx | 33 +++-- 2 files changed, 70 insertions(+), 100 deletions(-) diff --git a/src/components/pages/report/expense/tab/ReportDepreciationFilterModal.tsx b/src/components/pages/report/expense/tab/ReportDepreciationFilterModal.tsx index 4c4a814e..a334e137 100644 --- a/src/components/pages/report/expense/tab/ReportDepreciationFilterModal.tsx +++ b/src/components/pages/report/expense/tab/ReportDepreciationFilterModal.tsx @@ -1,6 +1,6 @@ 'use client'; -import { RefObject, useEffect, useMemo, useState } from 'react'; +import { RefObject } from 'react'; import { useFormik } from 'formik'; import * as yup from 'yup'; @@ -20,32 +20,34 @@ import { Location } from '@/types/api/master-data/location'; import { ProjectFlock } from '@/types/api/production/project-flock'; export type ReportDepreciationFilterValues = { - area_id: string | null; - location_id: string | null; - project_flock_id: string | null; + area?: OptionType; + location?: OptionType; + projectFlock?: OptionType; period: string | null; }; export const ReportDepreciationFilterSchema = yup.object({ - area_id: yup.string().nullable(), - location_id: yup.string().nullable(), - project_flock_id: yup.string().nullable(), + area: yup.mixed>().optional(), + location: yup.mixed>().optional(), + projectFlock: yup.mixed>().optional(), period: yup.string().nullable().required('Periode wajib dipilih'), -}) as yup.ObjectSchema; +}); interface ReportDepreciationFilterModalProps { ref: RefObject; - initialValues?: ReportDepreciationFilterValues; + initialValues?: Partial; onSubmit?: (values: Partial) => void; onReset?: () => void; } -const defaultInitialValues: ReportDepreciationFilterValues = { - area_id: null, - location_id: null, - project_flock_id: null, - period: null, -}; +const defaultInitialValues: ( + initialValues?: Partial +) => ReportDepreciationFilterValues = (initialValues) => ({ + area: undefined, + location: undefined, + projectFlock: undefined, + period: initialValues?.period ?? null, +}); const ReportDepreciationFilterModal = ({ ref, @@ -53,22 +55,19 @@ const ReportDepreciationFilterModal = ({ onSubmit, onReset, }: ReportDepreciationFilterModalProps) => { - const [selectedAreaId, setSelectedAreaId] = useState( - initialValues?.area_id || undefined - ); - const [selectedLocationId, setSelectedLocationId] = useState< - string | undefined - >(initialValues?.location_id || undefined); - - useEffect(() => { - setSelectedAreaId(initialValues?.area_id || undefined); - setSelectedLocationId(initialValues?.location_id || undefined); - }, [initialValues?.area_id, initialValues?.location_id]); - const closeModalHandler = () => { ref.current?.close(); }; + const formik = useFormik({ + initialValues: { ...defaultInitialValues(initialValues), ...initialValues }, + validationSchema: ReportDepreciationFilterSchema, + onSubmit: async (values) => { + onSubmit?.(values); + closeModalHandler(); + }, + }); + const { setInputValue: setAreaInputValue, options: areaOptions, @@ -82,7 +81,7 @@ const ReportDepreciationFilterModal = ({ isLoadingOptions: isLoadingLocationOptions, loadMore: loadMoreLocations, } = useSelect(LocationApi.basePath, 'id', 'name', 'search', { - area_id: selectedAreaId || '', + area_id: String(formik.values.area?.value ?? ''), }); const { @@ -96,73 +95,35 @@ const ReportDepreciationFilterModal = ({ 'flock_name', 'search', { - location_id: selectedLocationId || '', + location_id: String(formik.values.location?.value ?? ''), } ); - const formik = useFormik({ - initialValues: initialValues || defaultInitialValues, - enableReinitialize: true, - validationSchema: ReportDepreciationFilterSchema, - onSubmit: async (values) => { - onSubmit?.(values); - closeModalHandler(); - }, - onReset: (_) => { - onReset?.(); - closeModalHandler(); - }, - }); - - const areaValue = useMemo(() => { - if (!formik.values.area_id) return null; - return ( - areaOptions.find((opt) => String(opt.value) === formik.values.area_id) || - null - ); - }, [formik.values.area_id, areaOptions]); - - const locationValue = useMemo(() => { - if (!formik.values.location_id) return null; - return ( - locationOptions.find( - (opt) => String(opt.value) === formik.values.location_id - ) || null - ); - }, [formik.values.location_id, locationOptions]); - - const projectFlockValue = useMemo(() => { - if (!formik.values.project_flock_id) return null; - return ( - projectFlockOptions.find( - (opt) => String(opt.value) === formik.values.project_flock_id - ) || null - ); - }, [formik.values.project_flock_id, projectFlockOptions]); + const formikResetHandler = () => { + onReset?.(); + formik.resetForm({ values: defaultInitialValues(initialValues) }); + closeModalHandler(); + }; const areaChangeHandler = (val: OptionType | OptionType[] | null) => { - const areaId = val && !Array.isArray(val) ? String(val.value) : null; - - setSelectedAreaId(areaId || undefined); - formik.setFieldValue('area_id', areaId); - formik.setFieldValue('location_id', null); - formik.setFieldValue('project_flock_id', null); - setSelectedLocationId(undefined); + const area = + val && !Array.isArray(val) ? (val as OptionType) : undefined; + formik.setFieldValue('area', area); + formik.setFieldValue('location', undefined); + formik.setFieldValue('projectFlock', undefined); }; const locationChangeHandler = (val: OptionType | OptionType[] | null) => { - const locationId = val && !Array.isArray(val) ? String(val.value) : null; - - setSelectedLocationId(locationId || undefined); - formik.setFieldValue('location_id', locationId); - formik.setFieldValue('project_flock_id', null); + const location = + val && !Array.isArray(val) ? (val as OptionType) : undefined; + formik.setFieldValue('location', location); + formik.setFieldValue('projectFlock', undefined); }; const projectFlockChangeHandler = (val: OptionType | OptionType[] | null) => { - const projectFlockId = - val && !Array.isArray(val) ? String(val.value) : null; - - formik.setFieldValue('project_flock_id', projectFlockId); + const projectFlock = + val && !Array.isArray(val) ? (val as OptionType) : undefined; + formik.setFieldValue('projectFlock', projectFlock); }; return ( @@ -174,7 +135,7 @@ const ReportDepreciationFilterModal = ({ >
@@ -199,7 +160,7 @@ const ReportDepreciationFilterModal = ({ label='Area' placeholder='Pilih Area' options={areaOptions} - value={areaValue} + value={formik.values.area ?? null} onChange={areaChangeHandler} onInputChange={setAreaInputValue} onMenuScrollToBottom={loadMoreAreas} @@ -213,7 +174,7 @@ const ReportDepreciationFilterModal = ({ label='Lokasi' placeholder='Pilih Lokasi' options={locationOptions} - value={locationValue} + value={formik.values.location ?? null} onChange={locationChangeHandler} onInputChange={setLocationInputValue} onMenuScrollToBottom={loadMoreLocations} @@ -227,7 +188,7 @@ const ReportDepreciationFilterModal = ({ label='Project Flock' placeholder='Pilih Project Flock' options={projectFlockOptions} - value={projectFlockValue} + value={formik.values.projectFlock ?? null} onChange={projectFlockChangeHandler} onInputChange={setProjectFlockInputValue} onMenuScrollToBottom={loadMoreProjectFlocks} diff --git a/src/components/pages/report/expense/tab/ReportDepreciationTab.tsx b/src/components/pages/report/expense/tab/ReportDepreciationTab.tsx index 41c2f9e8..abab3df7 100644 --- a/src/components/pages/report/expense/tab/ReportDepreciationTab.tsx +++ b/src/components/pages/report/expense/tab/ReportDepreciationTab.tsx @@ -17,6 +17,7 @@ import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import { ReportDepreciation } from '@/types/api/report/report-expense'; import { DepreciationReportApi } from '@/services/api/report/expense-report'; import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { OptionType } from '@/components/input/SelectInput'; import { isResponseSuccess } from '@/lib/api-helper'; import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; @@ -32,20 +33,27 @@ const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => { setPageSize, toQueryString: getTableFilterQueryString, reset: resetFilter, - } = useTableFilter({ + } = useTableFilter<{ + area?: OptionType; + location?: OptionType; + projectFlock?: OptionType; + period: string; + }>({ initial: { - area_id: '', - location_id: '', - project_flock_id: '', + area: undefined, + location: undefined, + projectFlock: undefined, period: formatDate(Date.now(), 'YYYY-MM-DD'), }, paramMap: { pageSize: 'limit', - area_id: 'area_id', - location_id: 'location_id', - project_flock_id: 'project_flock_id', + area: 'area_id', + location: 'location_id', + projectFlock: 'project_flock_id', period: 'period', }, + persist: true, + storeName: 'report-depreciation-table', }); const { data: depreciationsResponse, isLoading: isLoadingDepreciations } = @@ -109,7 +117,7 @@ const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => { filterModal.openModal()} + onClick={filterModal.openModal} variant='outline' className='px-3 py-2.5' /> @@ -239,12 +247,13 @@ const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => { initialValues={tableFilterState} onReset={resetFilter} onSubmit={(values) => { - updateFilter('area_id', values.area_id ?? ''); - updateFilter('location_id', values.location_id ?? ''); - updateFilter('project_flock_id', values.project_flock_id ?? ''); + updateFilter('area', values.area, true); + updateFilter('location', values.location, true); + updateFilter('projectFlock', values.projectFlock, true); updateFilter( 'period', - values.period ? formatDate(values.period, 'YYYY-MM-DD') : '' + values.period ? formatDate(values.period, 'YYYY-MM-DD') : '', + true ); }} /> From 22b3350e4a8491ac919d050b73cb79c2473c38f5 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 25 May 2026 10:37:41 +0700 Subject: [PATCH 27/27] fix: set flock source and destination raw data accordingly --- .../TransferToLayingFormModal.tsx | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx b/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx index bbfe64f4..b7dbf35f 100644 --- a/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx +++ b/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx @@ -233,13 +233,8 @@ const TransferToLayingFormModal = () => { const [maxSourceQuantity, setMaxSourceQuantity] = useState(0); - const selectedFlockDestinationRawData = isResponseSuccess( - flockDestinationRawData - ) - ? flockDestinationRawData.data.find( - (item) => item.id === formik.values.flockDestination?.value - ) - : undefined; + const [selectedFlockDestinationRawData, setSelectedFlockDestinationRawData] = + useState(undefined); const { data: flockSourceKandangsAvailability } = useSWR( formik.values.flockSource @@ -456,15 +451,37 @@ const TransferToLayingFormModal = () => { }, [transferToLayingId, transferToLaying]); useEffect(() => { + if (!formik.values.flockSource) { + setSelectedFlockSourceRawData(undefined); + return; + } + if (isResponseSuccess(flockSourceRawData)) { - const currentSelectedFlockSourceRawData = flockSourceRawData.data.find( + const found = flockSourceRawData.data.find( (item) => item.id === formik.values.flockSource?.value ); - - setSelectedFlockSourceRawData(currentSelectedFlockSourceRawData); + if (found) { + setSelectedFlockSourceRawData(found); + } } }, [flockSourceRawData, formikFlockSource]); + useEffect(() => { + if (!formik.values.flockDestination) { + setSelectedFlockDestinationRawData(undefined); + return; + } + + if (isResponseSuccess(flockDestinationRawData)) { + const found = flockDestinationRawData.data.find( + (item) => item.id === formik.values.flockDestination?.value + ); + if (found) { + setSelectedFlockDestinationRawData(found); + } + } + }, [flockDestinationRawData, formik.values.flockDestination]); + useEffect(() => { formik.setFieldValue('totalQuantity', totalTransferedChicken); formik.setFieldValue('maxTotalQuantity', totalTransferedChicken);