From 080592ff01c31d3b4318aee3d7587c727bece8ca Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 4 Mar 2026 11:58:35 +0700 Subject: [PATCH 01/33] chore(FE): Remove unused variables and imports across components --- .../pages/dashboard/chart/DashboardLineChart.tsx | 6 +++--- src/components/pages/finance/FinanceDetail.tsx | 1 - .../pages/marketing/DeliveryOrderFormModal.tsx | 1 - .../pages/marketing/pdf/DeliveryOrderExport.tsx | 2 +- .../product-category/ProductCategoryTable.tsx | 8 +------- .../form/ProductionStandardForm.tsx | 9 +-------- .../production/recording/RecordingTable.tsx | 1 - .../production/uniformity/UniformityTable.tsx | 4 +--- src/components/pages/purchase/PurchaseTable.tsx | 16 +++++++--------- .../report/finance/tab/CustomerPaymentTab.tsx | 1 - .../tab/PurchasesPerSupplierTab.tsx | 2 +- .../report/marketing/tab/HppPerKandangTab.tsx | 1 - .../ProductionResultProjectFlockKandangTab.tsx | 2 +- src/lib/helper.ts | 2 +- 14 files changed, 17 insertions(+), 39 deletions(-) diff --git a/src/components/pages/dashboard/chart/DashboardLineChart.tsx b/src/components/pages/dashboard/chart/DashboardLineChart.tsx index bfb13d9a..b7e0e1c2 100644 --- a/src/components/pages/dashboard/chart/DashboardLineChart.tsx +++ b/src/components/pages/dashboard/chart/DashboardLineChart.tsx @@ -409,14 +409,14 @@ const DashboardLineChart = ({ axisLine={{ stroke: '#C1C1C180', opacity: 0.5 }} domain={(() => { // Calculate dynamic domain based on visible data - let seriesData: DashboardChartsSeries[] = []; + // let seriesData: DashboardChartsSeries[] = []; let dataset: DashboardChartsDataset[] = []; if ( analysisMode === 'OVERVIEW' && isOverviewCharts(data.charts) ) { - seriesData = data.charts[chartData]?.series || []; + // seriesData = data.charts[chartData]?.series || []; dataset = data.charts[chartData]?.dataset || []; } else if ( analysisMode === 'COMPARISON' && @@ -426,7 +426,7 @@ const DashboardLineChart = ({ data.charts.farm || data.charts.flock || data.charts.kandang; - seriesData = comparisonChart?.series || []; + // seriesData = comparisonChart?.series || []; dataset = comparisonChart?.dataset || []; } diff --git a/src/components/pages/finance/FinanceDetail.tsx b/src/components/pages/finance/FinanceDetail.tsx index 622fff6f..ddebe19e 100644 --- a/src/components/pages/finance/FinanceDetail.tsx +++ b/src/components/pages/finance/FinanceDetail.tsx @@ -2,7 +2,6 @@ import Button from '@/components/Button'; import Card from '@/components/Card'; import { FormHeader } from '@/components/helper/form/FormHeader'; import RequirePermission from '@/components/helper/RequirePermission'; -import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import Table from '@/components/Table'; diff --git a/src/components/pages/marketing/DeliveryOrderFormModal.tsx b/src/components/pages/marketing/DeliveryOrderFormModal.tsx index ae559328..b4dd1e6e 100644 --- a/src/components/pages/marketing/DeliveryOrderFormModal.tsx +++ b/src/components/pages/marketing/DeliveryOrderFormModal.tsx @@ -1,7 +1,6 @@ 'use client'; import AlertErrorList from '@/components/helper/form/FormErrors'; -import { OptionType } from '@/components/input/SelectInput'; import Modal, { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema'; diff --git a/src/components/pages/marketing/pdf/DeliveryOrderExport.tsx b/src/components/pages/marketing/pdf/DeliveryOrderExport.tsx index 5db89450..cdf18652 100644 --- a/src/components/pages/marketing/pdf/DeliveryOrderExport.tsx +++ b/src/components/pages/marketing/pdf/DeliveryOrderExport.tsx @@ -1,7 +1,7 @@ import Button from '@/components/Button'; import { BaseDeliveryOrder, Marketing } from '@/types/api/marketing/marketing'; import { Icon } from '@iconify/react'; -import { Document, Image, Page, pdf, Text, View } from '@react-pdf/renderer'; +import { Document, Page, pdf, Text, View } from '@react-pdf/renderer'; import { useMemo, useState } from 'react'; import { formatDate, formatNumber, formatVechicleNumber } from '@/lib/helper'; import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles'; diff --git a/src/components/pages/master-data/product-category/ProductCategoryTable.tsx b/src/components/pages/master-data/product-category/ProductCategoryTable.tsx index e758e81d..16efe401 100644 --- a/src/components/pages/master-data/product-category/ProductCategoryTable.tsx +++ b/src/components/pages/master-data/product-category/ProductCategoryTable.tsx @@ -1,12 +1,6 @@ 'use client'; -import { - ChangeEventHandler, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import { ChangeEventHandler, useEffect, useMemo, useState } from 'react'; import { usePathname } from 'next/navigation'; import useSWR from 'swr'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; 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 1b490fbb..b27f0de3 100644 --- a/src/components/pages/master-data/production-standard/form/ProductionStandardForm.tsx +++ b/src/components/pages/master-data/production-standard/form/ProductionStandardForm.tsx @@ -178,14 +178,7 @@ const ProductionStandardForm = ({ const router = useRouter(); // ===== Store ===== - const { - formData, - setFormData, - addDetail, - updateDetail, - deleteDetail, - clearCache, - } = useFormStore(); + const { formData, setFormData, clearCache } = useFormStore(); // ===== Formik ===== // Initial values - only recalculate when initialValue changes (for edit/detail mode) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index ca9a12eb..3c0db941 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -40,7 +40,6 @@ import { RecordingApi } from '@/services/api/production'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import toast from 'react-hot-toast'; -import Badge from '@/components/Badge'; import StatusBadge from '@/components/helper/StatusBadge'; import CheckboxInput from '@/components/input/CheckboxInput'; import { useUiStore } from '@/stores/ui/ui.store'; diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index 8c3f5b88..ca2c1c17 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -19,7 +19,6 @@ import { import { isResponseSuccess } from '@/lib/api-helper'; import { type BaseApiResponse } from '@/types/api/api-general'; import Table from '@/components/Table'; -import Badge from '@/components/Badge'; import StatusBadge from '@/components/helper/StatusBadge'; import CheckboxInput from '@/components/input/CheckboxInput'; import { useModal } from '@/components/Modal'; @@ -186,7 +185,7 @@ const UniformityTable = () => { const router = useRouter(); const searchParams = useSearchParams(); const pathname = usePathname(); - const { searchValue, setSearchValue, setTableState } = useUiStore(); + const { searchValue, setTableState } = useUiStore(); const isSuccess = useUniformityStore((s) => s.isSuccess); const setIsSuccess = useUniformityStore((s) => s.setIsSuccess); const createdUniformity = useUniformityStore((s) => s.createdUniformity); @@ -198,7 +197,6 @@ const UniformityTable = () => { state: tableFilterState, updateFilter, setPage, - setPageSize, toQueryString: getTableFilterQueryString, } = useTableFilter({ initial: { diff --git a/src/components/pages/purchase/PurchaseTable.tsx b/src/components/pages/purchase/PurchaseTable.tsx index e15676cd..43ddab1d 100644 --- a/src/components/pages/purchase/PurchaseTable.tsx +++ b/src/components/pages/purchase/PurchaseTable.tsx @@ -22,7 +22,6 @@ import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import PopoverButton from '@/components/popover/PopoverButton'; import PopoverContent from '@/components/popover/PopoverContent'; -import SelectInput, { OptionType } from '@/components/input/SelectInput'; import RequirePermission from '@/components/helper/RequirePermission'; import StatusBadge from '@/components/helper/StatusBadge'; import PurchaseTableSkeleton from '@/components/pages/purchase/skeleton/PurchaseTableSkeleton'; @@ -32,7 +31,6 @@ import { isResponseSuccess } from '@/lib/api-helper'; import { BaseApiResponse } from '@/types/api/api-general'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import { ROWS_OPTIONS } from '@/config/constant'; import { Purchase } from '@/types/api/purchase/purchase'; import { PurchaseApi } from '@/services/api/purchase'; import { ExpenseApi } from '@/services/api/expense'; @@ -412,13 +410,13 @@ const PurchaseTable = () => { [updateFilter, setSearchValue] ); - const pageSizeChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - setPageSize(newVal.value as number); - }, - [setPageSize] - ); + // const pageSizeChangeHandler = useCallback( + // (val: OptionType | OptionType[] | null) => { + // const newVal = val as OptionType; + // setPageSize(newVal.value as number); + // }, + // [setPageSize] + // ); return ( <> diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 26577109..1269affc 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -16,7 +16,6 @@ import { formatDate, formatNumber, formatTitleCase, - cn, } from '@/lib/helper'; import { CustomerPaymentReport, diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index afcae9a4..c1b77bc4 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -6,7 +6,7 @@ import { useSelect } from '@/components/input/SelectInput'; import Modal, { useModal } from '@/components/Modal'; import Table from '@/components/Table'; import { isResponseSuccess } from '@/lib/api-helper'; -import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper'; +import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { AreaApi } from '@/services/api/master-data'; import { SupplierApi } from '@/services/api/master-data'; import { ProductApi } from '@/services/api/master-data'; diff --git a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx index 80d9da2e..d77c96b2 100644 --- a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx @@ -31,7 +31,6 @@ import { import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; import Modal, { useModal } from '@/components/Modal'; -import { cn } from '@/lib/helper'; import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import HppPerKandangSkeleton from '@/components/pages/report/marketing/skeleton/HppPerKandangSkeleton'; import { useEffect as useEffectHook } from 'react'; diff --git a/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx index 9e844ad3..6a1674bb 100644 --- a/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx +++ b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx @@ -37,7 +37,7 @@ import ProductionResultReportPDF from '../export/ProductionResultExportPDF'; import { pdf } from '@react-pdf/renderer'; import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import Modal, { useModal } from '@/components/Modal'; -import { cn, formatNumber } from '@/lib/helper'; +import { formatNumber } from '@/lib/helper'; import Pagination from '@/components/Pagination'; import ProductionResultSkeleton from '@/components/pages/report/production-result/skeleton/ProductionResultSkeleton'; diff --git a/src/lib/helper.ts b/src/lib/helper.ts index 383f35c3..7ae4aaae 100644 --- a/src/lib/helper.ts +++ b/src/lib/helper.ts @@ -272,7 +272,7 @@ export function transformAdjustmentSubtypes( export function transformLegacyFlagAliases( aliases: ConstantsApiResponse['legacy_flag_aliases'] ): OptionType[] { - return Object.entries(aliases).map(([key, value]) => ({ + return Object.entries(aliases).map(([key]) => ({ value: key, label: formatConstantLabel(key), })); From b786acf71a8220b9e3b084c63c1de7e599766d23 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 4 Mar 2026 14:11:17 +0700 Subject: [PATCH 02/33] chore(FE): Add setTableState to useEffect dependency arrays --- src/components/pages/master-data/area/AreasTable.tsx | 2 +- src/components/pages/master-data/bank/BanksTable.tsx | 2 +- src/components/pages/master-data/customer/CustomersTable.tsx | 2 +- src/components/pages/master-data/flock/FlocksTable.tsx | 2 +- src/components/pages/master-data/nonstock/NonstocksTable.tsx | 2 +- .../pages/master-data/product-category/ProductCategoryTable.tsx | 2 +- src/components/pages/master-data/product/ProductTable.tsx | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/pages/master-data/area/AreasTable.tsx b/src/components/pages/master-data/area/AreasTable.tsx index d329bb20..95f91ee9 100644 --- a/src/components/pages/master-data/area/AreasTable.tsx +++ b/src/components/pages/master-data/area/AreasTable.tsx @@ -143,7 +143,7 @@ const AreasTable = () => { useEffect(() => { setTableState('areas-table', pathname); - }, [pathname]); + }, [pathname, setTableState]); const searchChangeHandler: ChangeEventHandler = (e) => { setSearchValue(e.target.value); diff --git a/src/components/pages/master-data/bank/BanksTable.tsx b/src/components/pages/master-data/bank/BanksTable.tsx index a269a90a..cc62cf70 100644 --- a/src/components/pages/master-data/bank/BanksTable.tsx +++ b/src/components/pages/master-data/bank/BanksTable.tsx @@ -143,7 +143,7 @@ const BanksTable = () => { useEffect(() => { setTableState('banks-table', pathname); - }, [pathname]); + }, [pathname, setTableState]); const searchChangeHandler: ChangeEventHandler = (e) => { setSearchValue(e.target.value); diff --git a/src/components/pages/master-data/customer/CustomersTable.tsx b/src/components/pages/master-data/customer/CustomersTable.tsx index 54fe988f..1f02428a 100644 --- a/src/components/pages/master-data/customer/CustomersTable.tsx +++ b/src/components/pages/master-data/customer/CustomersTable.tsx @@ -145,7 +145,7 @@ const CustomersTable = () => { useEffect(() => { setTableState('customers-table', pathname); - }, [pathname]); + }, [pathname, setTableState]); const searchChangeHandler: ChangeEventHandler = (e) => { setSearchValue(e.target.value); diff --git a/src/components/pages/master-data/flock/FlocksTable.tsx b/src/components/pages/master-data/flock/FlocksTable.tsx index 8d464781..ed9f4007 100644 --- a/src/components/pages/master-data/flock/FlocksTable.tsx +++ b/src/components/pages/master-data/flock/FlocksTable.tsx @@ -145,7 +145,7 @@ const FlockTable = () => { useEffect(() => { setTableState('flocks-table', pathname); - }, [pathname]); + }, [pathname, setTableState]); const searchChangeHandler: ChangeEventHandler = (e) => { setSearchValue(e.target.value); diff --git a/src/components/pages/master-data/nonstock/NonstocksTable.tsx b/src/components/pages/master-data/nonstock/NonstocksTable.tsx index 0e009313..cb1b4a17 100644 --- a/src/components/pages/master-data/nonstock/NonstocksTable.tsx +++ b/src/components/pages/master-data/nonstock/NonstocksTable.tsx @@ -128,7 +128,7 @@ const NonstocksTable = () => { useEffect(() => { setTableState('nonstocks-table', pathname); - }, [pathname]); + }, [pathname, setTableState]); const [sorting, setSorting] = useState([]); diff --git a/src/components/pages/master-data/product-category/ProductCategoryTable.tsx b/src/components/pages/master-data/product-category/ProductCategoryTable.tsx index 16efe401..331f87f7 100644 --- a/src/components/pages/master-data/product-category/ProductCategoryTable.tsx +++ b/src/components/pages/master-data/product-category/ProductCategoryTable.tsx @@ -216,7 +216,7 @@ const ProductCategoryTable = () => { useEffect(() => { setTableState('product-category-table', pathname); - }, [pathname]); + }, [pathname, setTableState]); return ( <> diff --git a/src/components/pages/master-data/product/ProductTable.tsx b/src/components/pages/master-data/product/ProductTable.tsx index ba7fe66b..c58caeed 100644 --- a/src/components/pages/master-data/product/ProductTable.tsx +++ b/src/components/pages/master-data/product/ProductTable.tsx @@ -222,7 +222,7 @@ const ProductsTable = () => { useEffect(() => { setTableState('product-table', pathname); - }, [pathname]); + }, [pathname, setTableState]); const searchChangeHandler: ChangeEventHandler = (e) => { setSearchValue(e.target.value); From 7ae04b3f3ed63abdd88726eaad86eb42bea3284d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 4 Mar 2026 14:12:45 +0700 Subject: [PATCH 03/33] refactor(FE): Refactor DashboardProduction callbacks for memoization --- .../pages/dashboard/DashboardProduction.tsx | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/src/components/pages/dashboard/DashboardProduction.tsx b/src/components/pages/dashboard/DashboardProduction.tsx index 674f3719..27db0039 100644 --- a/src/components/pages/dashboard/DashboardProduction.tsx +++ b/src/components/pages/dashboard/DashboardProduction.tsx @@ -150,33 +150,39 @@ const DashboardProduction = () => { }, }); + const { resetForm } = formik; + const handleResetFilter = useCallback(() => { - formik.resetForm(); + resetForm(); resetFilterValues(); // Clear stored filter values setAnalysisMode('OVERVIEW'); setEndpointUrl('/dashboards'); setSelectedLocationIds([]); - }, [resetFilterValues, filterValues, selectedLocationIds]); + }, [resetForm, resetFilterValues]); - const handleApplyFilter = (values: DashboardFilter) => { - // Build query params object, only include non-empty values - const params: Record = {}; + const handleApplyFilter = useCallback( + (values: DashboardFilter) => { + // Build query params object, only include non-empty values + const params: Record = {}; - if (values.start_date) params.start_date = values.start_date; - if (values.end_date) params.end_date = values.end_date; - if (values.analysis_mode) params.analysis_mode = values.analysis_mode; - if (values.location_ids.length > 0) - params.location_ids = values.location_ids.toString(); - if (values.flock_ids.length > 0) - params.flock_ids = values.flock_ids.toString(); - if (values.kandang_ids.length > 0) - params.kandang_ids = values.kandang_ids.toString(); - if (values.comparison_type) params.comparison_type = values.comparison_type; + if (values.start_date) params.start_date = values.start_date; + if (values.end_date) params.end_date = values.end_date; + if (values.analysis_mode) params.analysis_mode = values.analysis_mode; + if (values.location_ids.length > 0) + params.location_ids = values.location_ids.toString(); + if (values.flock_ids.length > 0) + params.flock_ids = values.flock_ids.toString(); + if (values.kandang_ids.length > 0) + params.kandang_ids = values.kandang_ids.toString(); + if (values.comparison_type) + params.comparison_type = values.comparison_type; - setEndpointUrl(`/dashboards?${new URLSearchParams(params).toString()}`); - filterModal.closeModal(); - refreshDashboardProductionData(); - }; + setEndpointUrl(`/dashboards?${new URLSearchParams(params).toString()}`); + filterModal.closeModal(); + refreshDashboardProductionData(); + }, + [filterModal, refreshDashboardProductionData] + ); // ===== Load filter from store on mount ===== useEffect(() => { @@ -190,20 +196,20 @@ const DashboardProduction = () => { kandang_ids: normalizeToArray(filterValues.kandang), comparison_type: filterValues.comparisonType, }); - }, [filterValues]); + }, [filterValues, handleApplyFilter]); // ===== Formik Error List ===== const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); // ===== Export PDF ===== - const handleExportPDF = async () => { + const handleExportPDF = useCallback(async () => { await generateDashboardPDF({ filterValues: formik.values, allStatsRef, allChartsRef, setExporting, }); - }; + }, [formik.values]); // ===== Register Navbar Actions ===== const openFilterModalRef = useRef(filterModal.openModal); @@ -253,7 +259,7 @@ const DashboardProduction = () => { ); - }, [formik.values, exporting, setNavbarActions]); + }, [formik.values, exporting, setNavbarActions, handleExportPDF]); // Cleanup only on unmount useEffect(() => { From f4a522fc0cb0ca52f2cf578ddf0fab0c82475054 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 4 Mar 2026 16:48:21 +0700 Subject: [PATCH 04/33] refactor(FE): Refactor form field updates to use destructured Formik methods --- .../form/request/PurchaseRequestForm.tsx | 168 ++++++++++-------- 1 file changed, 96 insertions(+), 72 deletions(-) diff --git a/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx index 53ff5027..1fdb1e65 100644 --- a/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx +++ b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useFormik } from 'formik'; import useSWR from 'swr'; import { useRouter } from 'next/navigation'; @@ -234,6 +234,8 @@ const PurchaseRequestForm = ({ }, }); + const { setFieldValue, setFieldTouched, handleBlur } = formik; + const handleValidateForm = async () => { const errors = await formik.validateForm(); @@ -317,51 +319,73 @@ const PurchaseRequestForm = ({ }; // ===== UTILITY FUNCTIONS ===== - const updateCreditTermBasedOnSupplier = useCallback( - (supplierId: number) => { - if (supplierId > 0 && isResponseSuccess(supplierRawData)) { - const supplierData = supplierRawData.data.find( - (s: Supplier) => s.id === supplierId - ); - if (supplierData?.due_date) { - formik.setFieldTouched('credit_term', false); - formik.setFieldValue('credit_term', supplierData.due_date.toString()); - } else { - formik.setFieldTouched('credit_term', false); - formik.setFieldValue('credit_term', ''); - } - } else { - formik.setFieldTouched('credit_term', false); - formik.setFieldValue('credit_term', ''); - } - }, - [supplierRawData] - ); + const prevSupplierIdRef = useRef(''); - const resetPurchaseItems = useCallback(() => { - if (formik.values.items) { - formik.values.items.forEach((_, idx) => { - formik.setFieldTouched(`items.${idx}.product`, false); - formik.setFieldValue(`items.${idx}.product`, null); - formik.setFieldTouched(`items.${idx}.product_id`, false); - formik.setFieldValue(`items.${idx}.product_id`, 0); - formik.setFieldTouched(`items.${idx}.qty`, false); - formik.setFieldValue(`items.${idx}.qty`, 0); - }); - } - }, []); - - // ===== SIDE EFFECTS ===== useEffect(() => { - if (formik.values.supplier_id && Number(formik.values.supplier_id) > 0) { - updateCreditTermBasedOnSupplier(Number(formik.values.supplier_id)); - resetPurchaseItems(); - } else { - formik.setFieldTouched('credit_term', false); - formik.setFieldValue('credit_term', ''); - resetPurchaseItems(); + const currentSupplierId = formik.values.supplier_id || ''; + + if (currentSupplierId === prevSupplierIdRef.current) { + return; } - }, [formik.values.supplier_id]); + + prevSupplierIdRef.current = currentSupplierId; + + if (currentSupplierId && Number(currentSupplierId) > 0) { + if (isResponseSuccess(supplierRawData)) { + const supplierData = supplierRawData.data.find( + (s: Supplier) => s.id === Number(currentSupplierId) + ); + const newCreditTerm = supplierData?.due_date || 0; + if (formik.values.credit_term !== newCreditTerm) { + setFieldTouched('credit_term', false); + setFieldValue('credit_term', newCreditTerm); + } + } + + const itemsNeedReset = formik.values.items?.filter( + (item) => item.product_id !== 0 || item.product !== null + ); + if (itemsNeedReset && itemsNeedReset.length > 0) { + formik.values.items.forEach((item, idx) => { + if (item.product_id !== 0 || item.product !== null) { + setFieldTouched(`items.${idx}.product`, false); + setFieldValue(`items.${idx}.product`, null); + setFieldTouched(`items.${idx}.product_id`, false); + setFieldValue(`items.${idx}.product_id`, 0); + setFieldTouched(`items.${idx}.qty`, false); + setFieldValue(`items.${idx}.qty`, 0); + } + }); + } + } else { + if (formik.values.credit_term !== 0) { + setFieldTouched('credit_term', false); + setFieldValue('credit_term', 0); + } + const itemsNeedReset = formik.values.items?.filter( + (item) => item.product_id !== 0 || item.product !== null + ); + if (itemsNeedReset && itemsNeedReset.length > 0) { + formik.values.items.forEach((item, idx) => { + if (item.product_id !== 0 || item.product !== null) { + setFieldTouched(`items.${idx}.product`, false); + setFieldValue(`items.${idx}.product`, null); + setFieldTouched(`items.${idx}.product_id`, false); + setFieldValue(`items.${idx}.product_id`, 0); + setFieldTouched(`items.${idx}.qty`, false); + setFieldValue(`items.${idx}.qty`, 0); + } + }); + } + } + }, [ + formik.values.supplier_id, + formik.values.items, + formik.values.credit_term, + supplierRawData, + setFieldTouched, + setFieldValue, + ]); useEffect(() => { if (type !== 'add' && initialValues) { @@ -381,63 +405,63 @@ const PurchaseRequestForm = ({ const supplier = val as OptionType | null; const supplierId = Number(supplier?.value); - formik.setFieldTouched('supplier', true); - formik.setFieldValue('supplier', supplier); - formik.setFieldTouched('supplier_id', true); - formik.setFieldValue('supplier_id', supplierId); + setFieldTouched('supplier', true); + setFieldValue('supplier', supplier); + setFieldTouched('supplier_id', true); + setFieldValue('supplier_id', supplierId); }, - [] + [setFieldTouched, setFieldValue] ); const handleCreditTermChange = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; - formik.setFieldTouched('credit_term', true); - formik.setFieldValue('credit_term', value); + setFieldTouched('credit_term', true); + setFieldValue('credit_term', value); }, - [] + [setFieldTouched, setFieldValue] ); const handleCreditTermBlur = useCallback( (e: React.FocusEvent) => { - formik.handleBlur(e); + handleBlur(e); }, - [formik] + [handleBlur] ); const handleAreaChange = useCallback( (val: OptionType | OptionType[] | null) => { const area = val as OptionType | null; - formik.setFieldTouched('area_id', true); - formik.setFieldValue('area_id', (area as OptionType)?.value || 0); - formik.setFieldTouched('area', true); - formik.setFieldValue('area', area); + setFieldTouched('area_id', true); + setFieldValue('area_id', (area as OptionType)?.value || 0); + setFieldTouched('area', true); + 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); + setFieldTouched('location_id', false); + setFieldValue('location_id', 0); + setFieldTouched('location', false); + setFieldValue('location', null); }, - [] + [setFieldTouched, setFieldValue] ); const handleLocationChange = useCallback( (val: OptionType | OptionType[] | null) => { const location = val as OptionType | null; - formik.setFieldTouched('location_id', true); - formik.setFieldValue('location_id', (location as OptionType)?.value || 0); - formik.setFieldTouched('location', true); - formik.setFieldValue('location', location); + setFieldTouched('location_id', true); + setFieldValue('location_id', (location as OptionType)?.value || 0); + setFieldTouched('location', true); + setFieldValue('location', location); setSelectedLocation((location as OptionType)?.value as string); }, - [] + [setFieldTouched, setFieldValue] ); const handleWarehouseChange = useCallback( @@ -445,12 +469,12 @@ const PurchaseRequestForm = ({ const warehouse = val as OptionType | null; const warehouseId = (warehouse as OptionType)?.value || 0; - formik.setFieldTouched(`items.${idx}.warehouse`, true); - formik.setFieldValue(`items.${idx}.warehouse`, warehouse); - formik.setFieldTouched(`items.${idx}.warehouse_id`, true); - formik.setFieldValue(`items.${idx}.warehouse_id`, warehouseId); + setFieldTouched(`items.${idx}.warehouse`, true); + setFieldValue(`items.${idx}.warehouse`, warehouse); + setFieldTouched(`items.${idx}.warehouse_id`, true); + setFieldValue(`items.${idx}.warehouse_id`, warehouseId); }, - [] + [setFieldTouched, setFieldValue] ); // ===== PURCHASE ITEM OPERATIONS ===== From 1938f6cbda059a30ae40083c29ed62e90ec3bebd Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 4 Mar 2026 17:13:30 +0700 Subject: [PATCH 05/33] refactor(FE): Refactor DeliveryOrderProductTable to support delivery order rendering --- .../table-view/DeliveryOrderProductTable.tsx | 278 +++++++++++++++--- 1 file changed, 235 insertions(+), 43 deletions(-) diff --git a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx index 12d97b9a..71a6040c 100644 --- a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx @@ -2,10 +2,11 @@ import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/for import Button from '@/components/Button'; import Card from '@/components/Card'; import { Icon } from '@iconify/react'; -import { useRef } from 'react'; +import { useRef, useMemo } from 'react'; import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport'; -import { Marketing } from '@/types/api/marketing/marketing'; +import { Marketing, BaseDelivery } from '@/types/api/marketing/marketing'; +import { Warehouse } from '@/types/api/master-data/warehouse'; type DeliveryOrderProductTableProps = { data: DeliveryOrderProductFormValues[]; @@ -42,7 +43,31 @@ const DeliveryOrderProductTable = ({ const approvalStepNumber = marketing?.latest_approval?.step_number; - const renderTableContent = (item: DeliveryOrderProductFormValues) => { + const hasDeliveryOrder = useMemo(() => { + return ( + marketing?.delivery_order && + marketing.delivery_order.length > 0 && + marketing.delivery_order.some( + (doItem) => doItem.deliveries && doItem.deliveries.length > 0 + ) + ); + }, [marketing?.delivery_order]); + + const deliveryItems = useMemo(() => { + if (!hasDeliveryOrder) return []; + return ( + marketing?.delivery_order?.flatMap((doItem) => + doItem.deliveries.map((delivery) => ({ + ...delivery, + do_number: doItem.do_number, + delivery_date: doItem.delivery_date, + warehouse: doItem.warehouse, + })) + ) ?? [] + ); + }, [marketing?.delivery_order, hasDeliveryOrder]); + + const renderSalesOrderContent = (item: DeliveryOrderProductFormValues) => { const doItem = marketing?.delivery_order?.find( (doItem) => doItem.do_number === item.do_number ); @@ -185,50 +210,217 @@ const DeliveryOrderProductTable = ({ ); }; + const renderDeliveryOrderContent = ( + item: BaseDelivery & { + do_number: string; + delivery_date: string; + warehouse: Warehouse; + } + ) => { + const parentDoItem = marketing?.delivery_order?.find( + (doItem) => doItem.do_number === item.do_number + ); + + return ( + <> + + + Label + + +
+
Value
+
+ + + <> + + Gudang + {item.warehouse?.name} + + + Produk + + {item.product_warehouse?.product?.name} + + + + Qty + + {item.qty + ? `${formatNumber(item.qty)} ${item.product_warehouse?.product?.uom?.name ?? ''}` + : '-'} + + + {Number(item.avg_weight ?? 0) > 0 && ( + + Avg Bobot + + {formatNumber(Number(item.avg_weight))} Kg + + + )} + {Number(item.total_weight ?? 0) > 0 && ( + + Total Bobot + + {formatNumber(Number(item.total_weight))} + + + )} + + Total Harga Satuan + + {formatCurrency(item.unit_price)} + + + + Total Penjualan + + {formatCurrency(item.total_price)} + + + + + + Label + + +
+
Value
+
+ + + <> + {approvalStepNumber !== 1 && ( + + Tanggal Pengiriman + + {item.delivery_date + ? formatDate(item.delivery_date, 'DD MMM YYYY') + : '-'} + + + )} + {item.do_number && ( + + No. Pengiriman + {item.do_number} + + )} + + No. Polisi + {item.vehicle_number} + + {parentDoItem && ( + + Dokumen Pengiriman + + + + + )} + + + ); + }; + return ( <>
- {data.map((item) => ( -
- {formType === 'success' ? ( -
- - {renderTableContent(item)} -
+ {hasDeliveryOrder + ? deliveryItems.map((item, index) => ( +
+ {formType === 'success' ? ( +
+ + + {renderDeliveryOrderContent(item)} + +
+
+ ) : ( + + + + {renderDeliveryOrderContent(item)} + +
+
+ )}
- ) : ( - - - {renderTableContent(item)} -
-
- )} -
- ))} + )) + : data.map((item) => ( +
+ {formType === 'success' ? ( +
+ + + {renderSalesOrderContent(item)} + +
+
+ ) : ( + + + + {renderSalesOrderContent(item)} + +
+
+ )} +
+ ))}
); From 4de21561b39b57fbc8ffa6b856ca451908f2c36d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Mar 2026 09:41:25 +0700 Subject: [PATCH 06/33] refactor(FE): Refactor delivery order value initialization logic --- .../marketing/DeliveryOrderFormModal.tsx | 54 +++++++++------- .../delivery-order/DeliverOrderProduct.tsx | 61 ++++++++++++++++--- 2 files changed, 86 insertions(+), 29 deletions(-) diff --git a/src/components/pages/marketing/DeliveryOrderFormModal.tsx b/src/components/pages/marketing/DeliveryOrderFormModal.tsx index b4dd1e6e..4635c826 100644 --- a/src/components/pages/marketing/DeliveryOrderFormModal.tsx +++ b/src/components/pages/marketing/DeliveryOrderFormModal.tsx @@ -111,16 +111,34 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => { useState(null); const [deliveryOrderValues, setDeliveryOrderValues] = useState< DeliveryOrderProductFormValues[] - >( - isResponseSuccess(marketing) - ? mergeSOwithDO( - marketing?.data.sales_order?.map(SalesProductToFieldValues) ?? [], - marketing?.data.delivery_order?.flatMap((delivery) => - DeliveryProductToFieldValues(marketing.data.sales_order, delivery) - ) ?? [], - true - ) - : [] + >([]); + + const getDeliveryOrderValues = useCallback( + (marketingData: Marketing): DeliveryOrderProductFormValues[] => { + const hasDeliveryOrder = + marketingData.delivery_order && + marketingData.delivery_order.length > 0 && + marketingData.delivery_order.some( + (doItem) => doItem.deliveries && doItem.deliveries.length > 0 + ); + + if (hasDeliveryOrder) { + return ( + marketingData.delivery_order?.flatMap((delivery) => + DeliveryProductToFieldValues(marketingData.sales_order, delivery) + ) ?? [] + ); + } + + return mergeSOwithDO( + marketingData.sales_order?.map(SalesProductToFieldValues) ?? [], + marketingData.delivery_order?.flatMap((delivery) => + DeliveryProductToFieldValues(marketingData.sales_order, delivery) + ) ?? [], + true + ); + }, + [] ); // ================== SETUP FORMIK ================== @@ -129,14 +147,8 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => { >(() => { if (!isResponseSuccess(marketing)) return {} as SalesOrderFormValues & DeliveryOrderFormValues; - const deliveryValues = mergeSOwithDO( - marketing?.data.sales_order?.map(SalesProductToFieldValues) ?? [], - marketing?.data.delivery_order?.flatMap((delivery) => - DeliveryProductToFieldValues(marketing.data.sales_order, delivery) - ) ?? [], - true - ); + const deliveryValues = getDeliveryOrderValues(marketing.data); setDeliveryOrderValues(deliveryValues); return { @@ -162,7 +174,7 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => { ) ?? [], delivery_order: deliveryValues, }; - }, [marketing]); + }, [marketing, getDeliveryOrderValues]); const formik = useFormik({ enableReinitialize: true, @@ -647,9 +659,8 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => { No. Order - {marketing.data.do_number - ? marketing.data.do_number - : marketing.data.so_number} + {marketing.data.do_number || + marketing.data.so_number} @@ -764,6 +775,7 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => { Promise; @@ -115,6 +120,36 @@ const DeliveryOrderProductForm = ({ }) ?.filter((item) => item != null) as OptionType[]; + const hasDeliveryOrder = useMemo(() => { + return ( + deliveryOrders && + deliveryOrders.length > 0 && + deliveryOrders.some( + (doItem) => doItem.deliveries && doItem.deliveries.length > 0 + ) + ); + }, [deliveryOrders]); + + const deliveryOrder = useMemo(() => { + if (!hasDeliveryOrder || !deliveryOrders) return null; + + for (const doItem of deliveryOrders) { + const found = doItem.deliveries.find( + (d) => + d.product_warehouse.id === + initialValues?.marketing_product?.product_warehouse_id + ); + if (found) { + return { + ...found, + delivery_date: doItem.delivery_date, + do_number: doItem.do_number, + }; + } + } + return null; + }, [deliveryOrders, hasDeliveryOrder, initialValues]); + const salesOrder = salesOrders.find( (item) => item.id === initialValues?.marketing_product_id ); @@ -122,15 +157,25 @@ const DeliveryOrderProductForm = ({ const formik = useFormik({ enableReinitialize: true, initialValues: { - delivery_date: initialValues?.delivery_date || undefined, - vehicle_number: initialValues?.vehicle_number || undefined, + delivery_date: + deliveryOrder?.delivery_date || + initialValues?.delivery_date || + undefined, + vehicle_number: + deliveryOrder?.vehicle_number || + initialValues?.vehicle_number || + undefined, marketing_product_id: salesOrder?.id || initialValues?.marketing_product_id || undefined, - unit_price: initialValues?.unit_price || undefined, - total_weight: initialValues?.total_weight || undefined, - qty: initialValues?.qty || undefined, - avg_weight: initialValues?.avg_weight || undefined, - total_price: initialValues?.total_price || undefined, + unit_price: + deliveryOrder?.unit_price ?? initialValues?.unit_price ?? undefined, + total_weight: + deliveryOrder?.total_weight ?? initialValues?.total_weight ?? undefined, + qty: deliveryOrder?.qty ?? initialValues?.qty ?? undefined, + avg_weight: + deliveryOrder?.avg_weight ?? initialValues?.avg_weight ?? undefined, + total_price: + deliveryOrder?.total_price ?? initialValues?.total_price ?? undefined, marketing_product: initialValues?.marketing_product || undefined, uom: initialValues?.uom || '', weight_per_convertion: From 807041834b8d39f326c07014558c0892ebfb49fc Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Mar 2026 09:54:50 +0700 Subject: [PATCH 07/33] refactor(FE): Refactor formik field updates to use destructured methods --- .../uniformity/form/UniformityForm.tsx | 53 ++++++++++--------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/src/components/pages/production/uniformity/form/UniformityForm.tsx b/src/components/pages/production/uniformity/form/UniformityForm.tsx index 6cbb134e..811764d8 100644 --- a/src/components/pages/production/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityForm.tsx @@ -279,6 +279,8 @@ const UniformityForm = ({ }, }); + const { setFieldValue, setFieldTouched } = formik; + const handleValidateForm = async () => { const errors = await formik.validateForm(); @@ -301,10 +303,10 @@ const UniformityForm = ({ 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); + setFieldTouched('location', true); + setFieldValue('location', location); + setFieldTouched('location_id', true); + setFieldValue('location_id', locationId); setSelectedLocation(location); setSelectedProjectFlock(null); @@ -312,7 +314,7 @@ const UniformityForm = ({ location ? location.value.toString() : '' ); }, - [] + [setFieldTouched, setFieldValue] ); const handleProjectFlockChange = useCallback( @@ -320,14 +322,14 @@ const UniformityForm = ({ 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); + setFieldTouched('project_flock', true); + setFieldValue('project_flock', projectFlock); + setFieldTouched('project_flock_id', true); + setFieldValue('project_flock_id', projectFlockId); setSelectedProjectFlock(projectFlock); }, - [] + [setFieldTouched, setFieldValue] ); const handleKandangChange = useCallback( @@ -335,24 +337,24 @@ const UniformityForm = ({ 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); + setFieldTouched('kandang', true); + setFieldValue('kandang', kandang); + setFieldTouched('kandang_id', true); + setFieldValue('kandang_id', kandangId); setSelectedKandang(kandang); }, - [] + [setFieldTouched, setFieldValue] ); const handleFileChange = useCallback( (e: React.ChangeEvent) => { const document = e.target.files?.[0]; - formik.setFieldTouched('document', true); + setFieldTouched('document', true); if (!document) { - formik.setFieldValue('document', undefined); + setFieldValue('document', undefined); return; } @@ -372,24 +374,24 @@ const UniformityForm = ({ return; } - formik.setFieldValue('document', document); + setFieldValue('document', document); }, - [] + [setFieldTouched, setFieldValue] ); const handleDateChange = useCallback( (e: React.ChangeEvent) => { - formik.setFieldValue('date', e.target.value); + setFieldValue('date', e.target.value); }, - [] + [setFieldValue] ); const handleRemoveFile = useCallback(() => { - formik.setFieldValue('document', undefined); + setFieldValue('document', undefined); if (fileInputRef.current) { fileInputRef.current.value = ''; } - }, [formik]); + }, [setFieldValue]); const handleDownloadTemplate = useCallback(() => { const population = projectFlockKandangLookup?.population; @@ -442,9 +444,9 @@ const UniformityForm = ({ const weeksDiff = Math.floor(daysDiff / 7); - formik.setFieldValue('week', initialWeek + weeksDiff); + setFieldValue('week', initialWeek + weeksDiff); } else { - formik.setFieldValue('week', initialWeek); + setFieldValue('week', initialWeek); } } }, [ @@ -452,6 +454,7 @@ const UniformityForm = ({ projectFlockKandangLookup?.project_flock_kandang_id, recordingsData, formik.values.date, + setFieldValue, ]); useEffect(() => { From 1acbc91cfe880270af2697fea54c9dff14d4c10e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Mar 2026 10:19:30 +0700 Subject: [PATCH 08/33] refactor(FE): Reset dependent fields when location or project flock changes --- .../uniformity/form/UniformityForm.tsx | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/components/pages/production/uniformity/form/UniformityForm.tsx b/src/components/pages/production/uniformity/form/UniformityForm.tsx index 811764d8..feb7ca08 100644 --- a/src/components/pages/production/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityForm.tsx @@ -393,6 +393,77 @@ const UniformityForm = ({ } }, [setFieldValue]); + // ===== RESET PROJECT FLOCK & KANDANG WHEN LOCATION CHANGES ===== + const prevLocationIdRef = useRef(''); + + useEffect(() => { + const currentLocationId = formik.values.location_id || ''; + + if (currentLocationId === prevLocationIdRef.current) { + return; + } + + prevLocationIdRef.current = currentLocationId; + + if (formik.values.project_flock !== null) { + setFieldTouched('project_flock', false); + setFieldValue('project_flock', null); + } + if (formik.values.project_flock_id !== 0) { + setFieldTouched('project_flock_id', false); + setFieldValue('project_flock_id', 0); + } + + if (formik.values.kandang !== null) { + setFieldTouched('kandang', false); + setFieldValue('kandang', null); + } + if (formik.values.kandang_id !== 0) { + setFieldTouched('kandang_id', false); + setFieldValue('kandang_id', 0); + } + + setSelectedProjectFlock(null); + setSelectedKandang(null); + }, [ + formik.values.location_id, + formik.values.project_flock, + formik.values.project_flock_id, + formik.values.kandang, + formik.values.kandang_id, + setFieldTouched, + setFieldValue, + ]); + + const prevProjectFlockIdRef = useRef(''); + + useEffect(() => { + const currentProjectFlockId = formik.values.project_flock_id || ''; + + if (currentProjectFlockId === prevProjectFlockIdRef.current) { + return; + } + + prevProjectFlockIdRef.current = currentProjectFlockId; + + if (formik.values.kandang !== null) { + setFieldTouched('kandang', false); + setFieldValue('kandang', null); + } + if (formik.values.kandang_id !== 0) { + setFieldTouched('kandang_id', false); + setFieldValue('kandang_id', 0); + } + + setSelectedKandang(null); + }, [ + formik.values.project_flock_id, + formik.values.kandang, + formik.values.kandang_id, + setFieldTouched, + setFieldValue, + ]); + const handleDownloadTemplate = useCallback(() => { const population = projectFlockKandangLookup?.population; From c98e7d8cb345f4a78a01df0ea599e53bc8e34016 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Mar 2026 10:24:39 +0700 Subject: [PATCH 09/33] refactor(FE): Fix population check to ensure proper boolean evaluation --- .../pages/production/uniformity/form/UniformityForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/production/uniformity/form/UniformityForm.tsx b/src/components/pages/production/uniformity/form/UniformityForm.tsx index feb7ca08..8840c021 100644 --- a/src/components/pages/production/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityForm.tsx @@ -760,7 +760,7 @@ const UniformityForm = ({
- {projectFlockKandangLookup?.population && ( + {!!projectFlockKandangLookup?.population && ( <>
From b5fc1d431040499116043b695ac90f1544e2b1c0 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Mar 2026 10:57:13 +0700 Subject: [PATCH 10/33] refactor(FE): Refactor tab actions to use memoized component --- .../report/finance/tab/CustomerPaymentTab.tsx | 177 ++++++++++-------- 1 file changed, 99 insertions(+), 78 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 1269affc..b04ce96a 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useCallback, useEffect } from 'react'; +import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Card from '@/components/Card'; @@ -66,6 +66,8 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { const [dateErrorShown, setDateErrorShown] = useState(false); const [hasDateError, setHasDateError] = useState(false); + const handleFilterModalOpenRef = useRef(() => {}); + const filterModal = useModal(); const dataTypeOptions = useMemo( @@ -83,11 +85,6 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { loadMore: loadMoreCustomers, } = useSelect(CustomerApi.basePath, 'id', 'name', 'search'); - const handleFilterModalOpen = () => { - filterModal.openModal(); - formik.validateForm(); - }; - // ===== FORMIK SETUP ===== const formik = useFormik({ initialValues: { @@ -122,6 +119,12 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { }, }); + // Set the ref callback after formik is initialized + handleFilterModalOpenRef.current = () => { + filterModal.openModal(); + formik.validateForm(); + }; + const getPaymentStatusBadgeColor = (notes: string): Color => { const normalizedValue = notes.toLowerCase(); @@ -212,7 +215,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { dataTypeOptions.find((opt) => opt.value === formik.values.filter_by) || null ); - }, [formik.values.filter_by]); + }, [formik.values.filter_by, dataTypeOptions]); // ===== DATA FETCHING ===== const { data: customerPayment, isLoading } = useSWR( @@ -349,90 +352,107 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { } }, [customerPaymentExport, filterParams, customerOptions]); - // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useTabActionsStore((state) => state.setTabActions); - const clearTabActions = useTabActionsStore((state) => state.clearTabActions); + // ===== TAB ACTIONS COMPONENT ===== + const TabActions = useMemo(() => { + return function TabActionsComponent() { + const setTabActions = useTabActionsStore((state) => state.setTabActions); + const clearTabActions = useTabActionsStore( + (state) => state.clearTabActions + ); - useEffect(() => { - setTabActions( - tabId, -
- + const formikValuesRef = useRef(formik.values); + formikValuesRef.current = formik.values; - { + setTabActions( + tabId, +
+ handleFilterModalOpenRef.current()} variant='outline' - color='none' - isLoading={isAnyExportLoading} - className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft' + className='px-3 py-2.5' + /> + + +
+ + + Export + +
+ + +
+ + } > -
- + + + +
+ ); + }, [setTabActions]); - Export + useEffect(() => { + return () => { + clearTabActions(tabId); + }; + }, [clearTabActions]); -
- - -
- - } - > - - - -
- ); + return null; + }; }, [ tabId, - formik.values, isAnyExportLoading, handleExportExcel, handleExportPdf, - filterModal.open, - setTabActions, + isExcelExportLoading, + isPdfExportLoading, + formik.values, ]); - useEffect(() => { - return () => { - clearTabActions(tabId); - }; - }, [tabId, clearTabActions]); + const TabActionsElement = useMemo(() => , [TabActions]); const getTableColumns = ( summary: CustomerPaymentSummary @@ -682,6 +702,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { return ( <> + {TabActionsElement}
{!isSubmitted ? ( Date: Thu, 5 Mar 2026 11:01:52 +0700 Subject: [PATCH 11/33] refactor(FE): Refactor CustomerPaymentTab to use filterParams instead of formik values --- .../pages/report/finance/tab/CustomerPaymentTab.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index b04ce96a..b0c2637e 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -119,7 +119,6 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { }, }); - // Set the ref callback after formik is initialized handleFilterModalOpenRef.current = () => { filterModal.openModal(); formik.validateForm(); @@ -360,15 +359,12 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { (state) => state.clearTabActions ); - const formikValuesRef = useRef(formik.values); - formikValuesRef.current = formik.values; - useEffect(() => { setTabActions( tabId,
handleFilterModalOpenRef.current()} variant='outline' @@ -449,7 +445,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { handleExportPdf, isExcelExportLoading, isPdfExportLoading, - formik.values, + filterParams, ]); const TabActionsElement = useMemo(() => , [TabActions]); From ea88c3ce8ee62ff8737e4d4d42ab53cb0ec006ed Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Mar 2026 11:10:36 +0700 Subject: [PATCH 12/33] refactor(FE): Refactor tab actions to use memoized component --- .../report/finance/tab/DebtSupplierTab.tsx | 181 ++++++++++-------- 1 file changed, 102 insertions(+), 79 deletions(-) diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index fb02e959..57d37a23 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -17,7 +17,7 @@ import { generateDebtSupplierExcel } from '@/components/pages/report/finance/exp import { generateDebtSupplierPDF } from '@/components/pages/report/finance/export/DebtSupllierExportPDF'; import { Icon } from '@iconify/react'; import { ColumnDef } from '@tanstack/react-table'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import toast from 'react-hot-toast'; import useSWR from 'swr'; import { DebtSupplierApi } from '@/services/api/report/debt-supplier'; @@ -91,6 +91,8 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { const [dateErrorShown, setDateErrorShown] = useState(false); const [hasDateError, setHasDateError] = useState(false); + const handleFilterModalOpenRef = useRef(() => {}); + const filterModal = useModal(); const { @@ -108,11 +110,6 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { [] ); - const handleFilterModalOpen = () => { - filterModal.openModal(); - formik.validateForm(); - }; - // ===== FORMIK SETUP ===== const formik = useFormik({ initialValues: { @@ -146,6 +143,11 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { }, }); + handleFilterModalOpenRef.current = () => { + filterModal.openModal(); + formik.validateForm(); + }; + // ===== DATA FETCHING ===== const { data: debtSupplier, isLoading } = useSWR( isSubmitted @@ -268,92 +270,112 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { } finally { setIsPdfExportLoading(false); } - }, [debtSupplierExport]); + }, [ + debtSupplierExport, + formik.values.supplierIds, + formik.values.filterBy, + formik.values.startDate, + formik.values.endDate, + ]); - // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useTabActionsStore((state) => state.setTabActions); - const clearTabActions = useTabActionsStore((state) => state.clearTabActions); + // ===== TAB ACTIONS COMPONENT ===== + const TabActions = useMemo(() => { + return function TabActionsComponent() { + const setTabActions = useTabActionsStore((state) => state.setTabActions); + const clearTabActions = useTabActionsStore( + (state) => state.clearTabActions + ); - useEffect(() => { - setTabActions( - tabId, -
- - - { + setTabActions( + tabId, +
+ handleFilterModalOpenRef.current()} variant='outline' - color='none' - isLoading={isAnyExportLoading} - className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft' + className='px-3 py-2.5' + /> + + +
+ + + Export + +
+ + +
+ + } > -
- + + + +
+ ); + }, [setTabActions]); - Export + useEffect(() => { + return () => { + clearTabActions(tabId); + }; + }, [clearTabActions]); -
- - -
- - } - > - - - -
- ); + return null; + }; }, [ tabId, - formik.values, + filterParams, isAnyExportLoading, handleExportExcel, handleExportPdf, - setTabActions, + isExcelExportLoading, + isPdfExportLoading, ]); - // Cleanup on unmount - useEffect(() => { - return () => { - clearTabActions(tabId); - }; - }, [tabId, clearTabActions]); + const TabActionsElement = useMemo(() => , [TabActions]); useEffect(() => { return () => { @@ -587,6 +609,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { ]; return ( <> + {TabActionsElement}
{!isSubmitted ? ( Date: Thu, 5 Mar 2026 11:21:56 +0700 Subject: [PATCH 13/33] refactor(FE): Refactor tab actions to use memoized component --- .../tab/PurchasesPerSupplierTab.tsx | 185 ++++++++++-------- 1 file changed, 103 insertions(+), 82 deletions(-) diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index c1b77bc4..c8ffa399 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -20,7 +20,7 @@ import { generatePurchasesPerSupplierExcel } from '@/components/pages/report/log import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF'; import { Icon } from '@iconify/react'; import { ColumnDef } from '@tanstack/react-table'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import toast from 'react-hot-toast'; import useSWR from 'swr'; import { useFormik } from 'formik'; @@ -65,6 +65,8 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { const [dateErrorShown, setDateErrorShown] = useState(false); const [hasDateError, setHasDateError] = useState(false); + const handleFilterModalOpenRef = useRef(() => {}); + const filterModal = useModal(); // ===== OPTIONS ===== @@ -104,11 +106,6 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { [] ); - const handleFilterModalOpen = () => { - filterModal.openModal(); - formik.validateForm(); - }; - // ===== FORMIK SETUP ===== const formik = useFormik({ initialValues: { @@ -151,11 +148,18 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { }, }); + handleFilterModalOpenRef.current = () => { + filterModal.openModal(); + formik.validateForm(); + }; + + const { setFieldValue } = formik; + // ===== DATE CHANGE HANDLERS ===== const handleStartDateChange = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; - formik.setFieldValue('start_date', value || null); + setFieldValue('start_date', value || null); if (value && formik.values.end_date) { const startDate = new Date(value); @@ -180,13 +184,13 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { setHasDateError(false); } }, - [formik, dateErrorShown] + [setFieldValue, dateErrorShown, formik.values.end_date] ); const handleEndDateChange = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; - formik.setFieldValue('end_date', value || null); + setFieldValue('end_date', value || null); if (value && formik.values.start_date) { const startDateObj = new Date(formik.values.start_date); @@ -210,7 +214,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { setDateErrorShown(false); } }, - [formik, dateErrorShown] + [setFieldValue, dateErrorShown, formik.values.start_date] ); // ===== DERIVED VALUES ===== @@ -443,88 +447,104 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { productCategoryOptions, ]); - // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useTabActionsStore((state) => state.setTabActions); - const clearTabActions = useTabActionsStore((state) => state.clearTabActions); + // ===== TAB ACTIONS COMPONENT ===== + const TabActions = useMemo(() => { + return function TabActionsComponent() { + const setTabActions = useTabActionsStore((state) => state.setTabActions); + const clearTabActions = useTabActionsStore( + (state) => state.clearTabActions + ); - useEffect(() => { - setTabActions( - tabId, -
- - - { + setTabActions( + tabId, +
+ handleFilterModalOpenRef.current()} variant='outline' - color='none' - isLoading={isAnyExportLoading} - className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft' + className='px-3 py-2.5' + /> + + +
+ + + Export + +
+ + +
+ + } > -
- + + + +
+ ); + }, [setTabActions]); - Export + useEffect(() => { + return () => { + clearTabActions(tabId); + }; + }, [clearTabActions]); -
- - -
- - } - > - - - -
- ); + return null; + }; }, [ tabId, - formik.values, + filterParams, isAnyExportLoading, - filterModal.open, - setTabActions, + handleExportExcel, + handleExportPdf, + isExcelExportLoading, + isPdfExportLoading, ]); - useEffect(() => { - return () => { - clearTabActions(tabId); - }; - }, [tabId, clearTabActions]); + const TabActionsElement = useMemo(() => , [TabActions]); const getTableColumns = ( summary: LogisticPurchasePerSupplierSummary @@ -704,6 +724,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { return ( <> + {TabActionsElement}
{!isSubmitted ? ( Date: Thu, 5 Mar 2026 11:40:32 +0700 Subject: [PATCH 14/33] refactor(FE): Refactor tab actions into a memoized component --- .../report/expense/tab/ReportExpenseTab.tsx | 255 ++++++++++-------- 1 file changed, 139 insertions(+), 116 deletions(-) diff --git a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx index 971536c5..03c35958 100644 --- a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx +++ b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx @@ -1,6 +1,12 @@ 'use client'; -import React, { useState, useCallback, useEffect, useMemo } from 'react'; +import React, { + useState, + useCallback, + useEffect, + useMemo, + useRef, +} from 'react'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; @@ -68,37 +74,10 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); + const handleFilterModalOpenRef = useRef(() => {}); + const filterModal = useModal(); - // ===== OPTIONS ===== - const { - setInputValue: setLocationInputValue, - options: locationOptions, - isLoadingOptions: isLoadingLocations, - loadMore: loadMoreLocations, - } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); - - const { - setInputValue: setSupplierInputValue, - options: supplierOptions, - isLoadingOptions: isLoadingSuppliers, - loadMore: loadMoreSuppliers, - } = useSelect(SupplierApi.basePath, 'id', 'name', 'search'); - - const { - setInputValue: setKandangInputValue, - options: kandangOptions, - isLoadingOptions: isLoadingKandangs, - loadMore: loadMoreKandangs, - } = useSelect(KandangApi.basePath, 'id', 'name', 'search'); - - const { - setInputValue: setNonstockInputValue, - options: nonstockOptions, - isLoadingOptions: isLoadingNonstocks, - loadMore: loadMoreNonstocks, - } = useSelect(NonstockApi.basePath, 'id', 'name', 'search'); - const categoryOptions = useMemo( () => [ { value: 'BOP', label: 'BOP' }, @@ -149,6 +128,48 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { }, }); + handleFilterModalOpenRef.current = () => { + filterModal.openModal(); + formik.validateForm(); + }; + + // ===== OPTIONS ===== + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocations, + loadMore: loadMoreLocations, + } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); + + const { + setInputValue: setSupplierInputValue, + options: supplierOptions, + isLoadingOptions: isLoadingSuppliers, + loadMore: loadMoreSuppliers, + } = useSelect(SupplierApi.basePath, 'id', 'name', 'search'); + + const { + setInputValue: setKandangInputValue, + options: kandangOptions, + isLoadingOptions: isLoadingKandangs, + loadMore: loadMoreKandangs, + } = useSelect( + KandangApi.basePath, + 'id', + 'name', + 'search', + formik.values.location_id?.value + ? { location_id: String(formik.values.location_id.value) } + : undefined + ); + + const { + setInputValue: setNonstockInputValue, + options: nonstockOptions, + isLoadingOptions: isLoadingNonstocks, + loadMore: loadMoreNonstocks, + } = useSelect(NonstockApi.basePath, 'id', 'name', 'search'); + // ===== FILTER VALUES ===== const locationValue = useMemo( () => formik.values.location_id, @@ -268,13 +289,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { return; } - const pdfParams = { - location_name: locationValue?.label, - supplier_name: supplierValue?.label, - realization_date: formik.values.realization_date || undefined, - }; - - await generateReportExpensePDF(allData, pdfParams); + await generateReportExpensePDF(allData); toast.success('PDF berhasil dibuat dan diunduh.'); } catch { @@ -282,98 +297,105 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { } finally { setIsPdfExportLoading(false); } - }, [ - reportExpenseExport, - locationValue, - supplierValue, - kandangValue, - nonstockValue, - categoryValue, - formik.values.realization_date, - ]); + }, [reportExpenseExport]); - // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useTabActionsStore((state) => state.setTabActions); - const clearTabActions = useTabActionsStore((state) => state.clearTabActions); + // ===== TAB ACTIONS COMPONENT ===== + const TabActions = useMemo(() => { + return function TabActionsComponent() { + const setTabActions = useTabActionsStore((state) => state.setTabActions); + const clearTabActions = useTabActionsStore( + (state) => state.clearTabActions + ); - useEffect(() => { - setTabActions( - tabId, -
- filterModal.openModal()} - variant='outline' - className='px-3 py-2.5' - /> - - { + setTabActions( + tabId, +
+ handleFilterModalOpenRef.current()} variant='outline' - color='none' - isLoading={isAnyExportLoading} - className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft' + className='px-3 py-2.5' + /> + + +
+ + + Export + +
+ + +
+ + } > -
- + + + +
+ ); + }, [setTabActions]); - Export + useEffect(() => { + return () => { + clearTabActions(tabId); + }; + }, [clearTabActions]); -
- - -
- - } - > - - - -
- ); + return null; + }; }, [ tabId, - formik.values, + filterParams, isAnyExportLoading, handleExportExcel, handleExportPDF, - setTabActions, + isExcelExportLoading, + isPdfExportLoading, ]); - useEffect(() => { - return () => { - clearTabActions(tabId); - }; - }, [tabId, clearTabActions]); + const TabActionsElement = useMemo(() => , [TabActions]); // ===== TABLE COLUMNS DEFINITION ===== const columns = useMemo((): ColumnDef[] => { @@ -505,6 +527,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { return ( <> + {TabActionsElement}
{!isSubmitted ? ( Date: Thu, 5 Mar 2026 11:41:43 +0700 Subject: [PATCH 15/33] refactor(FE): Remove unused `params` argument from generateReportExpensePDF --- .../pages/report/expense/export/ReportExpenseExportPDF.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/pages/report/expense/export/ReportExpenseExportPDF.tsx b/src/components/pages/report/expense/export/ReportExpenseExportPDF.tsx index 6ec2c559..352b0fa8 100644 --- a/src/components/pages/report/expense/export/ReportExpenseExportPDF.tsx +++ b/src/components/pages/report/expense/export/ReportExpenseExportPDF.tsx @@ -30,8 +30,7 @@ const getStatusColor = (action?: string): [number, number, number] => { }; export const generateReportExpensePDF = async ( - data: ReportExpense[], - params: PDFParams + data: ReportExpense[] ): Promise => { // Inisialisasi dokumen dengan tipe yang sudah diekstensi const doc = new jsPDF('l', 'mm', 'a4') as jsPDFWithAutoTable; From a7951b6c28417d98f02ae9d60bd0280e5a08f9a2 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Mar 2026 11:47:16 +0700 Subject: [PATCH 16/33] refactor(FE): Fix missing dependencies in UniformityTable effect hooks --- .../pages/production/uniformity/UniformityTable.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index ca2c1c17..55d74955 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -503,7 +503,7 @@ const UniformityTable = () => { filterFormik.resetForm(); filterModal.closeModal(); - }, [filterFormik, updateFilter]); + }, [filterFormik, updateFilter, filterModal]); const selectedRowIds = useMemo(() => { return Object.keys(rowSelection) @@ -551,7 +551,13 @@ const UniformityTable = () => { } } } - }, [searchParams, uniformities]); + }, [ + searchParams, + uniformities, + router, + singleApproveModal, + singleRejectModal, + ]); useEffect(() => { if (isSuccess) { From 2de6636bbfd2bc1902dac2f56c991da314ea0172 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Mar 2026 11:59:27 +0700 Subject: [PATCH 17/33] refactor(FE): Refactor tab actions to use memoized components --- .../marketing/tab/DailyMarketingTab.tsx | 217 +++++++++--------- .../report/marketing/tab/HppPerKandangTab.tsx | 173 +++++++------- ...ProductionResultProjectFlockKandangTab.tsx | 170 ++++++++------ 3 files changed, 306 insertions(+), 254 deletions(-) diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index d30e9c25..27195f5b 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useCallback } from 'react'; +import { useState, useMemo, useCallback, useRef } from 'react'; import useSWR from 'swr'; import { useSelect } from '@/components/input/SelectInput'; import DateInput from '@/components/input/DateInput'; @@ -83,6 +83,8 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { const [dateErrorShown, setDateErrorShown] = useState(false); const [hasDateError, setHasDateError] = useState(false); + const handleFilterModalOpenRef = useRef(() => {}); + const filterModal = useModal(); // ===== OPTIONS ===== @@ -102,11 +104,6 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { const { options: customerOptions, isLoadingOptions: isLoadingCustomers } = useSelect(CustomerApi.basePath, 'id', 'name', 'search'); - const handleFilterModalOpen = () => { - filterModal.openModal(); - formik.validateForm(); - }; - // ===== FORMIK SETUP ===== const formik = useFormik({ initialValues: { @@ -353,121 +350,126 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { } }, [dailyMarketingsExport, summaryTotal]); - // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useTabActionsStore((state) => state.setTabActions); - const clearTabActions = useTabActionsStore((state) => state.clearTabActions); + // ===== TAB ACTIONS COMPONENT ===== + const TabActions = useMemo(() => { + return function TabActionsComponent() { + const setTabActions = useTabActionsStore((state) => state.setTabActions); + const clearTabActions = useTabActionsStore( + (state) => state.clearTabActions + ); - useEffectHook(() => { - setTabActions( - tabId, -
- - } - className={{ - wrapper: 'w-full min-w-48 max-w-3xs', - inputWrapper: 'rounded-xl! shadow-button-soft', - input: 'placeholder:font-semibold placeholder:text-base-content/50', - }} - /> - - - - -
+ useEffectHook(() => { + setTabActions( + tabId, +
+ + } + className={{ + wrapper: 'w-full min-w-48 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', + }} + /> - Export + handleFilterModalOpenRef.current()} + variant='outline' + className='px-3 py-2.5' + /> -
+ +
+ - -
- - } - > - - -
-
- ); + Export + +
+ + +
+ + } + > + + + +
+ ); + }, [setTabActions]); + + useEffectHook(() => { + return () => { + clearTabActions(tabId); + }; + }, [clearTabActions]); + + return null; + }; }, [ tabId, + filterParams, searchValue, - formik.values, isAnyExportLoading, - filterModal.open, - setTabActions, + handleExportExcel, + handleExportPDF, + isExcelExportLoading, + isPdfExportLoading, + searchChangeHandler, ]); - useEffectHook(() => { - return () => { - clearTabActions(tabId); - }; - }, [tabId, clearTabActions]); - - useEffectHook(() => { - return () => { - if (dateErrorShown) { - toast.dismiss(); - } - }; - }, [dateErrorShown]); - - useEffectHook(() => { - return () => { - if (dateErrorShown) { - toast.dismiss(); - setDateErrorShown(false); - } - }; - }, [filterModal.open, dateErrorShown]); + const TabActionsElement = useMemo(() => , [TabActions]); const getTableColumns = (): ColumnDef[] => { const tableColumns: ColumnDef[] = [ @@ -639,6 +641,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { return ( <> + {TabActionsElement}
{!isSubmitted ? ( { // ===== FILTER STATE ===== const [filterParams, setFilterParams] = useState({}); + const handleFilterModalOpenRef = useRef(() => {}); + const filterModal = useModal(); // ===== OPTIONS ===== @@ -95,11 +97,6 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { [] ); - const handleFilterModalOpen = () => { - filterModal.openModal(); - formik.validateForm(); - }; - // ===== FORMIK SETUP ===== const formik = useFormik({ initialValues: { @@ -140,6 +137,11 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { }, }); + handleFilterModalOpenRef.current = () => { + filterModal.openModal(); + formik.validateForm(); + }; + // ===== WEIGHT CHANGE HANDLERS ===== const handleWeightMinChange = useCallback( (e: React.ChangeEvent) => { @@ -442,87 +444,103 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { allDocSuppliers, ]); - // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useTabActionsStore((state) => state.setTabActions); - const clearTabActions = useTabActionsStore((state) => state.clearTabActions); + // ===== TAB ACTIONS COMPONENT ===== + const TabActions = useMemo(() => { + return function TabActionsComponent() { + const setTabActions = useTabActionsStore((state) => state.setTabActions); + const clearTabActions = useTabActionsStore( + (state) => state.clearTabActions + ); - useEffectHook(() => { - setTabActions( - tabId, -
- - - { + setTabActions( + tabId, +
+ handleFilterModalOpenRef.current()} variant='outline' - color='none' - isLoading={isAnyExportLoading} - className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft' + className='px-3 py-2.5' + /> + + +
+ + + Export + +
+ + +
+ + } > -
- + + + +
+ ); + }, [setTabActions]); - Export + useEffectHook(() => { + return () => { + clearTabActions(tabId); + }; + }, [clearTabActions]); -
- - -
- - } - > - - - -
- ); + return null; + }; }, [ tabId, - formik.values, + filterParams, isAnyExportLoading, - filterModal.open, - setTabActions, + handleExportExcel, + handleExportPDF, + isExcelExportLoading, + isPdfExportLoading, ]); - useEffectHook(() => { - return () => { - clearTabActions(tabId); - }; - }, [tabId, clearTabActions]); + const TabActionsElement = useMemo(() => , [TabActions]); const getTableColumns = (): ColumnDef[] => { const tableColumns: ColumnDef[] = [ @@ -767,6 +785,7 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { return ( <> + {TabActionsElement}
{!isSubmitted ? ( { const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); + const handleFilterModalOpenRef = useRef(() => {}); + const filterModal = useModal(); // ===== TABLE COLUMNS ===== @@ -242,6 +250,11 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { }, }); + handleFilterModalOpenRef.current = () => { + filterModal.openModal(); + formik.validateForm(); + }; + // ===== OPTIONS ===== const { setInputValue: setAreaInputValue, @@ -519,91 +532,108 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { setIsPdfExportLoading(false); }, [filterParams]); - // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useTabActionsStore((state) => state.setTabActions); - const clearTabActions = useTabActionsStore((state) => state.clearTabActions); + // ===== TAB ACTIONS COMPONENT ===== + const TabActions = useMemo(() => { + return function TabActionsComponent() { + const setTabActions = useTabActionsStore((state) => state.setTabActions); + const clearTabActions = useTabActionsStore( + (state) => state.clearTabActions + ); - useEffect(() => { - setTabActions( - tabId, -
- filterModal.openModal()} - variant='outline' - className='px-3 py-2.5' - /> - - { + setTabActions( + tabId, +
+ handleFilterModalOpenRef.current()} variant='outline' - color='none' - isLoading={isAnyExportLoading} - className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft' + className='px-3 py-2.5' + /> + + +
+ + + Export + +
+ + +
+ + } > -
- + + + +
+ ); + }, [setTabActions]); - Export + useEffect(() => { + return () => { + clearTabActions(tabId); + }; + }, [clearTabActions]); -
- - -
- - } - > - - - -
- ); + return null; + }; }, [ tabId, filterParams, isAnyExportLoading, exportToExcelHandler, exportToPdfHandler, - setTabActions, + isExcelExportLoading, + isPdfExportLoading, ]); - useEffect(() => { - return () => { - clearTabActions(tabId); - }; - }, [tabId, clearTabActions]); + // Render the TabActions component + const TabActionsElement = useMemo(() => , [TabActions]); return ( <> + {TabActionsElement}
{!isSubmitted ? ( Date: Thu, 5 Mar 2026 12:03:54 +0700 Subject: [PATCH 18/33] fix(FE): Add handler to open filter modal and trigger form validation --- .../pages/report/marketing/tab/DailyMarketingTab.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index 27195f5b..49bb798e 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -142,6 +142,11 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { }, }); + handleFilterModalOpenRef.current = () => { + filterModal.openModal(); + formik.validateForm(); + }; + // ===== SEARCH CHANGE HANDLER ===== const searchChangeHandler = useCallback( (e: React.ChangeEvent) => { From a4cb4e202b4272a6a3e78a5ab44119ecb1f9739b Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Mar 2026 13:36:43 +0700 Subject: [PATCH 19/33] refactor(FE): Fix dependency arrays and refactor setFieldValue usage --- .../production/recording/RecordingTable.tsx | 5 +--- .../recording/form/RecordingForm.tsx | 29 ++++++++++--------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 3c0db941..460a335a 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -531,7 +531,7 @@ const RecordingTable = () => { useEffect(() => { setTableState('recording-table', pathname); - }, [pathname]); + }, [pathname, setTableState]); const searchChangeHandler = useCallback( (e: React.ChangeEvent) => { @@ -1118,14 +1118,11 @@ const RecordingTable = () => { }, ], [ - isRecordingApproved, tableFilterState.pageSize, tableFilterState.page, - selectedRecording, singleDeleteModal, approveModal, rejectModal, - rowSelection, setRowSelection, setApprovalNotes, setSelectedRecording, diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index b903a8af..fd3a818d 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -1007,6 +1007,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }); // ===== HELPER FUNCTIONS ===== + const { setFieldValue } = formik; + const getAvailableStock = useCallback( (productWarehouseId: number) => { if ((type as 'add' | 'edit' | 'detail') === 'detail') return 0; @@ -1098,16 +1100,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const isAlreadyRecorded = recordedProjectFlockKandangIds.has( projectFlockKandangLookup.project_flock_kandang_id ); - let color: 'neutral' | 'success' | 'warning' | 'error'; - - if (isAlreadyRecorded) { - color = 'warning'; - } else { - color = 'success'; - } + const color = isAlreadyRecorded ? 'warning' : 'success'; return ( - + Periode {projectFlockKandangLookup.project_flock?.period} ); @@ -1411,9 +1407,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } if (formik.values.project_flock_kandang_id !== projectFlockKandangId) { - formik.setFieldValue('project_flock_kandang_id', projectFlockKandangId); + setFieldValue('project_flock_kandang_id', projectFlockKandangId); - formik.setFieldValue('project_flock_kandang', { + setFieldValue('project_flock_kandang', { value: projectFlockKandangId, label: projectFlockKandangLookup ? `${projectFlockKandangLookup.project_flock.flock_name} - ${projectFlockKandangLookup.kandang.name}` @@ -1431,6 +1427,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { nextDayRecording, existingRecordings, today, + currentRecordDate, + duplicateErrorShown, + setFieldValue, ]); useEffect(() => { @@ -1472,11 +1471,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { formik.values.project_flock_kandang_id !== projectFlockKandangDetail.id ) { - formik.setFieldValue( + setFieldValue( 'project_flock_kandang_id', projectFlockKandangDetail.id ); - formik.setFieldValue('project_flock_kandang', { + setFieldValue('project_flock_kandang', { value: projectFlockKandangDetail.id, label: `${projectFlock.flock_name} - ${kandang.name}`, }); @@ -1490,6 +1489,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { type, enhancedProjectFlockOptions, formik.values.project_flock_kandang_id, + setFieldValue, ]); const approveHandler = async (notes: string) => { @@ -1653,10 +1653,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { if (isLayingCategory && (type as 'add' | 'edit' | 'detail') !== 'detail') { const layingValues = formik.values as RecordingLayingFormValues; if (!layingValues.eggs || layingValues.eggs.length === 0) { - formik.setFieldValue('eggs', [{ product_warehouse_id: 0, qty: '' }]); + setFieldValue('eggs', [{ product_warehouse_id: 0, qty: '' }]); } } - }, [isLayingCategory, type]); + }, [isLayingCategory, type, formik.values, setFieldValue]); useEffect(() => { if (type !== 'add') { @@ -3199,6 +3199,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { pageSize={100} className={{ tableWrapperClassName: 'overflow-x-auto', + containerClassName: 'mb-6!', tableClassName: 'w-full table-auto text-sm', headerRowClassName: 'border-b border-b-gray-200', headerColumnClassName: From 4b49cd18f5e217fccf34e26d124384d9f04c612a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Mar 2026 13:45:01 +0700 Subject: [PATCH 20/33] refactor(FE): Refactor form handlers to use consistent formik utilities --- .../order/PurchaseOrderAcceptApprovalForm.tsx | 121 ++++----- .../order/PurchaseOrderStaffApprovalForm.tsx | 239 +++++++++--------- 2 files changed, 186 insertions(+), 174 deletions(-) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index 464bee47..8000cf2f 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -95,62 +95,6 @@ const PurchaseOrderAcceptApprovalForm = ({ }; }; - // ===== SUBMISSION HANDLERS ===== - const createAcceptApprovalHandler = useCallback( - async (payload: CreateAcceptApprovalRequestPayload) => { - const purchaseRequestId = searchParams.get('purchaseId') - ? parseInt(searchParams.get('purchaseId')!) - : initialValues?.id || 1; - - if (!purchaseRequestId) { - setPurchaseOrderFormErrorMessage('Purchase Request ID is required'); - return; - } - - const res = await PurchaseApi.acceptApproval.create( - purchaseRequestId, - payload - ); - - if (isResponseError(res)) { - setPurchaseOrderFormErrorMessage(res.message); - return; - } - toast.success(res?.message as string); - refreshApprovals?.(); - onRefetchData?.(); - formik.resetForm(); - onCancel?.(); - onModalClose?.(); - router.refresh(); - }, - [ - initialValues?.id, - searchParams, - refreshApprovals, - onModalClose, - onRefetchData, - ] - ); - - const updateAcceptApprovalHandler = useCallback( - async (purchaseId: number, payload: CreateAcceptApprovalRequestPayload) => { - const res = await PurchaseApi.acceptApproval.create(purchaseId, payload); - if (isResponseError(res)) { - setPurchaseOrderFormErrorMessage(res.message); - return; - } - toast.success(res?.message as string); - refreshApprovals?.(); - onRefetchData?.(); - formik.resetForm(); - onCancel?.(); - onModalClose?.(); - router.refresh(); - }, - [refreshApprovals, onModalClose, onRefetchData] - ); - // ===== SELECT INPUT DATA ===== const { options: expeditionVendors, @@ -244,6 +188,67 @@ const PurchaseOrderAcceptApprovalForm = ({ }, }); + const { resetForm, setFieldValue } = formik; + + // ===== SUBMISSION HANDLERS ===== + const createAcceptApprovalHandler = useCallback( + async (payload: CreateAcceptApprovalRequestPayload) => { + const purchaseRequestId = searchParams.get('purchaseId') + ? parseInt(searchParams.get('purchaseId')!) + : initialValues?.id || 1; + + if (!purchaseRequestId) { + setPurchaseOrderFormErrorMessage('Purchase Request ID is required'); + return; + } + + const res = await PurchaseApi.acceptApproval.create( + purchaseRequestId, + payload + ); + + if (isResponseError(res)) { + setPurchaseOrderFormErrorMessage(res.message); + return; + } + toast.success(res?.message as string); + refreshApprovals?.(); + onRefetchData?.(); + resetForm(); + onCancel?.(); + onModalClose?.(); + router.refresh(); + }, + [ + initialValues?.id, + searchParams, + refreshApprovals, + onModalClose, + onRefetchData, + onCancel, + router, + resetForm, + ] + ); + + const updateAcceptApprovalHandler = useCallback( + async (purchaseId: number, payload: CreateAcceptApprovalRequestPayload) => { + const res = await PurchaseApi.acceptApproval.create(purchaseId, payload); + if (isResponseError(res)) { + setPurchaseOrderFormErrorMessage(res.message); + return; + } + toast.success(res?.message as string); + refreshApprovals?.(); + onRefetchData?.(); + resetForm(); + onCancel?.(); + onModalClose?.(); + router.refresh(); + }, + [refreshApprovals, onModalClose, onRefetchData, onCancel, router, resetForm] + ); + const handleValidateForm = async () => { const errors = await formik.validateForm(); @@ -307,9 +312,9 @@ const PurchaseOrderAcceptApprovalForm = ({ transport_per_item: item.transport_per_item || '', }; }); - formik.setFieldValue('items', updatedItems); + setFieldValue('items', updatedItems); } - }, [purchaseItems, initialValues, key]); + }, [purchaseItems, initialValues, key, setFieldValue]); // ===== HELPER FUNCTIONS ===== const getQuantityExceededError = useCallback( diff --git a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx index 1e674f4f..e96e7342 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx @@ -146,120 +146,6 @@ const PurchaseOrderStaffApprovalForm = ({ return !item.purchase_item_id || item.purchase_item_id === 0; }; - // ===== SUBMISSION HANDLERS ===== - const createStaffApprovalHandler = useCallback( - async (payload: CreateStaffApprovalRequestPayload) => { - const purchaseRequestId = searchParams.get('purchaseId') - ? parseInt(searchParams.get('purchaseId')!) - : initialValues?.id || 1; - - if (!purchaseRequestId) { - setPurchaseOrderFormErrorMessage('Purchase Request ID is required'); - return; - } - - const res = await PurchaseApi.staffApproval.create( - purchaseRequestId, - payload - ); - - if (isResponseError(res)) { - setPurchaseOrderFormErrorMessage(res.message); - return; - } - toast.success(res?.message as string); - refreshApprovals?.(); - onRefetchData?.(); - formik.resetForm(); - onCancel?.(); - onModalClose?.(); - router.refresh(); - }, - [ - initialValues?.id, - searchParams, - refreshApprovals, - onModalClose, - onRefetchData, - ] - ); - - const updateStaffApprovalHandler = useCallback( - async (purchaseId: number, payload: UpdateStaffApprovalRequestPayload) => { - const res = await PurchaseApi.staffApproval.update(purchaseId, payload); - if (isResponseError(res)) { - setPurchaseOrderFormErrorMessage(res.message); - return; - } - toast.success(res?.message as string); - refreshApprovals?.(); - onRefetchData?.(); - formik.resetForm(); - onCancel?.(); - onModalClose?.(); - router.refresh(); - }, - [refreshApprovals, onModalClose, onRefetchData] - ); - - // ===== DELETE HANDLER ===== - const deleteItemsHandler = useCallback(async () => { - const purchaseRequestId = searchParams.get('purchaseId') - ? parseInt(searchParams.get('purchaseId')!) - : initialValues?.id || 1; - - if (!purchaseRequestId) { - toast.error('Purchase Request ID is required'); - return; - } - - const itemIdsToDelete = selectedItemForDelete - ? [selectedItemForDelete] - : []; - - if (itemIdsToDelete.length === 0) { - toast.error('Tidak ada item yang dipilih untuk dihapus'); - return; - } - - try { - const res = await PurchaseApi.items.delete(purchaseRequestId, { - item_ids: itemIdsToDelete, - }); - - if (isResponseError(res)) { - toast.error(res.message || 'Gagal menghapus item pembelian'); - return; - } - - const successMessage = 'Item pembelian berhasil dihapus'; - toast.success(successMessage); - - refreshApprovals?.(); - onRefetchData?.(); - deleteModal.closeModal(); - setSelectedItemForDelete(null); - setSelectedItemIndex(null); - - if (selectedItemIndex !== null) { - const updatedPurchaseItems = formik.values.items?.filter( - (_, i) => i !== selectedItemIndex - ); - formik.setFieldValue('items', updatedPurchaseItems); - } - } catch { - toast.error('Terjadi kesalahan saat menghapus item pembelian'); - } - }, [ - initialValues?.id, - searchParams, - selectedItemForDelete, - selectedItemIndex, - refreshApprovals, - onRefetchData, - deleteModal, - ]); - // ===== API DATA FETCHING FOR SUPPLIER PRODUCTS ===== const { data: supplierData, isLoading: isLoadingSupplierProducts } = useSWR( initialValues?.supplier?.id @@ -418,6 +304,127 @@ const PurchaseOrderStaffApprovalForm = ({ }, }); + const { resetForm, setFieldValue } = formik; + + // ===== SUBMISSION HANDLERS ===== + const createStaffApprovalHandler = useCallback( + async (payload: CreateStaffApprovalRequestPayload) => { + const purchaseRequestId = searchParams.get('purchaseId') + ? parseInt(searchParams.get('purchaseId')!) + : initialValues?.id || 1; + + if (!purchaseRequestId) { + setPurchaseOrderFormErrorMessage('Purchase Request ID is required'); + return; + } + + const res = await PurchaseApi.staffApproval.create( + purchaseRequestId, + payload + ); + + if (isResponseError(res)) { + setPurchaseOrderFormErrorMessage(res.message); + return; + } + toast.success(res?.message as string); + refreshApprovals?.(); + onRefetchData?.(); + resetForm(); + onCancel?.(); + onModalClose?.(); + router.refresh(); + }, + [ + initialValues?.id, + searchParams, + refreshApprovals, + onModalClose, + onRefetchData, + resetForm, + onCancel, + router, + ] + ); + + const updateStaffApprovalHandler = useCallback( + async (purchaseId: number, payload: UpdateStaffApprovalRequestPayload) => { + const res = await PurchaseApi.staffApproval.update(purchaseId, payload); + if (isResponseError(res)) { + setPurchaseOrderFormErrorMessage(res.message); + return; + } + toast.success(res?.message as string); + refreshApprovals?.(); + onRefetchData?.(); + resetForm(); + onCancel?.(); + onModalClose?.(); + router.refresh(); + }, + [refreshApprovals, onModalClose, onRefetchData, resetForm, onCancel, router] + ); + + // ===== DELETE HANDLER ===== + const deleteItemsHandler = useCallback(async () => { + const purchaseRequestId = searchParams.get('purchaseId') + ? parseInt(searchParams.get('purchaseId')!) + : initialValues?.id || 1; + + if (!purchaseRequestId) { + toast.error('Purchase Request ID is required'); + return; + } + + const itemIdsToDelete = selectedItemForDelete + ? [selectedItemForDelete] + : []; + + if (itemIdsToDelete.length === 0) { + toast.error('Tidak ada item yang dipilih untuk dihapus'); + return; + } + + try { + const res = await PurchaseApi.items.delete(purchaseRequestId, { + item_ids: itemIdsToDelete, + }); + + if (isResponseError(res)) { + toast.error(res.message || 'Gagal menghapus item pembelian'); + return; + } + + const successMessage = 'Item pembelian berhasil dihapus'; + toast.success(successMessage); + + refreshApprovals?.(); + onRefetchData?.(); + deleteModal.closeModal(); + setSelectedItemForDelete(null); + setSelectedItemIndex(null); + + if (selectedItemIndex !== null) { + const updatedPurchaseItems = formik.values.items?.filter( + (_, i) => i !== selectedItemIndex + ); + setFieldValue('items', updatedPurchaseItems); + } + } catch { + toast.error('Terjadi kesalahan saat menghapus item pembelian'); + } + }, [ + initialValues?.id, + searchParams, + selectedItemForDelete, + selectedItemIndex, + refreshApprovals, + onRefetchData, + deleteModal, + setFieldValue, + formik.values.items, + ]); + const handleValidateForm = async () => { const errors = await formik.validateForm(); @@ -519,9 +526,9 @@ const PurchaseOrderStaffApprovalForm = ({ }; return itemData; }); - formik.setFieldValue('items', updatedItems); + setFieldValue('items', updatedItems); } - }, [purchaseItems, type, initialValues]); + }, [purchaseItems, type, initialValues, setFieldValue]); // ===== PURCHASE ITEM OPERATIONS ===== const addPurchaseItem = () => { From 5dac900a1ae3890ccbaadbd285f8315d3af1199d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Mar 2026 14:11:58 +0700 Subject: [PATCH 21/33] refactor(FE): Refactor date filter state management in UniformityTable --- .../production/uniformity/UniformityTable.tsx | 26 ++++++++++++++++--- .../uniformity/detail/UniformityDetail.tsx | 14 +++++++--- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index 55d74955..9f148bfb 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -249,6 +249,10 @@ const UniformityTable = () => { const [isSubmitted, setIsSubmitted] = useState(false); // ===== FILTER STATE ===== + const [filterDateValues, setFilterDateValues] = useState({ + start_date: tableFilterState.start_date, + end_date: tableFilterState.end_date, + }); const [filterLocation, setFilterLocation] = useState(null); const [filterProjectFlock, setFilterProjectFlock] = useState(null); @@ -343,8 +347,8 @@ const UniformityTable = () => { // ===== FORMIK FILTER ===== const filterFormik = useFormik({ initialValues: { - start_date: tableFilterState.start_date, - end_date: tableFilterState.end_date, + start_date: filterDateValues.start_date, + end_date: filterDateValues.end_date, location: filterLocation, project_flock: filterProjectFlock, project_flock_kandang_id: filterProjectFlockKandangId, @@ -381,6 +385,13 @@ const UniformityTable = () => { const { formErrorList, close, handleFormSubmit } = useFormikErrorList(filterFormik); + useEffect(() => { + setFilterDateValues({ + start_date: tableFilterState.start_date, + end_date: tableFilterState.end_date, + }); + }, [tableFilterState.start_date, tableFilterState.end_date]); + // ===== BUILD SWR KEY WITH FILTERS ===== const uniformitySwrKey = useMemo(() => { const basePath = UniformityApi.basePath; @@ -494,6 +505,7 @@ const UniformityTable = () => { setFilterKandang(null); setFilterProjectFlockKandangId(undefined); setFilterErrors({}); + setFilterDateValues({ start_date: '', end_date: '' }); updateFilter('start_date', ''); updateFilter('end_date', ''); @@ -1293,7 +1305,7 @@ const UniformityTable = () => {
{/* Rentang Waktu */}
-