From 8d586e7cb4e307060aeb2a555033740e7c24932e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 13:53:45 +0700 Subject: [PATCH 001/139] refactor(FE): Switch FinanceApi to production and remove import --- src/services/api/report/finance-report.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/services/api/report/finance-report.ts b/src/services/api/report/finance-report.ts index 9fa4f37c..3a564e32 100644 --- a/src/services/api/report/finance-report.ts +++ b/src/services/api/report/finance-report.ts @@ -1,7 +1,6 @@ import { BaseApiService } from '@/services/api/base'; import { BaseApiResponse } from '@/types/api/api-general'; import { CustomerPaymentReport } from '@/types/api/report/customer-payment'; -import { DebtSupplier } from '@/types/api/report/debt-supplier'; export class FinanceApiService extends BaseApiService< CustomerPaymentReport, @@ -39,8 +38,8 @@ export class FinanceApiService extends BaseApiService< } } -// export const FinanceApi = new FinanceApiService('reports'); +export const FinanceApi = new FinanceApiService('reports'); -export const FinanceApi = new FinanceApiService( - 'http://localhost:4010/api/reports/finance' -); +// export const FinanceApi = new FinanceApiService( +// 'http://localhost:4010/api/reports/finance' +// ); From 01e94b57c15a29ebec567ac353baa3940f04cc23 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 14:14:48 +0700 Subject: [PATCH 002/139] refactor(FE): Disable row selection for approved recordings --- .../pages/production/recording/RecordingTable.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 6b10b26e..a99cfbbc 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -656,13 +656,23 @@ const RecordingTable = () => { ); }, cell: ({ row }) => { + const recording = row.original; + const isDisabled = isRecordingApproved(recording); + + const handleToggleSelection = (e: unknown) => { + if (!isDisabled) { + row.getToggleSelectedHandler()(e); + } + }; + return ( -
+
); From 66fa65e4bb24e4a3b250c85f8ecd6eeac1abf5ef Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 14:29:40 +0700 Subject: [PATCH 003/139] refactor(FE): Disable sales and filter_by until backend ready --- .../export/CustomerPaymentExportPDF.tsx | 18 +++-- .../report/finance/tab/CustomerPaymentTab.tsx | 71 ++++++++++--------- src/services/api/report/finance-report.ts | 10 ++- 3 files changed, 55 insertions(+), 44 deletions(-) diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx index 5adcb694..5a656e7a 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx @@ -177,10 +177,12 @@ interface CustomerPaymentExportPDFParams { data: CustomerPaymentReport[]; params?: { customer_name?: string; - sales?: string; + // TODO: Uncomment when BE is ready + // sales?: string; start_date?: string; end_date?: string; - filter_by?: string; + // TODO: Uncomment when BE is ready + // filter_by?: string; }; } @@ -195,9 +197,10 @@ const getParameterText = ( paramsText.push('Semua Customer'); } - if (params?.sales) { - paramsText.push(`Sales: ${params.sales}`); - } + // TODO: Uncomment when BE is ready + // if (params?.sales) { + // paramsText.push(`Sales: ${params.sales}`); + // } if (params?.start_date && params?.end_date) { const startDate = formatDate(params.start_date, 'DD MMM YYYY'); @@ -242,9 +245,10 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { : '-'} - + {/* TODO: Uncomment when BE is ready */} + {/* Filter Tanggal: Tanggal DO - + */} Customer: {params.params?.customer_name || 'Semua Customer'} diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 9119e80d..f0b5850c 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -47,6 +47,8 @@ const CustomerPaymentTab = () => { const [filterCustomer, setFilterCustomer] = useState( [] ); + // TODO: Uncomment when BE is ready + // const [filterSales, setFilterSales] = useState([]); const [filterSales, setFilterSales] = useState([]); const [filterStartDate, setFilterStartDate] = useState(''); const [filterEndDate, setFilterEndDate] = useState(''); @@ -60,6 +62,7 @@ const CustomerPaymentTab = () => { hasMore: hasMoreCustomers, } = useSelect(CustomerApi.basePath, 'id', 'name', 'search'); + // TODO: Uncomment when BE is ready const { options: salesOptions, isLoadingOptions: isLoadingSales, @@ -133,23 +136,18 @@ const CustomerPaymentTab = () => { count += 1; } - // Sales filter - if (filterSales.length > 0) { - count += 1; - } - - // Filter by (always count if submitted) - if (isSubmitted) { - count += 1; - } + // TODO: Uncomment when BE is ready + // // Sales filter + // if (filterSales.length > 0) { + // count += 1; + // } return count; }, [ filterStartDate, filterEndDate, filterCustomer, - filterSales, - isSubmitted, + // filterSales, ]); const hasFilters = activeFiltersCount > 0; @@ -163,11 +161,12 @@ const CustomerPaymentTab = () => { filterCustomer.length > 0 ? filterCustomer.map((v) => String(v.value)).join(',') : undefined, - sales_id: - filterSales.length > 0 - ? filterSales.map((v) => String(v.value)).join(',') - : undefined, - filter_by: 'do_date' as const, + // TODO: Uncomment when BE is ready + // sales_id: + // filterSales.length > 0 + // ? filterSales.map((v) => String(v.value)).join(',') + // : undefined, + // filter_by: 'do_date' as const, start_date: filterStartDate || undefined, end_date: filterEndDate || undefined, page: currentPage, @@ -180,8 +179,8 @@ const CustomerPaymentTab = () => { ([, params]) => FinanceApi.getCustomerPaymentReport( params.customer_id, - params.sales_id, - params.filter_by, + undefined, // TODO: Change to params.sales_id when BE is ready + undefined, // TODO: Change to params.filter_by when BE is ready params.start_date, params.end_date, params.page, @@ -206,11 +205,11 @@ const CustomerPaymentTab = () => { filterCustomer.length > 0 ? filterCustomer.map((v) => String(v.value)).join(',') : undefined, - sales_id: - filterSales.length > 0 - ? filterSales.map((v) => String(v.value)).join(',') - : undefined, - filter_by: 'do_date' as const, + // TODO: Uncomment when BE is ready + // sales_id: + // filterSales.length > 0 + // ? filterSales.map((v) => String(v.value)).join(',') + // : undefined, start_date: filterStartDate || undefined, end_date: filterEndDate || undefined, limit: 100, @@ -219,8 +218,8 @@ const CustomerPaymentTab = () => { const response = await FinanceApi.getCustomerPaymentReport( params.customer_id, - params.sales_id, - params.filter_by, + undefined, // TODO: Change to params.sales_id when BE is ready + undefined, // TODO: Change to params.filter_by when BE is ready params.start_date, params.end_date, params.page, @@ -277,13 +276,15 @@ const CustomerPaymentTab = () => { filterCustomer.length > 0 ? filterCustomer.map((c) => c.label).join(', ') : undefined, - sales: - filterSales.length > 0 - ? filterSales.map((s) => s.label).join(', ') - : undefined, + // TODO: Uncomment when BE is ready + // sales: + // filterSales.length > 0 + // ? filterSales.map((s) => s.label).join(', ') + // : undefined, start_date: filterStartDate || undefined, end_date: filterEndDate || undefined, - filter_by: 'do_date', + // TODO: Uncomment when BE is ready + // filter_by: 'do_date' as const, }, }); toast.success('PDF berhasil dibuat dan diunduh.'); @@ -661,7 +662,8 @@ const CustomerPaymentTab = () => { />
-
+ {/* TODO: Uncomment when BE is ready */} + {/*
{ onMenuScrollToBottom={loadMoreSales} className={{ wrapper: 'w-full' }} /> -
+
*/} -
+ {/* TODO: Uncomment when BE is ready */} + {/*
{ isDisabled={true} className={{ wrapper: 'w-full' }} /> -
+
*/} {/* Action Buttons */} diff --git a/src/services/api/report/finance-report.ts b/src/services/api/report/finance-report.ts index 3a564e32..81f23481 100644 --- a/src/services/api/report/finance-report.ts +++ b/src/services/api/report/finance-report.ts @@ -13,8 +13,11 @@ export class FinanceApiService extends BaseApiService< async getCustomerPaymentReport( customer_id?: string, + // TODO: Uncomment when BE is ready + // sales_id?: string, + // filter_by?: 'do_date', sales_id?: string, - filter_by?: 'do_date', + filter_by?: 'do_date' | undefined, start_date?: string, end_date?: string, page?: number, @@ -26,8 +29,9 @@ export class FinanceApiService extends BaseApiService< method: 'GET', params: { customer_id: customer_id, - sales_id: sales_id, - filter_by: filter_by, + // TODO: Uncomment when BE is ready + // sales_id: sales_id, + // filter_by: filter_by, start_date: start_date, end_date: end_date, page: page, From 08c28f4077949cb3233717ff441f2876a2957f27 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 15:14:33 +0700 Subject: [PATCH 004/139] refactor(FE): Rename payment fields and add initial balance row --- .../export/CustomerPaymentExportPDF.tsx | 28 ++--- .../export/CustomerPaymentExportXLSX.tsx | 20 ++-- .../report/finance/tab/CustomerPaymentTab.tsx | 112 +++++++++++++++--- src/types/api/report/customer-payment.d.ts | 20 ++-- 4 files changed, 129 insertions(+), 51 deletions(-) diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx index 5a656e7a..d1092e22 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx @@ -347,13 +347,15 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { - {item.do_date ? formatDate(item.do_date, 'DD MMM YY') : '-'} + {item.trans_date + ? formatDate(item.trans_date, 'DD MMM YY') + : '-'} - {item.realization_date - ? formatDate(item.realization_date, 'DD MMM YY') + {item.delivery_date + ? formatDate(item.delivery_date, 'DD MMM YY') : '-'} @@ -366,7 +368,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { {item.reference || '-'} - {item.vehicle_plate || '-'} + {item.vehicle_numbers || '-'} {formatNumber(item.qty)} @@ -390,10 +392,10 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { {formatNumber(item.ppn)}% - {formatCurrency(item.total)} + {formatCurrency(item.total_price)} - {formatCurrency(item.payment)} + {formatCurrency(item.payment_amount)} @@ -401,30 +403,28 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { - {item.notes ? ( - {item.notes} - ) : ( + {item.status ? ( - {item.accounts_receivable === 0 - ? 'Lunas' - : 'Belum Lunas'} + {item.status === 'LUNAS' ? 'Lunas' : 'Belum Lunas'} + ) : ( + - )} {item.pickup_info || '-'} - {item.sales_marketing || '-'} + {item.sales_person || '-'} ))} diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx index d51aa3b7..ea7cfd77 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx @@ -24,17 +24,15 @@ export const generateCustomerPaymentExcel = ( const excelData: { [key: string]: string | number }[] = customerData.map( (item, index) => ({ No: index + 1, - 'Tanggal DO/Bayar': item.do_date - ? formatDate(item.do_date, 'DD MMM YYYY') + 'Tanggal DO/Bayar': item.trans_date + ? formatDate(item.trans_date, 'DD MMM YYYY') : '', - 'Tanggal Realisasi': item.realization_date - ? formatDate(item.realization_date, 'DD MMM YYYY') + 'Tanggal Realisasi': item.delivery_date + ? formatDate(item.delivery_date, 'DD MMM YYYY') : '', Aging: formatNumber(item.aging_day || 0), Referensi: item.reference || '', - 'Nomor Polisi': Array.isArray(item.vehicle_plate) - ? item.vehicle_plate.join(', ') - : '', + 'Nomor Polisi': item.vehicle_numbers || '', 'Ekor/Qty': formatNumber(item.qty || 0), 'Berat (Kg)': formatNumber(item.weight || 0), AVG: formatNumber(item.average_weight || 0), @@ -42,12 +40,12 @@ export const generateCustomerPaymentExcel = ( CN: formatCurrency(item.credit_note || 0), 'Harga Akhir': formatCurrency(item.final_price || 0), 'PPN (%)': formatNumber(item.ppn || 0), - Total: formatCurrency(item.total || 0), - Pembayaran: formatCurrency(item.payment || 0), + Total: formatCurrency(item.total_price || 0), + Pembayaran: formatCurrency(item.payment_amount || 0), 'Saldo Piutang': formatCurrency(item.accounts_receivable || 0), - Keterangan: item.notes || '', + Keterangan: item.status || '', Pengambilan: item.pickup_info || '', - 'Sales/Marketing': item.sales_marketing || '', + 'Sales/Marketing': item.sales_person || '', }) ); diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index f0b5850c..ef320c15 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -302,24 +302,37 @@ const CustomerPaymentTab = () => { { id: 'no', header: 'No', - cell: (props) => props.row.index + 1, + cell: (props) => { + const isInitialBalanceRow = + Object.keys(props.row.original).length === 1 && + 'accounts_receivable' in props.row.original; + return isInitialBalanceRow ? 'Saldo Awal' : props.row.index; + }, footer: () =>
Total
, }, { id: 'do_date_or_payment_date', header: 'Tanggal DO/Bayar', - accessorKey: 'do_date', + accessorKey: 'trans_date', cell: (props) => { - const value = props.row.original.do_date; + const isInitialBalanceRow = + Object.keys(props.row.original).length === 1 && + 'accounts_receivable' in props.row.original; + if (isInitialBalanceRow) return '-'; + const value = props.row.original.trans_date; return formatDate(value, 'DD MMM YYYY'); }, }, { id: 'realization_date', header: 'Tanggal Realisasi', - accessorKey: 'realization_date', + accessorKey: 'delivery_date', cell: (props) => { - const value = props.row.original.realization_date; + const isInitialBalanceRow = + Object.keys(props.row.original).length === 1 && + 'accounts_receivable' in props.row.original; + if (isInitialBalanceRow) return '-'; + const value = props.row.original.delivery_date; return formatDate(value, 'DD MMM YYYY'); }, }, @@ -328,6 +341,10 @@ const CustomerPaymentTab = () => { header: 'Aging', accessorKey: 'aging_day', cell: (props) => { + const isInitialBalanceRow = + Object.keys(props.row.original).length === 1 && + 'accounts_receivable' in props.row.original; + if (isInitialBalanceRow) return '-'; const value = props.row.original.aging_day; return (
@@ -341,6 +358,10 @@ const CustomerPaymentTab = () => { header: 'Referensi', accessorKey: 'reference', cell: (props) => { + const isInitialBalanceRow = + Object.keys(props.row.original).length === 1 && + 'accounts_receivable' in props.row.original; + if (isInitialBalanceRow) return '-'; const value = props.row.original.reference; return value || '-'; }, @@ -348,9 +369,13 @@ const CustomerPaymentTab = () => { { id: 'vehicle_plate', header: 'Nomor Polisi', - accessorKey: 'vehicle_plate', + accessorKey: 'vehicle_numbers', cell: (props) => { - const value = props.row.original.vehicle_plate; + const isInitialBalanceRow = + Object.keys(props.row.original).length === 1 && + 'accounts_receivable' in props.row.original; + if (isInitialBalanceRow) return '-'; + const value = props.row.original.vehicle_numbers; return value || '-'; }, }, @@ -359,6 +384,10 @@ const CustomerPaymentTab = () => { header: 'Ekor/Qty', accessorKey: 'qty', cell: (props) => { + const isInitialBalanceRow = + Object.keys(props.row.original).length === 1 && + 'accounts_receivable' in props.row.original; + if (isInitialBalanceRow) return
-
; const value = props.row.original.qty; return
{formatNumber(value)}
; }, @@ -373,6 +402,10 @@ const CustomerPaymentTab = () => { header: 'Berat (Kg)', accessorKey: 'weight', cell: (props) => { + const isInitialBalanceRow = + Object.keys(props.row.original).length === 1 && + 'accounts_receivable' in props.row.original; + if (isInitialBalanceRow) return
-
; const value = props.row.original.weight; return
{formatNumber(value)}
; }, @@ -387,6 +420,10 @@ const CustomerPaymentTab = () => { header: 'AVG', accessorKey: 'average_weight', cell: (props) => { + const isInitialBalanceRow = + Object.keys(props.row.original).length === 1 && + 'accounts_receivable' in props.row.original; + if (isInitialBalanceRow) return
-
; const value = props.row.original.average_weight; return
{formatNumber(value)}
; }, @@ -399,6 +436,10 @@ const CustomerPaymentTab = () => { header: 'Harga Awal', accessorKey: 'price', cell: (props) => { + const isInitialBalanceRow = + Object.keys(props.row.original).length === 1 && + 'accounts_receivable' in props.row.original; + if (isInitialBalanceRow) return
-
; const value = props.row.original.price; return
{formatCurrency(value)}
; }, @@ -413,6 +454,10 @@ const CustomerPaymentTab = () => { header: 'CN', accessorKey: 'credit_note', cell: (props) => { + const isInitialBalanceRow = + Object.keys(props.row.original).length === 1 && + 'accounts_receivable' in props.row.original; + if (isInitialBalanceRow) return
-
; const value = props.row.original.credit_note; return
{formatCurrency(value)}
; }, @@ -427,6 +472,10 @@ const CustomerPaymentTab = () => { header: 'Harga Akhir', accessorKey: 'final_price', cell: (props) => { + const isInitialBalanceRow = + Object.keys(props.row.original).length === 1 && + 'accounts_receivable' in props.row.original; + if (isInitialBalanceRow) return
-
; const value = props.row.original.final_price; return
{formatCurrency(value)}
; }, @@ -441,6 +490,10 @@ const CustomerPaymentTab = () => { header: 'PPN (%)', accessorKey: 'ppn', cell: (props) => { + const isInitialBalanceRow = + Object.keys(props.row.original).length === 1 && + 'accounts_receivable' in props.row.original; + if (isInitialBalanceRow) return
-
; const value = props.row.original.ppn; return
{formatNumber(value)}%
; }, @@ -451,9 +504,13 @@ const CustomerPaymentTab = () => { { id: 'total', header: 'Total', - accessorKey: 'total', + accessorKey: 'total_price', cell: (props) => { - const value = props.row.original.total; + const isInitialBalanceRow = + Object.keys(props.row.original).length === 1 && + 'accounts_receivable' in props.row.original; + if (isInitialBalanceRow) return
-
; + const value = props.row.original.total_price; return
{formatCurrency(value)}
; }, footer: () => ( @@ -465,9 +522,13 @@ const CustomerPaymentTab = () => { { id: 'payment', header: 'Pembayaran', - accessorKey: 'payment', + accessorKey: 'payment_amount', cell: (props) => { - const value = props.row.original.payment; + const isInitialBalanceRow = + Object.keys(props.row.original).length === 1 && + 'accounts_receivable' in props.row.original; + if (isInitialBalanceRow) return
-
; + const value = props.row.original.payment_amount; return
{formatCurrency(value)}
; }, footer: () => ( @@ -495,9 +556,13 @@ const CustomerPaymentTab = () => { { id: 'notes', header: 'Keterangan', - accessorKey: 'notes', + accessorKey: 'status', cell: (props) => { - const value = props.row.original.notes; + const isInitialBalanceRow = + Object.keys(props.row.original).length === 1 && + 'accounts_receivable' in props.row.original; + if (isInitialBalanceRow) return '-'; + const value = props.row.original.status; if (!value) { return '-'; @@ -522,6 +587,10 @@ const CustomerPaymentTab = () => { header: 'Pengambilan', accessorKey: 'pickup_info', cell: (props) => { + const isInitialBalanceRow = + Object.keys(props.row.original).length === 1 && + 'accounts_receivable' in props.row.original; + if (isInitialBalanceRow) return '-'; const value = props.row.original.pickup_info; return value || '-'; }, @@ -529,9 +598,13 @@ const CustomerPaymentTab = () => { { id: 'sales_marketing', header: 'Sales/Marketing', - accessorKey: 'sales_marketing', + accessorKey: 'sales_person', cell: (props) => { - const value = props.row.original.sales_marketing; + const isInitialBalanceRow = + Object.keys(props.row.original).length === 1 && + 'accounts_receivable' in props.row.original; + if (isInitialBalanceRow) return '-'; + const value = props.row.original.sales_person; return value || '-'; }, }, @@ -754,9 +827,14 @@ const CustomerPaymentTab = () => { collapsible={true} > 0} className={{ containerClassName: 'w-full', diff --git a/src/types/api/report/customer-payment.d.ts b/src/types/api/report/customer-payment.d.ts index bfa059c9..8c648cb9 100644 --- a/src/types/api/report/customer-payment.d.ts +++ b/src/types/api/report/customer-payment.d.ts @@ -2,12 +2,12 @@ import { BaseCustomer } from '@/types/api/master-data/customer'; import { BaseMetadata } from '@/types/api/api-general'; export type CustomerPaymentRow = { - id: number; - do_date: string; - realization_date: string; - aging_day: number | null; + transaction_type: string; + transaction_id: number; + trans_date: string; + delivery_date: string | null; reference: string; - vehicle_plate: string[]; + vehicle_numbers: string; qty: number; weight: number; average_weight: number; @@ -15,12 +15,13 @@ export type CustomerPaymentRow = { credit_note: number; final_price: number; ppn: number; - total: number; - payment: number; + total_price: number; + payment_amount: number; accounts_receivable: number; - notes: string; + aging_day: number; + status: string; pickup_info: string; - sales_marketing: string; + sales_person: string; }; export type CustomerPaymentSummary = { @@ -37,6 +38,7 @@ export type CustomerPaymentSummary = { export type CustomerPaymentReport = BaseMetadata & { customer: BaseCustomer; + initial_balance: number; rows: CustomerPaymentRow[]; summary: CustomerPaymentSummary; }; From cb8a1a17ac914fb4b8db0f274f120a208cd29abe Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 14 Jan 2026 15:36:21 +0700 Subject: [PATCH 005/139] feat(FE): adding export feature on dashboard --- src/components/helper/ButtonFilter.tsx | 8 +- src/components/helper/form/FormErrors.tsx | 2 +- .../pages/dashboard/DashboardProduction.tsx | 113 ++++++++---------- 3 files changed, 58 insertions(+), 65 deletions(-) diff --git a/src/components/helper/ButtonFilter.tsx b/src/components/helper/ButtonFilter.tsx index 81f70b92..90343fed 100644 --- a/src/components/helper/ButtonFilter.tsx +++ b/src/components/helper/ButtonFilter.tsx @@ -1,5 +1,6 @@ import Button, { ButtonProps } from '@/components/Button'; import { getFilledFormikValuesCount } from '@/lib/formik-helper'; +import { cn } from '@/lib/helper'; import { Icon } from '@iconify/react'; import { FormikValues } from 'formik'; @@ -13,11 +14,12 @@ const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => { - + } + className={{ + content: 'w-full', + }} > - - Export - - + + + + @@ -287,7 +276,7 @@ const DashboardProduction = () => { {/* Rentang Waktu */}
-
+
{ Boolean(formik.touched.startDate) } /> - +
{
)} - +
+ +
{/* Action Buttons */}
From 524036a6bf5e8246fc0e09d6f5a85b590fb95aa9 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 14 Jan 2026 15:43:53 +0700 Subject: [PATCH 006/139] fix(FE): implement lazy load options select --- .../pages/dashboard/DashboardProduction.tsx | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/components/pages/dashboard/DashboardProduction.tsx b/src/components/pages/dashboard/DashboardProduction.tsx index 68a4bc11..cf5eeaa2 100644 --- a/src/components/pages/dashboard/DashboardProduction.tsx +++ b/src/components/pages/dashboard/DashboardProduction.tsx @@ -70,22 +70,32 @@ const DashboardProduction = () => { : undefined; // ===== SELECT ===== - const { options: flockOptions, isLoadingOptions: isLoadingFlockOptions } = - useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', '', { - limit: 'limit', - location_id: selectedLocationIds ? selectedLocationIds.toString() : '', - }); const { + setInputValue: setInputValueFlock, + options: flockOptions, + isLoadingOptions: isLoadingFlockOptions, + loadMore: loadMoreFlock, + } = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', '', { + limit: 'limit', + location_id: selectedLocationIds ? selectedLocationIds.toString() : '', + }); + const { + setInputValue: setInputValueLocation, options: locationOptions, isLoadingOptions: isLoadingLocationOptions, + loadMore: loadMoreLocation, } = useSelect(LocationApi.basePath, 'id', 'name', '', { limit: 'limit', }); - const { options: kandangOptions, isLoadingOptions: isLoadingKandangOptions } = - useSelect(KandangApi.basePath, 'id', 'name', '', { - limit: 'limit', - location_id: selectedLocationIds ? selectedLocationIds.toString() : '', - }); + const { + setInputValue: setInputValueKandang, + options: kandangOptions, + isLoadingOptions: isLoadingKandangOptions, + loadMore: loadMoreKandang, + } = useSelect(KandangApi.basePath, 'id', 'name', '', { + limit: 'limit', + location_id: selectedLocationIds ? selectedLocationIds.toString() : '', + }); const comparisonTypeOptions = [ { value: 'FARM', label: 'Farm' }, { value: 'FLOCK', label: 'Flock' }, @@ -372,6 +382,8 @@ const DashboardProduction = () => { { formik.setFieldValue('location', selected); // Update selectedLocationIds for kandang filter @@ -411,6 +423,8 @@ const DashboardProduction = () => { formik.setFieldValue('flock', selected) } errorMessage={formik.errors.flock as string} + onInputChange={setInputValueFlock} + onMenuScrollToBottom={loadMoreFlock} options={flockOptions} isLoading={isLoadingFlockOptions} isMulti={ @@ -439,6 +453,8 @@ const DashboardProduction = () => { formik.setFieldValue('kandang', selected) } errorMessage={formik.errors.kandang as string} + onInputChange={setInputValueKandang} + onMenuScrollToBottom={loadMoreKandang} options={kandangOptions} isLoading={isLoadingKandangOptions} isMulti={ From ab2175d903b89e69140e8d987d9368e66037ca56 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 15:46:03 +0700 Subject: [PATCH 007/139] refactor(FE): Simplify table cells and render initial balance row --- .../report/finance/tab/CustomerPaymentTab.tsx | 121 +++++++----------- 1 file changed, 45 insertions(+), 76 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index ef320c15..d60d740a 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -302,49 +302,35 @@ const CustomerPaymentTab = () => { { id: 'no', header: 'No', - cell: (props) => { - const isInitialBalanceRow = - Object.keys(props.row.original).length === 1 && - 'accounts_receivable' in props.row.original; - return isInitialBalanceRow ? 'Saldo Awal' : props.row.index; - }, + cell: (props) => props.row.index, footer: () =>
Total
, }, { id: 'do_date_or_payment_date', header: 'Tanggal DO/Bayar', accessorKey: 'trans_date', + enableSorting: false, cell: (props) => { - const isInitialBalanceRow = - Object.keys(props.row.original).length === 1 && - 'accounts_receivable' in props.row.original; - if (isInitialBalanceRow) return '-'; const value = props.row.original.trans_date; - return formatDate(value, 'DD MMM YYYY'); + return value ? formatDate(value, 'DD MMM YYYY') : '-'; }, }, { id: 'realization_date', header: 'Tanggal Realisasi', accessorKey: 'delivery_date', + enableSorting: false, cell: (props) => { - const isInitialBalanceRow = - Object.keys(props.row.original).length === 1 && - 'accounts_receivable' in props.row.original; - if (isInitialBalanceRow) return '-'; const value = props.row.original.delivery_date; - return formatDate(value, 'DD MMM YYYY'); + return value ? formatDate(value, 'DD MMM YYYY') : '-'; }, }, { id: 'aging', header: 'Aging', accessorKey: 'aging_day', + enableSorting: false, cell: (props) => { - const isInitialBalanceRow = - Object.keys(props.row.original).length === 1 && - 'accounts_receivable' in props.row.original; - if (isInitialBalanceRow) return '-'; const value = props.row.original.aging_day; return (
@@ -357,11 +343,8 @@ const CustomerPaymentTab = () => { id: 'reference', header: 'Referensi', accessorKey: 'reference', + enableSorting: false, cell: (props) => { - const isInitialBalanceRow = - Object.keys(props.row.original).length === 1 && - 'accounts_receivable' in props.row.original; - if (isInitialBalanceRow) return '-'; const value = props.row.original.reference; return value || '-'; }, @@ -370,11 +353,8 @@ const CustomerPaymentTab = () => { id: 'vehicle_plate', header: 'Nomor Polisi', accessorKey: 'vehicle_numbers', + enableSorting: false, cell: (props) => { - const isInitialBalanceRow = - Object.keys(props.row.original).length === 1 && - 'accounts_receivable' in props.row.original; - if (isInitialBalanceRow) return '-'; const value = props.row.original.vehicle_numbers; return value || '-'; }, @@ -383,11 +363,8 @@ const CustomerPaymentTab = () => { id: 'qty', header: 'Ekor/Qty', accessorKey: 'qty', + enableSorting: false, cell: (props) => { - const isInitialBalanceRow = - Object.keys(props.row.original).length === 1 && - 'accounts_receivable' in props.row.original; - if (isInitialBalanceRow) return
-
; const value = props.row.original.qty; return
{formatNumber(value)}
; }, @@ -401,11 +378,8 @@ const CustomerPaymentTab = () => { id: 'weight', header: 'Berat (Kg)', accessorKey: 'weight', + enableSorting: false, cell: (props) => { - const isInitialBalanceRow = - Object.keys(props.row.original).length === 1 && - 'accounts_receivable' in props.row.original; - if (isInitialBalanceRow) return
-
; const value = props.row.original.weight; return
{formatNumber(value)}
; }, @@ -419,11 +393,8 @@ const CustomerPaymentTab = () => { id: 'average_weight', header: 'AVG', accessorKey: 'average_weight', + enableSorting: false, cell: (props) => { - const isInitialBalanceRow = - Object.keys(props.row.original).length === 1 && - 'accounts_receivable' in props.row.original; - if (isInitialBalanceRow) return
-
; const value = props.row.original.average_weight; return
{formatNumber(value)}
; }, @@ -435,11 +406,8 @@ const CustomerPaymentTab = () => { id: 'price', header: 'Harga Awal', accessorKey: 'price', + enableSorting: false, cell: (props) => { - const isInitialBalanceRow = - Object.keys(props.row.original).length === 1 && - 'accounts_receivable' in props.row.original; - if (isInitialBalanceRow) return
-
; const value = props.row.original.price; return
{formatCurrency(value)}
; }, @@ -453,11 +421,8 @@ const CustomerPaymentTab = () => { id: 'credit_note', header: 'CN', accessorKey: 'credit_note', + enableSorting: false, cell: (props) => { - const isInitialBalanceRow = - Object.keys(props.row.original).length === 1 && - 'accounts_receivable' in props.row.original; - if (isInitialBalanceRow) return
-
; const value = props.row.original.credit_note; return
{formatCurrency(value)}
; }, @@ -471,11 +436,8 @@ const CustomerPaymentTab = () => { id: 'final_price', header: 'Harga Akhir', accessorKey: 'final_price', + enableSorting: false, cell: (props) => { - const isInitialBalanceRow = - Object.keys(props.row.original).length === 1 && - 'accounts_receivable' in props.row.original; - if (isInitialBalanceRow) return
-
; const value = props.row.original.final_price; return
{formatCurrency(value)}
; }, @@ -489,11 +451,8 @@ const CustomerPaymentTab = () => { id: 'ppn', header: 'PPN (%)', accessorKey: 'ppn', + enableSorting: false, cell: (props) => { - const isInitialBalanceRow = - Object.keys(props.row.original).length === 1 && - 'accounts_receivable' in props.row.original; - if (isInitialBalanceRow) return
-
; const value = props.row.original.ppn; return
{formatNumber(value)}%
; }, @@ -505,11 +464,8 @@ const CustomerPaymentTab = () => { id: 'total', header: 'Total', accessorKey: 'total_price', + enableSorting: false, cell: (props) => { - const isInitialBalanceRow = - Object.keys(props.row.original).length === 1 && - 'accounts_receivable' in props.row.original; - if (isInitialBalanceRow) return
-
; const value = props.row.original.total_price; return
{formatCurrency(value)}
; }, @@ -523,11 +479,8 @@ const CustomerPaymentTab = () => { id: 'payment', header: 'Pembayaran', accessorKey: 'payment_amount', + enableSorting: false, cell: (props) => { - const isInitialBalanceRow = - Object.keys(props.row.original).length === 1 && - 'accounts_receivable' in props.row.original; - if (isInitialBalanceRow) return
-
; const value = props.row.original.payment_amount; return
{formatCurrency(value)}
; }, @@ -541,6 +494,7 @@ const CustomerPaymentTab = () => { id: 'accounts_receivable', header: 'Saldo Piutang', accessorKey: 'accounts_receivable', + enableSorting: false, cell: (props) => { const value = props.row.original.accounts_receivable; return ( @@ -557,11 +511,8 @@ const CustomerPaymentTab = () => { id: 'notes', header: 'Keterangan', accessorKey: 'status', + enableSorting: false, cell: (props) => { - const isInitialBalanceRow = - Object.keys(props.row.original).length === 1 && - 'accounts_receivable' in props.row.original; - if (isInitialBalanceRow) return '-'; const value = props.row.original.status; if (!value) { @@ -586,11 +537,8 @@ const CustomerPaymentTab = () => { id: 'pickup_info', header: 'Pengambilan', accessorKey: 'pickup_info', + enableSorting: false, cell: (props) => { - const isInitialBalanceRow = - Object.keys(props.row.original).length === 1 && - 'accounts_receivable' in props.row.original; - if (isInitialBalanceRow) return '-'; const value = props.row.original.pickup_info; return value || '-'; }, @@ -599,11 +547,8 @@ const CustomerPaymentTab = () => { id: 'sales_marketing', header: 'Sales/Marketing', accessorKey: 'sales_person', + enableSorting: false, cell: (props) => { - const isInitialBalanceRow = - Object.keys(props.row.original).length === 1 && - 'accounts_receivable' in props.row.original; - if (isInitialBalanceRow) return '-'; const value = props.row.original.sales_person; return value || '-'; }, @@ -854,6 +799,30 @@ const CustomerPaymentTab = () => { 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', paginationClassName: 'hidden', }} + renderCustomRow={(row) => { + if (row.index === 0) { + return ( +
+ + + + + ); + } + }} /> ); From c5baff6f335a4b43182583d187ee4a5612805dd8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 15:55:20 +0700 Subject: [PATCH 008/139] refactor(FE): Update column headers in CustomerPaymentTab --- .../pages/report/finance/tab/CustomerPaymentTab.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index d60d740a..8e642097 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -307,7 +307,7 @@ const CustomerPaymentTab = () => { }, { id: 'do_date_or_payment_date', - header: 'Tanggal DO/Bayar', + header: 'Tanggal Jual/Bayar', accessorKey: 'trans_date', enableSorting: false, cell: (props) => { @@ -361,7 +361,7 @@ const CustomerPaymentTab = () => { }, { id: 'qty', - header: 'Ekor/Qty', + header: 'Qty', accessorKey: 'qty', enableSorting: false, cell: (props) => { From 3cb11f615892ad637e564e7994866689191813bd Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 14 Jan 2026 16:27:13 +0700 Subject: [PATCH 009/139] fix(FE): add empty state overlay on chart null value --- .../dashboard/chart/DashboardLineChart.tsx | 544 ++++++++++-------- 1 file changed, 297 insertions(+), 247 deletions(-) diff --git a/src/components/pages/dashboard/chart/DashboardLineChart.tsx b/src/components/pages/dashboard/chart/DashboardLineChart.tsx index e586b4a3..348f6c43 100644 --- a/src/components/pages/dashboard/chart/DashboardLineChart.tsx +++ b/src/components/pages/dashboard/chart/DashboardLineChart.tsx @@ -283,261 +283,311 @@ const DashboardLineChart = ({ })()} - {/* Chart */} - - { - // Transform data based on analysisMode - if (analysisMode === 'OVERVIEW') { - // For OVERVIEW mode, use the selected chart data - if (isOverviewCharts(data.charts)) { - const selectedChartData = data.charts[chartData]; - if (!selectedChartData || !selectedChartData.dataset) return []; - return selectedChartData.dataset; + {/* Chart Container with Empty State Overlay */} +
+ {/* Chart */} + + { + // Transform data based on analysisMode + if (analysisMode === 'OVERVIEW') { + // For OVERVIEW mode, use the selected chart data + if (isOverviewCharts(data.charts)) { + const selectedChartData = data.charts[chartData]; + if (!selectedChartData || !selectedChartData.dataset) + return []; + return selectedChartData.dataset; + } + return []; + } else { + // For COMPARISON mode, use the first available comparison chart + if (isComparisonCharts(data.charts)) { + const chartData = + data.charts.location || + data.charts.flock || + data.charts.kandang; + + if (!chartData || !chartData.dataset) return []; + return chartData.dataset; + } + return []; } - return []; - } else { - // For COMPARISON mode, use the first available comparison chart - if (isComparisonCharts(data.charts)) { - const chartData = - data.charts.location || - data.charts.flock || - data.charts.kandang; - - if (!chartData || !chartData.dataset) return []; - return chartData.dataset; - } - return []; - } - })()} - margin={{ - top: 5, - right: 10, - left: 0, - bottom: 5, - }} - > - - - { - // Calculate dynamic domain based on visible data - let seriesData: DashboardChartsSeries[] = []; - let dataset: DashboardChartsDataset[] = []; - - if ( - analysisMode === 'OVERVIEW' && - isOverviewCharts(data.charts) - ) { - seriesData = data.charts[chartData]?.series || []; - dataset = data.charts[chartData]?.dataset || []; - } else if ( - analysisMode === 'COMPARISON' && - isComparisonCharts(data.charts) - ) { - const comparisonChart = - data.charts.location || - data.charts.flock || - data.charts.kandang; - seriesData = comparisonChart?.series || []; - dataset = comparisonChart?.dataset || []; - } - - // Get all values from visible series - const visibleSeriesIds = Array.from(visibleSeries); - const allValues: number[] = []; - - dataset.forEach((item: DashboardChartsDataset) => { - visibleSeriesIds.forEach((seriesId) => { - const value = item[seriesId]; - if (typeof value === 'number') { - allValues.push(value); - } - }); - }); - - if (allValues.length === 0) return [0, 100]; - - const minValue = Math.min(...allValues); - const maxValue = Math.max(...allValues); - - // Add padding (10% on each side) - const padding = (maxValue - minValue) * 0.1; - const domainMin = Math.floor(Math.max(0, minValue - padding)); - const domainMax = Math.ceil(maxValue + padding); - - return [domainMin, domainMax]; })()} - ticks={(() => { - // Calculate dynamic ticks based on domain - let seriesData: DashboardChartsSeries[] = []; - let dataset: DashboardChartsDataset[] = []; - - if ( - analysisMode === 'OVERVIEW' && - isOverviewCharts(data.charts) - ) { - seriesData = data.charts[chartData]?.series || []; - dataset = data.charts[chartData]?.dataset || []; - } else if ( - analysisMode === 'COMPARISON' && - isComparisonCharts(data.charts) - ) { - const comparisonChart = - data.charts.location || - data.charts.flock || - data.charts.kandang; - seriesData = comparisonChart?.series || []; - dataset = comparisonChart?.dataset || []; - } - - const visibleSeriesIds = Array.from(visibleSeries); - const allValues: number[] = []; - - dataset.forEach((item: DashboardChartsDataset) => { - visibleSeriesIds.forEach((seriesId) => { - const value = item[seriesId]; - if (typeof value === 'number') { - allValues.push(value); - } - }); - }); - - if (allValues.length === 0) return [0, 25, 50, 75, 100]; - - const minValue = Math.min(...allValues); - const maxValue = Math.max(...allValues); - const padding = (maxValue - minValue) * 0.1; - const domainMin = Math.floor(Math.max(0, minValue - padding)); - const domainMax = Math.ceil(maxValue + padding); - - // Generate 5 evenly spaced ticks - const range = domainMax - domainMin; - const step = range / 4; - - return [ - domainMin, - Math.round(domainMin + step), - Math.round(domainMin + step * 2), - Math.round(domainMin + step * 3), - domainMax, - ]; - })()} - /> - `Week ${value}`} - formatter={( - value: number | undefined, - name: string | undefined - ) => { - if (value === undefined || name === undefined) return ['', '']; + > + + + { + // Calculate dynamic domain based on visible data + let seriesData: DashboardChartsSeries[] = []; + let dataset: DashboardChartsDataset[] = []; - // Get series data to find the unit - let seriesData: DashboardChartsSeries[] = []; - if ( - analysisMode === 'OVERVIEW' && - isOverviewCharts(data.charts) - ) { - seriesData = data.charts[chartData]?.series || []; - } else if ( - analysisMode === 'COMPARISON' && - isComparisonCharts(data.charts) - ) { - const comparisonChart = - data.charts.location || - data.charts.flock || - data.charts.kandang; - seriesData = comparisonChart?.series || []; - } + if ( + analysisMode === 'OVERVIEW' && + isOverviewCharts(data.charts) + ) { + seriesData = data.charts[chartData]?.series || []; + dataset = data.charts[chartData]?.dataset || []; + } else if ( + analysisMode === 'COMPARISON' && + isComparisonCharts(data.charts) + ) { + const comparisonChart = + data.charts.location || + data.charts.flock || + data.charts.kandang; + seriesData = comparisonChart?.series || []; + dataset = comparisonChart?.dataset || []; + } - // Find the series that matches this line's name - const series = seriesData.find((s) => s.label === name); - const unit = series?.unit || ''; + // Get all values from visible series + const visibleSeriesIds = Array.from(visibleSeries); + const allValues: number[] = []; - return [`${value} ${unit}`, name]; - }} - /> - {/* Dynamic Line rendering based on visible series */} - {(() => { - let seriesData: DashboardChartsSeries[] = []; - - if (analysisMode === 'OVERVIEW' && isOverviewCharts(data.charts)) { - seriesData = data.charts[chartData]?.series || []; - } else if ( - analysisMode === 'COMPARISON' && - isComparisonCharts(data.charts) - ) { - const comparisonChart = - data.charts.location || - data.charts.flock || - data.charts.kandang; - seriesData = comparisonChart?.series || []; - } - - return seriesData - .filter((series) => visibleSeries.has(series.id)) - .map((series, index) => { - const isStandard = series.id - .toString() - .toLowerCase() - .includes('std'); - // Use series.id directly as dataKey to match dataset fields - const dataKey = series.id.toString(); - - return ( - { + visibleSeriesIds.forEach((seriesId) => { + const value = item[seriesId]; + if (typeof value === 'number') { + allValues.push(value); } - activeDot={isStandard ? undefined : { r: 5 }} + }); + }); + + if (allValues.length === 0) return [0, 100]; + + const minValue = Math.min(...allValues); + const maxValue = Math.max(...allValues); + + // Add padding (10% on each side) + const padding = (maxValue - minValue) * 0.1; + const domainMin = Math.floor(Math.max(0, minValue - padding)); + const domainMax = Math.ceil(maxValue + padding); + + return [domainMin, domainMax]; + })()} + ticks={(() => { + // Calculate dynamic ticks based on domain + let seriesData: DashboardChartsSeries[] = []; + let dataset: DashboardChartsDataset[] = []; + + if ( + analysisMode === 'OVERVIEW' && + isOverviewCharts(data.charts) + ) { + seriesData = data.charts[chartData]?.series || []; + dataset = data.charts[chartData]?.dataset || []; + } else if ( + analysisMode === 'COMPARISON' && + isComparisonCharts(data.charts) + ) { + const comparisonChart = + data.charts.location || + data.charts.flock || + data.charts.kandang; + seriesData = comparisonChart?.series || []; + dataset = comparisonChart?.dataset || []; + } + + const visibleSeriesIds = Array.from(visibleSeries); + const allValues: number[] = []; + + dataset.forEach((item: DashboardChartsDataset) => { + visibleSeriesIds.forEach((seriesId) => { + const value = item[seriesId]; + if (typeof value === 'number') { + allValues.push(value); + } + }); + }); + + if (allValues.length === 0) return [0, 25, 50, 75, 100]; + + const minValue = Math.min(...allValues); + const maxValue = Math.max(...allValues); + const padding = (maxValue - minValue) * 0.1; + const domainMin = Math.floor(Math.max(0, minValue - padding)); + const domainMax = Math.ceil(maxValue + padding); + + // Generate 5 evenly spaced ticks + const range = domainMax - domainMin; + const step = range / 4; + + return [ + domainMin, + Math.round(domainMin + step), + Math.round(domainMin + step * 2), + Math.round(domainMin + step * 3), + domainMax, + ]; + })()} + /> + `Week ${value}`} + formatter={( + value: number | undefined, + name: string | undefined + ) => { + if (value === undefined || name === undefined) return ['', '']; + + // Get series data to find the unit + let seriesData: DashboardChartsSeries[] = []; + if ( + analysisMode === 'OVERVIEW' && + isOverviewCharts(data.charts) + ) { + seriesData = data.charts[chartData]?.series || []; + } else if ( + analysisMode === 'COMPARISON' && + isComparisonCharts(data.charts) + ) { + const comparisonChart = + data.charts.location || + data.charts.flock || + data.charts.kandang; + seriesData = comparisonChart?.series || []; + } + + // Find the series that matches this line's name + const series = seriesData.find((s) => s.label === name); + const unit = series?.unit || ''; + + return [`${value} ${unit}`, name]; + }} + /> + {/* Dynamic Line rendering based on visible series */} + {(() => { + let seriesData: DashboardChartsSeries[] = []; + + if ( + analysisMode === 'OVERVIEW' && + isOverviewCharts(data.charts) + ) { + seriesData = data.charts[chartData]?.series || []; + } else if ( + analysisMode === 'COMPARISON' && + isComparisonCharts(data.charts) + ) { + const comparisonChart = + data.charts.location || + data.charts.flock || + data.charts.kandang; + seriesData = comparisonChart?.series || []; + } + + return seriesData + .filter((series) => visibleSeries.has(series.id)) + .map((series, index) => { + const isStandard = series.id + .toString() + .toLowerCase() + .includes('std'); + // Use series.id directly as dataKey to match dataset fields + const dataKey = series.id.toString(); + + return ( + + ); + }); + })()} + + + + {/* Empty State Overlay */} + {(() => { + // Get current dataset + let dataset: DashboardChartsDataset[] = []; + + if (analysisMode === 'OVERVIEW' && isOverviewCharts(data.charts)) { + dataset = data.charts[chartData]?.dataset || []; + } else if ( + analysisMode === 'COMPARISON' && + isComparisonCharts(data.charts) + ) { + const comparisonChart = + data.charts.location || data.charts.flock || data.charts.kandang; + dataset = comparisonChart?.dataset || []; + } + + // Show empty state if dataset is empty + if (dataset.length === 0) { + return ( +
+ {/* Chart icon */} +
+ - ); - }); - })()} - - +
+ + {/* Empty state text */} +

+ Data Not Yet Available +

+

+ Please change your filters to get the data. +

+
+ ); + } + return null; + })()} +
); }; From f22ba83dd0114bfb5237040079b985b13cf9c0f5 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 14 Jan 2026 16:44:50 +0700 Subject: [PATCH 010/139] feat(FE): hit api to endpoint closing finance kandang --- .../pages/closing/ClosingFinanceTable.tsx | 14 ++++++++++++-- src/services/api/closing.ts | 5 +++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/components/pages/closing/ClosingFinanceTable.tsx b/src/components/pages/closing/ClosingFinanceTable.tsx index 7eb34369..010bfc2f 100644 --- a/src/components/pages/closing/ClosingFinanceTable.tsx +++ b/src/components/pages/closing/ClosingFinanceTable.tsx @@ -8,6 +8,7 @@ import { HppPurchaseData, ProfitLossDataAmount, } from '@/types/api/closing'; +import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; type HppTableRow = @@ -55,9 +56,16 @@ const ClosingFinanceTable = ({ }: { projectFlockId: number; }) => { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + const { data: finance, isLoading } = useSWR( - `/closing/finance/${projectFlockId}`, - () => ClosingApi.getFinance(projectFlockId) + `/closing/finance/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`, + () => + ClosingApi.getFinance( + projectFlockId, + kandangId ? Number(kandangId) : undefined + ) ); const staticHppRows: Array<{ @@ -283,6 +291,7 @@ const ClosingFinanceTable = ({
data={hppTableData} + isLoading={isLoading} columns={[ { header: 'No.', @@ -455,6 +464,7 @@ const ClosingFinanceTable = ({
data={profitLossTableData} + isLoading={isLoading} columns={[ { header: 'Jenis', diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index ff6a0bcb..b2ba2b8f 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -148,10 +148,11 @@ export class ClosingApiService extends BaseApiService { } async getFinance( - id: number + id: number, + kandangId?: number ): Promise | undefined> { try { - const path = `${this.basePath}/${id}/keuangan`; + const path = `${this.basePath}/${id}${kandangId ? `/${kandangId}` : ''}/keuangan`; return await httpClient>(path, { method: 'GET', }); From e134f0994bafc0959c208c661920caff3e665dbc Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 16:57:52 +0700 Subject: [PATCH 011/139] refactor(FE): Remove credit_note and ppn fields --- .../export/CustomerPaymentExportPDF.tsx | 11 -------- .../export/CustomerPaymentExportXLSX.tsx | 4 --- .../report/finance/tab/CustomerPaymentTab.tsx | 28 ------------------- src/types/api/report/customer-payment.d.ts | 4 --- 4 files changed, 47 deletions(-) diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx index d1092e22..8887320c 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx @@ -382,15 +382,9 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { {formatCurrency(item.price)} - - {formatCurrency(item.credit_note)} - {formatCurrency(item.final_price)} - - {formatNumber(item.ppn)}% - {formatCurrency(item.total_price)} @@ -468,11 +462,6 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { )} - - - {formatCurrency(customerReport.summary.total_credit_note)} - - {formatCurrency(customerReport.summary.total_final_amount)} diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx index ea7cfd77..2fe29c76 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx @@ -37,9 +37,7 @@ export const generateCustomerPaymentExcel = ( 'Berat (Kg)': formatNumber(item.weight || 0), AVG: formatNumber(item.average_weight || 0), 'Harga Awal': formatCurrency(item.price || 0), - CN: formatCurrency(item.credit_note || 0), 'Harga Akhir': formatCurrency(item.final_price || 0), - 'PPN (%)': formatNumber(item.ppn || 0), Total: formatCurrency(item.total_price || 0), Pembayaran: formatCurrency(item.payment_amount || 0), 'Saldo Piutang': formatCurrency(item.accounts_receivable || 0), @@ -63,11 +61,9 @@ export const generateCustomerPaymentExcel = ( 'Harga Awal': formatCurrency( customerReport.summary.total_initial_amount || 0 ), - CN: formatCurrency(customerReport.summary.total_credit_note || 0), 'Harga Akhir': formatCurrency( customerReport.summary.total_final_amount || 0 ), - 'PPN (%)': '', Total: formatCurrency(customerReport.summary.total_grand_amount || 0), Pembayaran: formatCurrency(customerReport.summary.total_payment || 0), 'Saldo Piutang': formatCurrency( diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 8e642097..8b2debbf 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -417,21 +417,6 @@ const CustomerPaymentTab = () => {
), }, - { - id: 'credit_note', - header: 'CN', - accessorKey: 'credit_note', - enableSorting: false, - cell: (props) => { - const value = props.row.original.credit_note; - return
{formatCurrency(value)}
; - }, - footer: () => ( -
- {formatCurrency(summary.total_credit_note) || '-'} -
- ), - }, { id: 'final_price', header: 'Harga Akhir', @@ -447,19 +432,6 @@ const CustomerPaymentTab = () => {
), }, - { - id: 'ppn', - header: 'PPN (%)', - accessorKey: 'ppn', - enableSorting: false, - cell: (props) => { - const value = props.row.original.ppn; - return
{formatNumber(value)}%
; - }, - footer: () => ( -
-
- ), - }, { id: 'total', header: 'Total', diff --git a/src/types/api/report/customer-payment.d.ts b/src/types/api/report/customer-payment.d.ts index 8c648cb9..f25f510a 100644 --- a/src/types/api/report/customer-payment.d.ts +++ b/src/types/api/report/customer-payment.d.ts @@ -12,9 +12,7 @@ export type CustomerPaymentRow = { weight: number; average_weight: number; price: number; - credit_note: number; final_price: number; - ppn: number; total_price: number; payment_amount: number; accounts_receivable: number; @@ -28,9 +26,7 @@ export type CustomerPaymentSummary = { total_qty: number; total_weight: number; total_initial_amount: number; - total_credit_note: number; total_final_amount: number; - total_ppn: number; total_grand_amount: number; total_payment: number; total_accounts_receivable: number; From 916de1432b6b39ce2cb3170ff70fbc7d35dc1952 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 16:59:45 +0700 Subject: [PATCH 012/139] refactor(FE): Adjust column span in CustomerPaymentTab --- src/components/pages/report/finance/tab/CustomerPaymentTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 8b2debbf..7d34a08f 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -780,7 +780,7 @@ const CustomerPaymentTab = () => { >
- From 9237d4e731c062a5d402328b60266d46361ced66 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 14 Jan 2026 17:10:42 +0700 Subject: [PATCH 014/139] fix(FE): implement lazy load select project flock --- .../project-flock/form/ProjectFlockForm.tsx | 67 +++++++++++++------ 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx index 745a6b1e..0c252cb6 100644 --- a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx +++ b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx @@ -102,34 +102,47 @@ const ProjectFlockForm = ({ ); // Fetch Data - const { isLoadingOptions: isLoadingFlocks, options: optionsFlock } = - useSelect(FlockApi.basePath, 'id', 'name'); + const { + setInputValue: setInputValueFlock, + isLoadingOptions: isLoadingFlocks, + options: optionsFlock, + loadMore: loadMoreFlock, + } = useSelect(FlockApi.basePath, 'id', 'name', '', { + project_category: selectedCategory, + }); - const { options: optionsArea, isLoadingOptions: isLoadingAreas } = useSelect( - AreaApi.basePath, - 'id', - 'name' - ); + const { + setInputValue: setInputValueArea, + options: optionsArea, + isLoadingOptions: isLoadingAreas, + loadMore: loadMoreArea, + } = useSelect(AreaApi.basePath, 'id', 'name'); - const { options: optionsLocation, isLoadingOptions: isLoadingLocations } = - useSelect(LocationApi.basePath, 'id', 'name', '', { - area_id: - selectedArea != '' - ? selectedArea - : ((initialValues?.area?.id ?? '') as string), - }); + const { + options: optionsLocation, + isLoadingOptions: isLoadingLocations, + setInputValue: setInputValueLocation, + loadMore: loadMoreLocation, + } = useSelect(LocationApi.basePath, 'id', 'name', '', { + area_id: + selectedArea != '' + ? selectedArea + : ((initialValues?.area?.id ?? '') as string), + }); - const { options: optionsFcr, isLoadingOptions: isLoadingFcrs } = useSelect( - FcrApi.basePath, - 'id', - 'name' - ); + const { + options: optionsFcr, + isLoadingOptions: isLoadingFcrs, + setInputValue: setInputValueFcr, + loadMore: loadMoreFcr, + } = useSelect(FcrApi.basePath, 'id', 'name'); const { options: optionsProductionStandards, isLoadingOptions: isLoadingProductionStandards, + setInputValue: setInputValueProductionStandard, + loadMore: loadMoreProductionStandard, } = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', { - search: '', project_category: selectedCategory, }); @@ -153,6 +166,8 @@ const ProjectFlockForm = ({ options: optionsNonstock, rawData: nonstocks, isLoadingOptions: isLoadingNonstocks, + setInputValue: setInputValueNonstock, + loadMore: loadMoreNonstock, } = useSelect(NonstockApi.basePath, 'id', 'name'); useEffect(() => { @@ -722,6 +737,8 @@ const ProjectFlockForm = ({ formik.touched.area_id && Boolean(formik.errors.area_id) } errorMessage={formik.errors.area_id as string} + onInputChange={setInputValueArea} + onMenuScrollToBottom={loadMoreArea} isClearable isDisabled={formType != 'add'} /> @@ -740,6 +757,8 @@ const ProjectFlockForm = ({ formik.touched.location_id && Boolean(formik.errors.location_id) } + onInputChange={setInputValueLocation} + onMenuScrollToBottom={loadMoreLocation} errorMessage={formik.errors.location_id as string} isClearable isDisabled={formType != 'add' || disabledLocation} @@ -766,6 +785,8 @@ const ProjectFlockForm = ({ ); }} options={optionsFlock} + onInputChange={setInputValueFlock} + onMenuScrollToBottom={loadMoreFlock} isLoading={isLoadingFlocks} isError={ formik.touched.flock_name && Boolean(formik.errors.flock_name) @@ -781,6 +802,8 @@ const ProjectFlockForm = ({ onChange={(val) => { optionChangeHandler(val, 'fcr'); }} + onInputChange={setInputValueFcr} + onMenuScrollToBottom={loadMoreFcr} options={optionsFcr} isLoading={isLoadingFcrs} isError={formik.touched.fcr_id && Boolean(formik.errors.fcr_id)} @@ -808,6 +831,8 @@ const ProjectFlockForm = ({ onChange={(val) => { optionChangeHandler(val, 'production_standard'); }} + onInputChange={setInputValueProductionStandard} + onMenuScrollToBottom={loadMoreProductionStandard} options={optionsProductionStandards} isLoading={isLoadingProductionStandards} isError={ @@ -892,6 +917,8 @@ const ProjectFlockForm = ({ isLoading={isLoadingNonstocks} placeholder='Pilih barang non stock' value={formik.values.project_budgets[index].nonstock} + onInputChange={setInputValueNonstock} + onMenuScrollToBottom={loadMoreNonstock} onChange={(val) => { const updatedBudgets = [ ...formik.values.project_budgets, From 4137683d05628e8278b37eb832ba860ce9768b48 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 17:55:38 +0700 Subject: [PATCH 015/139] refactor(FE): Hide 'hari' for zero or negative aging days --- src/components/pages/report/finance/tab/CustomerPaymentTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 951bc8f1..c5e79d8b 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -334,7 +334,7 @@ const CustomerPaymentTab = () => { const value = props.row.original.aging_day; return (
- {value ? formatNumber(value) : '-'} hari + {value && value > 0 ? `${formatNumber(value)} hari` : '-'}
); }, From b290f7692a2213aa08d69f30a8e4d736bcdcc42c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 21:39:38 +0700 Subject: [PATCH 016/139] refactor(FE): Show current chick total in recording form --- .../recording/form/RecordingForm.tsx | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 4f9018fc..66164faa 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -570,6 +570,33 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { return recordedIds; }, [existingRecordings, today]); + const currentTotalChickQty = useMemo(() => { + if (!isResponseSuccess(existingRecordings) || !selectedKandang) return null; + + let projectFlockKandangId: number | undefined; + + if (projectFlockKandangLookup) { + projectFlockKandangId = + projectFlockKandangLookup.project_flock_kandang_id; + } else if (projectFlockKandangDetail) { + projectFlockKandangId = projectFlockKandangDetail.id; + } + + if (!projectFlockKandangId) return null; + + const recording = existingRecordings.data.find( + (rec) => + rec.project_flock.project_flock_kandang_id === projectFlockKandangId + ); + + return recording?.project_flock.total_chick_qty || null; + }, [ + existingRecordings, + selectedKandang, + projectFlockKandangLookup, + projectFlockKandangDetail, + ]); + const unifiedStockProducts = useMemo(() => { const options: OptionType[] = []; if (isResponseSuccess(stockProducts) && selectedKandang) { @@ -1436,6 +1463,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ? getProjectFlockBadgeAdornment() : undefined } + bottomLabel={ + currentTotalChickQty + ? `Jumlah ayam saat ini: ${formatNumber( + currentTotalChickQty + )}` + : undefined + } /> From dbe9b268189b43653f203f5f6e61f1a661036e0e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 21:42:36 +0700 Subject: [PATCH 017/139] feat(FE): Adjust recording form grid and add chick count --- .../production/recording/form/RecordingForm.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 66164faa..dec106b1 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -1487,7 +1487,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { body: 'flex flex-col gap-4', }} > -
+
{initialValues.approval && (
@@ -1575,6 +1575,18 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { )}

+
+ + Jumlah Ayam Saat Ini + +

+ {initialValues.project_flock?.total_chick_qty + ? formatNumber( + initialValues.project_flock.total_chick_qty + ) + : '-'} +

+
Hari

Hari ke-{initialValues.day}

From 359326e575c87b5dfaa24594ac064d3bb6d415c3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 21:45:52 +0700 Subject: [PATCH 018/139] refactor(FE): Rename 'Populasi Awal' column to 'Populasi Ayam' --- src/components/pages/production/recording/RecordingTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index a99cfbbc..96ac52ed 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -715,7 +715,7 @@ const RecordingTable = () => { formatDate(props.row.original.record_datetime, 'DD MMMM YYYY'), }, { - header: 'Populasi Awal', + header: 'Populasi Ayam', cell: (props) => props.row.original.project_flock?.total_chick_qty?.toLocaleString() || '-', From d8e134d404478869b28da032b0ed2ecbc731263f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 21:59:03 +0700 Subject: [PATCH 019/139] feat(FE): Add customer payment permission to finance route --- src/config/route-permission.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config/route-permission.ts b/src/config/route-permission.ts index 9a0c9d2e..176b4385 100644 --- a/src/config/route-permission.ts +++ b/src/config/route-permission.ts @@ -121,6 +121,7 @@ export const ROUTE_PERMISSIONS: Record = { '/report/finance/': [ 'lti.repport.finance.list', 'lti.repport.debtsupplier.list', + 'lti.repport.customerpayment.list', ], // Inventory From a72fbec5ceab4997fb462b5e23dde8a30e3a0554 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 22:02:48 +0700 Subject: [PATCH 020/139] refactor(FE): Remove CN and PPN columns and bold receivables --- .../report/finance/export/CustomerPaymentExportPDF.tsx | 3 --- .../report/finance/export/CustomerPaymentExportXLSX.tsx | 2 -- .../pages/report/finance/tab/CustomerPaymentTab.tsx | 7 +++++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx index 8887320c..8dce38ad 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx @@ -302,9 +302,6 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { Harga Awal - - CN - Harga Akhir diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx index 2fe29c76..b14b5ef6 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx @@ -88,9 +88,7 @@ export const generateCustomerPaymentExcel = ( { wch: 12 }, // Berat { wch: 10 }, // AVG { wch: 15 }, // Harga Awal - { wch: 10 }, // CN { wch: 15 }, // Harga Akhir - { wch: 10 }, // PPN { wch: 15 }, // Total { wch: 15 }, // Pembayaran { wch: 15 }, // Saldo Piutang diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index c5e79d8b..928c9c41 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -470,7 +470,11 @@ const CustomerPaymentTab = () => { cell: (props) => { const value = props.row.original.accounts_receivable; return ( -
+
{formatCurrency(value)}
); @@ -728,7 +732,6 @@ const CustomerPaymentTab = () => { total_initial_amount: 0, total_credit_note: 0, total_final_amount: 0, - total_ppn: 0, total_grand_amount: 0, total_payment: 0, total_accounts_receivable: 0, From f1dba4012aed8dfef95771863f937f1683451e72 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 22:17:24 +0700 Subject: [PATCH 021/139] refactor(FE): Update customer payment types and exports --- .../export/CustomerPaymentExportPDF.tsx | 18 ++++-------------- .../export/CustomerPaymentExportXLSX.tsx | 12 +++++++----- .../report/finance/tab/CustomerPaymentTab.tsx | 8 ++------ src/types/api/report/customer-payment.d.ts | 7 +++---- 4 files changed, 16 insertions(+), 29 deletions(-) diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx index 8dce38ad..71969e8a 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx @@ -284,7 +284,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { Aging - + Referensi @@ -305,9 +305,6 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { Harga Akhir - - Pajak - Total @@ -361,7 +358,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { {item.aging_day ? formatNumber(item.aging_day) : '-'} hari - + {item.reference || '-'} @@ -435,7 +432,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { - + @@ -453,20 +450,13 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { - - {formatCurrency( - customerReport.summary.total_initial_amount - )} - + {formatCurrency(customerReport.summary.total_final_amount)} - - - {formatCurrency(customerReport.summary.total_grand_amount)} diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx index b14b5ef6..3fb21488 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx @@ -32,7 +32,9 @@ export const generateCustomerPaymentExcel = ( : '', Aging: formatNumber(item.aging_day || 0), Referensi: item.reference || '', - 'Nomor Polisi': item.vehicle_numbers || '', + 'Nomor Polisi': Array.isArray(item.vehicle_numbers) + ? item.vehicle_numbers.join(', ') + : '', 'Ekor/Qty': formatNumber(item.qty || 0), 'Berat (Kg)': formatNumber(item.weight || 0), AVG: formatNumber(item.average_weight || 0), @@ -42,7 +44,9 @@ export const generateCustomerPaymentExcel = ( Pembayaran: formatCurrency(item.payment_amount || 0), 'Saldo Piutang': formatCurrency(item.accounts_receivable || 0), Keterangan: item.status || '', - Pengambilan: item.pickup_info || '', + Pengambilan: Array.isArray(item.pickup_info) + ? item.pickup_info.join(', ') + : '', 'Sales/Marketing': item.sales_person || '', }) ); @@ -58,9 +62,7 @@ export const generateCustomerPaymentExcel = ( 'Ekor/Qty': formatNumber(customerReport.summary.total_qty || 0), 'Berat (Kg)': formatNumber(customerReport.summary.total_weight || 0), AVG: '', - 'Harga Awal': formatCurrency( - customerReport.summary.total_initial_amount || 0 - ), + 'Harga Awal': '', 'Harga Akhir': formatCurrency( customerReport.summary.total_final_amount || 0 ), diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 928c9c41..18a8674a 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -412,9 +412,7 @@ const CustomerPaymentTab = () => { return
{formatCurrency(value)}
; }, footer: () => ( -
- {formatCurrency(summary.total_initial_amount) || '-'} -
+
-
), }, { @@ -510,7 +508,7 @@ const CustomerPaymentTab = () => { status: getPaymentStatusIndicatorColor(value), }} > - {getPaymentStatusText(value)} + {getPaymentStatusText(value)} ); }, @@ -729,8 +727,6 @@ const CustomerPaymentTab = () => { const summary = customerReport.summary || { total_qty: 0, total_weight: 0, - total_initial_amount: 0, - total_credit_note: 0, total_final_amount: 0, total_grand_amount: 0, total_payment: 0, diff --git a/src/types/api/report/customer-payment.d.ts b/src/types/api/report/customer-payment.d.ts index f25f510a..9169c99b 100644 --- a/src/types/api/report/customer-payment.d.ts +++ b/src/types/api/report/customer-payment.d.ts @@ -7,7 +7,7 @@ export type CustomerPaymentRow = { trans_date: string; delivery_date: string | null; reference: string; - vehicle_numbers: string; + vehicle_numbers: string[]; qty: number; weight: number; average_weight: number; @@ -16,16 +16,15 @@ export type CustomerPaymentRow = { total_price: number; payment_amount: number; accounts_receivable: number; - aging_day: number; + aging_day: number | null; status: string; - pickup_info: string; + pickup_info: string[]; sales_person: string; }; export type CustomerPaymentSummary = { total_qty: number; total_weight: number; - total_initial_amount: number; total_final_amount: number; total_grand_amount: number; total_payment: number; From 427c8aec341048f4ce95f12896d81ea303cc9002 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 22:28:45 +0700 Subject: [PATCH 022/139] feat(FE): Join array fields into comma-separated strings --- .../finance/export/CustomerPaymentExportPDF.tsx | 12 ++++++++++-- .../pages/report/finance/tab/CustomerPaymentTab.tsx | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx index 71969e8a..9b1fd640 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx @@ -362,7 +362,11 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { {item.reference || '-'}
- {item.vehicle_numbers || '-'} + + {Array.isArray(item.vehicle_numbers) + ? item.vehicle_numbers.join(', ') + : item.vehicle_numbers || '-'} + {formatNumber(item.qty)} @@ -409,7 +413,11 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { )} - {item.pickup_info || '-'} + + {Array.isArray(item.pickup_info) + ? item.pickup_info.join(', ') + : item.pickup_info || '-'} + {item.sales_person || '-'} diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 18a8674a..78d84800 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -356,7 +356,7 @@ const CustomerPaymentTab = () => { enableSorting: false, cell: (props) => { const value = props.row.original.vehicle_numbers; - return value || '-'; + return Array.isArray(value) ? value.join(', ') : value || '-'; }, }, { @@ -520,7 +520,7 @@ const CustomerPaymentTab = () => { enableSorting: false, cell: (props) => { const value = props.row.original.pickup_info; - return value || '-'; + return Array.isArray(value) ? value.join(', ') : value || '-'; }, }, { From 76e15d13ad40eec16236d185417ddbd830d67c51 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Thu, 15 Jan 2026 00:52:29 +0700 Subject: [PATCH 023/139] fix(FE): adding filter information and supplier category on export pdf --- .../finance/export/DebtSupllierExportPDF.tsx | 61 +++++++++++++++++++ .../report/finance/tab/DebtSupplierTab.tsx | 12 +++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx index 7782b212..869430b0 100644 --- a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx +++ b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx @@ -187,10 +187,30 @@ const pdfStyles = StyleSheet.create({ textAlign: 'center', whiteSpace: 'nowrap', }, + parameterBadge: { + backgroundColor: '#F5F5F5', + color: '#333333', + padding: 4, + borderRadius: 4, + fontSize: 8, + marginRight: 8, + marginBottom: 4, + }, + parameterContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + marginBottom: 8, + }, }); interface DebtSupplierExportPDFParams { data: DebtSupplier[]; + params?: { + supplier_name?: string; + start_date?: string; + end_date?: string; + filter_by?: string; + }; } const createPDFDocument = (params: DebtSupplierExportPDFParams) => { @@ -208,9 +228,50 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { Laporan > Rekapitulasi Hutang ke Supplier + + + + Periode:{' '} + {params.params?.start_date + ? formatDate(params.params.start_date, 'DD MMM YYYY') + : '-'}{' '} + s.d{' '} + {params.params?.end_date + ? formatDate(params.params.end_date, 'DD MMM YYYY') + : '-'} + + + {params.params?.filter_by && ( + + + Filter Tanggal:{' '} + {params.params.filter_by === 'po_date' + ? 'Tanggal PO' + : params.params.filter_by === 'received_date' + ? 'Tanggal Terima' + : params.params.filter_by === 'due_date' + ? 'Tanggal Jatuh Tempo' + : params.params.filter_by} + + + )} + + + Supplier: {params.params?.supplier_name || 'Semua Supplier'} + + + + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + + + {supplierReport.supplier.name} + + {supplierReport.supplier.category} + {/* Table */} diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index 1f5d79b9..0d53488c 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -247,7 +247,17 @@ const DebtSupplierTab = () => { return; } - await generateDebtSupplierPDF({ data: allDataForExport }); + 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, + }, + }); toast.success('PDF berhasil dibuat dan diunduh.'); } catch { toast.error('Gagal membuat PDF. Silakan coba lagi.'); From 3827204f134fa77e9c3dd846532ab8ec3f86d2cb Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 09:47:11 +0700 Subject: [PATCH 024/139] chore: make basePath nullable --- src/components/input/SelectInput.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx index 8d5b9170..9cc9fda5 100644 --- a/src/components/input/SelectInput.tsx +++ b/src/components/input/SelectInput.tsx @@ -325,7 +325,7 @@ const SelectInput = (props: SelectInputProps) => { }; const useSelect = ( - basePath: string, + basePath: string | null, valueKey: keyof T | string, labelKey: keyof T | string, searchKey: string = 'search', @@ -354,7 +354,7 @@ const useSelect = ( [limitKey]: String(limit), }).toString(); - return `${basePath}?${qs}`; + return basePath ? `${basePath}?${qs}` : null; }; const { From c75563491f904105340fce4113de0180797ccab2 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 09:48:02 +0700 Subject: [PATCH 025/139] feat: implement lazy loading in SelectInput --- .../pages/closing/ClosingsTable.tsx | 2 + .../form/InventoryAdjustmentForm.tsx | 112 +++++++----------- .../inventory/movement/form/MovementForm.tsx | 53 ++++----- .../report/DailyMarketingReportContent.tsx | 8 ++ .../report/expense/ReportExpenseTable.tsx | 86 ++++++++++---- .../report/finance/tab/CustomerPaymentTab.tsx | 4 + .../report/finance/tab/DebtSupplierTab.tsx | 15 ++- .../ProductionResultContent.tsx | 8 ++ .../report/sale/tab/HppPerKandangTab.tsx | 34 ++++-- 9 files changed, 182 insertions(+), 140 deletions(-) diff --git a/src/components/pages/closing/ClosingsTable.tsx b/src/components/pages/closing/ClosingsTable.tsx index 0cacb549..ec334104 100644 --- a/src/components/pages/closing/ClosingsTable.tsx +++ b/src/components/pages/closing/ClosingsTable.tsx @@ -163,6 +163,7 @@ const ClosingsTable = () => { setInputValue: setLocationInputValue, options: locationOptions, isLoadingOptions: isLoadingLocationOptions, + loadMore: loadMoreLocations, } = useSelect(LocationApi.basePath, 'id', 'name'); const [selectedLocation, setSelectedLocation] = useState( @@ -228,6 +229,7 @@ const ClosingsTable = () => { value={selectedLocation} onChange={locationChangeHandler} onInputChange={setLocationInputValue} + onMenuScrollToBottom={loadMoreLocations} isClearable className={{ wrapper: 'col-span-12 sm:col-span-6', diff --git a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx index c1fc25da..3bae393d 100644 --- a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx +++ b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx @@ -1,6 +1,6 @@ 'use client'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { isResponseError } from '@/lib/api-helper'; import { InventoryAdjustmentApi } from '@/services/api/inventory'; import { CreateInventoryAdjustmentPayload, @@ -22,12 +22,18 @@ import { } from '@/services/api/master-data'; import Button from '@/components/Button'; import { Icon } from '@iconify/react'; -import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; import TextInput from '@/components/input/TextInput'; import { RadioGroup } from '@/components/input/RadioInput'; import TextArea from '@/components/input/TextArea'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import AlertErrorList from '@/components/helper/form/FormErrors'; +import { ProductCategory } from '@/types/api/master-data/product-category'; +import { Product } from '@/types/api/master-data/product'; +import { Warehouse } from '@/types/api/master-data/warehouse'; interface InventoryAdjustmentFormProps { type?: 'add' | 'edit' | 'detail'; @@ -44,10 +50,7 @@ const InventoryAdjustmentForm = ({ InventoryAdjustmentFormErrorMessage, setInventoryAdjustmentFormErrorMessage, ] = useState(''); - const [selectedProductCategories, setSelectedProductCategories] = - useState(''); const [disabledProduct, setDisabledProduct] = useState(true); - const [optionsProduct, setOptionsProduct] = useState([]); const [quantityLabel, setQuantityLabel] = useState('Tambah Stok'); // Submit Handler @@ -108,45 +111,30 @@ const InventoryAdjustmentForm = ({ }); // Fetch Data - const productCategoriesUrl = `${ - ProductCategoryApi.basePath - }?${new URLSearchParams({ - search: '', - }).toString()}`; - const { data: productCategories, isLoading: isLoadingProductCategories } = - useSWR(productCategoriesUrl, ProductCategoryApi.getAllFetcher); + const { + setInputValue: setProductCategoryInputValue, + options: productCategoryOptions, + isLoadingOptions: isLoadingProductCategoryOptions, + loadMore: loadMoreProductCategories, + } = useSelect(ProductCategoryApi.basePath, 'id', 'name'); - const productUrl = `${ProductApi.basePath}?${new URLSearchParams({ - search: '', - product_category_id: selectedProductCategories, - }).toString()}`; - const { data: products, isLoading: isLoadingProducts } = useSWR( - productUrl, - ProductApi.getAllFetcher - ); + const { + setInputValue: setProductInputValue, + options: productOptions, + isLoadingOptions: isLoadingProductOptions, + loadMore: loadMoreProducts, + } = useSelect(ProductApi.basePath, 'id', 'name', 'search', { + product_category_id: formik.values.product_category_id + ? String(formik.values.product_category_id) + : '', + }); - const warehouseUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ - search: '', - limit: '100', - }).toString()}`; - const { data: warehouses, isLoading: isLoadingWarehouses } = useSWR( - warehouseUrl, - WarehouseApi.getAllFetcher - ); - - // Map Data to Options - const optionsProductCategory = isResponseSuccess(productCategories) - ? productCategories?.data.map((productCategory) => ({ - value: productCategory.id, - label: productCategory.name, - })) - : []; - const optionsWarehouse = isResponseSuccess(warehouses) - ? warehouses?.data.map((warehouse) => ({ - value: warehouse.id, - label: warehouse.name, - })) - : []; + const { + setInputValue: setWarehouseInputValue, + options: warehouseOptions, + isLoadingOptions: isLoadingWarehouseOptions, + loadMore: loadMoreWarehouses, + } = useSelect(WarehouseApi.basePath, 'id', 'name'); // Options Handler const productCategoryChangeHandler = ( @@ -157,7 +145,6 @@ const InventoryAdjustmentForm = ({ formik.setFieldValue('product_category', val); - setSelectedProductCategories((val as OptionType)?.value as string); const disabled = (val as OptionType)?.value == null; setDisabledProduct(disabled); formik.setFieldValue('product_id', 0); @@ -193,9 +180,6 @@ const InventoryAdjustmentForm = ({ // Effect useEffect(() => { if (initialValues?.product_warehouse?.product?.id) { - setSelectedProductCategories( - String(initialValues.product_warehouse.product.id) - ); setDisabledProduct(false); formik.setFieldValue( 'product_id', @@ -219,25 +203,10 @@ const InventoryAdjustmentForm = ({ ); formik.setFieldValue('note', initialValues.note); } - }, [ - formik, - initialValues, - setQuantityLabel, - setDisabledProduct, - setSelectedProductCategories, - ]); + }, [formik, initialValues, setQuantityLabel, setDisabledProduct]); useEffect(() => { formikSetValues(formikInitialValues as InventoryAdjustmentFormValues); }, [formikSetValues, formikInitialValues]); - useEffect(() => { - if (isResponseSuccess(products)) { - const options = products.data.map((p) => ({ - value: p.id, - label: p.name, - })); - setOptionsProduct(options); - } - }, [products]); // Utils Function const formatNumber = (value: string) => { @@ -282,9 +251,10 @@ const InventoryAdjustmentForm = ({ label='Kategori Produk' value={formik.values.product_category as OptionType} onChange={productCategoryChangeHandler} - onInputChange={setSelectedProductCategories} - options={optionsProductCategory} - isLoading={isLoadingProductCategories} + onInputChange={setProductCategoryInputValue} + options={productCategoryOptions} + onMenuScrollToBottom={loadMoreProductCategories} + isLoading={isLoadingProductCategoryOptions} isError={ formik.touched.product_category && Boolean(formik.errors.product_category) @@ -300,8 +270,10 @@ const InventoryAdjustmentForm = ({ label='Produk' value={formik.values.product as OptionType} onChange={productChangeHandler} - options={optionsProduct} - isLoading={isLoadingProducts} + onInputChange={setProductInputValue} + options={productOptions} + onMenuScrollToBottom={loadMoreProducts} + isLoading={isLoadingProductOptions} isError={formik.touched.product && Boolean(formik.errors.product)} errorMessage={formik.errors.product as string} isDisabled={type === 'detail' || disabledProduct} @@ -314,8 +286,10 @@ const InventoryAdjustmentForm = ({ label='Warehouse' value={formik.values.warehouse as OptionType} onChange={warehouseChangeHandler} - options={optionsWarehouse} - isLoading={isLoadingWarehouses} + onInputChange={setWarehouseInputValue} + options={warehouseOptions} + onMenuScrollToBottom={loadMoreWarehouses} + isLoading={isLoadingWarehouseOptions} isError={ formik.touched.warehouse && Boolean(formik.errors.warehouse) } diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index d9aef6cd..40e08c5d 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -38,6 +38,8 @@ import Card from '@/components/Card'; import { S3_PUBLIC_BASE_URL } from '@/config/constant'; import { getUniqueFormikErrors } from '@/lib/formik-helper'; import AlertErrorList from '@/components/helper/form/FormErrors'; +import { Warehouse } from '@/types/api/master-data/warehouse'; +import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; interface MovementFormProps { type?: 'add' | 'edit' | 'detail'; @@ -49,10 +51,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { // ===== STATE MANAGEMENT ===== const [movementFormErrorMessage, setMovementFormErrorMessage] = useState(''); - const [ - productWarehouseSelectInputValue, - setProductWarehouseSelectInputValue, - ] = useState(''); const [selectedProducts, setSelectedProducts] = useState([]); const [selectedDeliveries, setSelectedDeliveries] = useState([]); const [formErrorList, setFormErrorList] = useState([]); @@ -93,10 +91,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { // ===== USE SELECT HOOKS ===== const { - inputValue: warehouseSelectInputValue, setInputValue: setWarehouseSelectInputValue, isLoadingOptions: isLoadingWarehouses, - } = useSelect(WarehouseApi.basePath, 'id', 'name', 'search'); + loadMore: loadMoreWarehouses, + rawData: warehouses, + } = useSelect(WarehouseApi.basePath, 'id', 'name', 'search'); // ===== SELECT INPUT DATA ===== const { @@ -107,12 +106,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { category: 'BOP', }); - const warehousesUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ search: warehouseSelectInputValue }).toString()}`; - const { data: warehouses } = useSWR( - warehousesUrl, - WarehouseApi.getAllFetcher - ); - // ===== DATA PROCESSING ===== const warehouseStockMap = useMemo(() => { if (!isResponseSuccess(allProductWarehouses)) return new Map(); @@ -269,25 +262,22 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { }); // ===== PRODUCT WAREHOUSE FETCHING (after form initialization) ===== - const getProductWarehousesUrl = useCallback(() => { - const productWarehouseParams = new URLSearchParams({ - search: productWarehouseSelectInputValue, - }); - if (formik.values.source_warehouse_id) { - productWarehouseParams.append( - 'warehouse_id', - formik.values.source_warehouse_id.toString() - ); + const { + setInputValue: setProductWarehouseSelectInputValue, + isLoadingOptions: isLoadingProductWarehouses, + loadMore: loadMoreProductWarehouses, + rawData: productWarehouses, + } = useSelect( + formik.values.source_warehouse_id ? ProductWarehouseApi.basePath : null, + 'id', + 'name', + 'search', + { + warehouse_id: formik.values.source_warehouse_id + ? formik.values.source_warehouse_id.toString() + : '', } - return `${ProductWarehouseApi.basePath}?${productWarehouseParams.toString()}`; - }, [formik.values.source_warehouse_id, productWarehouseSelectInputValue]); - - const productWarehousesUrl = getProductWarehousesUrl(); - const { data: productWarehouses, isLoading: isLoadingProductWarehouses } = - useSWR( - formik.values.source_warehouse_id ? productWarehousesUrl : null, - ProductWarehouseApi.getAllFetcher - ); + ); const productWarehouseOptions = isResponseSuccess(productWarehouses) ? productWarehouses?.data.map((pw) => ({ @@ -1006,6 +996,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { }} options={warehouseOptions} onInputChange={setWarehouseSelectInputValue} + onMenuScrollToBottom={loadMoreWarehouses} isLoading={isLoadingWarehouses} isError={ formik.touched.source_warehouse_id && @@ -1104,6 +1095,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { options={warehouseOptions} onInputChange={setWarehouseSelectInputValue} isLoading={isLoadingWarehouses} + onMenuScrollToBottom={loadMoreWarehouses} isError={ formik.touched.destination_warehouse_id && Boolean(formik.errors.destination_warehouse_id) @@ -1263,6 +1255,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { }} options={productWarehouseOptions} onInputChange={setProductWarehouseSelectInputValue} + onMenuScrollToBottom={loadMoreProductWarehouses} isLoading={isLoadingProductWarehouses} isDisabled={ type === 'detail' || diff --git a/src/components/pages/report/DailyMarketingReportContent.tsx b/src/components/pages/report/DailyMarketingReportContent.tsx index d17df01e..01c360d0 100644 --- a/src/components/pages/report/DailyMarketingReportContent.tsx +++ b/src/components/pages/report/DailyMarketingReportContent.tsx @@ -87,6 +87,7 @@ const DailyMarketingReportContent = () => { setInputValue: setAreaInputValue, options: areaOptions, isLoadingOptions: isLoadingAreaOptions, + loadMore: loadMoreAreas, } = useSelect(AreaApi.basePath, 'id', 'name'); const areaChangeHandler = (val: OptionType | OptionType[] | null) => { @@ -101,6 +102,7 @@ const DailyMarketingReportContent = () => { setInputValue: setLocationInputValue, options: locationOptions, isLoadingOptions: isLoadingLocationOptions, + loadMore: loadMoreLocations, } = useSelect(LocationApi.basePath, 'id', 'name'); const locationChangeHandler = (val: OptionType | OptionType[] | null) => { @@ -118,6 +120,7 @@ const DailyMarketingReportContent = () => { setInputValue: setWarehouseInputValue, options: warehouseOptions, isLoadingOptions: isLoadingWarehouseOptions, + loadMore: loadMoreWarehouses, } = useSelect(WarehouseApi.basePath, 'id', 'name'); const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => { @@ -135,6 +138,7 @@ const DailyMarketingReportContent = () => { setInputValue: setCustomerInputValue, options: customerOptions, isLoadingOptions: isLoadingCustomerOptions, + loadMore: loadMoreCustomers, } = useSelect(CustomerApi.basePath, 'id', 'name'); const customerChangeHandler = (val: OptionType | OptionType[] | null) => { @@ -298,6 +302,7 @@ const DailyMarketingReportContent = () => { value={selectedArea} onChange={areaChangeHandler} onInputChange={setAreaInputValue} + onMenuScrollToBottom={loadMoreAreas} isClearable className={{ wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4', @@ -312,6 +317,7 @@ const DailyMarketingReportContent = () => { value={selectedLocation} onChange={locationChangeHandler} onInputChange={setLocationInputValue} + onMenuScrollToBottom={loadMoreLocations} isClearable className={{ wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4', @@ -326,6 +332,7 @@ const DailyMarketingReportContent = () => { value={selectedWarehouse} onChange={warehouseChangeHandler} onInputChange={setWarehouseInputValue} + onMenuScrollToBottom={loadMoreWarehouses} isClearable className={{ wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4', @@ -340,6 +347,7 @@ const DailyMarketingReportContent = () => { value={selectedCustomer} onChange={customerChangeHandler} onInputChange={setCustomerInputValue} + onMenuScrollToBottom={loadMoreCustomers} isClearable className={{ wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4', diff --git a/src/components/pages/report/expense/ReportExpenseTable.tsx b/src/components/pages/report/expense/ReportExpenseTable.tsx index c34072a2..c809c153 100644 --- a/src/components/pages/report/expense/ReportExpenseTable.tsx +++ b/src/components/pages/report/expense/ReportExpenseTable.tsx @@ -26,6 +26,15 @@ import MenuItem from '@/components/menu/MenuItem'; import * as XLSX from 'xlsx'; import { generateReportExpensePDF } from './pdf/ReportExpenseExport'; import toast from 'react-hot-toast'; +import { + KandangApi, + LocationApi, + NonstockApi, + SupplierApi, +} from '@/services/api/master-data'; +import { Supplier } from '@/types/api/master-data/supplier'; +import { Kandang } from '@/types/api/master-data/kandang'; +import { Nonstock } from '@/types/api/master-data/nonstock'; const ReportExpenseTable = () => { // ===== STATE MANAGEMENT ===== @@ -64,16 +73,33 @@ const ReportExpenseTable = () => { }); // ===== SELECT OPTIONS ===== - const { options: optionsLocation, isLoadingOptions: isLoadingLocation } = - useSelect(`/master-data/locations`, 'id', 'name'); - const { options: optionsSupplier, isLoadingOptions: isLoadingSupplier } = - useSelect(`/master-data/suppliers`, 'id', 'name'); - const { options: optionsKandang, isLoadingOptions: isLoadingKandang } = - useSelect(`/master-data/kandangs`, 'id', 'name', '', { - location_id: filterState.location_id, - }); - const { options: optionsNonstock, isLoadingOptions: isLoadingNonstock } = - useSelect(`/master-data/nonstocks`, 'id', 'name'); + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + loadMore: loadMoreLocations, + } = useSelect(LocationApi.basePath, 'id', 'name'); + + const { + setInputValue: setSupplierInputValue, + options: supplierOptions, + isLoadingOptions: isLoadingSupplierOptions, + loadMore: loadMoreSuppliers, + } = useSelect(SupplierApi.basePath, 'id', 'name'); + + const { + setInputValue: setKandangInputValue, + options: kandangOptions, + isLoadingOptions: isLoadingKandangOptions, + loadMore: loadMoreKandangs, + } = useSelect(KandangApi.basePath, 'id', 'name'); + + const { + setInputValue: setNonstockInputValue, + options: nonstockOptions, + isLoadingOptions: isLoadingNonstockOptions, + loadMore: loadMoreNonstocks, + } = useSelect(NonstockApi.basePath, 'id', 'name'); const categoryOptions = useMemo( () => [ @@ -86,31 +112,31 @@ const ReportExpenseTable = () => { // Mendapatkan value option select dari filter state const selectedLocation = useMemo( () => - optionsLocation.find( + locationOptions.find( (opt) => String(opt.value) === filterState.location_id ) || null, - [optionsLocation, filterState.location_id] + [locationOptions, filterState.location_id] ); const selectedSupplier = useMemo( () => - optionsSupplier.find( + supplierOptions.find( (opt) => String(opt.value) === filterState.supplier_id ) || null, - [optionsSupplier, filterState.supplier_id] + [supplierOptions, filterState.supplier_id] ); const selectedKandang = useMemo( () => - optionsKandang.find( + kandangOptions.find( (opt) => String(opt.value) === filterState.kandang_id ) || null, - [optionsKandang, filterState.kandang_id] + [kandangOptions, filterState.kandang_id] ); const selectedNonstock = useMemo( () => - optionsNonstock.find( + nonstockOptions.find( (opt) => String(opt.value) === filterState.nonstock_id ) || null, - [optionsNonstock, filterState.nonstock_id] + [nonstockOptions, filterState.nonstock_id] ); const selectedCategory = useMemo( () => @@ -756,38 +782,46 @@ const ReportExpenseTable = () => { { const { options: customerOptions, + setInputValue: setCustomerInputValue, isLoadingOptions: isLoadingCustomers, loadMore: loadMoreCustomers, hasMore: hasMoreCustomers, @@ -62,6 +63,7 @@ const CustomerPaymentTab = () => { const { options: salesOptions, + setInputValue: setSalesInputValue, isLoadingOptions: isLoadingSales, loadMore: loadMoreSales, hasMore: hasMoreSales, @@ -654,6 +656,7 @@ const CustomerPaymentTab = () => { Array.isArray(val) ? val : val ? [val] : [] ); }} + onInputChange={setCustomerInputValue} isLoading={isLoadingCustomers} isClearable onMenuScrollToBottom={loadMoreCustomers} @@ -670,6 +673,7 @@ const CustomerPaymentTab = () => { onChange={(val) => { setFilterSales(Array.isArray(val) ? val : val ? [val] : []); }} + onInputChange={setSalesInputValue} isLoading={isLoadingSales} isClearable onMenuScrollToBottom={loadMoreSales} diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index 2214ecd6..a2607e2b 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -33,6 +33,7 @@ import { } from '@/components/pages/report/finance/filter/DebtSupplierFilter'; import { getFilledFormikValuesCount } from '@/lib/formik-helper'; import ButtonFilter from '@/components/helper/ButtonFilter'; +import { Supplier } from '@/types/api/master-data/supplier'; const DebtSupplierTab = () => { // ===== STATE MANAGEMENT ===== @@ -51,10 +52,12 @@ const DebtSupplierTab = () => { const filterModal = useModal(); - const { options: supplierOptions, isLoadingOptions: isLoadingSuppliers } = - useSelect(SupplierApi.basePath, 'id', 'name', '', { - limit: 'limit', - }); + const { + setInputValue: setSupplierInputValue, + options: supplierOptions, + isLoadingOptions: isLoadingSupplierOptions, + loadMore: loadMoreSuppliers, + } = useSelect(SupplierApi.basePath, 'id', 'name'); const dataTypeOptions = useMemo( () => [ @@ -610,7 +613,9 @@ const DebtSupplierTab = () => { Array.isArray(val) ? val : val ? [val] : null ); }} - isLoading={isLoadingSuppliers} + onInputChange={setSupplierInputValue} + onMenuScrollToBottom={loadMoreSuppliers} + isLoading={isLoadingSupplierOptions} isClearable className={{ wrapper: 'w-full' }} isError={ diff --git a/src/components/pages/report/production-result/ProductionResultContent.tsx b/src/components/pages/report/production-result/ProductionResultContent.tsx index ae6f744b..7820ff53 100644 --- a/src/components/pages/report/production-result/ProductionResultContent.tsx +++ b/src/components/pages/report/production-result/ProductionResultContent.tsx @@ -62,6 +62,7 @@ const ProductionResultContent = () => { setInputValue: setAreaInputValue, options: areaOptions, isLoadingOptions: isLoadingAreaOptions, + loadMore: loadMoreAreas, } = useSelect(AreaApi.basePath, 'id', 'name'); const areaChangeHandler = (val: OptionType | OptionType[] | null) => { @@ -78,6 +79,7 @@ const ProductionResultContent = () => { setInputValue: setLocationInputValue, options: locationOptions, isLoadingOptions: isLoadingLocationOptions, + loadMore: loadMoreLocations, } = useSelect(LocationApi.basePath, 'id', 'name', 'search', { area_id: selectedArea ? ((selectedArea as OptionType).value as string) : '', }); @@ -94,6 +96,7 @@ const ProductionResultContent = () => { setInputValue: setProjectFlockInputValue, options: projectFlockOptions, isLoadingOptions: isLoadingProjectFlockOptions, + loadMore: loadMoreProjectFlocks, } = useSelect( ProjectFlockApi.basePath, 'id', @@ -120,6 +123,7 @@ const ProductionResultContent = () => { setInputValue: setProjectFlockKandangInputValue, options: projectFlockKandangOptions, isLoadingOptions: isLoadingProjectFlockKandangOptions, + loadMore: loadMoreProjectFlockKandangs, } = useSelect( ProjectFlockKandangApi.basePath, 'id', @@ -235,6 +239,7 @@ const ProductionResultContent = () => { value={selectedArea} onChange={areaChangeHandler} onInputChange={setAreaInputValue} + onMenuScrollToBottom={loadMoreAreas} isClearable className={{ wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4', @@ -251,6 +256,7 @@ const ProductionResultContent = () => { value={selectedLocation} onChange={locationChangeHandler} onInputChange={setLocationInputValue} + onMenuScrollToBottom={loadMoreLocations} isClearable isDisabled={!selectedArea} className={{ @@ -270,6 +276,7 @@ const ProductionResultContent = () => { value={selectedProjectFlock} onChange={projectFlockChangeHandler} onInputChange={setProjectFlockInputValue} + onMenuScrollToBottom={loadMoreProjectFlocks} isClearable isDisabled={!selectedArea || !selectedLocation} className={{ @@ -289,6 +296,7 @@ const ProductionResultContent = () => { value={selectedProjectFlockKandang} onChange={projectFlockKandangChangeHandler} onInputChange={setProjectFlockKandangInputValue} + onMenuScrollToBottom={loadMoreProjectFlockKandangs} isClearable isDisabled={!selectedProjectFlock} className={{ diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 7d6f0951..eda88d8c 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -58,18 +58,26 @@ const HppPerKandangTab = () => { }, }); - const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( - AreaApi.basePath, - 'id', - 'name', - 'search' - ); + const { + setInputValue: setAreaInputValue, + options: areaOptions, + isLoadingOptions: isLoadingAreas, + loadMore: loadMoreAreas, + } = useSelect(AreaApi.basePath, 'id', 'name', 'search'); - const { options: locationOptions, isLoadingOptions: isLoadingLocations } = - useSelect(LocationApi.basePath, 'id', 'name', 'search'); + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocations, + loadMore: loadMoreLocations, + } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); - const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } = - useSelect(KandangApi.basePath, 'id', 'name', 'search'); + const { + setInputValue: setKandangInputValue, + options: kandangOptions, + isLoadingOptions: isLoadingKandangs, + loadMore: loadMoreKandangs, + } = useSelect(KandangApi.basePath, 'id', 'name', 'search'); const showUnrecordedOptions: OptionType[] = [ { value: 'false', label: 'Sembunyikan' }, @@ -810,6 +818,8 @@ const HppPerKandangTab = () => { .includes(String(opt.value)) )} onChange={areaChangeHandler} + onInputChange={setAreaInputValue} + onMenuScrollToBottom={loadMoreAreas} isLoading={isLoadingAreas} isClearable /> @@ -824,6 +834,8 @@ const HppPerKandangTab = () => { .includes(String(opt.value)) )} onChange={locationChangeHandler} + onInputChange={setLocationInputValue} + onMenuScrollToBottom={loadMoreLocations} isLoading={isLoadingLocations} isClearable /> @@ -838,6 +850,8 @@ const HppPerKandangTab = () => { .includes(String(opt.value)) )} onChange={kandangChangeHandler} + onInputChange={setKandangInputValue} + onMenuScrollToBottom={loadMoreKandangs} isLoading={isLoadingKandangs} isClearable /> From cf332b534687d8475190bdc827e870c100f43476 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 10:00:56 +0700 Subject: [PATCH 026/139] refactor(FE): Add load-on-scroll for expedition vendor select --- .../purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index 2b18afee..2f619778 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -156,6 +156,8 @@ const PurchaseOrderAcceptApprovalForm = ({ setInputValue: setExpeditionsSelectInputValue, options: expeditionVendors, isLoadingOptions: isLoadingExpeditions, + loadMore: loadMoreExpeditions, + hasMore: hasMoreExpeditions, } = useSelect(SupplierApi.basePath, 'id', 'name', 'search', { category: 'BOP', }); @@ -570,6 +572,8 @@ const PurchaseOrderAcceptApprovalForm = ({ expeditionVendorChangeHandler(idx, val) } options={getExpeditionVendorOptions()} + isLoading={isLoadingExpeditions} + onMenuScrollToBottom={loadMoreExpeditions} isError={ isRepeaterInputError(idx, 'expedition_vendor_id') .isError From 4b88b684af451bcc4aa3e35fd9454fab669c1a84 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 10:12:52 +0700 Subject: [PATCH 027/139] refactor(FE): Load locations by area and disable location select --- .../form/request/PurchaseRequestForm.tsx | 74 ++++++++++++------- 1 file changed, 47 insertions(+), 27 deletions(-) diff --git a/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx index 0c319f5a..adab2fbe 100644 --- a/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx +++ b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx @@ -63,6 +63,10 @@ const PurchaseRequestForm = ({ useState(''); const [formErrorList, setFormErrorList] = useState([]); + const [selectedArea, setSelectedArea] = useState(''); + const [selectedLocation, setSelectedLocation] = useState(''); + const [disabledLocation, setDisabledLocation] = useState(true); + // ===== TYPE DEFINITIONS ===== interface ProductOptionType { value: number; @@ -160,6 +164,18 @@ const PurchaseRequestForm = ({ isLoadingOptions: isLoadingAreas, } = useSelect(AreaApi.basePath, 'id', 'name', 'search'); + const { + options: locationOptions, + isLoadingOptions: isLoadingLocations, + loadMore: loadMoreLocations, + hasMore: hasMoreLocations, + } = useSelect(LocationApi.basePath, 'id', 'name', '', { + area_id: + selectedArea != '' + ? selectedArea + : ((initialValues?.area?.id ?? '') as string), + }); + const { inputValue: warehouseSelectInputValue, setInputValue: setWarehouseSelectInputValue, @@ -267,31 +283,6 @@ const PurchaseRequestForm = ({ return data; }, [supplierData]); - const locationsUrl = useMemo(() => { - const params = new URLSearchParams({ - search: locationSelectInputValue, - ...(formik.values.area_id && formik.values.area_id > 0 - ? { area_id: formik.values.area_id.toString() } - : {}), - }); - return `${LocationApi.basePath}?${params.toString()}`; - }, [locationSelectInputValue, formik.values.area_id]); - - const { data: locations, isLoading: isLoadingLocations } = useSWR( - locationsUrl, - LocationApi.getAllFetcher - ); - - const locationOptions = useMemo(() => { - if (!isResponseSuccess(locations)) return []; - return ( - locations?.data.map((location) => ({ - value: location.id, - label: location.name, - })) || [] - ); - }, [locations]); - const warehousesUrl = useMemo(() => { const params = new URLSearchParams({ search: warehouseSelectInputValue }); @@ -407,6 +398,18 @@ const PurchaseRequestForm = ({ } }, [formik.values.supplier_id]); + useEffect(() => { + if (type !== 'add' && initialValues) { + if (initialValues.area?.id) { + setSelectedArea(initialValues.area.id.toString()); + setDisabledLocation(false); + } + if (initialValues.location?.id) { + setSelectedLocation(initialValues.location.id.toString()); + } + } + }, [type, initialValues]); + // ===== FORM HANDLERS ===== const handleSupplierChange = useCallback( (val: OptionType | OptionType[] | null) => { @@ -445,6 +448,16 @@ const PurchaseRequestForm = ({ formik.setFieldValue('area_id', (area as OptionType)?.value || 0); formik.setFieldTouched('area', true); formik.setFieldValue('area', area); + + setSelectedArea((area as OptionType)?.value as string); + setSelectedLocation(''); + const disabled = (area as OptionType)?.value == null; + setDisabledLocation(disabled); + + formik.setFieldTouched('location_id', false); + formik.setFieldValue('location_id', 0); + formik.setFieldTouched('location', false); + formik.setFieldValue('location', null); }, [] ); @@ -456,6 +469,8 @@ const PurchaseRequestForm = ({ formik.setFieldValue('location_id', (location as OptionType)?.value || 0); formik.setFieldTouched('location', true); formik.setFieldValue('location', location); + + setSelectedLocation((location as OptionType)?.value as string); }, [] ); @@ -596,10 +611,15 @@ const PurchaseRequestForm = ({ placeholder='Pilih Lokasi...' value={formik.values.location} onChange={handleLocationChange} - options={locationOptions} + options={ + selectedArea != '' || initialValues?.area?.id + ? locationOptions + : [] + } onInputChange={setLocationSelectInputValue} isLoading={isLoadingLocations} - isDisabled={type === 'detail'} + onMenuScrollToBottom={loadMoreLocations} + isDisabled={type === 'detail' || disabledLocation} isClearable={type !== 'detail'} /> From 3bc5030a3d3df1297886147199e3a7b26d2b1ce9 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 10:17:08 +0700 Subject: [PATCH 028/139] refactor(FE): Use useSelect for warehouse options --- .../form/request/PurchaseRequestForm.tsx | 64 +++++-------------- 1 file changed, 16 insertions(+), 48 deletions(-) diff --git a/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx index adab2fbe..9a54d537 100644 --- a/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx +++ b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx @@ -67,12 +67,6 @@ const PurchaseRequestForm = ({ const [selectedLocation, setSelectedLocation] = useState(''); const [disabledLocation, setDisabledLocation] = useState(true); - // ===== TYPE DEFINITIONS ===== - interface ProductOptionType { - value: number; - label: string; - } - // ===== UTILITY FUNCTIONS ===== const isRepeaterInputError = ( idx: number, @@ -179,8 +173,20 @@ const PurchaseRequestForm = ({ const { inputValue: warehouseSelectInputValue, setInputValue: setWarehouseSelectInputValue, + options: warehouseOptions, isLoadingOptions: isLoadingWarehouses, - } = useSelect(WarehouseApi.basePath, 'id', 'name', 'search'); + loadMore: loadMoreWarehouses, + hasMore: hasMoreWarehouses, + } = useSelect(WarehouseApi.basePath, 'id', 'name', 'search', { + area_id: + selectedArea != '' + ? selectedArea + : ((initialValues?.area?.id ?? '') as string), + location_id: + selectedLocation != '' + ? selectedLocation + : ((initialValues?.location?.id ?? '') as string), + }); // ===== FORM CONFIGURATION ===== const formikInitialValues = useMemo( @@ -283,45 +289,6 @@ const PurchaseRequestForm = ({ return data; }, [supplierData]); - const warehousesUrl = useMemo(() => { - const params = new URLSearchParams({ search: warehouseSelectInputValue }); - - if (formik.values.area_id && formik.values.area_id > 0) { - params.append('area_id', formik.values.area_id.toString()); - } - - if (formik.values.location_id && formik.values.location_id > 0) { - params.append('location_id', formik.values.location_id.toString()); - } - - return `${WarehouseApi.basePath}?${params.toString()}`; - }, [ - warehouseSelectInputValue, - formik.values.area_id, - formik.values.location_id, - ]); - - const { data: warehouses } = useSWR( - warehousesUrl, - WarehouseApi.getAllFetcher - ); - - const warehouseOptions = useMemo(() => { - if (!isResponseSuccess(warehouses)) return []; - - return ( - warehouses?.data.map((w) => ({ - value: w.id, - label: w.name, - area: w.area?.name, - location: - 'type' in w && (w.type === 'LOKASI' || w.type === 'KANDANG') - ? w.location?.name - : undefined, - })) || [] - ); - }, [warehouses]); - const addPurchaseItem = () => { const newItems = [ ...(formik.values.items || []), @@ -733,6 +700,7 @@ const PurchaseRequestForm = ({ options={warehouseOptions} onInputChange={setWarehouseSelectInputValue} isLoading={isLoadingWarehouses} + onMenuScrollToBottom={loadMoreWarehouses} isError={ isRepeaterInputError(idx, 'warehouse_id').isError } @@ -752,9 +720,9 @@ const PurchaseRequestForm = ({ required value={item.product ?? undefined} onChange={(val) => { - const product = val as ProductOptionType | null; + const product = val as OptionType | null; const productId = - (product as ProductOptionType)?.value || 0; + (product as OptionType)?.value || 0; formik.setFieldTouched( `items.${idx}.product`, From a1301121ac9a9dc12db15a644cc85647d7be8ba4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 10:44:27 +0700 Subject: [PATCH 029/139] refactor(FE): Refactor selects to use useSelect hook --- .../recording/form/RecordingForm.tsx | 299 ++++++++---------- 1 file changed, 129 insertions(+), 170 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index dec106b1..f492c987 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -12,7 +12,10 @@ import RequirePermission from '@/components/helper/RequirePermission'; import Card from '@/components/Card'; import Badge from '@/components/Badge'; import NumberInput from '@/components/input/NumberInput'; -import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; import CheckboxInput from '@/components/input/CheckboxInput'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; @@ -26,6 +29,7 @@ import { } from '@/services/api/production'; import { LocationApi } from '@/services/api/master-data'; import { ProductWarehouseApi } from '@/services/api/inventory'; +import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; import { CreateGrowingRecordingPayload, @@ -36,7 +40,10 @@ import { NextDayRecording, } from '@/types/api/production/recording'; import { type BaseApiResponse } from '@/types/api/api-general'; -import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock'; +import { + ProjectFlockKandangLookup, + ProjectFlock, +} from '@/types/api/production/project-flock'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; import { Kandang } from '@/types/api/master-data/kandang'; @@ -77,16 +84,27 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const [selectedDepletions, setSelectedDepletions] = useState([]); const [selectedEggs, setSelectedEggs] = useState([]); - const [locationSearchValue, setLocationSearchValue] = useState(''); const [selectedLocation, setSelectedLocation] = useState( null ); - const [projectFlockSearchValue, setProjectFlockSearchValue] = useState(''); const [selectedProjectFlock, setSelectedProjectFlock] = useState(null); const [selectedKandang, setSelectedKandang] = useState( null ); + const [selectedProjectFlockLocationId, setSelectedProjectFlockLocationId] = + useState(''); + const [stockProductsLocationId, setStockProductsLocationId] = + useState(''); + const [stockProductsKandangId, setStockProductsKandangId] = + useState(''); + const [depletionProductsLocationId, setDepletionProductsLocationId] = + useState(''); + const [depletionProductsKandangId, setDepletionProductsKandangId] = + useState(''); + const [eggProductsLocationId, setEggProductsLocationId] = + useState(''); + const [eggProductsKandangId, setEggProductsKandangId] = useState(''); const [isApproveLoading, setIsApproveLoading] = useState(false); const [isRejectLoading, setIsRejectLoading] = useState(false); @@ -210,26 +228,24 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }, [deleteModal, initialValues?.id, router]); // ===== API DATA FETCHING ===== - const locationsUrl = `${LocationApi.basePath}?${new URLSearchParams({ - search: locationSearchValue || '', - limit: '100', - }).toString()}`; - const { data: locations, isLoading: isLoadingLocations } = useSWR( - locationsUrl, - LocationApi.getAllFetcher - ); + const { + setInputValue: setLocationSearchValue, + options: locationOptions, + isLoadingOptions: isLoadingLocations, + loadMore: loadMoreLocations, + hasMore: hasMoreLocations, + } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); - const projectFlocksUrl = `${ProjectFlockApi.basePath}?${new URLSearchParams({ - search: projectFlockSearchValue || '', - limit: '100', - ...(selectedLocation - ? { location_id: selectedLocation.value.toString() } - : {}), - }).toString()}`; - const { data: projectFlocks, isLoading: isLoadingProjectFlocks } = useSWR( - projectFlocksUrl, - ProjectFlockApi.getAllFetcher - ); + const { + setInputValue: setProjectFlockSearchValue, + options: projectFlockOptions, + rawData: projectFlocksRawData, + isLoadingOptions: isLoadingProjectFlocks, + loadMore: loadMoreProjectFlocks, + hasMore: hasMoreProjectFlocks, + } = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', 'search', { + location_id: selectedProjectFlockLocationId, + }); const projectFlockKandangLookupUrl = useMemo(() => { if (!selectedProjectFlock || !selectedKandang) return null; @@ -279,46 +295,28 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ? projectFlockKandangDetailData.data : undefined; - const stockProductsUrl = useMemo(() => { - if (!selectedLocation || !selectedKandang) return null; - const params = new URLSearchParams({ - flags: 'PAKAN,OVK', - search: '', - limit: '100', - location_id: selectedLocation.value.toString(), - }); + const { + options: stockProductOptions, + rawData: stockProducts, + isLoadingOptions: isLoadingStockProducts, + loadMore: loadMoreStockProducts, + hasMore: hasMoreStockProducts, + } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', '', { + flags: 'PAKAN,OVK', + location_id: stockProductsLocationId, + kandang_id: stockProductsKandangId, + }); - if (projectFlockKandangLookup?.kandang?.id) { - params.append( - 'kandang_id', - projectFlockKandangLookup.kandang.id.toString() - ); - } else if (selectedKandang) { - params.append('kandang_id', selectedKandang.value.toString()); - } - - return `${ProductWarehouseApi.basePath}?${params.toString()}`; - }, [selectedLocation, selectedKandang, projectFlockKandangLookup]); - - const depletionProductsUrl = useMemo(() => { - if (!selectedLocation || !selectedKandang) return null; - const params = new URLSearchParams({ - search: '', - limit: '100', - location_id: selectedLocation.value.toString(), - }); - - if (projectFlockKandangLookup?.kandang?.id) { - params.append( - 'kandang_id', - projectFlockKandangLookup.kandang.id.toString() - ); - } else if (selectedKandang) { - params.append('kandang_id', selectedKandang.value.toString()); - } - - return `${ProductWarehouseApi.basePath}?${params.toString()}`; - }, [selectedLocation, selectedKandang, projectFlockKandangLookup]); + const { + options: depletionProductOptions, + rawData: depletionProductsData, + isLoadingOptions: isLoadingDepletionProducts, + loadMore: loadMoreDepletionProducts, + hasMore: hasMoreDepletionProducts, + } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', '', { + location_id: depletionProductsLocationId, + kandang_id: depletionProductsKandangId, + }); const today = new Date().toISOString().split('T')[0]; const existingRecordingsUrl = `${RecordingApi.basePath}`; @@ -360,38 +358,17 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } }, [nextDayRecordingData]); - const { data: stockProducts, isLoading: isLoadingStockProducts } = useSWR( - stockProductsUrl, - ProductWarehouseApi.getAllFetcher - ); - - const { data: depletionProductsData, isLoading: isLoadingDepletionProducts } = - useSWR(depletionProductsUrl, ProductWarehouseApi.getAllFetcher); - - const eggProductsUrl = useMemo(() => { - if (!selectedLocation || !selectedKandang) return null; - const params = new URLSearchParams({ - search: 'telur', - limit: '100', - location_id: selectedLocation.value.toString(), - }); - - if (projectFlockKandangLookup?.kandang?.id) { - params.append( - 'kandang_id', - projectFlockKandangLookup.kandang.id.toString() - ); - } else if (selectedKandang) { - params.append('kandang_id', selectedKandang.value.toString()); - } - - return `${ProductWarehouseApi.basePath}?${params.toString()}`; - }, [selectedLocation, selectedKandang, projectFlockKandangLookup]); - - const { data: eggProductsData, isLoading: isLoadingEggProducts } = useSWR( - eggProductsUrl, - ProductWarehouseApi.getAllFetcher - ); + const { + options: eggProductOptions, + rawData: eggProductsData, + isLoadingOptions: isLoadingEggProducts, + loadMore: loadMoreEggProducts, + hasMore: hasMoreEggProducts, + } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', { + search: 'telur', + location_id: eggProductsLocationId, + kandang_id: eggProductsKandangId, + }); const approvedProjectFlockKandangsUrl = useMemo(() => { const params = new URLSearchParams({ @@ -448,17 +425,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }); // ===== DATA PROCESSING ===== - const locationOptions = useMemo(() => { - let options: OptionType[] = []; - - if (isResponseSuccess(locations)) { - const locationOptionsList = - locations?.data.map((location) => ({ - value: location.id, - label: location.name || '', - })) || []; - options = options.concat(locationOptionsList); - } + const enhancedLocationOptions = useMemo(() => { + const options = [...locationOptions]; if (projectFlockKandangDetail && (type === 'edit' || type === 'detail')) { const currentLocation = projectFlockKandangDetail.project_flock.location; @@ -474,19 +442,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } return options; - }, [locations, projectFlockKandangDetail, type]); + }, [locationOptions, projectFlockKandangDetail, type]); - const projectFlockOptions = useMemo(() => { - let options: OptionType[] = []; - - if (isResponseSuccess(projectFlocks)) { - const flockOptions = - projectFlocks?.data.map((projectFlock) => ({ - value: projectFlock.id, - label: projectFlock.flock_name || '', - })) || []; - options = options.concat(flockOptions); - } + const enhancedProjectFlockOptions = useMemo(() => { + const options = [...projectFlockOptions]; if (projectFlockKandangDetail && (type === 'edit' || type === 'detail')) { const currentProjectFlock = projectFlockKandangDetail.project_flock; @@ -502,13 +461,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } return options; - }, [projectFlocks, projectFlockKandangDetail, type]); + }, [projectFlockOptions, projectFlockKandangDetail, type]); const kandangOptions = useMemo(() => { let options: OptionType[] = []; - if (selectedProjectFlock && isResponseSuccess(projectFlocks)) { - const selectedProjectFlockData = projectFlocks.data.find( + if (selectedProjectFlock && isResponseSuccess(projectFlocksRawData)) { + const data = projectFlocksRawData.data as ProjectFlock[]; + const selectedProjectFlockData = data.find( (pf) => pf.id === selectedProjectFlock.value ); @@ -548,7 +508,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { return options; }, [ selectedProjectFlock, - projectFlocks, + projectFlocksRawData, projectFlockKandangDetail, type, approvedProjectFlockKandangs, @@ -598,20 +558,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ]); const unifiedStockProducts = useMemo(() => { - const options: OptionType[] = []; - if (isResponseSuccess(stockProducts) && selectedKandang) { - stockProducts.data.forEach((product) => { - const hasPakanFlag = product.product.flags?.includes('PAKAN'); - const hasOvkFlag = product.product.flags?.includes('OVK'); - - if (hasPakanFlag || hasOvkFlag) { - options.push({ - value: product.id, - label: product.product.name, - }); - } - }); - } + const options = [...stockProductOptions]; if ( initialValues && @@ -635,12 +582,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } return options; - }, [stockProducts, initialValues, type, selectedKandang]); + }, [stockProductOptions, initialValues, type]); const depletionProducts = useMemo(() => { const options: OptionType[] = []; + if (isResponseSuccess(depletionProductsData) && selectedKandang) { - depletionProductsData.data.forEach((product) => { + const data = depletionProductsData.data as unknown as ProductWarehouse[]; + data.forEach((product) => { const productName = product.product.name; if ( @@ -680,8 +629,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const eggProducts = useMemo(() => { const options: OptionType[] = []; + if (isResponseSuccess(eggProductsData) && selectedKandang) { - eggProductsData.data.forEach((product) => { + const data = eggProductsData.data as unknown as ProductWarehouse[]; + data.forEach((product) => { const productName = product.product.name; if ( @@ -812,33 +763,12 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }; // ===== HELPER FUNCTIONS ===== - useCallback((): OptionType | null => { - if ( - !formik.values.project_flock_kandang || - !isResponseSuccess(projectFlocks) - ) { - return selectedLocation; - } - const projectFlockId = formik.values.project_flock_kandang.value; - const projectFlock = projectFlocks.data.find( - (pf) => pf.id === projectFlockId - ); - if (projectFlock && projectFlock.location) { - return { - value: projectFlock.location.id, - label: projectFlock.location.name, - }; - } - return selectedLocation; - }, [formik.values.project_flock_kandang, projectFlocks, selectedLocation]); - const getAvailableStock = useCallback( (productWarehouseId: number) => { if ((type as 'add' | 'edit' | 'detail') === 'detail') return 0; if (!isResponseSuccess(stockProducts)) return 0; - const productWarehouse = stockProducts.data.find( - (pw) => pw.id === productWarehouseId - ); + const data = stockProducts.data as unknown as ProductWarehouse[]; + const productWarehouse = data.find((pw) => pw.id === productWarehouseId); return productWarehouse?.quantity ?? 0; }, [stockProducts, type] @@ -915,9 +845,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { (productWarehouseId: number) => { if (!isResponseSuccess(stockProducts)) return null; - const productWarehouse = stockProducts.data.find( - (pw) => pw.id === productWarehouseId - ); + const data = stockProducts.data as unknown as ProductWarehouse[]; + const productWarehouse = data.find((pw) => pw.id === productWarehouseId); if (!productWarehouse) return null; const hasPakanFlag = productWarehouse.product.flags?.includes('PAKAN'); @@ -1002,9 +931,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { // ===== EVENT HANDLERS ===== const locationChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedLocation(val as OptionType); + const location = val as OptionType; + setSelectedLocation(location); setSelectedProjectFlock(null); setSelectedKandang(null); + setSelectedProjectFlockLocationId( + location ? location.value.toString() : '' + ); formik.setFieldValue('project_flock_kandang', null); formik.setFieldValue('project_flock_kandang_id', 0); }; @@ -1017,7 +950,23 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }; const kandangChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedKandang(val as OptionType); + const kandang = val as OptionType; + setSelectedKandang(kandang); + if (selectedLocation && kandang) { + setStockProductsLocationId(selectedLocation.value.toString()); + setStockProductsKandangId(kandang.value.toString()); + setDepletionProductsLocationId(selectedLocation.value.toString()); + setDepletionProductsKandangId(kandang.value.toString()); + setEggProductsLocationId(selectedLocation.value.toString()); + setEggProductsKandangId(kandang.value.toString()); + } else { + setStockProductsLocationId(''); + setStockProductsKandangId(''); + setDepletionProductsLocationId(''); + setDepletionProductsKandangId(''); + setEggProductsLocationId(''); + setEggProductsKandangId(''); + } formik.setFieldTouched('project_flock_kandang', true); formik.setFieldTouched('project_flock_kandang_id', true); }; @@ -1091,6 +1040,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { label: location.name || '', }; setSelectedLocation(locationOption); + setSelectedProjectFlockLocationId(location.id.toString()); if (projectFlock) { const projectFlockOption = { @@ -1106,6 +1056,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }; setSelectedKandang(kandangOption); + setStockProductsLocationId(location.id.toString()); + setStockProductsKandangId(kandang.id.toString()); + setDepletionProductsLocationId(location.id.toString()); + setDepletionProductsKandangId(kandang.id.toString()); + setEggProductsLocationId(location.id.toString()); + setEggProductsKandangId(kandang.id.toString()); + if ( formik.values.project_flock_kandang_id !== projectFlockKandangDetail.id @@ -1126,7 +1083,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }, [ projectFlockKandangDetail, type, - projectFlockOptions, + enhancedProjectFlockOptions, formik.values.project_flock_kandang_id, ]); @@ -1415,23 +1372,25 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { label='Lokasi' value={selectedLocation} onChange={locationChangeHandler} - options={locationOptions} + options={enhancedLocationOptions} onInputChange={setLocationSearchValue} isLoading={isLoadingLocations} + onMenuScrollToBottom={loadMoreLocations} placeholder='Pilih Lokasi' isClearable isSearchable /> Date: Thu, 15 Jan 2026 10:51:33 +0700 Subject: [PATCH 030/139] refactor(FE): Add onMenuScrollToBottom to product selects --- .../pages/production/recording/form/RecordingForm.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index f492c987..db61080e 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -1936,6 +1936,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { options={unifiedStockProducts} placeholder='Pilih Produk' isLoading={isLoadingStockProducts} + onMenuScrollToBottom={loadMoreStockProducts} isError={ isRepeaterInputError( 'stocks', @@ -2157,6 +2158,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { options={depletionProducts} placeholder='Pilih Kondisi' isLoading={isLoadingDepletionProducts} + onMenuScrollToBottom={loadMoreDepletionProducts} isError={ isRepeaterInputError( 'depletions', @@ -2376,6 +2378,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { options={eggProducts} placeholder='Pilih Kondisi Telur' isLoading={isLoadingEggProducts} + onMenuScrollToBottom={loadMoreEggProducts} isError={ isRepeaterInputError( 'eggs', From 57a148b6cf3a05f6420a537ab0bc798252d600a9 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 11:45:31 +0700 Subject: [PATCH 031/139] feat: add price per supplier input in product form --- .../product/form/ProductForm.schema.ts | 45 ++-- .../master-data/product/form/ProductForm.tsx | 219 ++++++++++++++---- 2 files changed, 210 insertions(+), 54 deletions(-) diff --git a/src/components/pages/master-data/product/form/ProductForm.schema.ts b/src/components/pages/master-data/product/form/ProductForm.schema.ts index 9dcf713e..b85d5d4c 100644 --- a/src/components/pages/master-data/product/form/ProductForm.schema.ts +++ b/src/components/pages/master-data/product/form/ProductForm.schema.ts @@ -3,7 +3,7 @@ import * as Yup from 'yup'; type ProductFormSchemaType = { name: string; brand: string; - sku: string; + sku?: string; uom?: { value: number; label: string; @@ -15,10 +15,16 @@ type ProductFormSchemaType = { } | null; product_category_id: number; product_price: number | string; - selling_price: number | string; - tax: number | string; - expiry_period: number | string; - supplier_ids: number[]; + selling_price?: number | string; + tax?: number | string; + expiry_period?: number | string; + suppliers: { + supplier: { + value: number; + label: string; + } | null; + price: number; + }[]; flags: string[]; }; @@ -26,7 +32,7 @@ export const ProductFormSchema: Yup.ObjectSchema = Yup.object({ name: Yup.string().required('Nama wajib diisi!'), brand: Yup.string().required('Merek wajib diisi!'), - sku: Yup.string().required('SKU wajib diisi!'), + sku: Yup.string(), uom: Yup.object({ value: Yup.number() @@ -58,23 +64,34 @@ export const ProductFormSchema: Yup.ObjectSchema = .min(1, 'Harga produk tidak boleh kurang dari 1!'), selling_price: Yup.number() - .required('Harga jual wajib diisi!') - .typeError('Harga jual wajib diisi!') + .typeError('Harga hanya boleh angka!') .min(1, 'Harga jual tidak boleh kurang dari 1!'), tax: Yup.number() - .required('Pajak wajib diisi!') - .typeError('Pajak wajib diisi!') + .typeError('Pajak hanya boleh angka!') .min(0, 'Pajak tidak boleh kurang dari 0!') .max(100, 'Pajak tidak boleh lebih dari 100%!'), expiry_period: Yup.number() - .required('Periode kadaluarsa wajib diisi!') - .typeError('Periode kadaluarsa wajib diisi!') + .typeError('Periode kadaluarsa hanya boleh angka!') .min(1, 'Periode kadaluarsa tidak boleh kurang dari 1 hari!'), - supplier_ids: Yup.array() - .of(Yup.number().required().typeError('Supplier tidak valid!')) + suppliers: Yup.array() + .of( + Yup.object({ + supplier: Yup.object({ + value: Yup.number() + .min(1, 'Supplier wajib dipilih!') + .required('Supplier wajib dipilih!') + .typeError('Supplier wajib dipilih!'), + label: Yup.string().required('Supplier wajib dipilih!'), + }).required('Supplier wajib dipilih!'), + price: Yup.number() + .min(1, 'Harga tidak boleh kurang dari 1!') + .required('Harga wajib diisi!') + .typeError('Harga wajib diisi!'), + }) + ) .min(1, 'Minimal harus ada 1 supplier!') .required('Supplier wajib diisi!'), diff --git a/src/components/pages/master-data/product/form/ProductForm.tsx b/src/components/pages/master-data/product/form/ProductForm.tsx index 2fc3b267..7e893f67 100644 --- a/src/components/pages/master-data/product/form/ProductForm.tsx +++ b/src/components/pages/master-data/product/form/ProductForm.tsx @@ -41,6 +41,8 @@ import { cn } from '@/lib/helper'; import { PRODUCT_FLAG_OPTIONS } from '@/config/constant'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import { Supplier } from '@/types/api/master-data/supplier'; +import Card from '@/components/Card'; +import { removeArrayItemAndSync } from '@/lib/utils/formik'; interface ProductFormProps { type?: 'add' | 'edit' | 'detail'; @@ -101,7 +103,15 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { selling_price: initialValues?.selling_price ?? '', tax: initialValues?.tax ?? '', expiry_period: initialValues?.expiry_period ?? '', - supplier_ids: initialValues?.suppliers?.map((s) => s.id) ?? [], + suppliers: initialValues?.suppliers + ? initialValues.suppliers.map((supplier) => ({ + supplier: { + value: supplier.id, + label: supplier.name, + }, + price: supplier.price, + })) + : [], flags: initialValues?.flags ?? [], }), [initialValues] @@ -120,12 +130,17 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { uom_id: values.uom_id, product_category_id: values.product_category_id, product_price: parseInt(values.product_price.toString()) || 0, - selling_price: parseInt(values.selling_price.toString()) || 0, - tax: parseInt(values.tax.toString()) || 0, - expiry_period: parseInt(values.expiry_period.toString()) || 0, - supplier_ids: values.supplier_ids.filter( - (id): id is number => typeof id === 'number' - ), + selling_price: values.selling_price + ? parseInt(values.selling_price.toString()) || 0 + : undefined, + tax: values.tax ? parseInt(values.tax.toString()) || 0 : undefined, + expiry_period: values.expiry_period + ? parseInt(values.expiry_period.toString()) || 0 + : undefined, + suppliers: values.suppliers.map((s) => ({ + supplier_id: s.supplier?.value as number, + price: parseInt(s.price.toString()) || 0, + })), flags: values.flags.filter((f): f is string => typeof f === 'string'), }; switch (type) { @@ -179,13 +194,29 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { category: 'SAPRONAK', }); - const supplierChangeHandler = (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - formik.setFieldTouched('supplier_ids', true); - formik.setFieldValue( - 'supplier_ids', - arr.map((v) => (v as OptionType).value) - ); + const filteredSupplierOptions = useMemo(() => { + return supplierOptions.filter((opt) => { + return !formik.values.suppliers.some( + (s) => s.supplier?.value === opt.value + ); + }); + }, [supplierOptions, formik.values.suppliers]); + + const addSupplierHandler = () => { + formik.setFieldValue('suppliers', [ + ...formik.values.suppliers, + { + supplier_id: '', + price: formik.values.product_price, + }, + ]); + }; + + const deleteSupplierItemHandler = (idx: number) => { + const path = 'suppliers'; + + // trims values, errors, and touched at idx + removeArrayItemAndSync(formik, path, idx); }; const deleteProductClickHandler = () => { @@ -201,6 +232,19 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { router.push('/master-data/product'); }; + const isSupplierRepeaterError = ( + column: 'supplier' | 'price', + supplierIdx: number + ) => { + return ( + formik.touched.suppliers?.[supplierIdx]?.[column] && + Boolean( + formik.errors.suppliers?.[supplierIdx] instanceof Object && + formik.errors.suppliers?.[supplierIdx]?.[column] + ) + ); + }; + useEffect(() => { formikSetValues(formikInitialValues); }, [formikSetValues, formikInitialValues]); @@ -271,7 +315,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { readOnly={type === 'detail'} /> { readOnly={type === 'detail'} /> {
{ readOnly={type === 'detail'} /> { readOnly={type === 'detail'} />
-
- - (formik.values.supplier_ids || []).includes(opt.value) - )} - onChange={supplierChangeHandler} - options={supplierOptions} - onInputChange={setSupplierSelectInputValue} - onMenuScrollToBottom={loadMoreSuppliers} - isLoading={isLoadingSuppliers} - isError={ - formik.touched.supplier_ids && - Boolean(formik.errors.supplier_ids) - } - errorMessage={formik.errors.supplier_ids as string} - isDisabled={type === 'detail'} - isClearable - /> +
{ isClearable />
+ +
+ {type !== 'detail' && formik.values.suppliers.length === 0 && ( + + )} + + {formik.values.suppliers.length > 0 && ( + +
+

Supplier

+
+ +
+
+
+ {formatCurrency(row.original.accounts_receivable)} +
+
From efde742518ef38d60ba0f33ff360c83da0ab0382 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 17:03:40 +0700 Subject: [PATCH 013/139] refactor(FE): Highlight negative accounts receivable values --- .../report/finance/tab/CustomerPaymentTab.tsx | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 7d34a08f..951bc8f1 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -470,11 +470,17 @@ const CustomerPaymentTab = () => { cell: (props) => { const value = props.row.original.accounts_receivable; return ( -
{formatCurrency(value)}
+
+ {formatCurrency(value)} +
); }, footer: () => ( -
+
{formatCurrency(summary.total_accounts_receivable) || '-'}
), @@ -782,8 +788,14 @@ const CustomerPaymentTab = () => { className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap' colSpan={13} >
-
+
+
{formatCurrency(row.original.accounts_receivable)}
+ + + + + + + + + + {formik.values.suppliers.map((supplier, idx) => ( + + + + {type !== 'detail' && ( + + )} + + ))} + +
+ Supplier + + Harga + Aksi
+ { + formik.setFieldValue( + `suppliers.${idx}.supplier`, + val + ); + }} + isError={isSupplierRepeaterError( + 'supplier', + idx + )} + isClearable + className={{ + wrapper: 'min-w-48 w-full', + }} + /> + + + + +
+
+ +
+ +
+ + )} +
{type !== 'add' && ( From 395270464396925bb38f2af68e5b00a3fb8167e2 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 11:45:51 +0700 Subject: [PATCH 032/139] chore: fix typo in placeholder --- src/components/pages/master-data/kandang/form/KandangForm.tsx | 2 +- src/components/pages/master-data/nonstock/form/NonstockForm.tsx | 2 +- .../pages/master-data/warehouse/form/WarehouseForm.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/pages/master-data/kandang/form/KandangForm.tsx b/src/components/pages/master-data/kandang/form/KandangForm.tsx index acced3c5..22ad91f8 100644 --- a/src/components/pages/master-data/kandang/form/KandangForm.tsx +++ b/src/components/pages/master-data/kandang/form/KandangForm.tsx @@ -215,7 +215,7 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => { required label='Nama' name='name' - placeholder='Masukkan nama lokasi' + placeholder='Masukkan nama kandang' value={formik.values.name} onChange={formik.handleChange} onBlur={formik.handleBlur} diff --git a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx index cd2c361b..4622a6a3 100644 --- a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx +++ b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx @@ -229,7 +229,7 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { required label='Nama' name='name' - placeholder='Masukkan nama lokasi' + placeholder='Masukkan nama nonstock' value={formik.values.name} onChange={formik.handleChange} onBlur={formik.handleBlur} diff --git a/src/components/pages/master-data/warehouse/form/WarehouseForm.tsx b/src/components/pages/master-data/warehouse/form/WarehouseForm.tsx index cab9f750..a6a53e3f 100644 --- a/src/components/pages/master-data/warehouse/form/WarehouseForm.tsx +++ b/src/components/pages/master-data/warehouse/form/WarehouseForm.tsx @@ -330,7 +330,7 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => { required label='Nama' name='name' - placeholder='Masukkan nama lokasi' + placeholder='Masukkan nama warehouse' value={formik.values.name} onChange={formik.handleChange} onBlur={formik.handleBlur} From 790b59066819aaccf4dbd931913c3da41ae75c8b Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 11:46:10 +0700 Subject: [PATCH 033/139] feat: use real permission for daily checklist --- src/config/route-permission.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/config/route-permission.ts b/src/config/route-permission.ts index 9a0c9d2e..165bc8ee 100644 --- a/src/config/route-permission.ts +++ b/src/config/route-permission.ts @@ -5,22 +5,22 @@ export const ROUTE_PERMISSIONS: Record = { '/dashboard/': ['lti.dashboard.list'], // Daily Checklist - // TODO: use real daily checklist permission name - // '/daily-checklist/': ['lti.daily_checklist.list'], - // '/daily-checklist/dashboard/': ['lti.daily_checklist.list'], - // '/daily-checklist/list-daily-checklist/': ['lti.daily_checklist.list'], - // '/daily-checklist/list-daily-checklist/detail/': ['lti.daily_checklist.detail'], - // '/daily-checklist/reports/': ['lti.daily_checklist.reports'], - // '/daily-checklist/master-data/employee/': ['lti.dashboard.master_data.employee'], - // '/daily-checklist/master-data/activity/': ['lti.dashboard.master_data.activity'], - '/daily-checklist/dashboard/': ['lti.dashboard.list'], - '/daily-checklist/daily-checklist/': ['lti.dashboard.list'], - '/daily-checklist/list-daily-checklist/': ['lti.dashboard.list'], - '/daily-checklist/list-daily-checklist/detail/': ['lti.dashboard.list'], - '/daily-checklist/reports/': ['lti.dashboard.list'], - '/daily-checklist/master-data/employee/': ['lti.dashboard.list'], - '/daily-checklist/master-data/activity/': ['lti.dashboard.list'], - '/daily-checklist/master-data/configuration/': ['lti.dashboard.list'], + '/daily-checklist/dashboard/': ['lti.daily_checklist.dashboard.list'], + '/daily-checklist/daily-checklist/': ['lti.daily_checklist.create'], + '/daily-checklist/list-daily-checklist/': ['lti.daily_checklist.list'], + '/daily-checklist/list-daily-checklist/detail/': [ + 'lti.daily_checklist.detail', + ], + '/daily-checklist/reports/': ['lti.daily_checklist.reports'], + '/daily-checklist/master-data/employee/': [ + 'lti.daily_checklist.master_data.employee', + ], + '/daily-checklist/master-data/activity/': [ + 'lti.daily_checklist.master_data.activity', + ], + '/daily-checklist/master-data/configuration/': [ + 'lti.daily_checklist.master_data.configuration', + ], // Production // Production - Project Flock From bf38178969dc6ec919201c0c4da1637894f920dc Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 11:46:19 +0700 Subject: [PATCH 034/139] chore: update product type --- src/types/api/master-data/product.d.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/types/api/master-data/product.d.ts b/src/types/api/master-data/product.d.ts index e82f857e..7fd2c7c1 100644 --- a/src/types/api/master-data/product.d.ts +++ b/src/types/api/master-data/product.d.ts @@ -1,20 +1,20 @@ import { BaseMetadata } from '@/types/api/api-general'; import { Uom } from '@/types/api/master-data/uom'; import { ProductCategory } from '@/types/api/master-data/product-category'; -import { Supplier } from '@/types/api/master-data/supplier'; +import { BaseSupplier, Supplier } from '@/types/api/master-data/supplier'; export type BaseProduct = { id: number; name: string; brand: string; - sku: string; + sku?: string; product_price: number; selling_price?: number; tax?: number; - expiry_period: number; + expiry_period?: number; uom: Uom; product_category: ProductCategory; - suppliers: Supplier[]; + suppliers: (BaseSupplier & { price: number })[]; flags: string[]; }; @@ -23,14 +23,17 @@ export type Product = BaseMetadata & BaseProduct; export type CreateProductPayload = { name: string; brand: string; - sku: string; + sku?: string; uom_id: number; product_category_id: number; product_price: number; - selling_price: number; - tax: number; - expiry_period: number; - supplier_ids: number[]; + selling_price?: number; + tax?: number; + expiry_period?: number; + suppliers: { + supplier_id: number; + price: number; + }[]; flags: string[]; }; From 294c843bd480e3414aa65afe3d3aad3d52935489 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 11:58:38 +0700 Subject: [PATCH 035/139] refactor(FE): Use useSelect for project flock filter --- .../production/uniformity/UniformityTable.tsx | 77 ++++++++----------- 1 file changed, 30 insertions(+), 47 deletions(-) diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index 63c446ac..c2049ab1 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -37,7 +37,10 @@ import DateInput from '@/components/input/DateInput'; import { LocationApi } from '@/services/api/master-data'; import { ProjectFlockApi } from '@/services/api/production'; import { Kandang } from '@/types/api/master-data/kandang'; -import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock'; +import { + ProjectFlockKandangLookup, + ProjectFlock, +} from '@/types/api/production/project-flock'; import { getStatusColor, getStatusIndicatorColor, @@ -229,63 +232,37 @@ const UniformityTable = () => { useState(undefined); const [filterStartDate, setFilterStartDate] = useState(''); const [filterEndDate, setFilterEndDate] = useState(''); - const [projectFlockSearchValue, setProjectFlockSearchValue] = useState(''); + const [filterProjectFlockLocationId, setFilterProjectFlockLocationId] = + useState(''); const [filterErrors, setFilterErrors] = useState>({}); const { setInputValue: setFilterLocationInputValue, options: filterLocationOptions, isLoadingOptions: isLoadingFilterLocations, - } = useSelect(LocationApi.basePath, 'id', 'name', 'search', { - limit: '100', - }); + loadMore: loadMoreFilterLocations, + hasMore: hasMoreFilterLocations, + } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); // ===== FETCH PROJECT FLOCKS DATA FOR FILTER ===== - const filterProjectFlocksUrl = useMemo(() => { - const params = new URLSearchParams({ - search: projectFlockSearchValue || '', - limit: '100', - }); - if (filterLocation) { - params.append('location_id', filterLocation.value.toString()); - } - return `${ProjectFlockApi.basePath}?${params.toString()}`; - }, [projectFlockSearchValue, filterLocation]); - const { - data: filterProjectFlocksData, - isLoading: isLoadingFilterProjectFlocks, - } = useSWR(filterProjectFlocksUrl, ProjectFlockApi.getAllFetcher); - - const filterProjectFlocksDataList = useMemo( - () => - isResponseSuccess(filterProjectFlocksData) - ? filterProjectFlocksData.data - : undefined, - [filterProjectFlocksData] - ); - - const filterProjectFlockOptions = useMemo(() => { - let options: OptionType[] = []; - - if (isResponseSuccess(filterProjectFlocksData)) { - const flockOptions = - filterProjectFlocksData?.data.map((projectFlock) => ({ - value: projectFlock.id, - label: projectFlock.flock_name || '', - })) || []; - options = options.concat(flockOptions); - } - - return options; - }, [filterProjectFlocksData]); + setInputValue: setFilterProjectFlockSearchValue, + options: filterProjectFlockOptions, + rawData: filterProjectFlocksRawData, + isLoadingOptions: isLoadingFilterProjectFlocks, + loadMore: loadMoreFilterProjectFlocks, + hasMore: hasMoreFilterProjectFlocks, + } = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', 'search', { + location_id: filterProjectFlockLocationId, + }); // ===== KANDANG OPTIONS FOR FILTER ===== const filterKandangOptions = useMemo(() => { let options: OptionType[] = []; - if (filterProjectFlock && filterProjectFlocksDataList) { - const selectedProjectFlockData = filterProjectFlocksDataList.find( + if (filterProjectFlock && isResponseSuccess(filterProjectFlocksRawData)) { + const data = filterProjectFlocksRawData.data as unknown as ProjectFlock[]; + const selectedProjectFlockData = data.find( (pf) => pf.id === filterProjectFlock.value ); @@ -301,7 +278,7 @@ const UniformityTable = () => { } return options; - }, [filterProjectFlock, filterProjectFlocksDataList]); + }, [filterProjectFlock, filterProjectFlocksRawData]); // ===== PROJECT FLOCK KANDANG LOOKUP ===== const projectFlockKandangLookupUrl = useMemo(() => { @@ -394,9 +371,13 @@ const UniformityTable = () => { // ===== FILTER HANDLERS ===== const handleFilterLocationChange = useCallback( (val: OptionType | OptionType[] | null) => { - setFilterLocation(val as OptionType | null); + const location = val as OptionType | null; + setFilterLocation(location); setFilterProjectFlock(null); setFilterKandang(null); + setFilterProjectFlockLocationId( + location ? location.value.toString() : '' + ); }, [] ); @@ -1206,6 +1187,7 @@ const UniformityTable = () => { options={filterLocationOptions} onInputChange={setFilterLocationInputValue} isLoading={isLoadingFilterLocations} + onMenuScrollToBottom={loadMoreFilterLocations} className={{ wrapper: 'w-full' }} /> {filterErrors.location && ( @@ -1225,8 +1207,9 @@ const UniformityTable = () => { setFilterErrors((prev) => ({ ...prev, project_flock: '' })); }} options={filterProjectFlockOptions} - onInputChange={setProjectFlockSearchValue} + onInputChange={setFilterProjectFlockSearchValue} isLoading={isLoadingFilterProjectFlocks} + onMenuScrollToBottom={loadMoreFilterProjectFlocks} isDisabled={!filterLocation} className={{ wrapper: 'w-full' }} /> From 6a7990e722789afde820c49475a8368a4b3662c7 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 11:59:25 +0700 Subject: [PATCH 036/139] fix: make suppliers optional --- .../product/form/ProductForm.schema.ts | 1 - .../master-data/product/form/ProductForm.tsx | 23 +++++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/components/pages/master-data/product/form/ProductForm.schema.ts b/src/components/pages/master-data/product/form/ProductForm.schema.ts index b85d5d4c..8a1d3de2 100644 --- a/src/components/pages/master-data/product/form/ProductForm.schema.ts +++ b/src/components/pages/master-data/product/form/ProductForm.schema.ts @@ -92,7 +92,6 @@ export const ProductFormSchema: Yup.ObjectSchema = .typeError('Harga wajib diisi!'), }) ) - .min(1, 'Minimal harus ada 1 supplier!') .required('Supplier wajib diisi!'), flags: Yup.array() diff --git a/src/components/pages/master-data/product/form/ProductForm.tsx b/src/components/pages/master-data/product/form/ProductForm.tsx index 7e893f67..8c04d594 100644 --- a/src/components/pages/master-data/product/form/ProductForm.tsx +++ b/src/components/pages/master-data/product/form/ProductForm.tsx @@ -527,6 +527,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { idx )} isClearable + isDisabled={type === 'detail'} className={{ wrapper: 'min-w-48 w-full', }} @@ -573,16 +574,18 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
-
- -
+ {type !== 'detail' && ( +
+ +
+ )} )} From dc3b4f1850174873ea1518bef00b770bb9a0bd07 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 12:05:34 +0700 Subject: [PATCH 037/139] refactor(FE): Use useSelect for ProjectFlock with pagination --- .../uniformity/form/UniformityForm.tsx | 77 ++++++++----------- 1 file changed, 30 insertions(+), 47 deletions(-) diff --git a/src/components/pages/production/uniformity/form/UniformityForm.tsx b/src/components/pages/production/uniformity/form/UniformityForm.tsx index 54b4ee2b..f46a15b3 100644 --- a/src/components/pages/production/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityForm.tsx @@ -36,7 +36,10 @@ import { VerifyUniformityPayload, } from '@/types/api/production/uniformity'; import { type BaseApiResponse } from '@/types/api/api-general'; -import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock'; +import { + ProjectFlockKandangLookup, + ProjectFlock, +} from '@/types/api/production/project-flock'; import { Kandang } from '@/types/api/master-data/kandang'; import UniformityPreviewForm from '@/components/pages/production/uniformity/form/UniformityPreviewForm'; import UniformityResultForm from '@/components/pages/production/uniformity/form/UniformityResultForm'; @@ -88,7 +91,9 @@ const UniformityForm = ({ null ); - const [projectFlockSearchValue, setProjectFlockSearchValue] = useState(''); + const [selectedProjectFlockLocationId, setSelectedProjectFlockLocationId] = + useState(''); + const [selectedProjectFlock, setSelectedProjectFlock] = useState(null); @@ -100,50 +105,21 @@ const UniformityForm = ({ setInputValue: setLocationSelectInputValue, options: locationOptions, isLoadingOptions: isLoadingLocations, - } = useSelect(LocationApi.basePath, 'id', 'name', 'search', { - page: '1', - limit: '100', + loadMore: loadMoreLocations, + hasMore: hasMoreLocations, + } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); + + const { + setInputValue: setProjectFlockSearchValue, + options: projectFlockOptions, + rawData: projectFlocksRawData, + isLoadingOptions: isLoadingProjectFlocks, + loadMore: loadMoreProjectFlocks, + hasMore: hasMoreProjectFlocks, + } = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', 'search', { + location_id: selectedProjectFlockLocationId, }); - // ===== FETCH PROJECT FLOCKS DATA ===== - const projectFlocksUrl = useMemo(() => { - const params = new URLSearchParams({ - search: projectFlockSearchValue || '', - page: '1', - limit: '100', - }); - if (selectedLocation) { - params.append('location_id', selectedLocation.value.toString()); - } - return `${ProjectFlockApi.basePath}?${params.toString()}`; - }, [projectFlockSearchValue, selectedLocation]); - - const { data: projectFlocksData, isLoading: isLoadingProjectFlocks } = useSWR( - projectFlocksUrl, - ProjectFlockApi.getAllFetcher - ); - - const projectFlocksDataList = - projectFlocksData?.status === 'success' - ? projectFlocksData.data - : undefined; - - // ===== PROJECT FLOCK OPTIONS ===== - const projectFlockOptions = useMemo(() => { - let options: OptionType[] = []; - - if (isResponseSuccess(projectFlocksData)) { - const flockOptions = - projectFlocksData?.data.map((projectFlock) => ({ - value: projectFlock.id, - label: projectFlock.flock_name || '', - })) || []; - options = options.concat(flockOptions); - } - - return options; - }, [projectFlocksData]); - // ===== APPROVED PROJECT FLOCK KANDANGS ===== const approvedProjectFlockKandangsUrl = useMemo(() => { const params = new URLSearchParams({ @@ -168,8 +144,9 @@ const UniformityForm = ({ const kandangOptions = useMemo(() => { let options: OptionType[] = []; - if (selectedProjectFlock && projectFlocksDataList) { - const selectedProjectFlockData = projectFlocksDataList.find( + if (selectedProjectFlock && isResponseSuccess(projectFlocksRawData)) { + const data = projectFlocksRawData.data as unknown as ProjectFlock[]; + const selectedProjectFlockData = data.find( (pf) => pf.id === selectedProjectFlock.value ); @@ -196,7 +173,7 @@ const UniformityForm = ({ return options; }, [ selectedProjectFlock, - projectFlocksDataList, + projectFlocksRawData, approvedProjectFlockKandangs, formType, ]); @@ -313,6 +290,10 @@ const UniformityForm = ({ formik.setFieldValue('location_id', locationId); setSelectedLocation(location); + setSelectedProjectFlock(null); + setSelectedProjectFlockLocationId( + location ? location.value.toString() : '' + ); }, [] ); @@ -513,6 +494,7 @@ const UniformityForm = ({ options={locationOptions} onInputChange={setLocationSelectInputValue} isLoading={isLoadingLocations} + onMenuScrollToBottom={loadMoreLocations} isError={ formik.touched.location_id && Boolean(formik.errors.location_id) } @@ -530,6 +512,7 @@ const UniformityForm = ({ options={projectFlockOptions} onInputChange={setProjectFlockSearchValue} isLoading={isLoadingProjectFlocks} + onMenuScrollToBottom={loadMoreProjectFlocks} isDisabled={!formik.values.location_id} isError={ formik.touched.project_flock_id && From c9bace04ec0b1342132a755ab305b762f6617655 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 12:07:55 +0700 Subject: [PATCH 038/139] refactor(FE): Use absolute import for Badge --- .../pages/production/uniformity/chart/UniformityStat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/production/uniformity/chart/UniformityStat.tsx b/src/components/pages/production/uniformity/chart/UniformityStat.tsx index ea8a5c0e..e7603e16 100644 --- a/src/components/pages/production/uniformity/chart/UniformityStat.tsx +++ b/src/components/pages/production/uniformity/chart/UniformityStat.tsx @@ -1,4 +1,4 @@ -import Badge from '../../../../Badge'; +import Badge from '@/components/Badge'; import Card from '@/components/Card'; import { Icon } from '@iconify/react'; import { formatNumber } from '@/lib/helper'; From f3b109189096f80e72ca0391b9bb6b4ca8cd1a00 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 12:12:03 +0700 Subject: [PATCH 039/139] refactor(FE): Add infinite scroll to flock selects --- .../transfer-to-laying/TransferToLayingsTable.tsx | 6 ++++++ .../transfer-to-laying/form/TransferToLayingForm.tsx | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx b/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx index 18ce404d..860c4616 100644 --- a/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx +++ b/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx @@ -179,12 +179,16 @@ const TransferToLayingsTable = () => { setInputValue: setFlockSourceInputValue, options: flockSourceOptions, isLoadingOptions: isLoadingFlockSourceOptions, + loadMore: loadMoreFlockSource, + hasMore: hasMoreFlockSource, } = useSelect(FlockApi.basePath, 'id', 'name'); const { setInputValue: setFlockDestinationInputValue, options: flockDestinationOptions, isLoadingOptions: isLoadingFlockDestinationOptions, + loadMore: loadMoreFlockDestination, + hasMore: hasMoreFlockDestination, } = useSelect(FlockApi.basePath, 'id', 'name'); // Flocks value @@ -595,6 +599,7 @@ const TransferToLayingsTable = () => { value={selectedFlockSource} onChange={flockSourceChangeHandler} onInputChange={setFlockSourceInputValue} + onMenuScrollToBottom={loadMoreFlockSource} isClearable className={{ wrapper: 'col-span-12 sm:col-span-3', @@ -608,6 +613,7 @@ const TransferToLayingsTable = () => { value={selectedFlockDestination} onChange={flockDestinationChangeHandler} onInputChange={setFlockDestinationInputValue} + onMenuScrollToBottom={loadMoreFlockDestination} isClearable className={{ wrapper: 'col-span-12 sm:col-span-3', diff --git a/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.tsx b/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.tsx index c5683fff..a257af0d 100644 --- a/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.tsx +++ b/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.tsx @@ -270,6 +270,8 @@ const TransferToLayingForm = ({ options: flockSourceOptions, isLoadingOptions: isLoadingFlockSourceOptions, rawData: flockSources, + loadMore: loadMoreFlockSource, + hasMore: hasMoreFlockSource, } = useSelect( '/production/project-flocks', 'id', @@ -360,6 +362,8 @@ const TransferToLayingForm = ({ options: flockDestinationOptions, isLoadingOptions: isLoadingFlockDestinationOptions, rawData: flockDestinations, + loadMore: loadMoreFlockDestination, + hasMore: hasMoreFlockDestination, } = useSelect( '/production/project-flocks', 'id', @@ -573,6 +577,7 @@ const TransferToLayingForm = ({ onChange={flockSourceChangeHandler} isLoading={isLoadingFlockSourceOptions} onInputChange={setFlockSourceInputValue} + onMenuScrollToBottom={loadMoreFlockSource} isError={ formik.touched.flockSource && Boolean(typeof formik.errors.flockSource === 'string') @@ -591,6 +596,7 @@ const TransferToLayingForm = ({ onChange={flockDestinationChangeHandler} isLoading={isLoadingFlockDestinationOptions} onInputChange={setFlockDestinationInputValue} + onMenuScrollToBottom={loadMoreFlockDestination} isError={ formik.touched.flockDestination && Boolean(typeof formik.errors.flockDestination === 'string') From 817420ee625a084d4ae0676e80088e560a941481 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 13:35:00 +0700 Subject: [PATCH 040/139] refactor(FE): Rename Ayam labels to Telur in HPP reports --- .../sale/export/HppPerkandangExport.tsx | 10 +++--- .../report/sale/tab/HppPerKandangTab.tsx | 32 +++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/components/pages/report/sale/export/HppPerkandangExport.tsx b/src/components/pages/report/sale/export/HppPerkandangExport.tsx index 0a712a6c..6883bdfb 100644 --- a/src/components/pages/report/sale/export/HppPerkandangExport.tsx +++ b/src/components/pages/report/sale/export/HppPerkandangExport.tsx @@ -226,7 +226,7 @@ const createPDFDocument = ( Rentang BW - Sisa Ekor + Sisa Butir Sisa Kg @@ -253,7 +253,7 @@ const createPDFDocument = ( Nilai Nominal Telur - HPP Ayam + HPP Telur HPP Telur (RP/KG) @@ -356,10 +356,10 @@ const createPDFDocument = ( Rata-Rata Bobot (Kg) - Sisa Ekor + Sisa Butir - Sisa Kg (Ayam) + Sisa Kg (Telur) Produksi Telur (Butir) @@ -380,7 +380,7 @@ const createPDFDocument = ( Nilai Nominal Telur - HPP Ayam + HPP Telur HPP Telur (RP/KG) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index eda88d8c..8e669878 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -335,8 +335,8 @@ const HppPerKandangTab = () => { ? `${formatNumber(item.weight_range.weight_min)} - ${formatNumber(item.weight_range.weight_max)}` : '', 'Rata-Rata Bobot (KG)': item.avg_weight_kg || 0, - 'Sisa Ayam (Ekor)': item.remaining_chicken_birds || 0, - 'Sisa Ayam (KG)': item.remaining_chicken_weight_kg || 0, + 'Sisa Telur (Butir)': item.remaining_chicken_birds || 0, + 'Sisa Telur (KG)': item.remaining_chicken_weight_kg || 0, 'Produksi Telur (Butir)': item.egg_production_pieces || 0, 'Produksi Telur (KG)': item.egg_production_kg || 0, 'Feed (Supplier)': @@ -349,9 +349,9 @@ const HppPerKandangTab = () => { .join(' | ') || '', 'Rata-Rata Harga DOC (RP)': item.average_doc_price_rp || 0, 'Nilai Nominal Telur (RP)': item.egg_value_rp || 0, - 'HPP Ayam (RP)': item.hpp_rp || 0, + 'HPP Telur (RP)': item.hpp_rp || 0, 'HPP Telur (RP/KG)': item.egg_hpp_rp_per_kg || 0, - 'Nilai Nominal Sisa Ayam (RP)': item.remaining_value_rp || 0, + 'Nilai Nominal Sisa Telur (RP)': item.remaining_value_rp || 0, }) ); @@ -360,8 +360,8 @@ const HppPerKandangTab = () => { Kandang: 'ALL', 'Rentang Bobot': '-', 'Rata-Rata Bobot (KG)': summaryTotal?.average_weight_kg || 0, - 'Sisa Ayam (Ekor)': summaryTotal?.total_remaining_chicken_birds || 0, - 'Sisa Ayam (KG)': summaryTotal?.total_remaining_chicken_weight_kg || 0, + 'Sisa Telur (Butir)': summaryTotal?.total_remaining_chicken_birds || 0, + 'Sisa Telur (KG)': summaryTotal?.total_remaining_chicken_weight_kg || 0, 'Produksi Telur (Butir)': summaryTotal?.total_egg_production_pieces || 0, 'Produksi Telur (KG)': summaryTotal?.total_egg_production_kg || 0, @@ -370,9 +370,9 @@ const HppPerKandangTab = () => { 'Rata-Rata Harga DOC (RP)': summaryTotal?.total_average_doc_price_rp || 0, 'Nilai Nominal Telur (RP)': summaryTotal?.total_egg_value_rp || 0, - 'HPP Ayam (RP)': summaryTotal?.total_hpp_rp || 0, + 'HPP Telur (RP)': summaryTotal?.total_hpp_rp || 0, 'HPP Telur (RP/KG)': summaryTotal?.average_egg_hpp_rp_per_kg || 0, - 'Nilai Nominal Sisa Ayam (RP)': + 'Nilai Nominal Sisa Telur (RP)': summaryTotal?.total_remaining_value_rp || 0, }); @@ -383,17 +383,17 @@ const HppPerKandangTab = () => { { wch: 30 }, // Kandang { wch: 15 }, // Rentang Bobot { wch: 18 }, // Rata-Rata Bobot (KG) - { wch: 15 }, // Sisa Ayam (Ekor) - { wch: 15 }, // Sisa Ayam (KG) + { wch: 15 }, // Sisa Telur (Butir) + { wch: 15 }, // Sisa Telur (KG) { wch: 18 }, // Produksi Telur (Butir) { wch: 18 }, // Produksi Telur (KG) { wch: 20 }, // Feed (Supplier) { wch: 20 }, // DOC (Supplier) { wch: 20 }, // Rata-Rata Harga DOC (RP) { wch: 20 }, // Nilai Nominal Telur (RP) - { wch: 15 }, // HPP Ayam (RP) + { wch: 15 }, // HPP Telur (RP) { wch: 18 }, // HPP Telur (RP/KG) - { wch: 25 }, // Nilai Nominal Sisa Ayam (RP) + { wch: 25 }, // Nilai Nominal Sisa Telur (RP) ]; worksheet['!cols'] = colWidths; @@ -535,7 +535,7 @@ const HppPerKandangTab = () => { }, { id: 'remaining_chicken_birds', - header: 'Sisa Ayam (Ekor)', + header: 'Sisa Telur (Butir)', accessorKey: 'remaining_chicken_birds', cell: (props) => { const value = props.row.original.remaining_chicken_birds; @@ -549,7 +549,7 @@ const HppPerKandangTab = () => { }, { id: 'remaining_chicken_weight_kg', - header: 'Sisa Ayam (KG)', + header: 'Sisa Telur (KG)', accessorKey: 'remaining_chicken_weight_kg', cell: (props) => { const value = props.row.original.remaining_chicken_weight_kg; @@ -655,7 +655,7 @@ const HppPerKandangTab = () => { }, { id: 'hpp_rp', - header: 'HPP Ayam (RP)', + header: 'HPP Telur (RP)', accessorKey: 'hpp_rp', cell: (props) => { const value = props.row.original.hpp_rp; @@ -683,7 +683,7 @@ const HppPerKandangTab = () => { }, { id: 'remaining_value_rp', - header: 'Nilai Nominal Sisa Ayam (RP)', + header: 'Nilai Nominal Sisa Telur (RP)', accessorKey: 'remaining_value_rp', cell: (props) => { const value = props.row.original.remaining_value_rp; From 9d7140beb611885b0a9b0ae81e604518a05f4351 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 13:52:21 +0700 Subject: [PATCH 041/139] refactor(FE): Use egg production as remaining columns --- .../sale/export/HppPerkandangExport.tsx | 60 +--------- .../report/sale/tab/HppPerKandangTab.tsx | 110 +++--------------- src/types/api/report/hpp-per-kandang.d.ts | 12 -- 3 files changed, 20 insertions(+), 162 deletions(-) diff --git a/src/components/pages/report/sale/export/HppPerkandangExport.tsx b/src/components/pages/report/sale/export/HppPerkandangExport.tsx index 6883bdfb..95f42df6 100644 --- a/src/components/pages/report/sale/export/HppPerkandangExport.tsx +++ b/src/components/pages/report/sale/export/HppPerkandangExport.tsx @@ -234,12 +234,6 @@ const createPDFDocument = ( Rata-Rata Bobot (Kg) - - Produksi Telur (Butir) - - - Produksi Telur (Kg) - Feed (Supplier) @@ -249,12 +243,6 @@ const createPDFDocument = ( Rata-Rata Harga DOC - - Nilai Nominal Telur - - - HPP Telur - HPP Telur (RP/KG) @@ -278,23 +266,15 @@ const createPDFDocument = ( {group.label} - - {formatNumber(group.remaining_chicken_birds)} - - - - {formatNumber(group.remaining_chicken_weight_kg)} - - - - {formatNumber(group.avg_weight_kg)} - {formatNumber(group.egg_production_pieces)} {formatNumber(group.egg_production_kg)} + + {formatNumber(group.avg_weight_kg)} + {group.feed_suppliers @@ -318,17 +298,11 @@ const createPDFDocument = ( {formatCurrency(group.average_doc_price_rp)} - - {formatCurrency(group.egg_value_rp)} - - - {formatCurrency(group.hpp_rp)} - {formatCurrency(group.egg_hpp_rp_per_kg)} - {formatCurrency(group.remaining_value_rp)} + {formatCurrency(group.egg_value_rp)} ) @@ -361,12 +335,6 @@ const createPDFDocument = ( Sisa Kg (Telur) - - Produksi Telur (Butir) - - - Produksi Telur (Kg) - Feed (Supplier) @@ -376,12 +344,6 @@ const createPDFDocument = ( Rata-Rata Harga DOC - - Nilai Nominal Telur - - - HPP Telur - HPP Telur (RP/KG) @@ -416,12 +378,6 @@ const createPDFDocument = ( {formatNumber(item.avg_weight_kg)} - - {formatNumber(item.remaining_chicken_birds)} - - - {formatNumber(item.remaining_chicken_weight_kg)} - {formatNumber(item.egg_production_pieces)} @@ -451,17 +407,11 @@ const createPDFDocument = ( {formatCurrency(item.average_doc_price_rp)} - - {formatCurrency(item.egg_value_rp)} - - - {formatCurrency(item.hpp_rp)} - {formatCurrency(item.egg_hpp_rp_per_kg)} - {formatCurrency(item.remaining_value_rp)} + {formatCurrency(item.egg_value_rp)} ))} diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 8e669878..22d80220 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -335,10 +335,8 @@ const HppPerKandangTab = () => { ? `${formatNumber(item.weight_range.weight_min)} - ${formatNumber(item.weight_range.weight_max)}` : '', 'Rata-Rata Bobot (KG)': item.avg_weight_kg || 0, - 'Sisa Telur (Butir)': item.remaining_chicken_birds || 0, - 'Sisa Telur (KG)': item.remaining_chicken_weight_kg || 0, - 'Produksi Telur (Butir)': item.egg_production_pieces || 0, - 'Produksi Telur (KG)': item.egg_production_kg || 0, + 'Sisa Telur (Butir)': item.egg_production_pieces || 0, + 'Sisa Telur (KG)': item.egg_production_kg || 0, 'Feed (Supplier)': item.feed_suppliers ?.map((s: { alias?: string; name: string }) => s.alias || s.name) @@ -348,10 +346,8 @@ const HppPerKandangTab = () => { ?.map((s: { alias?: string; name: string }) => s.alias || s.name) .join(' | ') || '', 'Rata-Rata Harga DOC (RP)': item.average_doc_price_rp || 0, - 'Nilai Nominal Telur (RP)': item.egg_value_rp || 0, - 'HPP Telur (RP)': item.hpp_rp || 0, 'HPP Telur (RP/KG)': item.egg_hpp_rp_per_kg || 0, - 'Nilai Nominal Sisa Telur (RP)': item.remaining_value_rp || 0, + 'Nilai Nominal Sisa Telur (RP)': item.egg_value_rp || 0, }) ); @@ -360,20 +356,14 @@ const HppPerKandangTab = () => { Kandang: 'ALL', 'Rentang Bobot': '-', 'Rata-Rata Bobot (KG)': summaryTotal?.average_weight_kg || 0, - 'Sisa Telur (Butir)': summaryTotal?.total_remaining_chicken_birds || 0, - 'Sisa Telur (KG)': summaryTotal?.total_remaining_chicken_weight_kg || 0, - 'Produksi Telur (Butir)': - summaryTotal?.total_egg_production_pieces || 0, - 'Produksi Telur (KG)': summaryTotal?.total_egg_production_kg || 0, + 'Sisa Telur (Butir)': summaryTotal?.total_egg_production_pieces || 0, + 'Sisa Telur (KG)': summaryTotal?.total_egg_production_kg || 0, 'Feed (Supplier)': allFeedSuppliers, 'DOC (Supplier)': allDocSuppliers, 'Rata-Rata Harga DOC (RP)': summaryTotal?.total_average_doc_price_rp || 0, - 'Nilai Nominal Telur (RP)': summaryTotal?.total_egg_value_rp || 0, - 'HPP Telur (RP)': summaryTotal?.total_hpp_rp || 0, 'HPP Telur (RP/KG)': summaryTotal?.average_egg_hpp_rp_per_kg || 0, - 'Nilai Nominal Sisa Telur (RP)': - summaryTotal?.total_remaining_value_rp || 0, + 'Nilai Nominal Sisa Telur (RP)': summaryTotal?.total_egg_value_rp || 0, }); const worksheet = XLSX.utils.json_to_sheet(excelData); @@ -385,13 +375,9 @@ const HppPerKandangTab = () => { { wch: 18 }, // Rata-Rata Bobot (KG) { wch: 15 }, // Sisa Telur (Butir) { wch: 15 }, // Sisa Telur (KG) - { wch: 18 }, // Produksi Telur (Butir) - { wch: 18 }, // Produksi Telur (KG) { wch: 20 }, // Feed (Supplier) { wch: 20 }, // DOC (Supplier) { wch: 20 }, // Rata-Rata Harga DOC (RP) - { wch: 20 }, // Nilai Nominal Telur (RP) - { wch: 15 }, // HPP Telur (RP) { wch: 18 }, // HPP Telur (RP/KG) { wch: 25 }, // Nilai Nominal Sisa Telur (RP) ]; @@ -533,37 +519,9 @@ const HppPerKandangTab = () => { ), }, - { - id: 'remaining_chicken_birds', - header: 'Sisa Telur (Butir)', - accessorKey: 'remaining_chicken_birds', - cell: (props) => { - const value = props.row.original.remaining_chicken_birds; - return
{formatNumber(value)}
; - }, - footer: () => ( -
- {formatNumber(summaryTotal?.total_remaining_chicken_birds || 0)} -
- ), - }, - { - id: 'remaining_chicken_weight_kg', - header: 'Sisa Telur (KG)', - accessorKey: 'remaining_chicken_weight_kg', - cell: (props) => { - const value = props.row.original.remaining_chicken_weight_kg; - return
{formatNumber(value)}
; - }, - footer: () => ( -
- {formatNumber(summaryTotal?.total_remaining_chicken_weight_kg || 0)} -
- ), - }, { id: 'egg_production_pieces', - header: 'Produksi Telur (Butir)', + header: 'Sisa Telur (Butir)', accessorKey: 'egg_production_pieces', cell: (props) => { const value = props.row.original.egg_production_pieces; @@ -577,7 +535,7 @@ const HppPerKandangTab = () => { }, { id: 'egg_production_kg', - header: 'Produksi Telur (KG)', + header: 'Sisa Telur (KG)', accessorKey: 'egg_production_kg', cell: (props) => { const value = props.row.original.egg_production_kg; @@ -585,7 +543,7 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatNumber(summaryTotal?.total_remaining_chicken_weight_kg || 0)} + {formatNumber(summaryTotal?.total_egg_production_kg || 0)}
), }, @@ -639,34 +597,6 @@ const HppPerKandangTab = () => { ), }, - { - id: 'egg_value_rp', - header: 'Nilai Nominal Telur (RP)', - accessorKey: 'egg_value_rp', - cell: (props) => { - const value = props.row.original.egg_value_rp; - return
{formatCurrency(value)}
; - }, - footer: () => ( -
- {formatCurrency(summaryTotal?.total_egg_value_rp || 0)} -
- ), - }, - { - id: 'hpp_rp', - header: 'HPP Telur (RP)', - accessorKey: 'hpp_rp', - cell: (props) => { - const value = props.row.original.hpp_rp; - return
{formatCurrency(value)}
; - }, - footer: () => ( -
- {formatCurrency(summaryTotal?.total_hpp_rp || 0)} -
- ), - }, { id: 'egg_hpp_rp_per_kg', header: 'HPP Telur (RP/KG)', @@ -682,16 +612,16 @@ const HppPerKandangTab = () => { ), }, { - id: 'remaining_value_rp', + id: 'egg_value_rp', header: 'Nilai Nominal Sisa Telur (RP)', - accessorKey: 'remaining_value_rp', + accessorKey: 'egg_value_rp', cell: (props) => { - const value = props.row.original.remaining_value_rp; + const value = props.row.original.egg_value_rp; return
{formatCurrency(value)}
; }, footer: () => (
- {formatCurrency(summaryTotal?.total_remaining_value_rp || 0)} + {formatCurrency(summaryTotal?.total_egg_value_rp || 0)}
), }, @@ -725,7 +655,7 @@ const HppPerKandangTab = () => { key={'rekapitulasi-row'} > Rekapitulasi per rentang bobot @@ -747,12 +677,6 @@ const HppPerKandangTab = () => { {formatNumber(item.avg_weight_kg)} - - {formatNumber(item.remaining_chicken_birds)} - - - {formatNumber(item.remaining_chicken_weight_kg)} - {formatNumber(item.egg_production_pieces)} @@ -772,15 +696,11 @@ const HppPerKandangTab = () => { {formatCurrency(item.average_doc_price_rp)} - - {formatCurrency(item.egg_value_rp)} - - {formatCurrency(item.hpp_rp)} {formatCurrency(item.egg_hpp_rp_per_kg)} - {formatCurrency(item.remaining_value_rp)} + {formatCurrency(item.egg_value_rp)} ); diff --git a/src/types/api/report/hpp-per-kandang.d.ts b/src/types/api/report/hpp-per-kandang.d.ts index 824a3837..1b3f7fcd 100644 --- a/src/types/api/report/hpp-per-kandang.d.ts +++ b/src/types/api/report/hpp-per-kandang.d.ts @@ -9,8 +9,6 @@ export type HppPerKandangRow = { weight_min: number; weight_max: number; }; - remaining_chicken_birds: number; - remaining_chicken_weight_kg: number; avg_weight_kg: number; egg_production_pieces: number; egg_production_kg: number; @@ -19,20 +17,14 @@ export type HppPerKandangRow = { feed_suppliers: Supplier[]; doc_suppliers: Supplier[]; average_doc_price_rp: number; - hpp_rp: number; - remaining_value_rp: number; }; export type HppPerKandangSummaryTotal = { - total_remaining_chicken_birds: number; - total_remaining_chicken_weight_kg: number; average_weight_kg: number; - total_remaining_value_rp: number; total_egg_production_pieces: number; total_egg_production_kg: number; average_egg_hpp_rp_per_kg: number; total_egg_value_rp: number; - total_hpp_rp: number; total_average_doc_price_rp: number; }; @@ -43,8 +35,6 @@ export type HppPerKandangPerWeightRange = { weight_max: number; }; label: string; - remaining_chicken_birds: number; - remaining_chicken_weight_kg: number; avg_weight_kg: number; egg_production_pieces: number; egg_production_kg: number; @@ -53,8 +43,6 @@ export type HppPerKandangPerWeightRange = { feed_suppliers: Supplier[]; doc_suppliers: Supplier[]; average_doc_price_rp: number; - hpp_rp: number; - remaining_value_rp: number; }; export type HppPerKandangSummary = { From dd080b1d19c355f7c3168eec00d92cf3fd2f07da Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 14:08:09 +0700 Subject: [PATCH 042/139] refactor(FE): Add record_date and DateInput to recording form --- .../recording/form/RecordingForm.schema.ts | 7 +++++ .../recording/form/RecordingForm.tsx | 29 +++++++++++++++---- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.schema.ts b/src/components/pages/production/recording/form/RecordingForm.schema.ts index 4901b349..8ebd4aa2 100644 --- a/src/components/pages/production/recording/form/RecordingForm.schema.ts +++ b/src/components/pages/production/recording/form/RecordingForm.schema.ts @@ -7,6 +7,7 @@ import { } from '@/types/api/production/recording'; type RecordingGrowingFormSchemaType = { + record_date: string; project_flock_kandang: { value: number; label: string; @@ -85,6 +86,9 @@ const EggObjectSchema: Yup.ObjectSchema = Yup.object({ export const RecordingGrowingFormSchema: Yup.ObjectSchema = Yup.object({ + record_date: Yup.string() + .required('Tanggal recording wajib diisi!') + .typeError('Tanggal recording wajib diisi!'), project_flock_kandang: Yup.object({ value: Yup.number().min(1).required(), label: Yup.string().required(), @@ -179,6 +183,9 @@ type RecordingFormData = Partial & { export const getRecordingGrowingFormInitialValues = ( initialValues?: RecordingFormData ): RecordingGrowingFormValues => ({ + record_date: initialValues?.record_datetime + ? new Date(initialValues.record_datetime).toISOString().split('T')[0] + : new Date().toISOString().split('T')[0], project_flock_kandang: initialValues?.project_flock_kandang_id ? { value: initialValues.project_flock_kandang_id, diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index db61080e..c418c5b2 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -12,6 +12,7 @@ import RequirePermission from '@/components/helper/RequirePermission'; import Card from '@/components/Card'; import Badge from '@/components/Badge'; import NumberInput from '@/components/input/NumberInput'; +import DateInput from '@/components/input/DateInput'; import SelectInput, { OptionType, useSelect, @@ -135,10 +136,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { // ===== PAYLOAD CREATION HELPERS ===== const createGrowingPayload = useCallback( (values: RecordingGrowingFormValues) => { - const today = new Date().toISOString().split('T')[0]; return { project_flock_kandang_id: values.project_flock_kandang_id, - record_date: today, + record_date: values.record_date, stocks: (values.stocks ?? []).map((stock) => ({ product_warehouse_id: stock.product_warehouse_id, qty: Number(stock.qty) || 0, @@ -154,10 +154,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const createLayingPayload = useCallback( (values: RecordingLayingFormValues) => { - const today = new Date().toISOString().split('T')[0]; return { project_flock_kandang_id: values.project_flock_kandang_id, - record_date: today, + record_date: values.record_date, stocks: (values.stocks ?? []).map((stock) => ({ product_warehouse_id: stock.product_warehouse_id, qty: Number(stock.qty) || 0, @@ -971,6 +970,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { formik.setFieldTouched('project_flock_kandang_id', true); }; + const handleRecordDateChange = useCallback( + (e: React.ChangeEvent) => { + formik.setFieldValue('record_date', e.target.value); + }, + [formik] + ); + useEffect(() => { if (projectFlockKandangLookup?.project_flock_kandang_id) { const projectFlockKandangId = @@ -1364,8 +1370,21 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { body: 'flex flex-col gap-6', }} > -
+
<> + Date: Thu, 15 Jan 2026 14:47:49 +0700 Subject: [PATCH 043/139] feat(FE): Add Periode and Gudang columns and show week --- .../production/recording/RecordingTable.tsx | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 96ac52ed..5d445fe1 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -690,6 +690,10 @@ const RecordingTable = () => { cell: (props) => props.row.original.project_flock?.flock_name || '-', }, + { + header: 'Periode', + cell: (props) => props.row.original.project_flock?.period || '-', + }, { header: 'Kategori', cell: (props) => { @@ -706,7 +710,21 @@ const RecordingTable = () => { }, { header: 'Umur (hari)', - cell: (props) => props.row.original.day, + cell: (props) => { + return ( + <> + + {props.row.original.day} (Minggu ke- + {props.row.original.project_flock.production_standart.week}) + + + ); + }, + }, + { + accessorKey: 'warehouse.name', + header: 'Gudang', + cell: (props) => props.row.original.warehouse?.name, }, { accessorKey: 'record_date', @@ -715,7 +733,7 @@ const RecordingTable = () => { formatDate(props.row.original.record_datetime, 'DD MMMM YYYY'), }, { - header: 'Populasi Ayam', + header: 'Populasi Akhir', cell: (props) => props.row.original.project_flock?.total_chick_qty?.toLocaleString() || '-', From ac84841b05516f9d16cabac9dc5ae70d468bd0e5 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 15:06:41 +0700 Subject: [PATCH 044/139] feat(FE): Add production metric columns and table styling --- .../production/recording/RecordingTable.tsx | 272 +++++++++++++++++- 1 file changed, 263 insertions(+), 9 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 5d445fe1..b854cd59 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -5,7 +5,7 @@ import { RefObject } from 'react'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; import { SortingState, CellContext } from '@tanstack/react-table'; -import { cn, formatDate } from '@/lib/helper'; +import { cn, formatDate, formatNumber } from '@/lib/helper'; import RequirePermission from '@/components/helper/RequirePermission'; import { useModal } from '@/components/Modal'; import Modal from '@/components/Modal'; @@ -679,7 +679,7 @@ const RecordingTable = () => { }, }, { - header: '#', + header: 'No', cell: (props) => tableFilterState.pageSize * (tableFilterState.page - 1) + props.row.index + @@ -735,8 +735,261 @@ const RecordingTable = () => { { header: 'Populasi Akhir', cell: (props) => - props.row.original.project_flock?.total_chick_qty?.toLocaleString() || - '-', + props.row.original.project_flock?.total_chick_qty != null + ? formatNumber(props.row.original.project_flock.total_chick_qty) + : '-', + }, + { + id: 'fcr', + header: 'FCR', + columns: [ + { + id: 'fcr_actual', + header: 'Actual', + cell: (props) => { + const value = props.row.original.fcr_value; + return ( +
+ {value !== null && value !== undefined + ? formatNumber(value) + : '-'} +
+ ); + }, + }, + { + id: 'fcr_standard', + header: 'Standard', + cell: (props) => { + const value = props.row.original.project_flock?.fcr?.fcr_std; + return ( +
+ {value !== null && value !== undefined + ? formatNumber(value) + : '-'} +
+ ); + }, + }, + ], + }, + { + id: 'feed_intake', + header: 'Feed Intake (KG)', + columns: [ + { + id: 'feed_intake_actual', + header: 'Actual', + cell: (props) => { + const value = props.row.original.feed_intake; + return ( +
+ {value !== null && value !== undefined + ? formatNumber(value) + : '-'} +
+ ); + }, + }, + { + id: 'feed_intake_standard', + header: 'Standard', + cell: (props) => { + const value = + props.row.original.project_flock?.production_standart + ?.feed_intake_std; + return ( +
+ {value !== null && value !== undefined + ? formatNumber(value) + : '-'} +
+ ); + }, + }, + ], + }, + { + id: 'mortality', + header: 'Mortality', + columns: [ + { + id: 'cum_depletion_rate_actual', + header: 'Cum Depletion Rate', + cell: (props) => { + const value = props.row.original.cum_depletion_rate; + return ( +
+ {value !== null && value !== undefined + ? `${value.toFixed(2)}%` + : '-'} +
+ ); + }, + }, + { + id: 'max_depletion_std', + header: 'Max Depletion Std', + cell: (props) => { + const value = + props.row.original.project_flock?.production_standart + ?.max_depletion_std; + return ( +
+ {value !== null && value !== undefined + ? `${value.toFixed(2)}%` + : '-'} +
+ ); + }, + }, + { + id: 'total_depletion', + header: 'Total Depletion', + cell: (props) => { + const value = props.row.original.total_depletion_qty; + return ( +
+ {value !== null && value !== undefined + ? formatNumber(value) + : '-'} +
+ ); + }, + }, + ], + }, + { + id: 'egg_production', + header: 'Egg Production', + columns: [ + { + id: 'egg_mass_actual', + header: 'Egg Mass Actual', + cell: (props) => { + const value = props.row.original.egg_mass; + return ( +
+ {value !== null && value !== undefined + ? formatNumber(value) + : '-'} +
+ ); + }, + }, + { + id: 'egg_mass_standard', + header: 'Egg Mass Standar', + cell: (props) => { + const value = + props.row.original.project_flock?.production_standart + ?.egg_mass_std; + return ( +
+ {value !== null && value !== undefined + ? formatNumber(value) + : '-'} +
+ ); + }, + }, + { + id: 'egg_weight_actual', + header: 'Egg Weight Actual', + cell: (props) => { + const value = props.row.original.egg_weight; + return ( +
+ {value !== null && value !== undefined + ? formatNumber(value) + : '-'} +
+ ); + }, + }, + { + id: 'egg_weight_standard', + header: 'Egg Weight Standar', + cell: (props) => { + const value = + props.row.original.project_flock?.production_standart + ?.egg_weight_std; + return ( +
+ {value !== null && value !== undefined + ? formatNumber(value) + : '-'} +
+ ); + }, + }, + ], + }, + { + id: 'hen_performance', + header: 'Hen Performance', + columns: [ + { + id: 'hen_day_actual', + header: 'Hen Day Actual', + cell: (props) => { + const value = props.row.original.hen_day; + return ( +
+ {value !== null && value !== undefined + ? `${value.toFixed(2)}%` + : '-'} +
+ ); + }, + }, + { + id: 'hen_day_standard', + header: 'Hen Day Standar', + cell: (props) => { + const value = + props.row.original.project_flock?.production_standart + ?.hen_day_std; + return ( +
+ {value !== null && value !== undefined + ? `${value.toFixed(2)}%` + : '-'} +
+ ); + }, + }, + { + id: 'hen_house_actual', + header: 'Hen House Actual', + cell: (props) => { + const value = props.row.original.hen_house; + return ( +
+ {value !== null && value !== undefined + ? `${value.toFixed(2)}%` + : '-'} +
+ ); + }, + }, + { + id: 'hen_house_standard', + header: 'Hen House Standar', + cell: (props) => { + const value = + props.row.original.project_flock?.production_standart + ?.hen_house_std; + return ( +
+ {value !== null && value !== undefined + ? `${value.toFixed(2)}%` + : '-'} +
+ ); + }, + }, + ], }, { header: 'Status Approval', @@ -902,14 +1155,15 @@ const RecordingTable = () => { 'mb-20': isResponseSuccess(recordings) && recordings?.data?.length === 0, }), - tableWrapperClassName: 'overflow-x-auto min-h-full!', - tableClassName: 'font-inter w-full table-auto min-h-full!', + tableWrapperClassName: 'overflow-x-auto', + tableClassName: 'w-full table-auto text-sm', headerRowClassName: 'border-b border-b-gray-200', headerColumnClassName: - 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', - bodyRowClassName: 'border-b border-b-gray-200', + 'px-4 py-3 text-xs font-semibold text-gray-500 whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-gray-200 first:border-t first:border-t-gray-200 border-l border-l-gray-200 border-r border-r-gray-200', bodyColumnClassName: - 'px-6 py-3 last:flex last:flex-row last:justify-end', + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} /> From 781a5ca0d975b0d5969403fddfe7261247f5d971 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 15:14:04 +0700 Subject: [PATCH 045/139] chore: use real permission for daily checklist menu --- src/config/constant.ts | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/config/constant.ts b/src/config/constant.ts index d3832613..b3621c8f 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -10,61 +10,65 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ text: 'Daily Checklist', link: '/daily-checklist', icon: 'heroicons-outline:clipboard-check', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: [ + 'lti.daily_checklist.dashboard.list', + 'lti.daily_checklist.create', + 'lti.daily_checklist.list', + 'lti.daily_checklist.detail', + 'lti.daily_checklist.reports', + 'lti.daily_checklist.master_data.employee', + 'lti.daily_checklist.master_data.activity', + 'lti.daily_checklist.master_data.configuration', + ], submenu: [ { text: 'Dashboard', link: '/daily-checklist/dashboard', icon: 'lucide:layout-dashboard', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: ['lti.daily_checklist.dashboard.list'], }, { text: 'Daily Checklist', link: '/daily-checklist/daily-checklist', icon: 'lucide:clipboard-check', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: ['lti.daily_checklist.create'], }, { text: 'Daftar Daily Checklist', link: '/daily-checklist/list-daily-checklist', icon: 'lucide:circle-check', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: ['lti.daily_checklist.list'], }, { text: 'Laporan', link: '/daily-checklist/reports', icon: 'lucide:file-text', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: ['lti.daily_checklist.reports'], }, { text: 'Master Data', link: '/daily-checklist/master-data', icon: 'lucide:database', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: [ + 'lti.daily_checklist.master_data.employee', + 'lti.daily_checklist.master_data.activity', + 'lti.daily_checklist.master_data.configuration', + ], submenu: [ { text: 'Employee (ABK)', link: '/daily-checklist/master-data/employee', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: ['lti.daily_checklist.master_data.employee'], }, { text: 'Aktivitas', link: '/daily-checklist/master-data/activity', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: ['lti.daily_checklist.master_data.activity'], }, { text: 'Konfigurasi', link: '/daily-checklist/master-data/configuration', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: ['lti.daily_checklist.master_data.configuration'], }, ], }, From 8f55ced55a3c89029a77493f128999ec9edba26f Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 15:14:17 +0700 Subject: [PATCH 046/139] feat: add export to pdf functionality --- .../ProductionResultContent.tsx | 102 +++++++++++++++++- 1 file changed, 100 insertions(+), 2 deletions(-) diff --git a/src/components/pages/report/production-result/ProductionResultContent.tsx b/src/components/pages/report/production-result/ProductionResultContent.tsx index 7820ff53..28d334e8 100644 --- a/src/components/pages/report/production-result/ProductionResultContent.tsx +++ b/src/components/pages/report/production-result/ProductionResultContent.tsx @@ -21,10 +21,18 @@ import { ProjectFlockApi, ProjectFlockKandangApi, } from '@/services/api/production'; -import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; -import { isResponseError } from '@/lib/api-helper'; +import { + BaseProjectFlockKandang, + ProjectFlockKandang, +} from '@/types/api/production/project-flock-kandang'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import Pagination from '@/components/Pagination'; import { ProductionResultReportApi } from '@/services/api/report/production-result'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { httpClient } from '@/services/http/client'; +import { ProductionResult } from '@/types/api/report/production-result'; +import ProductionResultReportPDF from './ProductionResultReportPDF'; +import { pdf } from '@react-pdf/renderer'; const ProductionResultContent = () => { const [projectFlockKandangs, setProjectFlockKandangs] = useState< @@ -49,6 +57,8 @@ const ProductionResultContent = () => { const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = useState(false); + const [isLoadingExportingToPdf, setIsLoadingExportingToPdf] = useState(false); + const [selectedArea, setSelectedArea] = useState(null); const [selectedLocation, setSelectedLocation] = useState( null @@ -158,6 +168,87 @@ const ProductionResultContent = () => { setIsLoadingExportingToExcel(false); }; + const exportToPdfHandler = async () => { + setIsLoadingExportingToPdf(true); + + try { + let projectFlockKandangsData: BaseProjectFlockKandang[] = []; + + if (selectedProjectFlockKandang) { + const projectFlockKandangResponse = + await ProjectFlockKandangApi.getSingle( + selectedProjectFlockKandang?.value as number + ); + + projectFlockKandangsData = isResponseSuccess( + projectFlockKandangResponse + ) + ? [projectFlockKandangResponse.data] + : []; + } else { + const projectFlockKandangsResponse = + await ProjectFlockKandangApi.getAll({ + area_id: selectedArea?.value, + project_flock_id: selectedProjectFlock?.value, + }); + + projectFlockKandangsData = isResponseSuccess( + projectFlockKandangsResponse + ) + ? projectFlockKandangsResponse.data + : []; + } + + const mappedProductionResults: { + projectFlockKandang: BaseProjectFlockKandang; + productionResult: ProductionResult[] | null; + }[] = await Promise.all( + projectFlockKandangsData.map(async (projectFlockKandang) => { + const getProductionResultPath = `${ProductionResultReportApi.basePath}/${projectFlockKandang.id}?page=1&limit=100`; + const getProductionResultRes = await httpClient< + BaseApiResponse + >(getProductionResultPath); + + return { + projectFlockKandang, + productionResult: isResponseSuccess(getProductionResultRes) + ? getProductionResultRes.data + : null, + }; + }) + ); + + if (mappedProductionResults.length === 0) { + toast.error('Tidak ada data untuk diexport.'); + setIsLoadingExportingToPdf(false); + return; + } + + const openPdf = async () => { + const productionResultPdfBlob = await pdf( + + ).toBlob(); + + const productionResultReportPdfUrl = URL.createObjectURL( + productionResultPdfBlob + ); + window.open(productionResultReportPdfUrl, '_blank'); + }; + + await openPdf(); + } catch (error) { + console.error(error); + toast.error('Gagal melakukan export laporan hasil produksi! Coba lagi.'); + } + // await ProductionResultReportApi.exportProductionResultToPdf( + // projectFlockKandangs + // ); + + setIsLoadingExportingToPdf(false); + }; + const searchHandler = async () => { setProjectFlockKandangs(null); setIsLoadingSearch(true); @@ -355,6 +446,13 @@ const ProductionResultContent = () => { onClick={exportToExcelHandler} className='text-nowrap' /> +
From e15b7e11d3bd7075d76dd2ffe418da6a3b48133e Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 15:14:33 +0700 Subject: [PATCH 047/139] feat: create ProductionResultReportPDF component --- .../ProductionResultReportPDF.tsx | 388 ++++++++++++++++++ 1 file changed, 388 insertions(+) create mode 100644 src/components/pages/report/production-result/ProductionResultReportPDF.tsx diff --git a/src/components/pages/report/production-result/ProductionResultReportPDF.tsx b/src/components/pages/report/production-result/ProductionResultReportPDF.tsx new file mode 100644 index 00000000..9bc27c4b --- /dev/null +++ b/src/components/pages/report/production-result/ProductionResultReportPDF.tsx @@ -0,0 +1,388 @@ +'use client'; + +import React from 'react'; +import { + Document, + Page, + StyleSheet, + Text, + View, + Image, +} from '@react-pdf/renderer'; + +import { formatDate, formatNumber } from '@/lib/helper'; +import { BaseProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; +import { ProductionResult } from '@/types/api/report/production-result'; + +type MappedProductionResultsItem = { + projectFlockKandang: BaseProjectFlockKandang; + productionResult: ProductionResult[] | null; +}; + +interface ProductionResultReportPDFProps { + mappedProductionResults?: MappedProductionResultsItem[]; +} + +const styles = StyleSheet.create({ + page: { + paddingTop: 24, + paddingBottom: 52, + paddingHorizontal: 16, + }, + + companyInfoHeader: { + width: '100%', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: 8, + }, + companyLogo: { + width: 64, + height: 'auto', + }, + companyInfoHeaderDate: { + paddingTop: 8, + fontSize: 10, + }, + companyName: { + fontSize: 12, + fontWeight: 'bold', + marginBottom: 4, + }, + companyAddress: { + fontSize: 8, + maxWidth: 420, + marginBottom: 10, + }, + doubleDivider: { + width: '100%', + height: 6, + borderTopWidth: 2, + borderTopColor: '#000', + borderBottomWidth: 2, + borderBottomColor: '#000', + }, + + title: { + marginTop: 14, + fontSize: 14, + lineHeight: '150%', + textAlign: 'center', + fontFamily: 'Times-Roman', + fontWeight: 'bold', + }, + + footer: { + width: '100%', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 16, + position: 'absolute', + fontSize: 8, + bottom: 22, + left: 0, + right: 0, + textAlign: 'center', + color: 'grey', + }, + + section: { + marginTop: 12, + borderWidth: 1, + borderColor: '#000', + padding: 8, + }, + + sectionHeader: { + marginBottom: 6, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'baseline', + }, + sectionTitle: { + fontSize: 10, + fontWeight: 'bold', + }, + sectionSubtitle: { + fontSize: 8, + color: '#444', + }, + + // Simple grid table (label/value pairs) + grid: { + width: '100%', + borderWidth: 1, + borderColor: '#000', + }, + gridRow: { + flexDirection: 'row', + borderBottomWidth: 1, + borderBottomColor: '#000', + }, + gridRowLast: { + borderBottomWidth: 0, + }, + gridCellLabel: { + width: '40%', + paddingVertical: 3, + paddingHorizontal: 6, + fontSize: 8, + borderRightWidth: 1, + borderRightColor: '#000', + fontWeight: 'bold', + }, + gridCellValue: { + width: '60%', + paddingVertical: 3, + paddingHorizontal: 6, + fontSize: 8, + textAlign: 'right', + }, + + // Subsection headings + groupTitle: { + marginTop: 8, + marginBottom: 4, + fontSize: 9, + fontWeight: 'bold', + }, + + emptyText: { + fontSize: 8, + color: '#666', + fontStyle: 'italic', + }, +}); + +function safeNum(v: unknown): number { + const n = typeof v === 'number' ? v : Number(v); + return Number.isFinite(n) ? n : 0; +} + +function valueText(v: unknown) { + if (v === null || v === undefined) return '-'; + if (typeof v === 'number') return formatNumber(v); + return String(v); +} + +/** + * Render label/value table for one ProductionResult. + * Uses a compact grid to keep page readable. + */ +function ProductionResultGrid({ pr }: { pr: ProductionResult }) { + const rows: Array<[string, string]> = [ + ['WOA', valueText(pr.woa)], + + // BW + ['BW', valueText(pr.bw)], + ['Std BW', valueText(pr.std_bw)], + ['Uniformity', valueText(pr.uniformity)], + ['Std Uniformity', valueText(pr.std_uniformity)], + + // Dep + ['Dep Kum', valueText(pr.dep_kum)], + ['Dep Std', valueText(pr.dep_std)], + + // Butiran + ['Butiran Utuh', valueText(pr.butiran_utuh)], + ['Butiran Putih', valueText(pr.butiran_putih)], + ['Butiran Retak', valueText(pr.butiran_retak)], + ['Butiran Pecah', valueText(pr.butiran_pecah)], + ['Butiran Jumlah', valueText(pr.butiran_jumlah)], + ['Total Butir', valueText(pr.total_butir)], + + // Kg + ['Kg Utuh', valueText(pr.kg_utuh)], + ['Kg Putih', valueText(pr.kg_putih)], + ['Kg Retak', valueText(pr.kg_retak)], + ['Kg Pecah', valueText(pr.kg_pecah)], + ['Kg Jumlah', valueText(pr.kg_jumlah)], + ['Total Kg', valueText(pr.total_kg)], + + // % + ['% Utuh', valueText(pr.persen_utuh)], + ['% Putih', valueText(pr.persen_putih)], + ['% Retak', valueText(pr.persen_retak)], + ['% Pecah', valueText(pr.persen_pecah)], + + // Produksi + ['HD', valueText(pr.hd)], + ['HD Std', valueText(pr.hd_std)], + ['FI', valueText(pr.fi)], + ['FI Std', valueText(pr.fi_std)], + ['EM', valueText(pr.em)], + ['EM Std', valueText(pr.em_std)], + ['EW', valueText(pr.ew)], + ['EW Std', valueText(pr.ew_std)], + ['FCR', valueText(pr.fcr)], + ['FCR Std', valueText(pr.fcr_std)], + ['HH', valueText(pr.hh)], + ['HH Std', valueText(pr.hh_std)], + ]; + + return ( + + {rows.map(([label, value], idx) => { + const isLast = idx === rows.length - 1; + return ( + + {label} + {value} + + ); + })} + + ); +} + +/** + * If there are multiple ProductionResult entries for a kandang, + * we show them sequentially with a small header per result. + * + * You can later change this to render only the latest WOA, or group by week. + */ +function ProductionResultList({ + productionResults, +}: { + productionResults: ProductionResult[]; +}) { + return ( + + {productionResults.map((pr, idx) => { + const kandangName = + pr.project_flock?.kandang?.name || + pr.project_flock?.kandang?.id?.toString() || + ''; + + // Optional: show a compact subheader + const headerLeft = `Data #${idx + 1}`; + const headerRight = + kandangName && pr.woa !== undefined + ? `${kandangName} • WOA ${safeNum(pr.woa)}` + : pr.woa !== undefined + ? `WOA ${safeNum(pr.woa)}` + : ''; + + return ( + + + {headerLeft} + {headerRight} + + + + + ); + })} + + ); +} + +/** + * ✅ Main PDF Component + */ +const ProductionResultReportPDF = ({ + mappedProductionResults = [], +}: ProductionResultReportPDFProps) => { + return ( + + + {/* Header */} + + + + + {formatDate(Date.now(), 'DD MMMM YYYY')} + + + + + PT LUMBUNG TELUR INDONESIA + + SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. + Cipedes, Kec. Sukajadi, Kota Bandung 40162 + + + + + + + Laporan Production Result + + {/* Sections per ProjectFlockKandang */} + {mappedProductionResults.length === 0 ? ( + + Tidak ada data. + + ) : ( + mappedProductionResults.map((item, idx) => { + const pfk = item.projectFlockKandang; + + // Try to display meaningful identifiers. + // Adjust these fields based on your real BaseProjectFlockKandang structure. + const kandangName = + pfk?.kandang?.name ?? `Kandang #${pfk?.kandang_id ?? idx + 1}`; + + const projectName = pfk?.project_flock?.name ?? ''; + + const locationName = pfk?.project_flock?.location?.name ?? ''; + + const areaName = pfk?.project_flock?.area?.name ?? ''; + + return ( + 0} // each kandang starts on a new page for clarity + > + + + {projectName + ? `${projectName} • ${kandangName}` + : kandangName} + + + {[areaName, locationName].filter(Boolean).join(' • ')} + + + + {item.productionResult && item.productionResult.length > 0 ? ( + + ) : ( + + Tidak ada production result untuk kandang ini. + + )} + + ); + }) + )} + + {/* Footer */} + + + `${pageNumber} / ${totalPages}` + } + fixed + /> + + + + ); +}; + +export default ProductionResultReportPDF; From 23e8487a9702733fc8651bef9c37cfcbcdf4cb84 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 15:21:16 +0700 Subject: [PATCH 048/139] refactor(FE): Show zero values in RecordingForm --- .../recording/form/RecordingForm.tsx | 50 ++++++------------- 1 file changed, 15 insertions(+), 35 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index c418c5b2..46fba9af 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -1640,15 +1640,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { FCR - {initialValues.fcr_value && - initialValues.fcr_value > 0 + {initialValues.fcr_value != null ? formatNumber(initialValues.fcr_value) : '-'} - {initialValues.project_flock?.fcr?.fcr_std && - initialValues.project_flock?.fcr?.fcr_std > 0 + {initialValues.project_flock?.fcr?.fcr_std != null ? formatNumber( initialValues.project_flock?.fcr?.fcr_std ) @@ -1659,17 +1657,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { Feed Intake (KG) - {initialValues.feed_intake && - initialValues.feed_intake > 0 + {initialValues.feed_intake != null ? formatNumber(initialValues.feed_intake) : '-'} {initialValues.project_flock?.production_standart - ?.feed_intake_std && - initialValues.project_flock?.production_standart - ?.feed_intake_std > 0 + ?.feed_intake_std != null ? formatNumber( initialValues.project_flock?.production_standart ?.feed_intake_std @@ -1696,8 +1691,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { Deplesi Kumulatif - {initialValues.cum_depletion_rate && - initialValues.cum_depletion_rate > 0 + {initialValues.cum_depletion_rate != null ? `${initialValues.cum_depletion_rate.toFixed(2)}%` : '-'} @@ -1706,8 +1700,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { Total Depletion - {initialValues.total_depletion_qty && - initialValues.total_depletion_qty > 0 + {initialValues.total_depletion_qty != null ? formatNumber(initialValues.total_depletion_qty) : '-'} @@ -1716,8 +1709,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { Total Ayam - {initialValues.project_flock?.total_chick_qty && - initialValues.project_flock?.total_chick_qty > 0 + {initialValues.project_flock?.total_chick_qty != null ? formatNumber( initialValues.project_flock?.total_chick_qty ) @@ -1759,17 +1751,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { Egg Mass - {initialValues.egg_mass && - initialValues.egg_mass > 0 + {initialValues.egg_mass != null ? formatNumber(initialValues.egg_mass) : '-'} {initialValues.project_flock?.production_standart - ?.egg_mass_std && - initialValues.project_flock?.production_standart - ?.egg_mass_std > 0 + ?.egg_mass_std != null ? formatNumber( initialValues.project_flock ?.production_standart?.egg_mass_std @@ -1783,17 +1772,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { - {initialValues.egg_weight && - initialValues.egg_weight > 0 + {initialValues.egg_weight != null ? formatNumber(initialValues.egg_weight) : '-'} {initialValues.project_flock?.production_standart - ?.egg_weight_std && - initialValues.project_flock?.production_standart - ?.egg_weight_std > 0 + ?.egg_weight_std != null ? formatNumber( initialValues.project_flock ?.production_standart?.egg_weight_std @@ -1805,17 +1791,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { Hen Day - {initialValues.hen_day && - initialValues.hen_day > 0 + {initialValues.hen_day != null ? formatNumber(initialValues.hen_day) : '-'} {initialValues.project_flock?.production_standart - ?.hen_day_std !== undefined && - initialValues.project_flock?.production_standart - ?.hen_day_std > 0 + ?.hen_day_std != null ? `${initialValues.project_flock?.production_standart?.hen_day_std}%` : '-'} @@ -1824,17 +1807,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { Hen House - {initialValues.hen_house && - initialValues.hen_house > 0 + {initialValues.hen_house != null ? formatNumber(initialValues.hen_house) : '-'} {initialValues.project_flock?.production_standart - ?.hen_house_std !== undefined && - initialValues.project_flock?.production_standart - ?.hen_house_std > 0 + ?.hen_house_std != null ? `${initialValues.project_flock?.production_standart?.hen_house_std}%` : '-'} From d786b7b5ba8795565aedad172554419f4f0c3160 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 15:41:11 +0700 Subject: [PATCH 049/139] feat(FE): Add warehouse display and standard detail modals --- .../recording/form/RecordingForm.tsx | 198 +++++++++++++++++- 1 file changed, 196 insertions(+), 2 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 46fba9af..13267c22 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -20,7 +20,7 @@ import SelectInput, { import CheckboxInput from '@/components/input/CheckboxInput'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; -import { useModal } from '@/components/Modal'; +import Modal, { useModal } from '@/components/Modal'; import AlertErrorList from '@/components/helper/form/FormErrors'; import { @@ -121,6 +121,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const approveModal = useModal(); const rejectModal = useModal(); const deleteModal = useModal(); + const fcrStandardModal = useModal(); + const productionStandardModal = useModal(); const isRecordingApproved = useCallback((recording?: Recording) => { return ( @@ -1565,9 +1567,20 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { : '-'}

+
+ Gudang +

+ {initialValues.warehouse?.name || '-'} +

+
Hari -

Hari ke-{initialValues.day}

+

+ Hari ke-{initialValues.day} (Minggu ke- + {initialValues.project_flock?.production_standart?.week || + '-'} + ) +

Kategori @@ -1604,6 +1617,41 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {

+
+ Standard FCR +
+ fcrStandardModal.openModal()} + > + {initialValues.project_flock?.fcr?.name || '-'} + +
+
+
+ + Standard Produksi + +
+ productionStandardModal.openModal()} + > + {initialValues.project_flock?.production_standart + ?.name || '-'} + +
+
)} @@ -2663,6 +2711,152 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { )} )} + + {/* FCR Standard Modal */} + +
+ {/* Modal Header */} +
+
+ +

Detail Standard FCR

+
+ +
+
+
+ Nama Standard +

+ {initialValues?.project_flock?.fcr?.name || '-'} +

+
+
+ FCR Standard +

+ {initialValues?.project_flock?.fcr?.fcr_std != null + ? formatNumber( + initialValues?.project_flock?.fcr?.fcr_std || 0 + ) + : '-'} +

+
+
+
+
+ + {/* Production Standard Modal */} + +
+ {/* Modal Header */} +
+
+ +

Detail Standard Produksi

+
+ +
+
+
+ Nama Standard +

+ {initialValues?.project_flock?.production_standart?.name || '-'} +

+
+
+ Minggu +

+ {initialValues?.project_flock?.production_standart?.week || '-'} +

+
+
+ + Hen Day Standard (%) + +

+ {initialValues?.project_flock?.production_standart + ?.hen_day_std != null + ? `${initialValues?.project_flock?.production_standart?.hen_day_std}%` + : '-'} +

+
+
+ + Hen House Standard (%) + +

+ {initialValues?.project_flock?.production_standart + ?.hen_house_std != null + ? `${initialValues?.project_flock?.production_standart?.hen_house_std}%` + : '-'} +

+
+
+ + Feed Intake Standard (KG) + +

+ {initialValues?.project_flock?.production_standart + ?.feed_intake_std != null + ? formatNumber( + initialValues?.project_flock?.production_standart + ?.feed_intake_std || 0 + ) + : '-'} +

+
+
+ Egg Mass Standard +

+ {initialValues?.project_flock?.production_standart + ?.egg_mass_std != null + ? formatNumber( + initialValues?.project_flock?.production_standart + ?.egg_mass_std || 0 + ) + : '-'} +

+
+
+ + Egg Weight Standard (KG) + +

+ {initialValues?.project_flock?.production_standart + ?.egg_weight_std != null + ? formatNumber( + initialValues?.project_flock?.production_standart + ?.egg_weight_std || 0 + ) + : '-'} +

+
+
+
+
); }; From 68f9e27b5f2c930709783006bbba233fa3f329b4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 15:48:18 +0700 Subject: [PATCH 050/139] refactor(FE): Reorder fields in RecordingForm --- .../recording/form/RecordingForm.tsx | 198 +++++++++--------- 1 file changed, 99 insertions(+), 99 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 13267c22..a644734e 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -1468,6 +1468,105 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }} >
+
+ Lokasi +

+ {projectFlockKandangLookup?.project_flock?.location + ?.name || + projectFlockKandangDetail?.project_flock?.location + ?.name || + '-'} +

+
+
+ Project Flock +

+ {projectFlockKandangLookup?.project_flock?.flock_name || + projectFlockKandangDetail?.project_flock?.flock_name || + '-'} +

+
+
+ Kandang +

+ {projectFlockKandangLookup?.kandang?.name || + projectFlockKandangDetail?.kandang?.name || + '-'} +

+
+
+ Kategori +

+ + {initialValues.project_flock?.project_flock_category} + +

+
+
+ Gudang +

+ {initialValues.warehouse?.name || '-'} +

+
+
+ + Jumlah Ayam Saat Ini + +

+ {initialValues.project_flock?.total_chick_qty + ? formatNumber( + initialValues.project_flock.total_chick_qty + ) + : '-'} +

+
+
+ Periode +

+ + Periode{' '} + {projectFlockKandangLookup?.project_flock?.period || + projectFlockKandangDetail?.project_flock?.period || + '-'} + +

+
+
+ + Tanggal Recording + +

+ {formatDate( + initialValues.record_datetime || '', + 'DD MMMM YYYY' + )} +

+
+
+ Hari +

+ Hari ke-{initialValues.day} (Minggu ke- + {initialValues.project_flock?.production_standart?.week || + '-'} + ) +

+
{initialValues.approval && (
@@ -1518,105 +1617,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
)} -
- Lokasi -

- {projectFlockKandangLookup?.project_flock?.location - ?.name || - projectFlockKandangDetail?.project_flock?.location - ?.name || - '-'} -

-
-
- Project Flock -

- {projectFlockKandangLookup?.project_flock?.flock_name || - projectFlockKandangDetail?.project_flock?.flock_name || - '-'} -

-
-
- Kandang -

- {projectFlockKandangLookup?.kandang?.name || - projectFlockKandangDetail?.kandang?.name || - '-'} -

-
-
- - Tanggal Recording - -

- {formatDate( - initialValues.record_datetime || '', - 'DD MMMM YYYY' - )} -

-
-
- - Jumlah Ayam Saat Ini - -

- {initialValues.project_flock?.total_chick_qty - ? formatNumber( - initialValues.project_flock.total_chick_qty - ) - : '-'} -

-
-
- Gudang -

- {initialValues.warehouse?.name || '-'} -

-
-
- Hari -

- Hari ke-{initialValues.day} (Minggu ke- - {initialValues.project_flock?.production_standart?.week || - '-'} - ) -

-
-
- Kategori -

- - {initialValues.project_flock?.project_flock_category} - -

-
-
- Periode -

- - Periode{' '} - {projectFlockKandangLookup?.project_flock?.period || - projectFlockKandangDetail?.project_flock?.period || - '-'} - -

-
Standard FCR
From bd64694c7324619a6394fe801edf3f1e1b436e29 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 15:56:30 +0700 Subject: [PATCH 051/139] feat: implement closing sapronak per kandang --- .../pages/closing/ClosingIncomingSapronaksTable.tsx | 6 +++++- .../pages/closing/ClosingOutgoingSapronaksTable.tsx | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx b/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx index 53e45710..eda7e756 100644 --- a/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx +++ b/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx @@ -1,6 +1,7 @@ 'use client'; import { ChangeEventHandler, useEffect, useState } from 'react'; +import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; @@ -23,6 +24,9 @@ interface ClosingIncomingSapronaksTableProps { const ClosingIncomingSapronaksTable = ({ projectFlockId, }: ClosingIncomingSapronaksTableProps) => { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + const { state: tableFilterState, updateFilter, @@ -43,7 +47,7 @@ const ClosingIncomingSapronaksTable = ({ const { data: incomingSapronaks, isLoading: isLoadingIncomingSapronaks } = useSWR( - `${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=incoming`, + `${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=incoming&kandang_id=${kandangId ? `${kandangId}` : ''}`, ClosingApi.getAllIncomingSapronakFetcher, { keepPreviousData: true, diff --git a/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx b/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx index 5662cff1..ac918561 100644 --- a/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx +++ b/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx @@ -1,6 +1,7 @@ 'use client'; import { ChangeEventHandler, useEffect, useState } from 'react'; +import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; @@ -23,6 +24,9 @@ interface ClosingOutgoingSapronaksTableProps { const ClosingOutgoingSapronaksTable = ({ projectFlockId, }: ClosingOutgoingSapronaksTableProps) => { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + const { state: tableFilterState, updateFilter, @@ -43,7 +47,7 @@ const ClosingOutgoingSapronaksTable = ({ const { data: outgoingSapronaks, isLoading: isLoadingOutgoingSapronaks } = useSWR( - `${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=outgoing`, + `${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=outgoing&kandang_id=${kandangId ? `${kandangId}` : ''}`, ClosingApi.getAllOutgoingSapronakFetcher, { keepPreviousData: true, From fce2cfee736efea0828bcca11e62fbf85e6a49a5 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 15:56:47 +0700 Subject: [PATCH 052/139] feat: implement closing production data per kandang --- .../pages/closing/ClosingProductionDataTabContent.tsx | 8 ++++++-- src/services/api/closing.ts | 5 +++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/pages/closing/ClosingProductionDataTabContent.tsx b/src/components/pages/closing/ClosingProductionDataTabContent.tsx index 0f15d5b9..9295d283 100644 --- a/src/components/pages/closing/ClosingProductionDataTabContent.tsx +++ b/src/components/pages/closing/ClosingProductionDataTabContent.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ClosingApi } from '@/services/api/closing'; import { isResponseSuccess } from '@/lib/api-helper'; @@ -12,9 +13,12 @@ interface ClosingProductionDataTabContentProps { const ClosingProductionDataTabContent = ({ projectFlockId, }: ClosingProductionDataTabContentProps) => { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + const { data: productionData, isLoading } = useSWR( - `${ClosingApi.basePath}/${projectFlockId}/production-data`, - () => ClosingApi.getProductionData(projectFlockId) + `${ClosingApi.basePath}/${projectFlockId}/production-data?kandang_id=${kandangId ? `${kandangId}` : ''}`, + () => ClosingApi.getProductionData(projectFlockId, Number(kandangId)) ); if (isLoading) { diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index b2ba2b8f..323e09e8 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -91,10 +91,11 @@ export class ClosingApiService extends BaseApiService { } async getProductionData( - id: number + id: number, + kandangId?: number ): Promise | undefined> { try { - const getProductionDataPath = `${this.basePath}/${id}/production-data`; + const getProductionDataPath = `${this.basePath}/${id}/production-data?kandang_id=${kandangId ? `${kandangId}` : ''}`; const getProductionDataRes = await httpClient< BaseApiResponse >(getProductionDataPath); From 438082c94cfb1d51042cfb7bdbddac5a9e3b2f67 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Thu, 15 Jan 2026 16:05:05 +0700 Subject: [PATCH 053/139] fix(FE): fixing error message on submit and fixing ui --- .../form/ProductionStandardForm.schema.ts | 34 ++--- .../form/ProductionStandardForm.tsx | 131 ++++++++++++------ src/services/hooks/useFormikErrorList.ts | 3 + 3 files changed, 104 insertions(+), 64 deletions(-) diff --git a/src/components/pages/master-data/production-standard/form/ProductionStandardForm.schema.ts b/src/components/pages/master-data/production-standard/form/ProductionStandardForm.schema.ts index 13183e71..eb59a9c0 100644 --- a/src/components/pages/master-data/production-standard/form/ProductionStandardForm.schema.ts +++ b/src/components/pages/master-data/production-standard/form/ProductionStandardForm.schema.ts @@ -2,34 +2,30 @@ import * as Yup from 'yup'; // Schema for LAYING category (production_standard_details is required) const LayingRepeaterFormSchema = Yup.object({ - week: Yup.number().required('Minggu wajib diisi!'), + week: Yup.number().required('Wajib diisi!'), production_standard_uniformity_details: Yup.object({ - target_mean_bw: Yup.number().required('Berat rata-rata wajib diisi!'), - max_depletion: Yup.number().required('Maksimal depletion wajib diisi!'), - min_uniformity: Yup.number().required('Minimal uniformitas wajib diisi!'), - feed_intake: Yup.number().required('Pengambilan makanan wajib diisi!'), + target_mean_bw: Yup.number().required('Wajib diisi!'), + max_depletion: Yup.number().required('Wajib diisi!'), + min_uniformity: Yup.number().required('Wajib diisi!'), + feed_intake: Yup.number().required('Wajib diisi!'), }), production_standard_details: Yup.object({ - target_hen_day_production: Yup.number().required( - 'Produksi telur per hari wajib diisi!' - ), - target_hen_house_production: Yup.number().required( - 'Produksi telur per kandang wajib diisi!' - ), - target_egg_weight: Yup.number().required('Berat telur wajib diisi!'), - target_egg_mass: Yup.number().required('Massa telur wajib diisi!'), - standard_fcr: Yup.number().required('FCR wajib diisi!'), + target_hen_day_production: Yup.number().required('Wajib diisi!'), + target_hen_house_production: Yup.number().required('Wajib diisi!'), + target_egg_weight: Yup.number().required('Wajib diisi!'), + target_egg_mass: Yup.number().required('Wajib diisi!'), + standard_fcr: Yup.number().required('Wajib diisi!'), }).required(), }); // Schema for GROWING category (production_standard_details is optional) const GrowingRepeaterFormSchema = Yup.object({ - week: Yup.number().required('Minggu wajib diisi!'), + week: Yup.number().required('Wajib diisi!'), production_standard_uniformity_details: Yup.object({ - target_mean_bw: Yup.number().required('Berat rata-rata wajib diisi!'), - max_depletion: Yup.number().required('Maksimal depletion wajib diisi!'), - min_uniformity: Yup.number().required('Minimal uniformitas wajib diisi!'), - feed_intake: Yup.number().required('Pengambilan makanan wajib diisi!'), + target_mean_bw: Yup.number().required('Wajib diisi!'), + max_depletion: Yup.number().required('Wajib diisi!'), + min_uniformity: Yup.number().required('Wajib diisi!'), + feed_intake: Yup.number().required('Wajib diisi!'), }), production_standard_details: Yup.object({ target_hen_day_production: Yup.number().optional(), diff --git a/src/components/pages/master-data/production-standard/form/ProductionStandardForm.tsx b/src/components/pages/master-data/production-standard/form/ProductionStandardForm.tsx index 8dfc5f45..4512f474 100644 --- a/src/components/pages/master-data/production-standard/form/ProductionStandardForm.tsx +++ b/src/components/pages/master-data/production-standard/form/ProductionStandardForm.tsx @@ -344,7 +344,7 @@ const ProductionStandardForm = ({ const columns = useMemo[]>(() => { const baseColumns: ColumnDef[] = [ { - header: 'Minggu', + header: 'Week', accessorKey: 'week', enableSorting: false, }, @@ -358,30 +358,40 @@ const ProductionStandardForm = ({ header: 'Hen Day', accessorFn: (row) => row.production_standard_details?.target_hen_day_production, + cell: ({ row }) => + `${row.original.production_standard_details?.target_hen_day_production}%`, enableSorting: false, }, { header: 'Hen House', accessorFn: (row) => row.production_standard_details?.target_hen_house_production, + cell: ({ row }) => + `${row.original.production_standard_details?.target_hen_house_production} pc`, enableSorting: false, }, { header: 'Egg Weight', accessorFn: (row) => row.production_standard_details?.target_egg_weight, + cell: ({ row }) => + `${row.original.production_standard_details?.target_egg_weight} g`, enableSorting: false, }, { header: 'Egg Mass', accessorFn: (row) => row.production_standard_details?.target_egg_mass, + cell: ({ row }) => + `${row.original.production_standard_details?.target_egg_mass} g`, enableSorting: false, }, { header: 'FCR', accessorFn: (row) => row.production_standard_details?.standard_fcr, + cell: ({ row }) => + `${row.original.production_standard_details?.standard_fcr} g`, enableSorting: false, }, ] @@ -393,24 +403,32 @@ const ProductionStandardForm = ({ header: 'Mean BW', accessorFn: (row) => row.production_standard_uniformity_details?.target_mean_bw, + cell: ({ row }) => + `${row.original.production_standard_uniformity_details?.target_mean_bw} g`, enableSorting: false, }, { header: 'Max Depletion', accessorFn: (row) => row.production_standard_uniformity_details?.max_depletion, + cell: ({ row }) => + `${row.original.production_standard_uniformity_details?.max_depletion}%`, enableSorting: false, }, { header: 'Min Uniformity', accessorFn: (row) => row.production_standard_uniformity_details?.min_uniformity, + cell: ({ row }) => + `${row.original.production_standard_uniformity_details?.min_uniformity}%`, enableSorting: false, }, { header: 'Feed Intake', accessorFn: (row) => row.production_standard_uniformity_details?.feed_intake, + cell: ({ row }) => + `${row.original.production_standard_uniformity_details?.feed_intake} g`, enableSorting: false, }, ]; @@ -728,7 +746,52 @@ const ProductionStandardForm = ({ }; // ===== Formik Error List ===== - const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); + const { formErrorList, close, handleFormSubmit } = useFormikErrorList( + formik, + { + onBeforeSubmit: (e) => { + e.preventDefault(); + + // For GROWING category, clear production_standard_details errors and set default values + if (formik.values.project_category === 'GROWING') { + // Set default values for production_standard_details + formik.values.details?.forEach((detail) => { + detail.production_standard_details = { + target_hen_day_production: 0, + target_hen_house_production: 0, + target_egg_weight: 0, + target_egg_mass: 0, + standard_fcr: 0, + }; + }); + + // Clear any errors related to production_standard_details + const currentErrors = { ...formik.errors }; + if (currentErrors.details && Array.isArray(currentErrors.details)) { + const cleanedDetails = currentErrors.details + .map((detailError) => { + if (detailError && typeof detailError === 'object') { + const { production_standard_details, ...rest } = detailError; + return Object.keys(rest).length > 0 ? rest : undefined; + } + return detailError; + }) + .filter( + (error): error is Exclude => + error !== undefined + ); + + currentErrors.details = ( + cleanedDetails.length > 0 ? cleanedDetails : undefined + ) as typeof currentErrors.details; + } + formik.setErrors(currentErrors); + } + + return true; + }, + } + ); return ( <> @@ -821,19 +884,20 @@ const ProductionStandardForm = ({ key={`row-${row.index}`} className='sticky bottom-0 bg-base-100 shadow-lg' > - +
} + bottomLabel='Persen (%)' errorMessage={getProductionDetailsError( repeaterFormik.errors .production_standard_details, @@ -894,11 +958,7 @@ const ProductionStandardForm = ({ } onChange={repeaterFormik.handleChange} onBlur={repeaterFormik.handleBlur} - endAdornment={ -
- Butir -
- } + bottomLabel='Butir (pc)' errorMessage={getProductionDetailsError( repeaterFormik.errors .production_standard_details, @@ -930,11 +990,7 @@ const ProductionStandardForm = ({ } onChange={repeaterFormik.handleChange} onBlur={repeaterFormik.handleBlur} - endAdornment={ -
- gr -
- } + bottomLabel='Gram (g)' errorMessage={getProductionDetailsError( repeaterFormik.errors .production_standard_details, @@ -959,17 +1015,13 @@ const ProductionStandardForm = ({ name='production_standard_details.target_egg_mass' label='Egg Mass' placeholder='1' + bottomLabel='Gram (g)' value={ repeaterFormik.values .production_standard_details?.target_egg_mass } onChange={repeaterFormik.handleChange} onBlur={repeaterFormik.handleBlur} - endAdornment={ -
- gr -
- } errorMessage={getProductionDetailsError( repeaterFormik.errors .production_standard_details, @@ -1000,11 +1052,7 @@ const ProductionStandardForm = ({ } onChange={repeaterFormik.handleChange} onBlur={repeaterFormik.handleBlur} - endAdornment={ -
- gr -
- } + bottomLabel='Gram (g)' errorMessage={getProductionDetailsError( repeaterFormik.errors .production_standard_details, @@ -1038,11 +1086,7 @@ const ProductionStandardForm = ({ } onChange={repeaterFormik.handleChange} onBlur={repeaterFormik.handleBlur} - endAdornment={ -
- gr -
- } + bottomLabel='Gram (g)' errorMessage={ repeaterFormik.errors .production_standard_uniformity_details @@ -1072,7 +1116,7 @@ const ProductionStandardForm = ({ } onChange={repeaterFormik.handleChange} onBlur={repeaterFormik.handleBlur} - endAdornment={} + bottomLabel='Persen (%)' errorMessage={ repeaterFormik.errors .production_standard_uniformity_details @@ -1102,7 +1146,7 @@ const ProductionStandardForm = ({ } onChange={repeaterFormik.handleChange} onBlur={repeaterFormik.handleBlur} - endAdornment={} + bottomLabel='Persen (%)' errorMessage={ repeaterFormik.errors .production_standard_uniformity_details @@ -1132,11 +1176,8 @@ const ProductionStandardForm = ({ } onChange={repeaterFormik.handleChange} onBlur={repeaterFormik.handleBlur} - endAdornment={ -
- gr/ekor -
- } + bottomLabel='Gram/Ekor (g)' + endAdornment errorMessage={ repeaterFormik.errors .production_standard_uniformity_details @@ -1162,7 +1203,7 @@ const ProductionStandardForm = ({ type='button' color='error' variant='outline' - className='min-w-24' + className='min-w-xs' onClick={handleCancelEdit} > Batal @@ -1178,7 +1219,7 @@ const ProductionStandardForm = ({
-
-
- Nama Standard -

- {initialValues?.project_flock?.fcr?.name || '-'} +

+ {isLoadingFcrStandards ? ( +
+ +
+ ) : fcrStandards.length > 0 ? ( + + data={fcrStandards} + columns={fcrStandardColumns} + className={{ + tableWrapperClassName: 'overflow-x-auto', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-500 whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-gray-200 first:border-t first:border-t-gray-200 border-l border-l-gray-200 border-r border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + paginationClassName: 'hidden', + }} + /> + ) : ( +

+ Tidak ada data FCR standards

-
-
- FCR Standard -

- {initialValues?.project_flock?.fcr?.fcr_std != null - ? formatNumber( - initialValues?.project_flock?.fcr?.fcr_std || 0 - ) - : '-'} -

-
+ )}
From 7114470c131cc657ab402202d7bdd1b2acc187ab Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 16:11:12 +0700 Subject: [PATCH 055/139] chore: access correct UOM id --- .../pages/master-data/nonstock/form/NonstockForm.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx index 4622a6a3..83b53a7d 100644 --- a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx +++ b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx @@ -83,7 +83,7 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { const formikInitialValues = useMemo(() => { return { name: initialValues?.name ?? '', - uomId: initialValues?.uom_id ?? 0, + uomId: initialValues?.uom?.id ?? 0, uom: initialValues?.uom ? { value: initialValues?.uom?.id, @@ -199,6 +199,10 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { // ===== Formik Error List ===== const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); + console.log({ + values: formik.values, + }); + return ( <>
From b2ce9c93b72529ccb1e4595ea6f766245bedd985 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 16:13:54 +0700 Subject: [PATCH 056/139] chore: remove unnecessary data --- .../pages/master-data/nonstock/form/NonstockForm.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx index 83b53a7d..bfe88c0e 100644 --- a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx +++ b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx @@ -199,10 +199,6 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { // ===== Formik Error List ===== const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); - console.log({ - values: formik.values, - }); - return ( <>
From 4285e2e26963eefd254ea2c8dbf21c6429e83410 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 16:16:32 +0700 Subject: [PATCH 057/139] refactor(FE): Enable backdrop close and set FCR table pageSize --- .../pages/production/recording/form/RecordingForm.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 3ea4ae42..67dae969 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -2773,6 +2773,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {/* FCR Standard Modal */} {
-
+
{isLoadingFcrStandards ? (
@@ -2802,6 +2803,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { data={fcrStandards} columns={fcrStandardColumns} + pageSize={100} className={{ tableWrapperClassName: 'overflow-x-auto', tableClassName: 'w-full table-auto text-sm', From c7b4361cb610d4cbf40b17f17d9707bb1f1fb6a0 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 16:24:45 +0700 Subject: [PATCH 058/139] feat(FE): Add Production Standard modal and table --- .../recording/form/RecordingForm.tsx | 242 ++++++++++++------ 1 file changed, 166 insertions(+), 76 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 67dae969..435150d9 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -30,8 +30,12 @@ import { RecordingApi, ProjectFlockApi, } from '@/services/api/production'; -import { FcrApi } from '@/services/api/master-data'; +import { FcrApi, ProductionStandardApi } from '@/services/api/master-data'; import { FcrWithStandards, FcrStandard } from '@/types/api/master-data/fcr'; +import { + ProductionStandard, + StandardDetails, +} from '@/types/api/master-data/production-standard'; import { LocationApi } from '@/services/api/master-data'; import { ProductWarehouseApi } from '@/services/api/inventory'; import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; @@ -98,6 +102,94 @@ const fcrStandardColumns: ColumnDef[] = [ }, ]; +const productionStandardColumns: ColumnDef[] = [ + { + accessorKey: 'week', + header: 'Minggu', + cell: (props) => `Minggu ${props.getValue() as number}`, + }, + { + accessorKey: 'growth_standard_detail.target_mean_bw', + header: 'Target Mean BW (gram)', + cell: (props) => + formatNumber( + (props.row.original.growth_standard_detail?.target_mean_bw as number) || + 0 + ), + }, + { + accessorKey: 'growth_standard_detail.max_depletion', + header: 'Max Depletion (%)', + cell: (props) => + `${ + (props.row.original.growth_standard_detail?.max_depletion as number) || + 0 + }%`, + }, + { + accessorKey: 'growth_standard_detail.min_uniformity', + header: 'Min Uniformity (%)', + cell: (props) => + `${ + (props.row.original.growth_standard_detail?.min_uniformity as number) || + 0 + }%`, + }, + { + accessorKey: 'growth_standard_detail.feed_intake', + header: 'Feed Intake (gram)', + cell: (props) => + formatNumber( + (props.row.original.growth_standard_detail?.feed_intake as number) || 0 + ), + }, + { + accessorKey: 'egg_production_standard_detail.target_hen_day_production', + header: 'Target Hen Day (%)', + cell: (props) => + `${ + (props.row.original.egg_production_standard_detail + ?.target_hen_day_production as number) || 0 + }%`, + }, + { + accessorKey: 'egg_production_standard_detail.target_hen_house_production', + header: 'Target Hen House (%)', + cell: (props) => + `${ + (props.row.original.egg_production_standard_detail + ?.target_hen_house_production as number) || 0 + }%`, + }, + { + accessorKey: 'egg_production_standard_detail.target_egg_weight', + header: 'Target Egg Weight (gram)', + cell: (props) => + formatNumber( + (props.row.original.egg_production_standard_detail + ?.target_egg_weight as number) || 0 + ), + }, + { + accessorKey: 'egg_production_standard_detail.target_egg_mass', + header: 'Target Egg Mass (gram)', + cell: (props) => + formatNumber( + (props.row.original.egg_production_standard_detail + ?.target_egg_mass as number) || 0 + ), + }, + { + accessorKey: 'egg_production_standard_detail.standard_fcr', + header: 'Standard FCR', + cell: (props) => + formatNumber( + (props.row.original.egg_production_standard_detail + ?.standard_fcr as number) || 0 + ), + }, +]; + const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { // ===== HOOKS & ROUTER ===== const router = useRouter(); @@ -147,8 +239,12 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const productionStandardModal = useModal(); const [fcrStandards, setFcrStandards] = useState([]); + const [productionStandards, setProductionStandards] = + useState(null); const [isFcrModalOpen, setIsFcrModalOpen] = useState(false); + const [isProductionStandardModalOpen, setIsProductionStandardModalOpen] = + useState(false); useEffect(() => { const checkFcrModalOpen = () => { @@ -169,6 +265,25 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { return () => observer.disconnect(); }, [fcrStandardModal.ref]); + useEffect(() => { + const checkProductionStandardModalOpen = () => { + const isOpen = productionStandardModal.ref.current?.open || false; + setIsProductionStandardModalOpen(isOpen); + }; + + checkProductionStandardModalOpen(); + + const observer = new MutationObserver(checkProductionStandardModalOpen); + if (productionStandardModal.ref.current) { + observer.observe(productionStandardModal.ref.current, { + attributes: true, + attributeFilter: ['open'], + }); + } + + return () => observer.disconnect(); + }, [productionStandardModal.ref]); + const { data: fcr, isLoading: isLoadingFcrStandards } = useSWR( isFcrModalOpen && initialValues?.project_flock?.fcr?.id ? `fcr-detail-${initialValues.project_flock.fcr.id}` @@ -182,6 +297,26 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } }, [fcr]); + const { data: productionStandard, isLoading: isLoadingProductionStandards } = + useSWR( + isProductionStandardModalOpen && + initialValues?.project_flock?.production_standart?.id + ? `production-standard-detail-${initialValues.project_flock.production_standart.id}` + : null, + () => + ProductionStandardApi.getSingle( + initialValues!.project_flock!.production_standart!.id! + ) + ); + + useEffect(() => { + if (productionStandard?.status === 'success') { + setProductionStandards( + productionStandard.data as ProductionStandard | null + ); + } + }, [productionStandard]); + const isRecordingApproved = useCallback((recording?: Recording) => { return ( recording?.approval?.action === 'APPROVED' && @@ -2828,10 +2963,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {/* Production Standard Modal */}
@@ -2849,81 +2985,35 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
-
-
- Nama Standard -

- {initialValues?.project_flock?.production_standart?.name || '-'} +

+ {isLoadingProductionStandards ? ( +
+ +
+ ) : productionStandards?.details && + productionStandards.details.length > 0 ? ( + + data={productionStandards.details} + columns={productionStandardColumns} + pageSize={100} + className={{ + tableWrapperClassName: 'overflow-x-auto', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-500 whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-gray-200 first:border-t first:border-t-gray-200 border-l border-l-gray-200 border-r border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + paginationClassName: 'hidden', + }} + /> + ) : ( +

+ Tidak ada data Production standards

-
-
- Minggu -

- {initialValues?.project_flock?.production_standart?.week || '-'} -

-
-
- - Hen Day Standard (%) - -

- {initialValues?.project_flock?.production_standart - ?.hen_day_std != null - ? `${initialValues?.project_flock?.production_standart?.hen_day_std}%` - : '-'} -

-
-
- - Hen House Standard (%) - -

- {initialValues?.project_flock?.production_standart - ?.hen_house_std != null - ? `${initialValues?.project_flock?.production_standart?.hen_house_std}%` - : '-'} -

-
-
- - Feed Intake Standard (KG) - -

- {initialValues?.project_flock?.production_standart - ?.feed_intake_std != null - ? formatNumber( - initialValues?.project_flock?.production_standart - ?.feed_intake_std || 0 - ) - : '-'} -

-
-
- Egg Mass Standard -

- {initialValues?.project_flock?.production_standart - ?.egg_mass_std != null - ? formatNumber( - initialValues?.project_flock?.production_standart - ?.egg_mass_std || 0 - ) - : '-'} -

-
-
- - Egg Weight Standard (KG) - -

- {initialValues?.project_flock?.production_standart - ?.egg_weight_std != null - ? formatNumber( - initialValues?.project_flock?.production_standart - ?.egg_weight_std || 0 - ) - : '-'} -

-
+ )}
From 0a5414a3ac1dc4272a96ef60fc8c5d87c870e2e8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 16:28:32 +0700 Subject: [PATCH 059/139] refactor(FE): Add bottom margin to production standards text --- .../pages/production/recording/form/RecordingForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 435150d9..9dd3df1f 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -3010,7 +3010,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }} /> ) : ( -

+

Tidak ada data Production standards

)} From 76c1b2f62885dffe4a65f8265ec89e22e9a64146 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 16:35:32 +0700 Subject: [PATCH 060/139] refactor(FE): Derive current total chick qty from flock data --- .../recording/form/RecordingForm.tsx | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 9dd3df1f..32c8c4c6 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -725,7 +725,26 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }, [existingRecordings, today]); const currentTotalChickQty = useMemo(() => { - if (!isResponseSuccess(existingRecordings) || !selectedKandang) return null; + if (type === 'add' && projectFlockKandangLookup) { + return projectFlockKandangLookup.available_quantity || null; + } + + if ((type === 'edit' || type === 'detail') && projectFlockKandangDetail) { + if ( + projectFlockKandangDetail.available_qtys && + projectFlockKandangDetail.available_qtys.length > 0 + ) { + return projectFlockKandangDetail.available_qtys.reduce( + (sum, item) => sum + (item.available_qty || 0), + 0 + ); + } + return null; + } + + if (!isResponseSuccess(existingRecordings) || !selectedKandang) { + return null; + } let projectFlockKandangId: number | undefined; @@ -745,6 +764,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { return recording?.project_flock.total_chick_qty || null; }, [ + type, existingRecordings, selectedKandang, projectFlockKandangLookup, From 4a9cbdc219573dd4a4889014ceea222ff45a4bb0 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 16:41:24 +0700 Subject: [PATCH 061/139] refactor(FE): Use initialValues total_chick_qty for edit/detail --- .../production/recording/form/RecordingForm.tsx | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 32c8c4c6..5312c0be 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -729,17 +729,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { return projectFlockKandangLookup.available_quantity || null; } - if ((type === 'edit' || type === 'detail') && projectFlockKandangDetail) { - if ( - projectFlockKandangDetail.available_qtys && - projectFlockKandangDetail.available_qtys.length > 0 - ) { - return projectFlockKandangDetail.available_qtys.reduce( - (sum, item) => sum + (item.available_qty || 0), - 0 - ); - } - return null; + if ((type === 'edit' || type === 'detail') && initialValues) { + return initialValues.project_flock?.total_chick_qty || null; } if (!isResponseSuccess(existingRecordings) || !selectedKandang) { @@ -765,6 +756,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { return recording?.project_flock.total_chick_qty || null; }, [ type, + initialValues, existingRecordings, selectedKandang, projectFlockKandangLookup, From e349b9dfa446b73cf31e9bf7e1a32cb9fbad1ac6 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Thu, 15 Jan 2026 16:48:11 +0700 Subject: [PATCH 062/139] fix(FE): implement lazy loading select button on finance module --- src/components/pages/finance/FinanceTable.tsx | 26 +++++++--------- .../pages/finance/add/FormFinanceAdd.tsx | 14 ++++++--- .../FormFinanceAddInitialBalance.tsx | 30 ++++++++++++------- .../add/injection/FormFinanceInjection.tsx | 6 +++- 4 files changed, 44 insertions(+), 32 deletions(-) diff --git a/src/components/pages/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx index 2d43d273..227c3731 100644 --- a/src/components/pages/finance/FinanceTable.tsx +++ b/src/components/pages/finance/FinanceTable.tsx @@ -1,21 +1,17 @@ import { ChangeEventHandler, useMemo, useState } from 'react'; -import { CellContext, Row } from '@tanstack/react-table'; +import { CellContext } from '@tanstack/react-table'; import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import Button from '@/components/Button'; import Card from '@/components/Card'; -import Dropdown from '@/components/dropdown/Dropdown'; import DateInput from '@/components/input/DateInput'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import SelectInput, { OptionType, useSelect, } from '@/components/input/SelectInput'; -import Menu from '@/components/menu/Menu'; -import MenuItem from '@/components/menu/MenuItem'; import Table from '@/components/Table'; -import Tooltip from '@/components/Tooltip'; import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { Finance } from '@/types/api/finance/finance'; @@ -23,11 +19,10 @@ import { FINANCE_INITIAL_BALANCE_STATUS, FINANCE_INJECTION_STATUS, FINANCE_TRANSACTION_STATUS, - ROWS_OPTIONS, } from '@/config/constant'; import { FinanceApi } from '@/services/api/finance'; import { isResponseSuccess } from '@/lib/api-helper'; -import { BankApi, CustomerApi, SupplierApi } from '@/services/api/master-data'; +import { BankApi } from '@/services/api/master-data'; import { Bank } from '@/types/api/master-data/bank'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; @@ -219,15 +214,12 @@ const FinanceTable = () => { { label: 'Tanggal Dibuat', value: 'created_at' }, ]; }, []); - const { options: bankOptions, rawData: bankRawData } = useSelect( - BankApi.basePath, - 'id', - 'alias', - '', - { - limit: 'limit', - } - ); + const { + options: bankOptions, + rawData: bankRawData, + setInputValue: bankInputValue, + loadMore: bankLoadMore, + } = useSelect(BankApi.basePath, 'id', 'alias'); // ===== Handler ===== const searchChangeHandler: ChangeEventHandler = (e) => { @@ -501,6 +493,8 @@ const FinanceTable = () => { label='Bank' value={selectedBank} onChange={bankChangeHandler} + onInputChange={bankInputValue} + onMenuScrollToBottom={bankLoadMore} isClearable /> ( formik.values.party_type_option?.value === 'CUSTOMER' ? CustomerApi.basePath : SupplierApi.basePath, 'id', - 'name', - '', - { limit: 'limit' } + 'name' ); const { options: bankOptions, rawData: bankRawData, isLoadingOptions: isLoadingBankOptions, - } = useSelect(BankApi.basePath, 'id', 'name', '', { limit: 'limit' }); + setInputValue: setBankInputValue, + loadMore: loadMoreBankOptions, + } = useSelect(BankApi.basePath, 'id', 'name'); // ===== Helper Functions ===== const transformFormValuesToPayload = ( @@ -219,6 +221,8 @@ const FormFinanceAdd = ({ placeholder={`Pilih ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'jenis transaksi dahulu'}`} options={partyOptions} value={formik.values.party_id_option} + onInputChange={setPartyInputValue} + onMenuScrollToBottom={loadMorePartyOptions} onChange={(value) => { formik.setFieldValue('party_id_option', value); if (isResponseSuccess(partyRawData) && value) { @@ -304,6 +308,8 @@ const FormFinanceAdd = ({ : [] } value={formik.values.bank_id_option} + onInputChange={setBankInputValue} + onMenuScrollToBottom={loadMoreBankOptions} onChange={(value) => { formik.setFieldValue('bank_id_option', value); }} diff --git a/src/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance.tsx b/src/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance.tsx index 85b63ac3..7bcdbccf 100644 --- a/src/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance.tsx +++ b/src/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance.tsx @@ -104,21 +104,25 @@ const FormFinanceAddInitialBalance = ({ }); // ===== Options ===== - const { options: partyOptions, isLoadingOptions: isLoadingPartyOptions } = - useSelect( - formik.values.party_type_option?.value === 'CUSTOMER' - ? CustomerApi.basePath - : SupplierApi.basePath, - 'id', - 'name', - '', - { limit: 'limit' } - ); + const { + options: partyOptions, + isLoadingOptions: isLoadingPartyOptions, + setInputValue: setPartyInputValue, + loadMore: loadMorePartyOptions, + } = useSelect( + formik.values.party_type_option?.value === 'CUSTOMER' + ? CustomerApi.basePath + : SupplierApi.basePath, + 'id', + 'name' + ); const { options: bankOptions, rawData: bankRawData, isLoadingOptions: isLoadingBankOptions, - } = useSelect(BankApi.basePath, 'id', 'name', '', { limit: 'limit' }); + setInputValue: setBankInputValue, + loadMore: loadMoreBankOptions, + } = useSelect(BankApi.basePath, 'id', 'name'); // ===== Helper Functions ===== const transformFormValuesToPayload = ( @@ -189,6 +193,8 @@ const FormFinanceAddInitialBalance = ({ placeholder='Pilih jenis pihak' options={FINANCE_PARTY_TYPE_OPTIONS} value={formik.values.party_type_option} + onInputChange={setPartyInputValue} + onMenuScrollToBottom={loadMorePartyOptions} onChange={(value) => { formik.setFieldValue('party_type_option', value); formik.setFieldValue('party_id_option', null); @@ -218,6 +224,8 @@ const FormFinanceAddInitialBalance = ({ placeholder={`Pilih ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'jenis pihak dahulu'}`} options={partyOptions} value={formik.values.party_id_option} + onInputChange={setPartyInputValue} + onMenuScrollToBottom={loadMorePartyOptions} onChange={(value) => { formik.setFieldValue('party_id_option', value); }} diff --git a/src/components/pages/finance/add/injection/FormFinanceInjection.tsx b/src/components/pages/finance/add/injection/FormFinanceInjection.tsx index 4ddd282c..22663f75 100644 --- a/src/components/pages/finance/add/injection/FormFinanceInjection.tsx +++ b/src/components/pages/finance/add/injection/FormFinanceInjection.tsx @@ -80,7 +80,9 @@ const FormFinanceInjection = ({ options: bankOptions, rawData: bankRawData, isLoadingOptions: isLoadingBankOptions, - } = useSelect(BankApi.basePath, 'id', 'name', '', { limit: 'limit' }); + setInputValue: setBankInputValue, + loadMore: loadMoreBankOptions, + } = useSelect(BankApi.basePath, 'id', 'name'); // ===== Helper Functions ===== const transformFormValuesToPayload = ( @@ -162,6 +164,8 @@ const FormFinanceInjection = ({ : [] } value={formik.values.bank_id_option} + onInputChange={setBankInputValue} + onMenuScrollToBottom={loadMoreBankOptions} onChange={(value) => { formik.setFieldValue('bank_id_option', value); }} From 73100aa1ceeca1a2a54a7825bffa3fe3fde7bd93 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Thu, 15 Jan 2026 16:53:39 +0700 Subject: [PATCH 063/139] fix(FE): implement lazy loading select button on project flock index --- .../project-flock/ProjectFlockTable.tsx | 83 +++++++------------ 1 file changed, 28 insertions(+), 55 deletions(-) diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index 5ca0e789..6b935812 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -5,7 +5,10 @@ import Button from '@/components/Button'; import FloatingActionsButton from '@/components/FloatingActionsButton'; import CheckboxInput from '@/components/input/CheckboxInput'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; -import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; @@ -59,9 +62,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { const selectedRowIds = Object.keys(rowSelection) .filter((id) => rowSelection[id]) .map((id) => parseInt(id)); - const [locationSelectInputValue, setLocationSelectInputValue] = useState(''); - const [areaSelectInputValue, setAreaSelectInputValue] = useState(''); - const [kandangSelectInputValue, setKandangSelectInputValue] = useState(''); const [selectedArea, setSelectedArea] = useState(null); const [selectedLocation, setSelectedLocation] = useState( null @@ -90,55 +90,25 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { { revalidateOnMount: true } ); - const areaUrl = `${AreaApi.basePath}?${new URLSearchParams({ - search: areaSelectInputValue, - limit: '100', - }).toString()}`; - const { data: areas, isLoading: isLoadingAreas } = useSWR( - areaUrl, - AreaApi.getAllFetcher - ); - - const locationUrl = `${LocationApi.basePath}?${new URLSearchParams({ - search: locationSelectInputValue, - area_id: selectedArea != null ? selectedArea.value.toString() : '', - limit: '100', - }).toString()}`; - const { data: locations, isLoading: isLoadingLocations } = useSWR( - locationUrl, - LocationApi.getAllFetcher - ); - - const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({ - search: kandangSelectInputValue, - location_id: - selectedLocation != null ? selectedLocation.value.toString() : '', - limit: '100', - }).toString()}`; - const { data: kandangs, isLoading: isLoadingKandang } = useSWR( - kandangUrl, - KandangApi.getAllFetcher - ); - - // ===== Data to Options Mapping ====== - const optionsArea = isResponseSuccess(areas) - ? areas?.data.map((area) => ({ - value: area.id, - label: area.name, - })) - : []; - const optionsKandang = isResponseSuccess(kandangs) - ? kandangs?.data.map((kandang) => ({ - value: kandang.id, - label: kandang.name, - })) - : []; - const optionsLocation = isResponseSuccess(locations) - ? locations?.data.map((location) => ({ - value: location.id, - label: location.name, - })) - : []; + // ===== Fetch Data Select ===== + const { + options: optionsArea, + isLoadingOptions: isLoadingArea, + setInputValue: setAreaSelectInputValue, + loadMore: loadMoreArea, + } = useSelect(AreaApi.basePath, 'id', 'name'); + const { + options: optionsLocation, + isLoadingOptions: isLoadingLocation, + setInputValue: setLocationSelectInputValue, + loadMore: loadMoreLocation, + } = useSelect(LocationApi.basePath, 'id', 'name'); + const { + options: optionsKandang, + isLoadingOptions: isLoadingKandang, + setInputValue: setKandangSelectInputValue, + loadMore: loadMoreKandang, + } = useSelect(KandangApi.basePath, 'id', 'name'); // ====== HANDLER ====== const confirmationModalDeleteClickHandler = async () => { @@ -385,7 +355,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { { setSelectedArea(val as OptionType); @@ -395,12 +365,13 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { ); }} onInputChange={setAreaSelectInputValue} + onMenuScrollToBottom={loadMoreArea} isClearable /> { setSelectedLocation(val as OptionType); @@ -410,6 +381,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { ); }} onInputChange={setLocationSelectInputValue} + onMenuScrollToBottom={loadMoreLocation} isClearable /> void }) => { ); }} onInputChange={setKandangSelectInputValue} + onMenuScrollToBottom={loadMoreKandang} isClearable /> Date: Thu, 15 Jan 2026 16:55:16 +0700 Subject: [PATCH 064/139] chore: implement delete error message toast --- .../pages/master-data/area/AreasTable.tsx | 11 +++++++++-- .../pages/master-data/bank/BanksTable.tsx | 11 +++++++++-- .../pages/master-data/customer/CustomersTable.tsx | 13 +++++++++++-- .../pages/master-data/fcr/FcrsTable.tsx | 11 +++++++++-- .../pages/master-data/flock/FlocksTable.tsx | 11 +++++++++-- .../pages/master-data/kandang/KandangsTable.tsx | 13 +++++++++++-- .../pages/master-data/location/LocationsTable.tsx | 13 +++++++++++-- .../pages/master-data/nonstock/NonstocksTable.tsx | 13 +++++++++++-- .../product-category/ProductCategoryTable.tsx | 13 +++++++++++-- .../pages/master-data/product/ProductTable.tsx | 15 +++++++++++++-- .../ProductionStandardTable.tsx | 11 +++++++++-- .../pages/master-data/supplier/SupplierTable.tsx | 13 +++++++++++-- .../pages/master-data/uom/UomsTable.tsx | 11 +++++++++-- .../master-data/warehouse/WarehousesTable.tsx | 13 +++++++++++-- 14 files changed, 144 insertions(+), 28 deletions(-) diff --git a/src/components/pages/master-data/area/AreasTable.tsx b/src/components/pages/master-data/area/AreasTable.tsx index 45c4fdff..d92c7840 100644 --- a/src/components/pages/master-data/area/AreasTable.tsx +++ b/src/components/pages/master-data/area/AreasTable.tsx @@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission'; import { Area } from '@/types/api/master-data/area'; import { AreaApi } from '@/services/api/master-data'; import { cn } from '@/lib/helper'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ROWS_OPTIONS } from '@/config/constant'; @@ -164,7 +164,14 @@ const AreasTable = () => { const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); - await AreaApi.delete(selectedArea?.id as number); + const deleteResponse = await AreaApi.delete(selectedArea?.id as number); + + if (isResponseError(deleteResponse)) { + toast.error(deleteResponse.message); + setIsDeleteLoading(false); + return; + } + refreshAreas(); deleteModal.closeModal(); diff --git a/src/components/pages/master-data/bank/BanksTable.tsx b/src/components/pages/master-data/bank/BanksTable.tsx index f28f4bd0..c5a564fe 100644 --- a/src/components/pages/master-data/bank/BanksTable.tsx +++ b/src/components/pages/master-data/bank/BanksTable.tsx @@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission'; import { Bank } from '@/types/api/master-data/bank'; import { BankApi } from '@/services/api/master-data'; import { cn } from '@/lib/helper'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ROWS_OPTIONS } from '@/config/constant'; @@ -177,7 +177,14 @@ const BanksTable = () => { const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); - await BankApi.delete(selectedBank?.id as number); + const deleteResponse = await BankApi.delete(selectedBank?.id as number); + + if (isResponseError(deleteResponse)) { + toast.error(deleteResponse.message); + setIsDeleteLoading(false); + return; + } + refreshBanks(); deleteModal.closeModal(); diff --git a/src/components/pages/master-data/customer/CustomersTable.tsx b/src/components/pages/master-data/customer/CustomersTable.tsx index 3e442620..e605d9f7 100644 --- a/src/components/pages/master-data/customer/CustomersTable.tsx +++ b/src/components/pages/master-data/customer/CustomersTable.tsx @@ -11,7 +11,7 @@ import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import RequirePermission from '@/components/helper/RequirePermission'; import { ROWS_OPTIONS } from '@/config/constant'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { cn } from '@/lib/helper'; import { CustomerApi } from '@/services/api/master-data'; import { useTableFilter } from '@/services/hooks/useTableFilter'; @@ -186,7 +186,16 @@ const CustomersTable = () => { const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); - await CustomerApi.delete(selectedCustomer?.id as number); + const deleteResponse = await CustomerApi.delete( + selectedCustomer?.id as number + ); + + if (isResponseError(deleteResponse)) { + toast.error(deleteResponse.message); + setIsDeleteLoading(false); + return; + } + refreshCustomers(); deleteModal.closeModal(); diff --git a/src/components/pages/master-data/fcr/FcrsTable.tsx b/src/components/pages/master-data/fcr/FcrsTable.tsx index 2d65a406..2eb8d8da 100644 --- a/src/components/pages/master-data/fcr/FcrsTable.tsx +++ b/src/components/pages/master-data/fcr/FcrsTable.tsx @@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission'; import { Fcr } from '@/types/api/master-data/fcr'; import { FcrApi } from '@/services/api/master-data'; import { cn } from '@/lib/helper'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ROWS_OPTIONS } from '@/config/constant'; @@ -164,7 +164,14 @@ const FcrsTable = () => { const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); - await FcrApi.delete(selectedFcr?.id as number); + const deleteResponse = await FcrApi.delete(selectedFcr?.id as number); + + if (isResponseError(deleteResponse)) { + toast.error(deleteResponse.message); + setIsDeleteLoading(false); + return; + } + refreshFcrs(); deleteModal.closeModal(); diff --git a/src/components/pages/master-data/flock/FlocksTable.tsx b/src/components/pages/master-data/flock/FlocksTable.tsx index ce8f701a..baeaa69e 100644 --- a/src/components/pages/master-data/flock/FlocksTable.tsx +++ b/src/components/pages/master-data/flock/FlocksTable.tsx @@ -19,7 +19,7 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import SelectInput, { OptionType } from '@/components/input/SelectInput'; import { ROWS_OPTIONS } from '@/config/constant'; import Table from '@/components/Table'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; const RowsOptions = ({ @@ -182,7 +182,14 @@ const FlockTable = () => { const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); - await FlockApi.delete(selectedFlock?.id as number); + const deleteResponse = await FlockApi.delete(selectedFlock?.id as number); + + if (isResponseError(deleteResponse)) { + toast.error(deleteResponse.message); + setIsDeleteLoading(false); + return; + } + refreshFlocks(); deleteModal.closeModal(); diff --git a/src/components/pages/master-data/kandang/KandangsTable.tsx b/src/components/pages/master-data/kandang/KandangsTable.tsx index 1bd7badb..7d79d456 100644 --- a/src/components/pages/master-data/kandang/KandangsTable.tsx +++ b/src/components/pages/master-data/kandang/KandangsTable.tsx @@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission'; import { Kandang } from '@/types/api/master-data/kandang'; import { KandangApi } from '@/services/api/master-data'; import { cn, formatNumber } from '@/lib/helper'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ROWS_OPTIONS } from '@/config/constant'; @@ -199,7 +199,16 @@ const KandangsTable = () => { const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); - await KandangApi.delete(selectedKandang?.id as number); + const deleteResponse = await KandangApi.delete( + selectedKandang?.id as number + ); + + if (isResponseError(deleteResponse)) { + toast.error(deleteResponse.message); + setIsDeleteLoading(false); + return; + } + refreshKandangs(); deleteModal.closeModal(); diff --git a/src/components/pages/master-data/location/LocationsTable.tsx b/src/components/pages/master-data/location/LocationsTable.tsx index 10fe46c9..a35ffd09 100644 --- a/src/components/pages/master-data/location/LocationsTable.tsx +++ b/src/components/pages/master-data/location/LocationsTable.tsx @@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission'; import { Location } from '@/types/api/master-data/location'; import { LocationApi } from '@/services/api/master-data'; import { cn } from '@/lib/helper'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ROWS_OPTIONS } from '@/config/constant'; @@ -186,7 +186,16 @@ const LocationsTable = () => { const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); - await LocationApi.delete(selectedLocation?.id as number); + const deleteResponse = await LocationApi.delete( + selectedLocation?.id as number + ); + + if (isResponseError(deleteResponse)) { + toast.error(deleteResponse.message); + setIsDeleteLoading(false); + return; + } + refreshLocations(); deleteModal.closeModal(); diff --git a/src/components/pages/master-data/nonstock/NonstocksTable.tsx b/src/components/pages/master-data/nonstock/NonstocksTable.tsx index 7066c19a..6aeb3f99 100644 --- a/src/components/pages/master-data/nonstock/NonstocksTable.tsx +++ b/src/components/pages/master-data/nonstock/NonstocksTable.tsx @@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission'; import { Nonstock } from '@/types/api/master-data/nonstock'; import { NonstockApi } from '@/services/api/master-data'; import { cn } from '@/lib/helper'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ROWS_OPTIONS } from '@/config/constant'; @@ -198,7 +198,16 @@ const NonstocksTable = () => { const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); - await NonstockApi.delete(selectedNonstock?.id as number); + const deleteResponse = await NonstockApi.delete( + selectedNonstock?.id as number + ); + + if (isResponseError(deleteResponse)) { + toast.error(deleteResponse.message); + setIsDeleteLoading(false); + return; + } + refreshNonstocks(); deleteModal.closeModal(); diff --git a/src/components/pages/master-data/product-category/ProductCategoryTable.tsx b/src/components/pages/master-data/product-category/ProductCategoryTable.tsx index a9b98bcb..e25dfd56 100644 --- a/src/components/pages/master-data/product-category/ProductCategoryTable.tsx +++ b/src/components/pages/master-data/product-category/ProductCategoryTable.tsx @@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission'; import { ProductCategory } from '@/types/api/master-data/product-category'; import { ProductCategoryApi } from '@/services/api/master-data'; import { cn } from '@/lib/helper'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ROWS_OPTIONS } from '@/config/constant'; @@ -170,7 +170,16 @@ const ProductCategoryTable = () => { const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); - await ProductCategoryApi.delete(selectedProductCategory?.id as number); + const deleteResponse = await ProductCategoryApi.delete( + selectedProductCategory?.id as number + ); + + if (isResponseError(deleteResponse)) { + toast.error(deleteResponse.message); + setIsDeleteLoading(false); + return; + } + refreshProductCategories(); deleteModal.closeModal(); diff --git a/src/components/pages/master-data/product/ProductTable.tsx b/src/components/pages/master-data/product/ProductTable.tsx index 957d0551..74137a14 100644 --- a/src/components/pages/master-data/product/ProductTable.tsx +++ b/src/components/pages/master-data/product/ProductTable.tsx @@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission'; import { Product } from '@/types/api/master-data/product'; import { ProductApi } from '@/services/api/master-data'; import { cn } from '@/lib/helper'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ROWS_OPTIONS } from '@/config/constant'; @@ -230,8 +230,19 @@ const ProductsTable = () => { const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); - await ProductApi.delete(selectedProduct?.id as number); + + const deleteResponse = await ProductApi.delete( + selectedProduct?.id as number + ); + + if (isResponseError(deleteResponse)) { + toast.error(deleteResponse.message); + setIsDeleteLoading(false); + return; + } + refreshProducts(); + deleteModal.closeModal(); toast.success('Successfully delete Product!'); setIsDeleteLoading(false); diff --git a/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx b/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx index afa41295..b56e31bd 100644 --- a/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx +++ b/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx @@ -7,7 +7,7 @@ import { ProductionStandard } from '@/types/api/master-data/production-standard' import { Icon } from '@iconify/react'; import useSWR from 'swr'; import { ProductionStandardApi } from '@/services/api/master-data'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import { CellContext } from '@tanstack/react-table'; import { useModal } from '@/components/Modal'; @@ -94,9 +94,16 @@ const ProductionStandardTable = () => { const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); - await ProductionStandardApi.delete( + const deleteResponse = await ProductionStandardApi.delete( selectedProductionStandard?.id as number ); + + if (isResponseError(deleteResponse)) { + toast.error(deleteResponse.message); + setIsDeleteLoading(false); + return; + } + refreshProductionStandards(); deleteModal.closeModal(); diff --git a/src/components/pages/master-data/supplier/SupplierTable.tsx b/src/components/pages/master-data/supplier/SupplierTable.tsx index 3e10c9c8..2620c9e6 100644 --- a/src/components/pages/master-data/supplier/SupplierTable.tsx +++ b/src/components/pages/master-data/supplier/SupplierTable.tsx @@ -11,7 +11,7 @@ import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import RequirePermission from '@/components/helper/RequirePermission'; import { ROWS_OPTIONS } from '@/config/constant'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { cn } from '@/lib/helper'; import { SupplierApi } from '@/services/api/master-data'; import { useTableFilter } from '@/services/hooks/useTableFilter'; @@ -205,7 +205,16 @@ const SuppliersTable = () => { const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); - await SupplierApi.delete(selectedSupplier?.id as number); + const deleteResponse = await SupplierApi.delete( + selectedSupplier?.id as number + ); + + if (isResponseError(deleteResponse)) { + toast.error(deleteResponse.message); + setIsDeleteLoading(false); + return; + } + refreshSuppliers(); deleteModal.closeModal(); diff --git a/src/components/pages/master-data/uom/UomsTable.tsx b/src/components/pages/master-data/uom/UomsTable.tsx index 851647b9..51e95661 100644 --- a/src/components/pages/master-data/uom/UomsTable.tsx +++ b/src/components/pages/master-data/uom/UomsTable.tsx @@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission'; import { Uom } from '@/types/api/master-data/uom'; import { UomApi } from '@/services/api/master-data'; import { cn } from '@/lib/helper'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ROWS_OPTIONS } from '@/config/constant'; @@ -164,7 +164,14 @@ const UomsTable = () => { const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); - await UomApi.delete(selectedUom?.id as number); + const deleteResponse = await UomApi.delete(selectedUom?.id as number); + + if (isResponseError(deleteResponse)) { + toast.error(deleteResponse.message); + setIsDeleteLoading(false); + return; + } + refreshUoms(); deleteModal.closeModal(); diff --git a/src/components/pages/master-data/warehouse/WarehousesTable.tsx b/src/components/pages/master-data/warehouse/WarehousesTable.tsx index fe694322..62c39574 100644 --- a/src/components/pages/master-data/warehouse/WarehousesTable.tsx +++ b/src/components/pages/master-data/warehouse/WarehousesTable.tsx @@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission'; import { Warehouse } from '@/types/api/master-data/warehouse'; import { WarehouseApi } from '@/services/api/master-data'; import { cn } from '@/lib/helper'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ROWS_OPTIONS } from '@/config/constant'; @@ -220,7 +220,16 @@ const WarehousesTable = () => { const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); - await WarehouseApi.delete(selectedWarehouse?.id as number); + const deleteResponse = await WarehouseApi.delete( + selectedWarehouse?.id as number + ); + + if (isResponseError(deleteResponse)) { + toast.error(deleteResponse.message); + setIsDeleteLoading(false); + return; + } + refreshWarehouses(); deleteModal.closeModal(); From a1bbe4e2d70485ecf100a7b3d7967e748a01f74e Mon Sep 17 00:00:00 2001 From: randy-ar Date: Thu, 15 Jan 2026 17:03:25 +0700 Subject: [PATCH 065/139] fix(FE): implement lazy loading select button on marketing index --- src/components/pages/marketing/MarketingTable.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index 1c37dbbb..467c2e00 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -184,12 +184,16 @@ const MarketingTable = () => { const { options: productsOptions, isLoadingOptions: isLoadingProductsOptions, + setInputValue: setProductsInputValue, + loadMore: loadMoreProducts, } = useSelect(ProductApi.basePath, 'id', 'name', '', { limit: 'limit', }); const { options: customersOptions, isLoadingOptions: isLoadingCustomersOptions, + setInputValue: setCustomersInputValue, + loadMore: loadMoreCustomers, } = useSelect(CustomerApi.basePath, 'id', 'name', '', { limit: 'limit', }); @@ -400,6 +404,8 @@ const MarketingTable = () => { .join(',') || '' ) } + onInputChange={setProductsInputValue} + onMenuScrollToBottom={loadMoreProducts} isMulti /> {/* select status */} @@ -444,6 +450,8 @@ const MarketingTable = () => { (value as OptionType)?.value.toString() || '' ) } + onInputChange={setCustomersInputValue} + onMenuScrollToBottom={loadMoreCustomers} />
From 8fe51c976bd4e4372271dcc7a5fed9ae67b7d027 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 17:29:09 +0700 Subject: [PATCH 066/139] refactor(FE): Show additional flock info in recording form --- .../recording/form/RecordingForm.tsx | 48 +++++++++++++++++++ src/types/api/production/project-flock.d.ts | 2 + 2 files changed, 50 insertions(+) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 5312c0be..b06fb172 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -1658,6 +1658,54 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { />
+ + {/* Additional Information Fields */} + {(projectFlockKandangLookup || projectFlockKandangDetail) && ( +
+
+ Gudang +

+ {projectFlockKandangLookup?.warehouse?.name || + initialValues?.warehouse?.name || + '-'} +

+
+
+ Umur +

+ {nextDayRecording + ? `Hari ke-${nextDayRecording.next_day} (Minggu ke-${Math.ceil(nextDayRecording.next_day / 7)})` + : initialValues?.day + ? `Hari ke-${initialValues.day} (Minggu ke-${Math.ceil(initialValues.day / 7)})` + : '-'} +

+
+
+ Standard FCR +

+ {projectFlockKandangLookup?.project_flock?.fcr?.name || + initialValues?.project_flock?.fcr?.name || + '-'} +

+
+
+ + Standard Produksi + +

+ {projectFlockKandangLookup?.project_flock + ?.production_standard_id + ? `ID: ${projectFlockKandangLookup.project_flock.production_standard_id}` + : initialValues?.project_flock?.production_standart + ?.name || '-'} +

+
+
+ )} )} diff --git a/src/types/api/production/project-flock.d.ts b/src/types/api/production/project-flock.d.ts index 66cc39ed..dcc1a348 100644 --- a/src/types/api/production/project-flock.d.ts +++ b/src/types/api/production/project-flock.d.ts @@ -6,6 +6,7 @@ import { Location } from '@/types/api/master-data/location'; import { BaseApproval, BaseMetadata } from '@/types/api/api-general'; import { Nonstock } from '@/types/api/master-data/nonstock'; import { ProductionStandard } from '@/types/api/master-data/production-standard'; +import { Warehouse } from '@/types/api/master-data/warehouse'; export type BaseProjectFlock = { id: number; @@ -71,6 +72,7 @@ export type ProjectFlockKandangLookup = { kandang_id: number; kandang: Kandang; project_flock: ProjectFlock; + warehouse: Warehouse; quantity: number; available_quantity?: number; population: number; From 7d3a4c1ecc32585ec0134f0a9610aa5f58fb6fbf Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 17:47:03 +0700 Subject: [PATCH 067/139] refactor(FE): Use memoized IDs and badges for standards --- .../recording/form/RecordingForm.tsx | 120 +++++++++++------- 1 file changed, 75 insertions(+), 45 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index b06fb172..b45f72d0 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -284,39 +284,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { return () => observer.disconnect(); }, [productionStandardModal.ref]); - const { data: fcr, isLoading: isLoadingFcrStandards } = useSWR( - isFcrModalOpen && initialValues?.project_flock?.fcr?.id - ? `fcr-detail-${initialValues.project_flock.fcr.id}` - : null, - () => FcrApi.getSingle(initialValues!.project_flock!.fcr!.id!) - ); - - useEffect(() => { - if (fcr?.status === 'success') { - setFcrStandards((fcr.data as FcrWithStandards).fcr_standards || []); - } - }, [fcr]); - - const { data: productionStandard, isLoading: isLoadingProductionStandards } = - useSWR( - isProductionStandardModalOpen && - initialValues?.project_flock?.production_standart?.id - ? `production-standard-detail-${initialValues.project_flock.production_standart.id}` - : null, - () => - ProductionStandardApi.getSingle( - initialValues!.project_flock!.production_standart!.id! - ) - ); - - useEffect(() => { - if (productionStandard?.status === 'success') { - setProductionStandards( - productionStandard.data as ProductionStandard | null - ); - } - }, [productionStandard]); - const isRecordingApproved = useCallback((recording?: Recording) => { return ( recording?.approval?.action === 'APPROVED' && @@ -465,6 +432,47 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ? projectFlockKandangLookupData.data : undefined; + const fcrId = useMemo(() => { + if (type === 'add') { + return projectFlockKandangLookup?.project_flock?.fcr?.id; + } + return initialValues?.project_flock?.fcr?.id; + }, [type, projectFlockKandangLookup, initialValues]); + + const { data: fcr, isLoading: isLoadingFcrStandards } = useSWR( + isFcrModalOpen && fcrId ? `fcr-detail-${fcrId}` : null, + () => FcrApi.getSingle(fcrId!) + ); + + useEffect(() => { + if (fcr?.status === 'success') { + setFcrStandards((fcr.data as FcrWithStandards).fcr_standards || []); + } + }, [fcr]); + + const productionStandardId = useMemo(() => { + if (type === 'add') { + return projectFlockKandangLookup?.project_flock?.production_standard_id; + } + return initialValues?.project_flock?.production_standart?.id; + }, [type, projectFlockKandangLookup, initialValues]); + + const { data: productionStandard, isLoading: isLoadingProductionStandards } = + useSWR( + isProductionStandardModalOpen && productionStandardId + ? `production-standard-detail-${productionStandardId}` + : null, + () => ProductionStandardApi.getSingle(productionStandardId!) + ); + + useEffect(() => { + if (productionStandard?.status === 'success') { + setProductionStandards( + productionStandard.data as ProductionStandard | null + ); + } + }, [productionStandard]); + const projectFlockKandangDetailUrl = useMemo(() => { if ( type === 'add' || @@ -1686,23 +1694,45 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
Standard FCR -

- {projectFlockKandangLookup?.project_flock?.fcr?.name || - initialValues?.project_flock?.fcr?.name || - '-'} -

+
+ fcrStandardModal.openModal()} + > + {projectFlockKandangLookup?.project_flock?.fcr?.name || + initialValues?.project_flock?.fcr?.name || + '-'} + +
Standard Produksi -

- {projectFlockKandangLookup?.project_flock - ?.production_standard_id - ? `ID: ${projectFlockKandangLookup.project_flock.production_standard_id}` - : initialValues?.project_flock?.production_standart - ?.name || '-'} -

+
+ productionStandardModal.openModal()} + > + {productionStandards?.name || + initialValues?.project_flock?.production_standart + ?.name || + (projectFlockKandangLookup?.project_flock + ?.production_standard_id + ? `Production Standard ${projectFlockKandangLookup.project_flock.production_standard_id}` + : '-')} + +
)} From 0a24c4541f7b5a003debe66437976567bc5bddfd Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 17:53:18 +0700 Subject: [PATCH 068/139] refactor(FE): Make feed and doc supplier arrays nullable --- src/types/api/report/hpp-per-kandang.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/api/report/hpp-per-kandang.d.ts b/src/types/api/report/hpp-per-kandang.d.ts index 1b3f7fcd..208a2cdb 100644 --- a/src/types/api/report/hpp-per-kandang.d.ts +++ b/src/types/api/report/hpp-per-kandang.d.ts @@ -14,8 +14,8 @@ export type HppPerKandangRow = { egg_production_kg: number; egg_hpp_rp_per_kg: number; egg_value_rp: number; - feed_suppliers: Supplier[]; - doc_suppliers: Supplier[]; + feed_suppliers: Supplier[] | null; + doc_suppliers: Supplier[] | null; average_doc_price_rp: number; }; From 87bf474cf60db156c2539bb35df3e52e72af5f66 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 18:01:15 +0700 Subject: [PATCH 069/139] refactor(FE): Add customer address as card subtitle --- src/components/pages/report/finance/tab/CustomerPaymentTab.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 3254cf03..ef748b5f 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -743,11 +743,14 @@ const CustomerPaymentTab = () => { Date: Thu, 15 Jan 2026 19:29:03 +0700 Subject: [PATCH 070/139] refactor(FE): Make vehicle/expedition/transport fields optional --- .../order/PurchaseOrderAcceptApprovalForm.tsx | 30 ++++-------- .../form/order/PurchaseOrderForm.schema.ts | 46 +++++++++++-------- src/types/api/purchase/purchase.d.ts | 8 ++-- 3 files changed, 40 insertions(+), 44 deletions(-) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index 2f619778..8585a091 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -185,8 +185,8 @@ const PurchaseOrderAcceptApprovalForm = ({ purchase_item_id: formItem.purchase_item_id || 0, received_date: formItem.received_date || '', travel_number: formItem.travel_number || '', - vehicle_number: formItem.vehicle_number || '', - expedition_vendor_id: formItem.expedition_vendor_id || 0, + vehicle_number: formItem.vehicle_number || null, + expedition_vendor_id: formItem.expedition_vendor_id || null, received_qty: typeof formItem.received_qty === 'string' ? parseFloat(formItem.received_qty) || 0 @@ -194,10 +194,13 @@ const PurchaseOrderAcceptApprovalForm = ({ transport_per_item: typeof formItem.transport_per_item === 'string' ? parseFloat(formItem.transport_per_item) || 0 - : formItem.transport_per_item || 0, + : formItem.transport_per_item || null, }; }) || [], - travel_documents: values.travel_documents || [], + travel_documents: + values.travel_documents + ?.filter((file): file is File => file instanceof File) + .filter(Boolean) || undefined, }; switch (type) { @@ -405,22 +408,13 @@ const PurchaseOrderAcceptApprovalForm = ({ Dokumen Surat Jalan * - - Nomor Kendaraan - * - - - Vendor Ekspedisi - * - + Nomor Kendaraan + Vendor Ekspedisi Jumlah Diterima * - - Transport/Item - * - + Transport/Item @@ -538,7 +532,6 @@ const PurchaseOrderAcceptApprovalForm = ({ @@ -684,7 +675,6 @@ const PurchaseOrderAcceptApprovalForm = ({
0); } - ) - .typeError('Vendor ekspedisi harus dipilih!'), + ), received_qty: Yup.mixed() .required('Jumlah diterima wajib diisi!') .test( @@ -217,13 +221,14 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema() - .required('Biaya transport per item wajib diisi!') + .nullable() + .optional() .test( 'is-valid-transport-per-item', 'Biaya transport per item harus berupa angka lebih dari atau sama dengan 0!', function (value) { if (value === '' || value === null || value === undefined) - return false; + return true; const numValue = typeof value === 'string' ? parseFloat(value) : value; return !isNaN(numValue) && numValue >= 0; @@ -389,16 +394,17 @@ export const PurchaseRequestAcceptApprovalFormSchema: Yup.ObjectSchema() - .required('Dokumen surat jalan wajib diupload!') + .nullable() + .optional() .test('fileSize', 'Ukuran dokumen maksimal 5 MB', (value) => { if (!value) return true; if (value instanceof File) return value.size <= 5 * 1024 * 1024; return true; }) ) - .required('Dokumen surat jalan wajib diupload!') - .min(1, 'Minimal upload 1 dokumen surat jalan!') - .typeError('Dokumen surat jalan wajib diupload!'), + .nullable() + .optional() + .typeError('Dokumen surat jalan harus berupa array!'), }); export const PurchaseRequestAcceptApprovalFormInitialValues: PurchaseRequestAcceptApprovalFormSchemaType = diff --git a/src/types/api/purchase/purchase.d.ts b/src/types/api/purchase/purchase.d.ts index 34798ac3..9ad59f8b 100644 --- a/src/types/api/purchase/purchase.d.ts +++ b/src/types/api/purchase/purchase.d.ts @@ -120,12 +120,12 @@ export type CreateAcceptApprovalRequestPayload = { purchase_item_id: number; received_date: string; travel_number: string; - vehicle_number: string; - expedition_vendor_id: number; + vehicle_number?: string | null; + expedition_vendor_id?: number | null; received_qty: number; - transport_per_item: number; + transport_per_item?: number | null; }[]; - travel_documents?: File[]; + travel_documents?: File[] | null; }; export type DeletePurchaseRequestItemPayload = { From 45700be730333f3c889d8d97e38652ba058922e8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 15 Jan 2026 19:32:09 +0700 Subject: [PATCH 071/139] refactor(FE): Improve vehicle number validation message and set expedition flag on supplier --- .../purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx | 1 + .../pages/purchase/form/order/PurchaseOrderForm.schema.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index 8585a091..de27a169 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -160,6 +160,7 @@ const PurchaseOrderAcceptApprovalForm = ({ hasMore: hasMoreExpeditions, } = useSelect(SupplierApi.basePath, 'id', 'name', 'search', { category: 'BOP', + flag: 'EKSPEDISI', }); // ===== FORM CONFIGURATION ===== diff --git a/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts b/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts index 1ee2e054..5df7c8e6 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts +++ b/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts @@ -186,7 +186,7 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema Date: Thu, 15 Jan 2026 19:37:55 +0700 Subject: [PATCH 072/139] refactor(FE): Relax expedition vendor validation --- .../purchase/form/order/PurchaseOrderForm.schema.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts b/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts index 5df7c8e6..6a299703 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts +++ b/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts @@ -196,16 +196,7 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema 0); - } - ), + .typeError('Vendor ekspedisi harus berupa angka!'), received_qty: Yup.mixed() .required('Jumlah diterima wajib diisi!') .test( From 8c6a87c011c2747af7818eee1f34ce9393e14b2a Mon Sep 17 00:00:00 2001 From: randy-ar Date: Thu, 15 Jan 2026 20:08:21 +0700 Subject: [PATCH 073/139] feat(FE): adding closing finance per kandang --- .../pages/closing/ClosingFinanceTable.tsx | 419 +++++------------- src/types/api/closing.d.ts | 79 ++-- 2 files changed, 139 insertions(+), 359 deletions(-) diff --git a/src/components/pages/closing/ClosingFinanceTable.tsx b/src/components/pages/closing/ClosingFinanceTable.tsx index 010bfc2f..0f574c75 100644 --- a/src/components/pages/closing/ClosingFinanceTable.tsx +++ b/src/components/pages/closing/ClosingFinanceTable.tsx @@ -3,54 +3,11 @@ import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table'; import { isResponseSuccess } from '@/lib/api-helper'; import { formatCurrency, formatTitleCase } from '@/lib/helper'; import { ClosingApi } from '@/services/api/closing'; -import { - DataSummarySubTotal, - HppPurchaseData, - ProfitLossDataAmount, -} from '@/types/api/closing'; +import { HppItem, ProfitLossItem } from '@/types/api/closing'; import { useSearchParams } from 'next/navigation'; +import { useMemo } from 'react'; import useSWR from 'swr'; -type HppTableRow = - | (HppPurchaseData & { - group_name: string; - group_index: number; - isGroupHeader?: boolean; - }) - | { - group_name: string; - group_index: number; - isGroupHeader: true; - type?: never; - budgeting?: never; - realization?: never; - } - | { - type: string; - group_name: string; - group_index: number; - isGroupHeader: false; - budgeting?: { rp_per_bird: number; rp_per_kg: number; amount: number }; - realization?: { rp_per_bird: number; rp_per_kg: number; amount: number }; - }; - -type ProfitLossTableRow = - | (DataSummarySubTotal & { - type: string; - group_name: string; - group_index: number; - isGroupHeader?: boolean; - }) - | { - group_name: string; - group_index: number; - isGroupHeader: true; - type?: never; - rp_per_bird?: never; - rp_per_kg?: never; - amount?: never; - }; - const ClosingFinanceTable = ({ projectFlockId, }: { @@ -68,167 +25,60 @@ const ClosingFinanceTable = ({ ) ); - const staticHppRows: Array<{ - group_name: string; - type: string; - group_index: number; - }> = [ - { - group_name: 'HPP dan Pengeluaran', - type: 'Pembelian PAKAN', - group_index: 0, - }, - { - group_name: 'HPP dan Pengeluaran', - type: 'Pembelian STARTER', - group_index: 0, - }, - { - group_name: 'HPP dan Pengeluaran', - type: 'Pembelian DOC', - group_index: 0, - }, - { - group_name: 'HPP dan Pengeluaran', - type: 'Pembelian PULLET', - group_index: 0, - }, - { - group_name: 'HPP dan Pengeluaran', - type: 'Pembelian LAYER', - group_index: 0, - }, - { - group_name: 'HPP dan Bahan Baku', - type: 'Pengeluaran Overhead', - group_index: 1, - }, - { - group_name: 'HPP dan Bahan Baku', - type: 'Beban Ekspedisi', - group_index: 1, - }, - ]; + const hppTableData: HppItem[] = useMemo(() => { + if (isResponseSuccess(finance)) { + const customItems = { + label: 'HPP dan Pengeluaran', + code: 'custom_row', + } as HppItem; + const purchases = finance.data.hpp.items.filter( + (item) => item.category === 'purchase' + ); + const totalBudgeting = { + label: 'HPP dan Bahan Baku', + code: 'custom_row', + } as HppItem; + const overheads = finance.data.hpp.items.filter( + (item) => item.category === 'overhead' + ); + return [customItems, ...purchases, totalBudgeting, ...overheads]; + } + return []; + }, []); - const hppTableData: HppTableRow[] = [ - { - group_name: 'HPP dan Pengeluaran', - group_index: 0, - isGroupHeader: true as const, - }, - ...staticHppRows - .filter((row) => row.group_index === 0) - .map((staticRow) => { - const apiData = isResponseSuccess(finance) - ? finance.data.hpp_purchases.hpp - .find((g) => g.group_name === staticRow.group_name) - ?.data.find((d) => d.type === staticRow.type) - : null; - - return { - group_name: staticRow.group_name, - group_index: staticRow.group_index, - type: staticRow.type, - budgeting: apiData?.budgeting || { - rp_per_bird: 0, - rp_per_kg: 0, - amount: 0, - }, - realization: apiData?.realization || { - rp_per_bird: 0, - rp_per_kg: 0, - amount: 0, - }, - isGroupHeader: false as const, - }; - }), - { - group_name: 'HPP dan Bahan Baku', - group_index: 1, - isGroupHeader: true as const, - }, - ...staticHppRows - .filter((row) => row.group_index === 1) - .map((staticRow) => { - const apiData = isResponseSuccess(finance) - ? finance.data.hpp_purchases.hpp - .find((g) => g.group_name === staticRow.group_name) - ?.data.find((d) => d.type === staticRow.type) - : null; - - return { - group_name: staticRow.group_name, - group_index: staticRow.group_index, - type: staticRow.type, - budgeting: apiData?.budgeting || { - rp_per_bird: 0, - rp_per_kg: 0, - amount: 0, - }, - realization: apiData?.realization || { - rp_per_bird: 0, - rp_per_kg: 0, - amount: 0, - }, - isGroupHeader: false as const, - }; - }), - { - group_name: 'HPP', - group_index: 2, - isGroupHeader: true as const, - }, - ]; - - const profitLossTableData: ProfitLossTableRow[] = isResponseSuccess(finance) - ? [ - // Pembelian group - ...finance.data.profit_loss.data.pembelian.map((item) => ({ - label: 'Pembelian', - group_name: 'Pembelian', - group_index: 1, - type: item.type, - rp_per_bird: item.rp_per_bird, - rp_per_kg: item.rp_per_kg, - amount: item.amount, - isGroupHeader: false as const, - })), - { - label: finance.data.profit_loss.data.summary.gross_profit.label, - group_name: 'Penjualan', - group_index: 0, - isGroupHeader: true as const, - type: finance.data.profit_loss.data.summary.gross_profit.label, - rp_per_bird: - finance.data.profit_loss.data.summary.gross_profit.rp_per_bird, - rp_per_kg: - finance.data.profit_loss.data.summary.gross_profit.rp_per_kg, - amount: finance.data.profit_loss.data.summary.gross_profit.amount, - }, - // Penjualan group - ...finance.data.profit_loss.data.penjualan.map((item) => ({ - label: 'Penjualan', - group_name: 'Penjualan', - group_index: 0, - type: item.type, - rp_per_bird: item.rp_per_bird, - rp_per_kg: item.rp_per_kg, - amount: item.amount, - isGroupHeader: false as const, - })), - { - label: finance.data.profit_loss.data.summary.sub_total.label, - group_name: 'Pembelian', - group_index: 1, - isGroupHeader: true as const, - type: finance.data.profit_loss.data.summary.sub_total.label, - rp_per_bird: - finance.data.profit_loss.data.summary.sub_total.rp_per_bird, - rp_per_kg: finance.data.profit_loss.data.summary.sub_total.rp_per_kg, - amount: finance.data.profit_loss.data.summary.sub_total.amount, - }, - ] - : []; + const profitLossTableData: ProfitLossItem[] = useMemo(() => { + if (isResponseSuccess(finance)) { + const incomes = finance.data.profit_loss.items.filter( + (item) => item.type === 'income' + ); + const purchases = finance.data.profit_loss.items.filter( + (item) => item.type === 'purchase' + ); + const overheads = finance.data.profit_loss.items.filter( + (item) => item.type === 'overhead' + ); + const grossProfit = { + label: 'LABA RUGI BRUTO', + code: 'custom_row', + type: 'gross_profit', + rp_per_bird: + finance.data.profit_loss.summary.gross_profit.rp_per_bird ?? 0, + rp_per_kg: finance.data.profit_loss.summary.gross_profit.rp_per_kg ?? 0, + amount: finance.data.profit_loss.summary.gross_profit.amount ?? 0, + } as ProfitLossItem; + const subtotal = { + label: 'Subtotal', + code: 'custom_row', + type: 'subtotal', + rp_per_bird: + finance.data.profit_loss.summary.sub_total.rp_per_bird ?? 0, + rp_per_kg: finance.data.profit_loss.summary.sub_total.rp_per_kg ?? 0, + amount: finance.data.profit_loss.summary.sub_total.amount ?? 0, + } as ProfitLossItem; + return [...incomes, ...purchases, grossProfit, ...overheads, subtotal]; + } + return []; + }, []); return (
@@ -241,47 +91,30 @@ const ClosingFinanceTable = ({ >
-
- {isResponseSuccess(finance) - ? formatTitleCase( - finance.data.profit_loss.data.summary.gross_profit - .label || '-' - ) - : 'Laba Rugi Brutto'} -
+
Laba Rugi Brutto
{isResponseSuccess(finance) ? formatCurrency( - finance.data.profit_loss.data.summary.gross_profit.amount + finance.data.profit_loss.summary.gross_profit.amount ) : '-'}
-
- {isResponseSuccess(finance) - ? formatTitleCase( - finance.data.profit_loss.data.summary.net_profit.label || - '-' - ) - : 'Laba Rugi Netto'} -
+
Laba Rugi Netto
{isResponseSuccess(finance) ? formatCurrency( - finance.data.profit_loss.data.summary.net_profit.amount + finance.data.profit_loss.summary.net_profit.amount ) : '-'}
+ {JSON.stringify(finance)}
- + data={hppTableData} isLoading={isLoading} columns={[ @@ -297,10 +130,10 @@ const ClosingFinanceTable = ({ header: 'No.', enableSorting: false, accessorFn: (item, index) => { - if (item.isGroupHeader) return '-'; + if (item.code === 'custom_row') return '-'; const dataRowsBefore = hppTableData .slice(0, index) - .filter((row) => !row.isGroupHeader).length; + .filter((row) => row.code !== 'custom_row').length; return dataRowsBefore + 1; }, footer: (props) => { @@ -310,7 +143,7 @@ const ClosingFinanceTable = ({ { header: 'Jenis', enableSorting: false, - accessorFn: (item) => formatTitleCase(item.type || '-'), + accessorFn: (item) => formatTitleCase(item.label || '-'), }, { header: 'Budgeting', @@ -326,7 +159,7 @@ const ClosingFinanceTable = ({ return props.column.id === 'budgeting_rp_per_bird' && isResponseSuccess(finance) ? formatCurrency( - finance.data.hpp_purchases.summary_hpp?.budgeting + finance.data.hpp.summary?.budgeting ?.rp_per_bird || 0 ) : '-'; @@ -342,8 +175,8 @@ const ClosingFinanceTable = ({ return props.column.id === 'budgeting_rp_per_kg' && isResponseSuccess(finance) ? formatCurrency( - finance.data.hpp_purchases.summary_hpp?.budgeting - ?.rp_per_kg || 0 + finance.data.hpp.summary?.budgeting?.rp_per_kg || + 0 ) : '-'; }, @@ -358,8 +191,7 @@ const ClosingFinanceTable = ({ return props.column.id === 'budgeting_amount' && isResponseSuccess(finance) ? formatCurrency( - finance.data.hpp_purchases.summary_hpp?.budgeting - ?.amount || 0 + finance.data.hpp.summary?.budgeting?.amount || 0 ) : '-'; }, @@ -380,8 +212,8 @@ const ClosingFinanceTable = ({ return props.column.id === 'realization_rp_per_bird' && isResponseSuccess(finance) ? formatCurrency( - finance.data.hpp_purchases.summary_hpp - ?.realization?.rp_per_bird || 0 + finance.data.hpp.summary?.realization + ?.rp_per_bird || 0 ) : '-'; }, @@ -396,8 +228,8 @@ const ClosingFinanceTable = ({ return props.column.id === 'realization_rp_per_kg' && isResponseSuccess(finance) ? formatCurrency( - finance.data.hpp_purchases.summary_hpp - ?.realization?.rp_per_kg || 0 + finance.data.hpp.summary?.realization + ?.rp_per_kg || 0 ) : '-'; }, @@ -412,8 +244,7 @@ const ClosingFinanceTable = ({ return props.column.id === 'realization_amount' && isResponseSuccess(finance) ? formatCurrency( - finance.data.hpp_purchases.summary_hpp - ?.realization?.amount || 0 + finance.data.hpp.summary?.realization?.amount || 0 ) : '-'; }, @@ -423,7 +254,7 @@ const ClosingFinanceTable = ({ ]} renderCustomRow={(row) => { const rowData = row.original; - if (rowData.isGroupHeader) { + if (rowData.code === 'custom_row') { return (
- {formatTitleCase(rowData.group_name ?? '-')} + {formatTitleCase(rowData.label ?? '-')}
@@ -450,11 +281,7 @@ const ClosingFinanceTable = ({
- + data={profitLossTableData} isLoading={isLoading} columns={[ { header: 'Jenis', enableSorting: false, - accessorFn: (item) => item.type, + accessorFn: (item) => item.label, cell: (item) => (
- {formatTitleCase(item.row.original.type || '-')} + {formatTitleCase(item.row.original.label || '-')}
), - footer: (item) => ( -
- {isResponseSuccess(finance) - ? formatTitleCase( - finance.data.profit_loss.data.summary.net_profit - .label || '-' - ) - : '-'} -
+ footer: () => ( +
LABA RUGI NETTO
), }, { header: 'Rp/Ekor', enableSorting: false, accessorFn: (item) => formatCurrency(item.rp_per_bird || 0), - footer: (item) => ( + footer: () => (
{isResponseSuccess(finance) ? formatCurrency( - finance.data.profit_loss.data.summary.net_profit + finance.data.profit_loss.summary.net_profit .rp_per_bird || 0 ) : formatCurrency(0)} @@ -505,11 +325,11 @@ const ClosingFinanceTable = ({ header: 'Rp/Kg', enableSorting: false, accessorFn: (item) => formatCurrency(item.rp_per_kg || 0), - footer: (item) => ( + footer: () => (
{isResponseSuccess(finance) ? formatCurrency( - finance.data.profit_loss.data.summary.net_profit + finance.data.profit_loss.summary.net_profit .rp_per_kg || 0 ) : formatCurrency(0)} @@ -520,11 +340,11 @@ const ClosingFinanceTable = ({ header: 'Jumlah (Rp)', enableSorting: false, accessorFn: (item) => formatCurrency(item.amount || 0), - footer: (item) => ( + footer: () => (
{isResponseSuccess(finance) ? formatCurrency( - finance.data.profit_loss.data.summary.net_profit + finance.data.profit_loss.summary.net_profit .amount || 0 ) : formatCurrency(0)} @@ -534,55 +354,30 @@ const ClosingFinanceTable = ({ ]} renderCustomRow={(row) => { const rowData = row.original; - if (rowData.isGroupHeader) { - if (rowData.amount) { - return ( - - -
- {formatTitleCase(rowData.label ?? '-')} -
- - -
- {formatCurrency(rowData.rp_per_bird ?? 0)} -
- - -
- {formatCurrency(rowData.rp_per_kg ?? 0)} -
- - -
- {formatCurrency(rowData.amount ?? 0)} -
- - - ); - } + if (rowData.code === 'custom_row') { return ( - + +
+ {formatTitleCase(rowData.label ?? '-')} +
+ +
- {formatTitleCase(rowData.group_name ?? '-')} + {formatCurrency(rowData.rp_per_bird ?? 0)} +
+ + +
+ {formatCurrency(rowData.rp_per_kg ?? 0)} +
+ + +
+ {formatCurrency(rowData.amount ?? 0)}
diff --git a/src/types/api/closing.d.ts b/src/types/api/closing.d.ts index 56406ada..ff35fd28 100644 --- a/src/types/api/closing.d.ts +++ b/src/types/api/closing.d.ts @@ -219,64 +219,30 @@ export type ClosingSales = BaseMetadata & BaseClosingSales; // ====== FINANCE ====== export interface ClosingFinance { - project_flock_id: number; - period: number; - project_type: string; - volume_base: ClosingFinanceVolumeBase; - hpp_purchases: ClosingFinanceHppPurchases; + hpp: ClosingFinanceHpp; profit_loss: ClosingFinanceProfitLoss; } -export interface ClosingFinanceProfitLoss { - title: string; - data: ProfitLossData; +export interface ClosingFinanceHpp { + items: HppItem[]; + summary: HppSummary; } -export interface ClosingFinanceHppPurchases { - title: string; - hpp: GroupHppPurchase[]; - summary_hpp: HppPurchasesSummary; -} - -export interface ClosingFinanceVolumeBase { - total_birds: number; - total_weight_kg: number; -} - -export interface ProfitLossData { - penjualan: ProfitLossDataAmount[]; - pembelian: ProfitLossDataAmount[]; - summary: ProfitLossDataSummary; -} - -export interface GroupHppPurchase { - group_name: string; - data: HppPurchaseData[]; -} - -export interface ProfitLossDataSummary { - gross_profit: DataSummarySubTotal; - sub_total: DataSummarySubTotal; - net_profit: DataSummarySubTotal; -} - -export interface ProfitLossDataAmount { - type: string; - rp_per_bird: number; - rp_per_kg: number; - amount: number; -} - -export interface HppPurchasesSummary { +export interface HppItem { + id: number; + category: string; + code: string; label: string; budgeting: HppPurchaseDataAmount; realization: HppPurchaseDataAmount; } -export interface HppPurchaseData { - type: string; +export interface HppSummary { + label: string; budgeting: HppPurchaseDataAmount; realization: HppPurchaseDataAmount; + egg_budgeting: HppPurchaseDataAmount; + egg_realization: HppPurchaseDataAmount; } export interface HppPurchaseDataAmount { @@ -285,8 +251,27 @@ export interface HppPurchaseDataAmount { amount: number; } -export interface DataSummarySubTotal { +export interface ClosingFinanceProfitLoss { + items: ProfitLossItem[]; + summary: ProfitLossSummary; +} + +export interface ProfitLossItem { + code: string; label: string; + type: string; + rp_per_bird: number; + rp_per_kg: number; + amount: number; +} + +export interface ProfitLossSummary { + gross_profit: ProfitLossAmount; + sub_total: ProfitLossAmount; + net_profit: ProfitLossAmount; +} + +export interface ProfitLossAmount { rp_per_bird: number; rp_per_kg: number; amount: number; From 01b8841e3cbbe94c1b5b4fdeb4fd41b8fb9ac278 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Thu, 15 Jan 2026 21:14:55 +0700 Subject: [PATCH 074/139] fix(FE): adjust ui closing overhead kandang --- .../pages/closing/ClosingOverheadTable.tsx | 241 +++++++++++------- 1 file changed, 150 insertions(+), 91 deletions(-) diff --git a/src/components/pages/closing/ClosingOverheadTable.tsx b/src/components/pages/closing/ClosingOverheadTable.tsx index ed74ca66..9ef6694f 100644 --- a/src/components/pages/closing/ClosingOverheadTable.tsx +++ b/src/components/pages/closing/ClosingOverheadTable.tsx @@ -32,101 +32,160 @@ const ClosingOverheadTable = ({ ); // Helper function to create columns with footer support - const createColumns = (total?: OverheadTotal): ColumnDef[] => [ - // Group untuk kolom tanpa footer - { - header: 'Nama Item', - accessorFn: (props) => props.item_name, - footer: 'Total Pengeluaran Overhead', - }, - { - header: 'Satuan', - accessorFn: (props) => props.uom_name, - }, - { - header: 'Budget Pengajuan', - footer: '', - columns: [ - { - id: 'budget_quantity', - header: 'Jumlah', - accessorFn: (props) => - props.budget_quantity ? formatNumber(props.budget_quantity) : '-', - footer: total ? () => formatNumber(total.budget_quantity) : '', - }, - { - id: 'budget_unit_price', - header: 'Harga Satuan', - accessorFn: (props) => - props.budget_unit_price - ? formatCurrency(props.budget_unit_price) - : '-', - footer: '', - }, - { - id: 'budget_total_amount', - header: 'Total', - accessorFn: (props) => - props.budget_total_amount - ? formatCurrency(props.budget_total_amount) - : '-', - footer: total ? () => formatCurrency(total.budget_total_amount) : '', - }, - ], - }, - { - header: 'Realisasi', - footer: '', - columns: [ - { - id: 'actual_date', - header: 'Tanggal', - accessorFn: (props) => - props.actual_date - ? formatDate(props.actual_date, 'DD MMM, YYYY') - : '-', - footer: '', - }, - { - id: 'actual_quantity', - header: 'Jumlah', - accessorFn: (props) => - props.actual_quantity ? formatNumber(props.actual_quantity) : '-', - footer: total ? () => formatNumber(total.actual_quantity) : '', - }, - { - id: 'actual_unit_price', - header: 'Harga Satuan', - accessorFn: (props) => - props.actual_unit_price - ? formatCurrency(props.actual_unit_price) - : '-', - footer: '', - }, - { - id: 'actual_total_amount', - header: 'Total', - accessorFn: (props) => - props.actual_total_amount - ? formatCurrency(props.actual_total_amount) - : '-', - footer: total ? () => formatCurrency(total.actual_total_amount) : '', - }, - ], - }, - { - id: 'cost_per_bird', - header: 'Rp/Ekor', - accessorFn: (props) => - props.cost_per_bird ? formatCurrency(props.cost_per_bird) : '-', - footer: total ? () => formatCurrency(total.cost_per_bird) : '', - }, - ]; + const createColumns = ( + total?: OverheadTotal, + kandangId?: number + ): ColumnDef[] => { + const flockColumn: ColumnDef[] = [ + { + header: 'Budget Pengajuan', + footer: '', + columns: [ + { + id: 'budget_quantity', + header: 'Jumlah', + accessorFn: (props) => + props.budget_quantity ? formatNumber(props.budget_quantity) : '-', + footer: total ? () => formatNumber(total.budget_quantity) : '', + }, + { + id: 'budget_unit_price', + header: 'Harga Satuan', + accessorFn: (props) => + props.budget_unit_price + ? formatCurrency(props.budget_unit_price) + : '-', + footer: '', + }, + { + id: 'budget_total_amount', + header: 'Total', + accessorFn: (props) => + props.budget_total_amount + ? formatCurrency(props.budget_total_amount) + : '-', + footer: total + ? () => formatCurrency(total.budget_total_amount) + : '', + }, + ], + }, + { + header: 'Realisasi', + footer: '', + columns: [ + { + id: 'actual_date', + header: 'Tanggal', + accessorFn: (props) => + props.actual_date + ? formatDate(props.actual_date, 'DD MMM, YYYY') + : '-', + footer: '', + }, + { + id: 'actual_quantity', + header: 'Jumlah', + accessorFn: (props) => + props.actual_quantity ? formatNumber(props.actual_quantity) : '-', + footer: total ? () => formatNumber(total.actual_quantity) : '', + }, + { + id: 'actual_unit_price', + header: 'Harga Satuan', + accessorFn: (props) => + props.actual_unit_price + ? formatCurrency(props.actual_unit_price) + : '-', + footer: '', + }, + { + id: 'actual_total_amount', + header: 'Total', + accessorFn: (props) => + props.actual_total_amount + ? formatCurrency(props.actual_total_amount) + : '-', + footer: total + ? () => formatCurrency(total.actual_total_amount) + : '', + }, + ], + }, + ]; + + const kandangColumn: ColumnDef[] = [ + { + id: 'actual_date', + header: 'Tanggal', + accessorFn: (props) => + props.actual_date + ? formatDate(props.actual_date, 'DD MMM, YYYY') + : '-', + footer: '', + }, + { + id: 'actual_quantity', + header: 'Jumlah', + accessorFn: (props) => + props.actual_quantity ? formatNumber(props.actual_quantity) : '-', + footer: total ? () => formatNumber(total.actual_quantity) : '', + }, + { + id: 'actual_unit_price', + header: 'Harga Satuan', + accessorFn: (props) => + props.actual_unit_price + ? formatCurrency(props.actual_unit_price) + : '-', + footer: '', + }, + { + id: 'actual_total_amount', + header: 'Total', + accessorFn: (props) => + props.actual_total_amount + ? formatCurrency(props.actual_total_amount) + : '-', + footer: total ? () => formatCurrency(total.actual_total_amount) : '', + }, + ]; + + const finalColumns: ColumnDef[] = [ + // Group untuk kolom tanpa footer + { + header: 'No', + accessorFn: (_, index) => index, + cell: (props) => props.row.index + 1, + }, + { + header: 'Nama Item', + accessorFn: (props) => props.item_name, + footer: 'Total Pengeluaran Overhead', + }, + { + header: 'Satuan', + accessorFn: (props) => props.uom_name, + }, + ...(kandangId ? kandangColumn : flockColumn), + { + id: 'cost_per_bird', + header: 'Rp/Ekor', + accessorFn: (props) => + props.cost_per_bird ? formatCurrency(props.cost_per_bird) : '-', + footer: total ? () => formatCurrency(total.cost_per_bird) : '', + }, + ]; + return finalColumns; + }; const columns = useMemo( () => isResponseSuccess(overhead) - ? createColumns(overhead.data?.total) + ? createColumns( + overhead.data?.total, + kandangId ? Number(kandangId) : undefined + ) : createColumns(), [overhead] ); From 9a3617edf1a22987d772f0a0b259150ba921608d Mon Sep 17 00:00:00 2001 From: randy-ar Date: Fri, 16 Jan 2026 21:04:23 +0700 Subject: [PATCH 075/139] fix(FE): hot fix closing finance per kandang --- src/components/pages/closing/ClosingFinanceTable.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/pages/closing/ClosingFinanceTable.tsx b/src/components/pages/closing/ClosingFinanceTable.tsx index 0f574c75..0d4aea5f 100644 --- a/src/components/pages/closing/ClosingFinanceTable.tsx +++ b/src/components/pages/closing/ClosingFinanceTable.tsx @@ -112,7 +112,6 @@ const ClosingFinanceTable = ({
- {JSON.stringify(finance)} Date: Fri, 16 Jan 2026 21:11:46 +0700 Subject: [PATCH 076/139] fix(FE): delete console log debug --- src/app/finance/detail/page.tsx | 2 -- .../pages/dashboard/DashboardProduction.tsx | 5 ---- .../project-flock/form/ProjectFlockForm.tsx | 3 -- .../pages/report/DailyMarketingsTable.tsx | 2 +- .../daily-checklist/DailyChecklistContent.tsx | 30 +++++++++---------- 5 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/app/finance/detail/page.tsx b/src/app/finance/detail/page.tsx index 1d20e9f5..b80e8acb 100644 --- a/src/app/finance/detail/page.tsx +++ b/src/app/finance/detail/page.tsx @@ -24,8 +24,6 @@ const FinanceDetailPage = () => { ); } - console.log(finance); - // if (!finance || isResponseError(finance)) { // router.replace('/404'); // return; diff --git a/src/components/pages/dashboard/DashboardProduction.tsx b/src/components/pages/dashboard/DashboardProduction.tsx index cf5eeaa2..0175a8be 100644 --- a/src/components/pages/dashboard/DashboardProduction.tsx +++ b/src/components/pages/dashboard/DashboardProduction.tsx @@ -118,8 +118,6 @@ const DashboardProduction = () => { } as DashboardFilterType, validationSchema: getDashboardFilterSchema(analysisMode), onSubmit: (values) => { - console.log(values); - handleApplyFilter({ start_date: values.startDate || '', end_date: values.endDate || '', @@ -139,8 +137,6 @@ const DashboardProduction = () => { }; const handleApplyFilter = (values: DashboardFilter) => { - console.log(values); - // Build query params object, only include non-empty values const params: Record = {}; @@ -156,7 +152,6 @@ const DashboardProduction = () => { if (values.comparison_type) params.comparison_type = values.comparison_type; setEndpointUrl(`/dashboards?${new URLSearchParams(params).toString()}`); - console.log(endpointUrl); filterModal.closeModal(); refreshDashboardProductionData(); }; diff --git a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx index 0c252cb6..d032b67c 100644 --- a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx +++ b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx @@ -557,15 +557,12 @@ const ProjectFlockForm = ({ }; const onDeleteBudgetRowHandler = (nonstock_id: number, index?: number) => { - console.log(`nonstock_id: ${nonstock_id}, index: ${index}`); if (!nonstock_id) { const updatedBudgets = formik.values.project_budgets .map((budget, i) => { if (i == index) { - console.log(`buget: ${null}, index: ${index}, i: ${i}`); return null; } else { - console.log(`buget: ${budget}, index: ${index}, i: ${i}`); return budget; } }) diff --git a/src/components/pages/report/DailyMarketingsTable.tsx b/src/components/pages/report/DailyMarketingsTable.tsx index 2dba0309..d270d6d7 100644 --- a/src/components/pages/report/DailyMarketingsTable.tsx +++ b/src/components/pages/report/DailyMarketingsTable.tsx @@ -168,7 +168,7 @@ const DailyMarketingsTable = ({ ]; useEffect(() => { - console.log({ sorting }); + // console.log({ sorting }); if (sorting.length === 1) { onFilterByChange(sorting[0].id); diff --git a/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx b/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx index 7bd0be83..314381fd 100644 --- a/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx +++ b/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx @@ -601,15 +601,15 @@ export function DailyChecklistContent() { ) => { const taskId = taskIdsByPhaseActivityId[activityId]; - console.log('[CHECKBOX] Click detected:', { - activityId, - employeeId, - checked, - taskId, - hasTaskId: !!taskId, - checklistStatus, - isEditable, - }); + // console.log('[CHECKBOX] Click detected:', { + // activityId, + // employeeId, + // checked, + // taskId, + // hasTaskId: !!taskId, + // checklistStatus, + // isEditable, + // }); if (!taskId) { console.error('[CHECKBOX] No taskId found for activityId:', activityId); @@ -638,10 +638,10 @@ export function DailyChecklistContent() { }, }, }; - console.log( - '[CHECKBOX] State updated optimistically:', - updated[taskId]?.[employeeId] - ); + // console.log( + // '[CHECKBOX] State updated optimistically:', + // updated[taskId]?.[employeeId] + // ); return updated; }); @@ -653,7 +653,7 @@ export function DailyChecklistContent() { note: assignments[taskId]?.[employeeId]?.note || null, }; - console.log('[CHECKBOX] Saving to database:', payload); + // console.log('[CHECKBOX] Saving to database:', payload); const checkOrUncheckAssignmentRes = await DailyChecklistApi.checkOrUncheckAssignment(payload); @@ -679,7 +679,7 @@ export function DailyChecklistContent() { return; } - console.log('[CHECKBOX] Saved successfully'); + // console.log('[CHECKBOX] Saved successfully'); }; const handleNoteChange = async ( From f371d063863336753817b3463bae9a536c5971a5 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Fri, 16 Jan 2026 21:55:39 +0700 Subject: [PATCH 077/139] fix(FE): add depedency to useMemo rows data --- src/components/pages/closing/ClosingFinanceTable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/closing/ClosingFinanceTable.tsx b/src/components/pages/closing/ClosingFinanceTable.tsx index 0d4aea5f..6225f5e7 100644 --- a/src/components/pages/closing/ClosingFinanceTable.tsx +++ b/src/components/pages/closing/ClosingFinanceTable.tsx @@ -44,7 +44,7 @@ const ClosingFinanceTable = ({ return [customItems, ...purchases, totalBudgeting, ...overheads]; } return []; - }, []); + }, [finance]); const profitLossTableData: ProfitLossItem[] = useMemo(() => { if (isResponseSuccess(finance)) { @@ -78,7 +78,7 @@ const ClosingFinanceTable = ({ return [...incomes, ...purchases, grossProfit, ...overheads, subtotal]; } return []; - }, []); + }, [finance]); return (
From c55081f358a9ce9e683adbc74ac4f8fc2ef4c1a6 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sat, 17 Jan 2026 09:48:37 +0700 Subject: [PATCH 078/139] fix(FE): fix limit fetch data kandang --- .../pages/production/project-flock/form/ProjectFlockForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx index d032b67c..89769787 100644 --- a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx +++ b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx @@ -149,7 +149,7 @@ const ProjectFlockForm = ({ const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({ search: '', location_id: selectedLocation == '' ? '0' : selectedLocation, - limit: 'limit', + limit: '500', }).toString()}`; const { data: kandang, From a26919f0370e3385b1fcc59a56fbbe9e9267cded Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sat, 17 Jan 2026 10:49:41 +0700 Subject: [PATCH 079/139] fix(FE): show server error --- .../pages/master-data/flock/FlocksTable.tsx | 32 +++++----- .../master-data/flock/form/FlockForm.tsx | 64 ++++++++++++++----- 2 files changed, 64 insertions(+), 32 deletions(-) diff --git a/src/components/pages/master-data/flock/FlocksTable.tsx b/src/components/pages/master-data/flock/FlocksTable.tsx index baeaa69e..dd6ebfe8 100644 --- a/src/components/pages/master-data/flock/FlocksTable.tsx +++ b/src/components/pages/master-data/flock/FlocksTable.tsx @@ -33,22 +33,6 @@ const RowsOptions = ({ }) => { return ( - - - + + + )} {formType !== 'detail' && (
{
)}
- - {flockFormErrorMessage && ( -
- - {flockFormErrorMessage} -
- )} From 13abc6d7ce3003ad913c313cf7283b994b9c721b Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sat, 17 Jan 2026 11:02:22 +0700 Subject: [PATCH 080/139] fix(FE): change status badge --- .../project-flock/ProjectFlockTable.tsx | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index 6b935812..ab14ef84 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -15,7 +15,7 @@ import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWith import Table from '@/components/Table'; import { ROWS_OPTIONS } from '@/config/constant'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { cn, formatDate } from '@/lib/helper'; +import { cn, formatDate, formatTitleCase } from '@/lib/helper'; import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data'; import { ProjectFlockApi } from '@/services/api/production/project-flock'; import { useTableFilter } from '@/services/hooks/useTableFilter'; @@ -254,7 +254,8 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { header: 'Status', cell: (props) => { const approval = props.row.original.approval; - + const isRejected = approval?.action == 'REJECTED'; + const isApproved = approval?.action == 'APPROVED'; return ( void }) => { badge: 'rounded-lg px-2 w-full flex flex-row justify-start', }} color={ - approval?.step_number == 1 - ? 'neutral' - : approval?.step_number == 2 - ? 'success' - : 'error' + isRejected + ? 'error' + : isApproved + ? approval?.step_number == 1 + ? 'neutral' + : approval?.step_number == 2 + ? 'primary' + : approval?.step_number == 3 + ? 'success' + : 'neutral' + : 'neutral' } > void }) => { approval?.step_number == 1 ? 'neutral' : approval?.step_number == 2 - ? 'success' - : 'error' + ? 'primary' + : approval?.step_number == 3 + ? 'success' + : 'neutral' } /> - {approval?.step_name} + {isRejected + ? 'Ditolak' + : formatTitleCase(approval?.step_name || '')} ); }, From 835ba077d8e1176397259d9ca3b8c25a3510f748 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sat, 17 Jan 2026 11:08:25 +0700 Subject: [PATCH 081/139] fix(FE): change status badge kandang aktif --- .../production/chickin/form/ChickinForm.tsx | 4 ++-- .../project-flock/detail/ProjectFlockDetail.tsx | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/pages/production/chickin/form/ChickinForm.tsx b/src/components/pages/production/chickin/form/ChickinForm.tsx index b5b1dc4d..d484e1c6 100644 --- a/src/components/pages/production/chickin/form/ChickinForm.tsx +++ b/src/components/pages/production/chickin/form/ChickinForm.tsx @@ -75,12 +75,12 @@ const ChickinFormKandang = ({
- {' '} + {' '} Aktif
diff --git a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx index 4a998c83..50b258fb 100644 --- a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx +++ b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx @@ -156,9 +156,9 @@ const ProjectFlockDetail = ({ projectFlock.approval?.step_number == 1 ? 'neutral' : projectFlock.approval?.step_number == 2 - ? 'success' - : projectFlock.approval?.step_number >= 3 - ? 'error' + ? 'primary' + : projectFlock.approval?.step_number == 3 + ? 'success' : undefined } className={{ @@ -173,9 +173,9 @@ const ProjectFlockDetail = ({ projectFlock.approval?.step_number == 1 ? 'neutral' : projectFlock.approval?.step_number == 2 - ? 'success' - : projectFlock.approval?.step_number >= 3 - ? 'error' + ? 'primary' + : projectFlock.approval?.step_number == 3 + ? 'success' : undefined } />{' '} @@ -273,7 +273,7 @@ const ProjectFlockDetail = ({
{' '} Kandang Aktif ({projectFlock.kandangs?.length}) From 4c4c70e10f84efd4a55331a144abc9c8b2c970dd Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 17 Jan 2026 12:03:32 +0700 Subject: [PATCH 082/139] refactor(FE): Validate duplicates by selected record date --- .../recording/form/RecordingForm.schema.ts | 2 +- .../recording/form/RecordingForm.tsx | 87 ++++++++++++++++--- 2 files changed, 75 insertions(+), 14 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.schema.ts b/src/components/pages/production/recording/form/RecordingForm.schema.ts index 8ebd4aa2..18dc9823 100644 --- a/src/components/pages/production/recording/form/RecordingForm.schema.ts +++ b/src/components/pages/production/recording/form/RecordingForm.schema.ts @@ -104,7 +104,7 @@ export const RecordingGrowingFormSchema: Yup.ObjectSchema; diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index b45f72d0..3e40b404 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -56,6 +56,7 @@ import { import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; import { Kandang } from '@/types/api/master-data/kandang'; +import * as Yup from 'yup'; import { RecordingGrowingFormSchema, RecordingLayingFormSchema, @@ -231,6 +232,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const [, setNewRecordingData] = useState(null); const [nextDayRecording, setNextDayRecording] = useState(null); + const [currentRecordDate, setCurrentRecordDate] = useState( + new Date().toISOString().split('T')[0] + ); + const [duplicateErrorShown, setDuplicateErrorShown] = useState(false); const approveModal = useModal(); const rejectModal = useModal(); @@ -719,18 +724,18 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const recordedProjectFlockKandangIds = useMemo(() => { if (!isResponseSuccess(existingRecordings)) return new Set(); - const todayRecordings = existingRecordings.data; + const allRecordings = existingRecordings.data; const recordedIds = new Set(); - todayRecordings.forEach((recording) => { + allRecordings.forEach((recording) => { const recordingDate = recording.record_datetime?.split('T')[0]; - if (recordingDate === today) { + if (recordingDate === currentRecordDate) { recordedIds.add(recording.project_flock?.project_flock_kandang_id); } }); return recordedIds; - }, [existingRecordings, today]); + }, [existingRecordings, currentRecordDate]); const currentTotalChickQty = useMemo(() => { if (type === 'add' && projectFlockKandangLookup) { @@ -910,14 +915,38 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { initialValues: formikInitialValues, enableReinitialize: true, validationSchema: (() => { + let schema; if (isLayingCategory) { - return type === 'edit' - ? UpdateRecordingLayingFormSchema - : RecordingLayingFormSchema; + schema = + type === 'edit' + ? UpdateRecordingLayingFormSchema + : RecordingLayingFormSchema; + } else { + schema = + type === 'edit' + ? UpdateRecordingGrowingFormSchema + : RecordingGrowingFormSchema; } - return type === 'edit' - ? UpdateRecordingGrowingFormSchema - : RecordingGrowingFormSchema; + return schema.clone().concat( + Yup.object().shape({ + project_flock_kandang_id: Yup.number().test( + 'not-already-recorded', + 'Project Flock ini sudah direcord pada tanggal tersebut!', + function (value) { + if (type !== 'add') return true; + if (value && recordedProjectFlockKandangIds.has(value)) { + if (this.createError) { + return this.createError({ + message: `Project Flock ini sudah direcord pada tanggal ${formatDate(currentRecordDate, 'DD MMMM YYYY')}!`, + }); + } + return false; + } + return true; + } + ), + }) + ); })(), validateOnChange: true, validateOnBlur: true, @@ -1149,6 +1178,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { setSelectedLocation(location); setSelectedProjectFlock(null); setSelectedKandang(null); + if (duplicateErrorShown) { + toast.dismiss(); + setDuplicateErrorShown(false); + } setSelectedProjectFlockLocationId( location ? location.value.toString() : '' ); @@ -1159,6 +1192,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const projectFlockChangeHandler = (val: OptionType | OptionType[] | null) => { setSelectedProjectFlock(val as OptionType); setSelectedKandang(null); + if (duplicateErrorShown) { + toast.dismiss(); + setDuplicateErrorShown(false); + } formik.setFieldValue('project_flock_kandang', null); formik.setFieldValue('project_flock_kandang_id', 0); }; @@ -1166,6 +1203,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const kandangChangeHandler = (val: OptionType | OptionType[] | null) => { const kandang = val as OptionType; setSelectedKandang(kandang); + if (duplicateErrorShown) { + toast.dismiss(); + setDuplicateErrorShown(false); + } if (selectedLocation && kandang) { setStockProductsLocationId(selectedLocation.value.toString()); setStockProductsKandangId(kandang.value.toString()); @@ -1187,9 +1228,18 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const handleRecordDateChange = useCallback( (e: React.ChangeEvent) => { - formik.setFieldValue('record_date', e.target.value); + const newDate = e.target.value; + formik.setFieldValue('record_date', newDate); + setCurrentRecordDate(newDate); + if (duplicateErrorShown) { + toast.dismiss(); + setDuplicateErrorShown(false); + } + setTimeout(() => { + formik.validateField('project_flock_kandang_id'); + }, 0); }, - [formik] + [formik, duplicateErrorShown] ); useEffect(() => { @@ -1199,8 +1249,19 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { if (type === 'add') { if (recordedProjectFlockKandangIds.has(projectFlockKandangId)) { - toast.error('Project Flock Kandang ini sudah direcord hari ini!'); + if (!duplicateErrorShown) { + toast.error( + `Project Flock Kandang ini sudah direcord pada tanggal ${formatDate(currentRecordDate, 'DD MMMM YYYY')}!`, + { duration: Infinity } + ); + setDuplicateErrorShown(true); + } return; + } else { + if (duplicateErrorShown) { + toast.dismiss(); + setDuplicateErrorShown(false); + } } if ( From cd9fa31ad71415fb109284080dcf7a18502b4ed8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 17 Jan 2026 12:11:30 +0700 Subject: [PATCH 083/139] refactor(FE): Disable form fields in edit mode --- .../pages/production/recording/form/RecordingForm.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 3e40b404..106fc220 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -1660,6 +1660,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { Boolean(formik.errors.record_date) } errorMessage={formik.errors.record_date as string} + disabled={type === 'edit'} /> { placeholder='Pilih Lokasi' isClearable isSearchable + isDisabled={type === 'edit'} /> { onInputChange={setProjectFlockSearchValue} isLoading={isLoadingProjectFlocks} onMenuScrollToBottom={loadMoreProjectFlocks} - isDisabled={!selectedLocation} + isDisabled={!selectedLocation || type === 'edit'} placeholder={ selectedLocation ? 'Pilih Project Flock' @@ -1704,7 +1706,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { onChange={kandangChangeHandler} options={kandangOptions} isLoading={false} - isDisabled={!selectedProjectFlock} + isDisabled={!selectedProjectFlock || type === 'edit'} placeholder={ selectedProjectFlock ? 'Pilih Kandang' From f32b77c5527bfbda7f64eeae60db77b1b1c90ded Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 17 Jan 2026 12:24:32 +0700 Subject: [PATCH 084/139] refactor(FE): Add location, project_flock and kandang fields --- .../recording/form/RecordingForm.schema.ts | 45 ++++++++++++++++++ .../recording/form/RecordingForm.tsx | 47 ++++++++++++++++--- 2 files changed, 85 insertions(+), 7 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.schema.ts b/src/components/pages/production/recording/form/RecordingForm.schema.ts index 18dc9823..59d5ac43 100644 --- a/src/components/pages/production/recording/form/RecordingForm.schema.ts +++ b/src/components/pages/production/recording/form/RecordingForm.schema.ts @@ -8,6 +8,21 @@ import { type RecordingGrowingFormSchemaType = { record_date: string; + location?: { + value: number; + label: string; + } | null; + location_id: number; + project_flock?: { + value: number; + label: string; + } | null; + project_flock_id: number; + kandang?: { + value: number; + label: string; + } | null; + kandang_id: number; project_flock_kandang: { value: number; label: string; @@ -89,6 +104,30 @@ export const RecordingGrowingFormSchema: Yup.ObjectSchema { // ===== EVENT HANDLERS ===== const locationChangeHandler = (val: OptionType | OptionType[] | null) => { - const location = val as OptionType; + const location = val as OptionType | null; + const locationId = Number(location?.value); + + formik.setFieldTouched('location', true); + formik.setFieldValue('location', location); + formik.setFieldTouched('location_id', true); + formik.setFieldValue('location_id', locationId); + setSelectedLocation(location); setSelectedProjectFlock(null); setSelectedKandang(null); @@ -1185,23 +1192,34 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { setSelectedProjectFlockLocationId( location ? location.value.toString() : '' ); - formik.setFieldValue('project_flock_kandang', null); - formik.setFieldValue('project_flock_kandang_id', 0); }; const projectFlockChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedProjectFlock(val as OptionType); + const projectFlock = val as OptionType | null; + const projectFlockId = Number(projectFlock?.value); + + formik.setFieldTouched('project_flock', true); + formik.setFieldValue('project_flock', projectFlock); + formik.setFieldTouched('project_flock_id', true); + formik.setFieldValue('project_flock_id', projectFlockId); + + setSelectedProjectFlock(projectFlock); setSelectedKandang(null); if (duplicateErrorShown) { toast.dismiss(); setDuplicateErrorShown(false); } - formik.setFieldValue('project_flock_kandang', null); - formik.setFieldValue('project_flock_kandang_id', 0); }; const kandangChangeHandler = (val: OptionType | OptionType[] | null) => { - const kandang = val as OptionType; + const kandang = val as OptionType | null; + const kandangId = Number(kandang?.value); + + formik.setFieldTouched('kandang', true); + formik.setFieldValue('kandang', kandang); + formik.setFieldTouched('kandang_id', true); + formik.setFieldValue('kandang_id', kandangId); + setSelectedKandang(kandang); if (duplicateErrorShown) { toast.dismiss(); @@ -1676,6 +1694,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { isClearable isSearchable isDisabled={type === 'edit'} + isError={ + formik.touched.location_id && + Boolean(formik.errors.location_id) + } + errorMessage={formik.errors.location_id as string} /> { } isClearable isSearchable + isError={ + formik.touched.project_flock_id && + Boolean(formik.errors.project_flock_id) + } + errorMessage={formik.errors.project_flock_id as string} /> { } isClearable isSearchable + isError={ + formik.touched.kandang_id && + Boolean(formik.errors.kandang_id) + } + errorMessage={formik.errors.kandang_id as string} startAdornment={ projectFlockKandangLookup || projectFlockKandangDetail ? getProjectFlockBadgeAdornment() From 0cdbff69546d9c68b94c04c4ebf906d6e26d7c4a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 17 Jan 2026 12:32:55 +0700 Subject: [PATCH 085/139] refactor(FE): Validate recording date and handle null location --- .../pages/production/recording/form/RecordingForm.schema.ts | 1 + .../pages/production/recording/form/RecordingForm.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.schema.ts b/src/components/pages/production/recording/form/RecordingForm.schema.ts index 59d5ac43..82b59036 100644 --- a/src/components/pages/production/recording/form/RecordingForm.schema.ts +++ b/src/components/pages/production/recording/form/RecordingForm.schema.ts @@ -103,6 +103,7 @@ export const RecordingGrowingFormSchema: Yup.ObjectSchema { // ===== EVENT HANDLERS ===== const locationChangeHandler = (val: OptionType | OptionType[] | null) => { const location = val as OptionType | null; - const locationId = Number(location?.value); + const locationId = location ? Number(location.value) : 0; formik.setFieldTouched('location', true); formik.setFieldValue('location', location); From 1152b6d2c3b47f6b9a5fad3670ca30f2214ca42d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 17 Jan 2026 12:45:11 +0700 Subject: [PATCH 086/139] refactor(FE): Disable Submit button when duplicate error shown --- .../production/recording/form/RecordingForm.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 7f66ef6d..0995f27f 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -2990,7 +2990,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { color='primary' className='px-4' isLoading={formik.isSubmitting} - disabled={hasExceededStock || formik.isSubmitting} + disabled={ + hasExceededStock || + formik.isSubmitting || + duplicateErrorShown + } > Submit @@ -3015,7 +3019,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { color='primary' className='px-4' isLoading={formik.isSubmitting} - disabled={hasExceededStock || formik.isSubmitting} + disabled={ + hasExceededStock || + formik.isSubmitting || + duplicateErrorShown + } > Submit From f4abfd4279c9f72a6c140d43e630848f73158be5 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 17 Jan 2026 12:54:16 +0700 Subject: [PATCH 087/139] refactor(FE): Dismiss toast notifications on unmount --- .../pages/production/recording/form/RecordingForm.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 0995f27f..d7f913e8 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -237,6 +237,12 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ); const [duplicateErrorShown, setDuplicateErrorShown] = useState(false); + useEffect(() => { + return () => { + toast.dismiss(); + }; + }, []); + const approveModal = useModal(); const rejectModal = useModal(); const deleteModal = useModal(); From d19b1e885e74769bc2758d8681f18d46ebd0f7af Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 17 Jan 2026 12:59:41 +0700 Subject: [PATCH 088/139] refactor(FE): Compute item total on qty change and limit reset --- .../order/PurchaseOrderStaffApprovalForm.tsx | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx index 729b6782..a232347d 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx @@ -633,8 +633,18 @@ const PurchaseOrderStaffApprovalForm = ({ formik.setFieldValue(`items.${idx}.qty`, numValue); - formik.setFieldValue(`items.${idx}.price`, ''); - formik.setFieldValue(`items.${idx}.total_price`, ''); + if ( + formItem.price !== '' && + formItem.price !== undefined && + formItem.price !== null && + numValue !== '' && + numValue > 0 + ) { + const calculatedTotal = Number(formItem.price) * Number(numValue); + formik.setFieldValue(`items.${idx}.total_price`, calculatedTotal); + } else if (numValue === '') { + formik.setFieldValue(`items.${idx}.total_price`, ''); + } } if (field === 'price' || field === 'total_price') { @@ -1184,8 +1194,10 @@ const PurchaseOrderStaffApprovalForm = ({ color='warning' className='px-4' onClick={() => { - formik.setValues(formikInitialValues); - formik.resetForm(); + if (type === 'add') { + formik.setValues(formikInitialValues); + formik.resetForm(); + } setPurchaseOrderFormErrorMessage(''); onCancel?.(); onModalClose?.(); From 138ad6a7c91f8c590919958707d518c1b2c38ff1 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sat, 17 Jan 2026 15:59:08 +0700 Subject: [PATCH 089/139] hotfix(FE): change select input component to disabled --- .../product/detail/InventoryProductDetail.tsx | 2 +- .../pages/marketing/MarketingTable.tsx | 50 ++++++++++++++++++- .../marketing/detail/MarketingDetail.tsx | 47 ++++++++++++++++- .../pages/marketing/form/MarketingForm.tsx | 4 +- 4 files changed, 98 insertions(+), 5 deletions(-) diff --git a/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx b/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx index 42c177d7..39609b06 100644 --- a/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx +++ b/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx @@ -91,7 +91,7 @@ const InventoryProductDetail = ({ : {inventoryProduct?.tax - ? formatCurrency(inventoryProduct?.tax) + ? formatNumber(inventoryProduct?.tax) + '%' : '-'} diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index 467c2e00..8f1a6cf9 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -16,7 +16,7 @@ import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector'; import { TableToolbar } from '@/components/table/TableToolbar'; import { ROWS_OPTIONS } from '@/config/constant'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { cn, formatCurrency, formatDate } from '@/lib/helper'; +import { cn, formatCurrency, formatDate, formatTitleCase } from '@/lib/helper'; import { MarketingApi, SalesOrderApi, @@ -33,6 +33,7 @@ import RequirePermission from '@/components/helper/RequirePermission'; import { useAuth } from '@/services/hooks/useAuth'; import { CustomerApi, ProductApi } from '@/services/api/master-data'; import { MARKETING_APPROVAL_LINE } from '@/config/approval-line'; +import Badge from '@/components/Badge'; const RowsOptionsMenu = ({ type = 'dropdown', @@ -520,8 +521,53 @@ const MarketingTable = () => { }, }, { - accessorKey: 'latest_approval.step_name', + accessorKey: 'approval.step_name', header: 'Status', + cell: (props) => { + const approval = props.row.original.latest_approval; + const isRejected = approval?.action == 'REJECTED'; + const isApproved = approval?.action == 'APPROVED'; + return ( + + + {isRejected + ? 'Ditolak' + : formatTitleCase(approval?.step_name || '')} + + ); + }, }, { accessorKey: 'customer.name', diff --git a/src/components/pages/marketing/detail/MarketingDetail.tsx b/src/components/pages/marketing/detail/MarketingDetail.tsx index 12ebda20..91e02928 100644 --- a/src/components/pages/marketing/detail/MarketingDetail.tsx +++ b/src/components/pages/marketing/detail/MarketingDetail.tsx @@ -16,6 +16,7 @@ import { formatCurrency, formatDate, formatNumber, + formatTitleCase, formatVechicleNumber, } from '@/lib/helper'; import { @@ -34,6 +35,7 @@ import toast from 'react-hot-toast'; import SalesOrderExport from '@/components/pages/marketing/pdf/SalesOrderExport'; import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport'; import RequirePermission from '@/components/helper/RequirePermission'; +import Badge from '@/components/Badge'; const MarketingDetail = ({ initialValues, @@ -121,6 +123,10 @@ const MarketingDetail = ({ ); }; + const approval = initialValues?.latest_approval; + const isRejected = approval?.action == 'REJECTED'; + const isApproved = approval?.action == 'APPROVED'; + return ( <>
@@ -230,7 +236,46 @@ const MarketingDetail = ({ Status : - {initialValues?.latest_approval?.step_name} + + + + {isRejected + ? 'Ditolak' + : formatTitleCase(approval?.step_name || '')} + + Tanggal Penjualan diff --git a/src/components/pages/marketing/form/MarketingForm.tsx b/src/components/pages/marketing/form/MarketingForm.tsx index be4367cb..f20a44b5 100644 --- a/src/components/pages/marketing/form/MarketingForm.tsx +++ b/src/components/pages/marketing/form/MarketingForm.tsx @@ -621,7 +621,9 @@ const MarketingForm = ({ isClearable placeholder='Pilih Pelanggan' isDisabled={ - formType === 'add_deliver' || formType === 'edit_deliver' + formType === 'add_deliver' || + formType === 'edit_deliver' || + formType === 'edit' } /> Date: Sat, 17 Jan 2026 16:16:34 +0700 Subject: [PATCH 090/139] refactor(FE): Rename price to unit_price in customer payments --- .../report/finance/export/CustomerPaymentExportPDF.tsx | 4 ++-- .../report/finance/export/CustomerPaymentExportXLSX.tsx | 6 +++--- .../pages/report/finance/tab/CustomerPaymentTab.tsx | 8 ++++---- src/types/api/report/customer-payment.d.ts | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx index 9b1fd640..aa04b4f0 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx @@ -300,7 +300,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { Rata-Rata - Harga Awal + Harga/Unit Harga Akhir @@ -378,7 +378,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { {formatNumber(item.average_weight)} - {formatCurrency(item.price)} + {formatCurrency(item.unit_price)} {formatCurrency(item.final_price)} diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx index 3fb21488..830df633 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx @@ -38,7 +38,7 @@ export const generateCustomerPaymentExcel = ( 'Ekor/Qty': formatNumber(item.qty || 0), 'Berat (Kg)': formatNumber(item.weight || 0), AVG: formatNumber(item.average_weight || 0), - 'Harga Awal': formatCurrency(item.price || 0), + 'Harga/Unit': formatCurrency(item.unit_price || 0), 'Harga Akhir': formatCurrency(item.final_price || 0), Total: formatCurrency(item.total_price || 0), Pembayaran: formatCurrency(item.payment_amount || 0), @@ -62,7 +62,7 @@ export const generateCustomerPaymentExcel = ( 'Ekor/Qty': formatNumber(customerReport.summary.total_qty || 0), 'Berat (Kg)': formatNumber(customerReport.summary.total_weight || 0), AVG: '', - 'Harga Awal': '', + 'Harga/Unit': '', 'Harga Akhir': formatCurrency( customerReport.summary.total_final_amount || 0 ), @@ -89,7 +89,7 @@ export const generateCustomerPaymentExcel = ( { wch: 10 }, // Ekor/Qty { wch: 12 }, // Berat { wch: 10 }, // AVG - { wch: 15 }, // Harga Awal + { wch: 15 }, // Harga/Unit { wch: 15 }, // Harga Akhir { wch: 15 }, // Total { wch: 15 }, // Pembayaran diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index ef748b5f..0e3afe40 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -405,12 +405,12 @@ const CustomerPaymentTab = () => { ), }, { - id: 'price', - header: 'Harga Awal', - accessorKey: 'price', + id: 'unit_price', + header: 'Harga/Unit', + accessorKey: 'unit_price', enableSorting: false, cell: (props) => { - const value = props.row.original.price; + const value = props.row.original.unit_price; return
{formatCurrency(value)}
; }, footer: () => ( diff --git a/src/types/api/report/customer-payment.d.ts b/src/types/api/report/customer-payment.d.ts index 9169c99b..90834cdc 100644 --- a/src/types/api/report/customer-payment.d.ts +++ b/src/types/api/report/customer-payment.d.ts @@ -11,7 +11,7 @@ export type CustomerPaymentRow = { qty: number; weight: number; average_weight: number; - price: number; + unit_price: number; final_price: number; total_price: number; payment_amount: number; From 36da05890a4376ef38eb1bbba5b6f3d033ccfa56 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sat, 17 Jan 2026 16:45:56 +0700 Subject: [PATCH 091/139] hotfix(FE): fixing failed test scenario in module finance --- .../pages/finance/FinanceDetail.tsx | 41 ++++---- src/components/pages/finance/FinanceTable.tsx | 99 +++++++++++-------- .../pages/finance/add/FormFinanceAdd.tsx | 41 ++++++-- .../FormFinanceAddInitialBalance.schema.ts | 8 +- .../FormFinanceAddInitialBalance.tsx | 29 ++++-- .../add/injection/FormFinanceInjection.tsx | 20 +++- 6 files changed, 151 insertions(+), 87 deletions(-) diff --git a/src/components/pages/finance/FinanceDetail.tsx b/src/components/pages/finance/FinanceDetail.tsx index 03291420..76d2d1a0 100644 --- a/src/components/pages/finance/FinanceDetail.tsx +++ b/src/components/pages/finance/FinanceDetail.tsx @@ -34,7 +34,7 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => { }, { label: 'Pihak', - value: finance.party.id ? finance.party.name : '-', + value: finance.party?.id ? finance.party?.name : '-', }, { label: 'Tanggal', @@ -56,25 +56,21 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => { }, { label: 'Nomor Rekening', - value: `${finance.bank.alias} - ${finance.bank.account_number} - ${finance.bank.owner}`, + value: `${finance.bank?.alias} - ${finance.bank?.account_number} - ${finance.bank?.owner}`, }, { - label: `Rekening ${formatTitleCase(finance.party.type)}`, - value: finance.party.account_number, + label: `Rekening ${formatTitleCase(finance.party?.type)}`, + value: finance.party?.account_number, }, { label: 'Nominal', - value: formatCurrency(finance.expense_amount), - }, - { - label: 'Sisa', - value: formatCurrency(finance.income_amount), + value: formatCurrency(finance.nominal), }, ].filter((item) => { // Hide party account number row if transaction type is INJECTION if ( FINANCE_INJECTION_STATUS.includes(finance.transaction_type) && - item.label === `Rekening ${formatTitleCase(finance.party.type)}` + item.label === `Rekening ${formatTitleCase(finance.party?.type)}` ) { return false; } @@ -148,18 +144,19 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
- {FINANCE_TRANSACTION_STATUS.includes(finance.transaction_type) && ( - - - - )} + {FINANCE_TRANSACTION_STATUS.includes(finance.transaction_type) && + finance.party?.type !== 'SUPPLIER' && ( + + + + )} {FINANCE_INITIAL_BALANCE_STATUS.includes(finance.transaction_type) && ( - - )} + ) && + props.row.original.party?.type !== 'SUPPLIER' && ( + + + + )} {FINANCE_INITIAL_BALANCE_STATUS.includes( props.row.original.transaction_type @@ -194,20 +199,25 @@ const FinanceTable = () => { // ===== Options ===== const transactionTypeOptions = useMemo(() => { - return [ - { label: 'Transfer', value: 'TRANSFER' }, - { label: 'Cash', value: 'CASH' }, - { label: 'Card', value: 'CARD' }, - { label: 'Cheque', value: 'CHEQUE' }, - { label: 'Saldo', value: 'SALDO' }, - ]; - }, []); - const partyTypeOptions = useMemo(() => { return [ { label: 'Customer', value: 'CUSTOMER' }, { label: 'Supplier', value: 'SUPPLIER' }, ]; }, []); + const { + options: partyTypeOptions, + isLoadingOptions: partyTypeIsLoadingOptions, + setInputValue: partyTypeInputValue, + loadMore: partyTypeLoadMore, + } = useSelect( + selectedTransactionType + ? selectedTransactionType.value === 'CUSTOMER' + ? CustomerApi.basePath + : SupplierApi.basePath + : '', + 'id', + 'name' + ); const sortByOptions = useMemo(() => { return [ { label: 'Tanggal Pembayaran', value: 'payment_date' }, @@ -336,10 +346,10 @@ const FinanceTable = () => { }, { header: 'Pihak', - accessorFn: (finance: Finance) => finance.party.name, + accessorFn: (finance: Finance) => finance.party?.name, cell: (props: CellContext) => { - if (props.row.original.party.id) { - return {props.row.original.party.name}; + if (props.row.original.party?.id) { + return {props.row.original.party?.name}; } return {'-'}; }, @@ -360,12 +370,12 @@ const FinanceTable = () => { { header: 'Bank', accessorFn: (finance: Finance) => - `${finance.bank.alias} - ${finance.bank.account_number} - ${finance.bank.owner}`, + `${finance.bank?.alias} - ${finance.bank?.account_number} - ${finance.bank?.owner}`, }, { header: 'Pengeluaran (Rp)', accessorFn: (finance: Finance) => - formatCurrency(finance.expense_amount), + formatCurrency(Math.abs(finance.expense_amount)), }, { header: 'Pemasukan (Rp)', @@ -468,25 +478,41 @@ const FinanceTable = () => {
+ ({ label: - bankRawData.data.find((data) => data.id === bank.value) + bankRawData.data.find((data) => data.id === bank?.value) ?.alias + ' - ' + - bankRawData.data.find((data) => data.id === bank.value) + bankRawData.data.find((data) => data.id === bank?.value) ?.account_number + ' - ' + - bankRawData.data.find((data) => data.id === bank.value) + bankRawData.data.find((data) => data.id === bank?.value) ?.owner, - value: bank.value, + value: bank?.value, })) : [] } @@ -497,13 +523,6 @@ const FinanceTable = () => { onMenuScrollToBottom={bankLoadMore} isClearable /> - { const router = useRouter(); + const [serverErrorMessage, setServerErrorMessage] = useState(''); + const [isSupplier, setIsSupplier] = useState( + initialValues?.party?.type === 'SUPPLIER' + ); // ===== Formik ===== const formikInitialValues = useMemo((): FinanceFormValues => { return { party_type_option: FINANCE_PARTY_TYPE_OPTIONS.find( - (option) => option.value === initialValues?.party.type + (option) => option.value === initialValues?.party?.type ) || null, party_id_option: initialValues?.party ? { - label: initialValues?.party.name || '', - value: initialValues?.party.id || 0, + label: initialValues?.party?.name || '', + value: initialValues?.party?.id || 0, } : null, payment_date: initialValues?.payment_date || '', @@ -72,11 +78,11 @@ const FormFinanceAdd = ({ ) || null, bank_id_option: initialValues?.bank ? { - label: initialValues.bank.name, - value: initialValues.bank.id, + label: initialValues?.bank?.name, + value: initialValues?.bank?.id, } : null, - party_account_number: initialValues?.party.account_number || '', + party_account_number: initialValues?.party?.account_number || '', reference_number: initialValues?.reference_number || '', nominal: initialValues?.nominal.toString() || '', notes: initialValues?.notes || '', @@ -153,6 +159,7 @@ const FormFinanceAdd = ({ if (isResponseError(response)) { toast.error(response.message); + setServerErrorMessage(response.message); return; } @@ -168,6 +175,7 @@ const FormFinanceAdd = ({ if (isResponseError(response)) { toast.error(response.message); + setServerErrorMessage(response.message); return; } @@ -207,6 +215,7 @@ const FormFinanceAdd = ({ ? formik.errors.party_type_option : '' } + isDisabled={type === 'edit' || isSupplier} required isClearable /> @@ -245,7 +254,7 @@ const FormFinanceAdd = ({ } required isClearable - isDisabled={!formik.values.party_type_option?.value} + isDisabled={!formik.values.party_type_option?.value || isSupplier} />