From cce84a3a6f8f8ea5c35666e9ae76fc43c09d4796 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 09:46:19 +0700 Subject: [PATCH 01/41] refactor(FE): Refactor ButtonFilter to support excluded fields with useMemo --- src/components/helper/ButtonFilter.tsx | 30 ++++++++++++++++++++------ 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/components/helper/ButtonFilter.tsx b/src/components/helper/ButtonFilter.tsx index cff1d167..b403a83f 100644 --- a/src/components/helper/ButtonFilter.tsx +++ b/src/components/helper/ButtonFilter.tsx @@ -3,15 +3,33 @@ import { getFilledFormikValuesCount } from '@/lib/formik-helper'; import { cn } from '@/lib/helper'; import { Icon } from '@iconify/react'; import { FormikValues } from 'formik'; +import { useMemo } from 'react'; export type ButtonFilterProps = ButtonProps & { values: FormikValues; onClick: () => void; + excludeFields?: string[]; }; // 'bg-gradient-to-t from-blue-50 to-blue-100 border-blue-500 text-blue-600 hover:from-blue-100 hover:to-blue-200 -const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => { +const ButtonFilter = ({ + values, + onClick, + excludeFields = [], + ...props +}: ButtonFilterProps) => { + const filteredValues = useMemo(() => { + const result: FormikValues = {}; + Object.keys(values).forEach((key) => { + if (!excludeFields.includes(key)) { + result[key] = values[key]; + } + }); + return result; + }, [values, excludeFields]); + + const activeCount = getFilledFormikValuesCount(filteredValues); return ( From 47a243977790bdb768ac4c969641852c3dbce8a6 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 10:07:26 +0700 Subject: [PATCH 02/41] feat(FE): Add support for grouped fields in ButtonFilter active count calculation --- src/components/helper/ButtonFilter.tsx | 30 ++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/components/helper/ButtonFilter.tsx b/src/components/helper/ButtonFilter.tsx index b403a83f..4ea9bb4f 100644 --- a/src/components/helper/ButtonFilter.tsx +++ b/src/components/helper/ButtonFilter.tsx @@ -9,6 +9,7 @@ export type ButtonFilterProps = ButtonProps & { values: FormikValues; onClick: () => void; excludeFields?: string[]; + fieldGroups?: string[][]; }; // 'bg-gradient-to-t from-blue-50 to-blue-100 border-blue-500 text-blue-600 hover:from-blue-100 hover:to-blue-200 @@ -17,19 +18,36 @@ const ButtonFilter = ({ values, onClick, excludeFields = [], + fieldGroups = [], ...props }: ButtonFilterProps) => { - const filteredValues = useMemo(() => { - const result: FormikValues = {}; + const activeCount = useMemo(() => { + const filteredValues: FormikValues = {}; Object.keys(values).forEach((key) => { if (!excludeFields.includes(key)) { - result[key] = values[key]; + filteredValues[key] = values[key]; } }); - return result; - }, [values, excludeFields]); - const activeCount = getFilledFormikValuesCount(filteredValues); + let count = getFilledFormikValuesCount(filteredValues); + + fieldGroups.forEach((group) => { + const groupFields = group.filter( + (field) => !excludeFields.includes(field) + ); + const filledGroupFields = groupFields.filter( + (field) => filteredValues[field] + ); + if ( + filledGroupFields.length === groupFields.length && + groupFields.length > 1 + ) { + count -= groupFields.length - 1; + } + }); + + return count; + }, [values, excludeFields, fieldGroups]); return ( + className='px-3 py-2.5' + /> diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index 0bf00833..de43ba68 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -30,6 +30,7 @@ import PopoverButton from '@/components/popover/PopoverButton'; import PopoverContent from '@/components/popover/PopoverContent'; import StatusBadge from '@/components/helper/StatusBadge'; import MarketingFilterModal from '@/components/pages/marketing/MarketingFilter'; +import ButtonFilter from '@/components/helper/ButtonFilter'; const RowsOptionsMenu = ({ props, @@ -214,32 +215,6 @@ const MarketingTable = () => { updateFilter('customer_id', ''); }; - // ===== ACTIVE FILTERS COUNT ===== - const activeFiltersCount = useMemo(() => { - let count = 0; - - // Product filter - if (tableFilterState.product_ids) { - count += 1; - } - - // Status filter - if (tableFilterState.status) { - count += 1; - } - - // Customer filter - if (tableFilterState.customer_id) { - count += 1; - } - - return count; - }, [ - tableFilterState.product_ids, - tableFilterState.status, - tableFilterState.customer_id, - ]); - const approveClickHandler = () => { setApproveAction('APPROVED'); confirmationModal.openModal(); @@ -588,28 +563,14 @@ const MarketingTable = () => { )}
- + className='px-3 py-2.5' + /> void }) => { ); }, [formik.values.period, periodOptions]); - // ===== ACTIVE FILTERS COUNT ===== - const activeFiltersCount = useMemo(() => { - let count = 0; - if (tableFilterState.area_id) count += 1; - if (tableFilterState.location_id) count += 1; - if (tableFilterState.kandang_id) count += 1; - if (tableFilterState.category) count += 1; - if (tableFilterState.period) count += 1; - return count; - }, [ - tableFilterState.area_id, - tableFilterState.location_id, - tableFilterState.kandang_id, - tableFilterState.category, - tableFilterState.period, - ]); - - const hasFilters = activeFiltersCount > 0; - // ===== FILTER DEPENDENCY HANDLERS ===== const handleFilterAreaChange = (area: OptionType | null) => { const areaId = area?.value ? String(area.value) : undefined; @@ -961,25 +943,12 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { }} /> - + className='px-3 py-2.5' + /> = { @@ -511,36 +512,6 @@ const RecordingTable = () => { ); }, [formik.values.kandang_id, kandangOptions]); - // ===== ACTIVE FILTERS COUNT ===== - const activeFiltersCount = useMemo(() => { - let count = 0; - - if (tableFilterState.areaFilter) { - count += 1; - } - - if (tableFilterState.locationFilter) { - count += 1; - } - - if (tableFilterState.kandangFilter) { - count += 1; - } - - if (tableFilterState.projectFlockKandangFilter) { - count += 1; - } - - return count; - }, [ - tableFilterState.areaFilter, - tableFilterState.locationFilter, - tableFilterState.kandangFilter, - tableFilterState.projectFlockKandangFilter, - ]); - - const hasFilters = activeFiltersCount > 0; - // ===== HANDLE FILTER MODAL OPEN ===== const handleFilterModalOpen = () => { filterModal.openModal(); @@ -1264,25 +1235,12 @@ const RecordingTable = () => { }} /> - + className='px-3 py-2.5' + />
diff --git a/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx b/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx index bf4c31e3..9a9813f0 100644 --- a/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx +++ b/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx @@ -34,6 +34,7 @@ import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { Color } from '@/types/theme'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import ButtonFilter from '@/components/helper/ButtonFilter'; const RowOptionsMenu = ({ props, @@ -159,30 +160,6 @@ const TransferToLayingsTable = () => { TransferToLayingApi.getAllFetcher ); - const filterCount = useMemo(() => { - let count = 0; - - if (tableFilterState.startDate && tableFilterState.endDate) { - count += 1; - } - - if (tableFilterState.flockSource.length > 0) { - count += 1; - } - - if (tableFilterState.flockDestination.length > 0) { - count += 1; - } - - if (tableFilterState.status.length > 0) { - count += 1; - } - - return count; - }, [tableFilterState]); - - const isFilterActive = filterCount > 0; - const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = useState(false); @@ -559,30 +536,19 @@ const TransferToLayingsTable = () => { }} /> - + className='px-3 py-2.5' + /> { const { state: tableFilterState, + updateFilter, setPage, + setPageSize, toQueryString: getTableFilterQueryString, } = useTableFilter({ initial: { search: '', + start_date: '', + end_date: '', + location_id: '', + project_flock_id: '', + kandang_id: '', }, paramMap: { page: 'page', pageSize: 'limit', search: 'search', + start_date: 'start_date', + end_date: 'end_date', + location_id: 'location_id', + project_flock_id: 'project_flock_id', + kandang_id: 'kandang_id', }, }); @@ -233,8 +246,6 @@ const UniformityTable = () => { const [filterKandang, setFilterKandang] = useState(null); const [filterProjectFlockKandangId, setFilterProjectFlockKandangId] = useState(undefined); - const [filterStartDate, setFilterStartDate] = useState(''); - const [filterEndDate, setFilterEndDate] = useState(''); const [filterProjectFlockLocationId, setFilterProjectFlockLocationId] = useState(''); const [, setFilterErrors] = useState>({}); @@ -319,8 +330,8 @@ const UniformityTable = () => { // ===== FORMIK FILTER ===== const filterFormik = useFormik({ initialValues: { - start_date: filterStartDate, - end_date: filterEndDate, + start_date: tableFilterState.start_date, + end_date: tableFilterState.end_date, location: filterLocation, project_flock: filterProjectFlock, project_flock_kandang_id: filterProjectFlockKandangId, @@ -329,8 +340,21 @@ const UniformityTable = () => { validationSchema: UniformityTableFilterSchema, enableReinitialize: true, onSubmit: async (values) => { - setFilterStartDate(values.start_date); - setFilterEndDate(values.end_date); + updateFilter('start_date', values.start_date); + updateFilter('end_date', values.end_date); + updateFilter( + 'location_id', + values.location?.value ? String(values.location.value) : '' + ); + updateFilter( + 'project_flock_id', + values.project_flock?.value ? String(values.project_flock.value) : '' + ); + updateFilter( + 'kandang_id', + values.kandang?.value ? String(values.kandang.value) : '' + ); + setFilterLocation(values.location ?? null); setFilterProjectFlock(values.project_flock ?? null); setFilterKandang(values.kandang ?? null); @@ -356,11 +380,11 @@ const UniformityTable = () => { filterProjectFlockKandangId.toString() ); } - if (filterStartDate) { - queryParams.append('start_date', filterStartDate); + if (tableFilterState.start_date) { + queryParams.append('start_date', tableFilterState.start_date); } - if (filterEndDate) { - queryParams.append('end_date', filterEndDate); + if (tableFilterState.end_date) { + queryParams.append('end_date', tableFilterState.end_date); } queryParams.append('with_chart', 'true'); } @@ -379,8 +403,8 @@ const UniformityTable = () => { }, [ isSubmitted, filterProjectFlockKandangId, - filterStartDate, - filterEndDate, + tableFilterState.start_date, + tableFilterState.end_date, getTableFilterQueryString, ]); @@ -456,30 +480,16 @@ const UniformityTable = () => { setFilterProjectFlock(null); setFilterKandang(null); setFilterProjectFlockKandangId(undefined); - setFilterStartDate(''); - setFilterEndDate(''); setFilterErrors({}); + updateFilter('start_date', ''); + updateFilter('end_date', ''); + updateFilter('location_id', ''); + updateFilter('project_flock_id', ''); + updateFilter('kandang_id', ''); + filterFormik.resetForm(); - }, [filterFormik]); - - const handleFilterStartDateChange = useCallback( - (e: React.ChangeEvent) => { - const value = e.target.value; - setFilterStartDate(value); - filterFormik.setFieldValue('start_date', value); - }, - [filterFormik] - ); - - const handleFilterEndDateChange = useCallback( - (e: React.ChangeEvent) => { - const value = e.target.value; - setFilterEndDate(value); - filterFormik.setFieldValue('end_date', value); - }, - [filterFormik] - ); + }, [filterFormik, updateFilter]); const selectedRowIds = useMemo(() => { return Object.keys(rowSelection) @@ -662,11 +672,11 @@ const UniformityTable = () => { filterProjectFlockKandangId.toString() ); } - if (filterStartDate) { - queryParams.append('start_date', filterStartDate); + if (tableFilterState.start_date) { + queryParams.append('start_date', tableFilterState.start_date); } - if (filterEndDate) { - queryParams.append('end_date', filterEndDate); + if (tableFilterState.end_date) { + queryParams.append('end_date', tableFilterState.end_date); } queryParams.append('limit', '100'); queryParams.append('page', '1'); @@ -677,7 +687,7 @@ const UniformityTable = () => { const response = await UniformityApi.getAllFetcher(url); return isResponseSuccess(response) ? response.data : null; - }, [filterProjectFlockKandangId, filterStartDate, filterEndDate]); + }, [filterProjectFlockKandangId, tableFilterState.start_date, tableFilterState.end_date]); const handleExportExcel = useCallback(async () => { setIsExcelExportLoading(true); @@ -698,8 +708,8 @@ const UniformityTable = () => { location_name: locationName, project_flock_name: projectFlockName, kandang_name: kandangName, - start_date: filterStartDate, - end_date: filterEndDate, + start_date: tableFilterState.start_date, + end_date: tableFilterState.end_date, }); toast.success('Excel berhasil dibuat dan diunduh.'); @@ -713,8 +723,8 @@ const UniformityTable = () => { filterLocation, filterProjectFlock, filterKandang, - filterStartDate, - filterEndDate, + tableFilterState.start_date, + tableFilterState.end_date, ]); const handleExportPDF = useCallback(async () => { @@ -736,8 +746,8 @@ const UniformityTable = () => { location_name: locationName, project_flock_name: projectFlockName, kandang_name: kandangName, - start_date: filterStartDate, - end_date: filterEndDate, + start_date: tableFilterState.start_date, + end_date: tableFilterState.end_date, }); toast.success('PDF berhasil dibuat dan diunduh.'); @@ -751,8 +761,8 @@ const UniformityTable = () => { filterLocation, filterProjectFlock, filterKandang, - filterStartDate, - filterEndDate, + tableFilterState.start_date, + tableFilterState.end_date, ]); useEffect(() => { @@ -883,37 +893,6 @@ const UniformityTable = () => { [] ); - // ===== CALCULATE FILTER COUNT ===== - const filterCount = useMemo(() => { - let count = 0; - - if (filterStartDate && filterEndDate) { - count += 1; - } - - if (filterLocation) { - count += 1; - } - - if (filterProjectFlock) { - count += 1; - } - - if (filterKandang) { - count += 1; - } - - return count; - }, [ - filterStartDate, - filterEndDate, - filterLocation, - filterProjectFlock, - filterKandang, - ]); - - const isFilterActive = filterCount > 0; - return ( <>
@@ -932,30 +911,13 @@ const UniformityTable = () => {
- + className='px-3 py-2.5' + /> { placeholder='Tanggal Mulai' value={filterFormik.values.start_date} errorMessage={filterFormik.errors.start_date} - onChange={handleFilterStartDateChange} + onChange={(e) => filterFormik.setFieldValue('start_date', e.target.value)} isError={ filterFormik.touched.start_date && Boolean(filterFormik.errors.start_date) @@ -1291,7 +1253,7 @@ const UniformityTable = () => { placeholder='Tanggal Akhir' value={filterFormik.values.end_date} errorMessage={filterFormik.errors.end_date} - onChange={handleFilterEndDateChange} + onChange={(e) => filterFormik.setFieldValue('end_date', e.target.value)} isError={ filterFormik.touched.end_date && Boolean(filterFormik.errors.end_date) diff --git a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx index 121338b7..b9296f89 100644 --- a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx +++ b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx @@ -38,6 +38,7 @@ import { Nonstock } from '@/types/api/master-data/nonstock'; import { ColumnDef } from '@tanstack/react-table'; import { httpClient } from '@/services/http/client'; import { BaseApiResponse } from '@/types/api/api-general'; +import ButtonFilter from '@/components/helper/ButtonFilter'; interface ReportExpenseTabProps { tabId: string; @@ -169,20 +170,6 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { [formik.values.category] ); - // ===== ACTIVE FILTERS COUNT ===== - const activeFiltersCount = useMemo(() => { - let count = 0; - if (filterParams.location_id) count += 1; - if (filterParams.supplier_id) count += 1; - if (filterParams.kandang_id) count += 1; - if (filterParams.nonstock_id) count += 1; - if (filterParams.realization_date) count += 1; - if (filterParams.category) count += 1; - return count; - }, [filterParams]); - - const hasFilters = activeFiltersCount > 0; - // ===== DATA FETCHING ===== const { data: reportExpenseResponse, isLoading } = useSWR( isSubmitted @@ -312,25 +299,12 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { setTabActions( tabId,
- + variant='outline' + className='px-3 py-2.5' + /> { ); }, [ tabId, - hasFilters, - activeFiltersCount, + formik.values, isAnyExportLoading, handleExportExcel, handleExportPDF, diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 9ee90fae..1e89ca43 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -38,6 +38,7 @@ import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton'; import { OptionType } from '@/components/table/TableRowSizeSelector'; import { Color } from '@/types/theme'; +import ButtonFilter from '@/components/helper/ButtonFilter'; interface CustomerPaymentTabProps { tabId: string; @@ -213,30 +214,6 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { ); }, [formik.values.filter_by]); - // ===== ACTIVE FILTERS COUNT ===== - const activeFiltersCount = useMemo(() => { - let count = 0; - - // Date filter (start_date + end_date = 1 filter) - if (filterParams.start_date || filterParams.end_date) { - count += 1; - } - - // Customer filter - if (filterParams.customer_ids) { - count += 1; - } - - // Filter by type filter (hanya dihitung jika ada nilai yang dipilih) - if (filterParams.filter_by) { - count += 1; - } - - return count; - }, [filterParams]); - - const hasFilters = activeFiltersCount > 0; - // ===== DATA FETCHING ===== const { data: customerPayment, isLoading } = useSWR( isSubmitted @@ -380,25 +357,13 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { setTabActions( tabId,
- + variant='outline' + className='px-3 py-2.5' + /> { ); }, [ tabId, - hasFilters, - activeFiltersCount, + formik.values, isAnyExportLoading, handleExportExcel, handleExportPdf, diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index d1b7425d..56bf3259 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -274,6 +274,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
{ ); }, [formik.values.sort_by, sortByOptions]); - // ===== ACTIVE FILTERS COUNT ===== - const activeFiltersCount = useMemo(() => { - let count = 0; - - if (filterParams.start_date || filterParams.end_date) { - count += 1; - } - - if (filterParams.area_id) { - count += 1; - } - - if (filterParams.supplier_id) { - count += 1; - } - - if (filterParams.product_id) { - count += 1; - } - - if (filterParams.product_category_id) { - count += 1; - } - - if (filterParams.filter_by) { - count += 1; - } - - if (filterParams.sort_by) { - count += 1; - } - - return count; - }, [filterParams]); - - const hasFilters = activeFiltersCount > 0; - // ===== DATA FETCHING ===== const { data: purchasePerSupplier, isLoading } = useSWR( isSubmitted @@ -486,25 +450,13 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { setTabActions( tabId,
- + variant='outline' + className='px-3 py-2.5' + /> { ); }, [ tabId, - hasFilters, - activeFiltersCount, + formik.values, isAnyExportLoading, filterModal.open, setTabActions, diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index eda89750..219a380d 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -47,6 +47,7 @@ import { MARKETING_TYPE_OPTIONS, } from '@/config/constant'; import Badge from '@/components/Badge'; +import ButtonFilter from '@/components/helper/ButtonFilter'; interface DailyMarketingTabProps { tabId: string; @@ -202,47 +203,6 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { ); }, [formik.values.marketing_type]); - // ===== ACTIVE FILTERS COUNT ===== - const activeFiltersCount = useMemo(() => { - let count = 0; - - if (filterParams.area_id) { - count += 1; - } - - if (filterParams.location_id) { - count += 1; - } - - if (filterParams.warehouse_id) { - count += 1; - } - - if (filterParams.customer_id) { - count += 1; - } - - if (filterParams.start_date || filterParams.end_date) { - count += 1; - } - - if (filterParams.filter_by) { - count += 1; - } - - if (filterParams.marketing_type) { - count += 1; - } - - if (filterParams.sort_by) { - count += 1; - } - - return count; - }, [filterParams]); - - const hasFilters = activeFiltersCount > 0; - // ===== DATA FETCHING ===== const { data: dailyMarketings, isLoading } = useSWR( isSubmitted @@ -412,30 +372,13 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { }} /> - + variant='outline' + className='px-3 py-2.5' + /> { }, [ tabId, searchValue, - hasFilters, - activeFiltersCount, + formik.values, isAnyExportLoading, filterModal.open, setTabActions, diff --git a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx index dc487003..2f784af8 100644 --- a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx @@ -17,6 +17,7 @@ import { } from '@/types/api/report/hpp-per-kandang'; import { isResponseSuccess } from '@/lib/api-helper'; import Button from '@/components/Button'; +import ButtonFilter from '@/components/helper/ButtonFilter'; import Dropdown from '@/components/Dropdown'; import { generateHppPerKandangPDF } from '@/components/pages/report/marketing/export/HppPerkandangExportPDF'; import { generateHppPerKandangExcel } from '@/components/pages/report/marketing/export/HppPerkandangExportXLSX'; @@ -233,42 +234,6 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { ); }, [formik.values.show_unrecorded, showUnrecordedOptions]); - // ===== ACTIVE FILTERS COUNT ===== - const activeFiltersCount = useMemo(() => { - let count = 0; - - if (filterParams.period) { - count += 1; - } - - if (filterParams.area_id) { - count += 1; - } - - if (filterParams.location_id) { - count += 1; - } - - if (filterParams.kandang_id) { - count += 1; - } - - if (filterParams.weight_min || filterParams.weight_max) { - count += 1; - } - - if (filterParams.show_unrecorded !== undefined) { - count += 1; - } - - if (filterParams.sort_by) { - count += 1; - } - - return count; - }, [filterParams]); - - const hasFilters = activeFiltersCount > 0; // ===== DATA FETCHING ===== const { data: hppPerKandang, isLoading } = useSWR( @@ -486,25 +451,12 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { setTabActions( tabId,
- + variant='outline' + className='px-3 py-2.5' + /> { ); }, [ tabId, - hasFilters, - activeFiltersCount, + formik.values, isAnyExportLoading, filterModal.open, setTabActions, diff --git a/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx index cbefadfe..20dddff1 100644 --- a/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx +++ b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx @@ -7,6 +7,7 @@ import toast from 'react-hot-toast'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; +import ButtonFilter from '@/components/helper/ButtonFilter'; import Dropdown from '@/components/dropdown/Dropdown'; import SelectInput, { useSelect } from '@/components/input/SelectInput'; import ProductionResultProjectFlockKandangTable from '@/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTable'; @@ -324,20 +325,6 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { [formik.values.kandang_id] ); - // ===== ACTIVE FILTERS COUNT ===== - const activeFiltersCount = useMemo(() => { - let count = 0; - - if (filterParams.area_id) count += 1; - if (filterParams.location_id) count += 1; - if (filterParams.project_flock_id) count += 1; - if (filterParams.project_flock_kandang_id) count += 1; - - return count; - }, [filterParams]); - - const hasFilters = activeFiltersCount > 0; - // ===== DATA FETCHING ===== const { data: projectFlockKandangsData, isLoading } = useSWR< BaseApiResponse @@ -539,25 +526,12 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { setTabActions( tabId,
- + variant='outline' + className='px-3 py-2.5' + /> { ); }, [ tabId, - hasFilters, - activeFiltersCount, + filterParams, isAnyExportLoading, exportToExcelHandler, exportToPdfHandler, From dae9a24a7c7e860b5db93db8bce4651dba0d5f3d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 10:41:34 +0700 Subject: [PATCH 04/41] refactor(FE): Refactor array formatting and improve code readability --- .../production/uniformity/UniformityTable.tsx | 14 +++++++++++--- .../report/marketing/tab/HppPerKandangTab.tsx | 1 - 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index c28a8a43..f45e803d 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -687,7 +687,11 @@ const UniformityTable = () => { const response = await UniformityApi.getAllFetcher(url); return isResponseSuccess(response) ? response.data : null; - }, [filterProjectFlockKandangId, tableFilterState.start_date, tableFilterState.end_date]); + }, [ + filterProjectFlockKandangId, + tableFilterState.start_date, + tableFilterState.end_date, + ]); const handleExportExcel = useCallback(async () => { setIsExcelExportLoading(true); @@ -1241,7 +1245,9 @@ const UniformityTable = () => { placeholder='Tanggal Mulai' value={filterFormik.values.start_date} errorMessage={filterFormik.errors.start_date} - onChange={(e) => filterFormik.setFieldValue('start_date', e.target.value)} + onChange={(e) => + filterFormik.setFieldValue('start_date', e.target.value) + } isError={ filterFormik.touched.start_date && Boolean(filterFormik.errors.start_date) @@ -1253,7 +1259,9 @@ const UniformityTable = () => { placeholder='Tanggal Akhir' value={filterFormik.values.end_date} errorMessage={filterFormik.errors.end_date} - onChange={(e) => filterFormik.setFieldValue('end_date', e.target.value)} + onChange={(e) => + filterFormik.setFieldValue('end_date', e.target.value) + } isError={ filterFormik.touched.end_date && Boolean(filterFormik.errors.end_date) diff --git a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx index 2f784af8..953b4a88 100644 --- a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx @@ -234,7 +234,6 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { ); }, [formik.values.show_unrecorded, showUnrecordedOptions]); - // ===== DATA FETCHING ===== const { data: hppPerKandang, isLoading } = useSWR( isSubmitted From 93a2d99b7f0b1753d2abe772af8df6892d47eee0 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 10:52:03 +0700 Subject: [PATCH 05/41] refactor(FE): Disable filter button if kandang_id is not selected --- src/components/pages/production/recording/RecordingTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 7a7e32d8..5f5cb091 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -1396,7 +1396,7 @@ const RecordingTable = () => { From 90942b41b9da86ff055afd408dbbc9bf6bb8e128 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 10:53:32 +0700 Subject: [PATCH 06/41] refactor(FE): Fix formatting of disabled condition in RecordingTable button --- .../pages/production/recording/RecordingTable.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 5f5cb091..688df7f4 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -1396,7 +1396,11 @@ const RecordingTable = () => { From e2e64f093f28a8a3333c54bb9a08f57b0b8e353a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 11:02:26 +0700 Subject: [PATCH 07/41] refactor(FE): Refactor TransferToLayingFilterModal to use schema validation --- .../TransferToLayingFilterModal.tsx | 49 ++++++++++++------- .../TransferToLayingsTable.tsx | 3 +- .../filter/TransferToLayingFilter.ts | 33 +++++++++++++ 3 files changed, 64 insertions(+), 21 deletions(-) create mode 100644 src/components/pages/production/transfer-to-laying/filter/TransferToLayingFilter.ts diff --git a/src/components/pages/production/transfer-to-laying/TransferToLayingFilterModal.tsx b/src/components/pages/production/transfer-to-laying/TransferToLayingFilterModal.tsx index db758762..95d8aa6c 100644 --- a/src/components/pages/production/transfer-to-laying/TransferToLayingFilterModal.tsx +++ b/src/components/pages/production/transfer-to-laying/TransferToLayingFilterModal.tsx @@ -13,6 +13,10 @@ import { OptionType, useSelect } from '@/components/input/SelectInput'; import { ProjectFlockApi } from '@/services/api/production'; import { Flock } from '@/types/api/master-data/flock'; import { TransferToLayingFilter } from '@/types/api/production/transfer-to-laying'; +import { + TransferToLayingFilterSchema, + TransferToLayingFilterValues, +} from '@/components/pages/production/transfer-to-laying/filter/TransferToLayingFilter'; interface TransferToLayingFilterModal { ref: RefObject; @@ -49,13 +53,7 @@ const TransferToLayingFilterModal = ({ category: 'LAYING', }); - const formik = useFormik<{ - startDate: string; - endDate: string; - flockSource: { value: number; label: string }[]; - flockDestination: { value: number; label: string }[]; - status: { value: number; label: string }[]; - }>({ + const formik = useFormik({ initialValues: { startDate: '', endDate: '', @@ -63,15 +61,22 @@ const TransferToLayingFilterModal = ({ flockDestination: [], status: [], }, + validationSchema: TransferToLayingFilterSchema, onSubmit: async (values) => { const formattedValues = { ...values, - flockSource: values.flockSource.map((item) => item.value), - flockDestination: values.flockDestination.map((item) => item.value), - status: values.status.map((item) => item.value), + flockSource: values.flockSource + ? (values.flockSource as OptionType[]).map((item) => item.value) + : [], + flockDestination: values.flockDestination + ? (values.flockDestination as OptionType[]).map((item) => item.value) + : [], + status: values.status + ? (values.status as OptionType[]).map((item) => item.value) + : [], }; - onSubmit?.(formattedValues); + onSubmit?.(formattedValues as TransferToLayingFilter); closeModalHandler(); }, onReset: () => { @@ -81,17 +86,17 @@ const TransferToLayingFilterModal = ({ }); const flockSourceChangeHandler = (val: OptionType | OptionType[] | null) => { - formik.setFieldValue('flockSource', val as OptionType[]); + formik.setFieldValue('flockSource', val); }; const flockDestinationChangeHandler = ( val: OptionType | OptionType[] | null ) => { - formik.setFieldValue('flockDestination', val as OptionType[]); + formik.setFieldValue('flockDestination', val); }; const statusChangeHandler = (val: OptionType | OptionType[] | null) => { - formik.setFieldValue('status', val as OptionType[]); + formik.setFieldValue('status', val); }; return ( @@ -132,7 +137,7 @@ const TransferToLayingFilterModal = ({ @@ -140,16 +145,22 @@ const TransferToLayingFilterModal = ({
+ {formik.touched.endDate && formik.errors.endDate && ( + + {formik.errors.endDate} + + )}
diff --git a/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx b/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx index 9a9813f0..06852fe1 100644 --- a/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx +++ b/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx @@ -1,6 +1,6 @@ 'use client'; -import { ChangeEventHandler, useEffect, useMemo, useState } from 'react'; +import { ChangeEventHandler, useEffect, useState } from 'react'; import useSWR from 'swr'; import { CellContext, @@ -17,7 +17,6 @@ import { useModal } from '@/components/Modal'; import CheckboxInput from '@/components/input/CheckboxInput'; import RequirePermission from '@/components/helper/RequirePermission'; import PopoverButton from '@/components/popover/PopoverButton'; -import Badge from '@/components/Badge'; import PopoverContent from '@/components/popover/PopoverContent'; import Dropdown from '@/components/Dropdown'; import StatusBadge from '@/components/helper/StatusBadge'; diff --git a/src/components/pages/production/transfer-to-laying/filter/TransferToLayingFilter.ts b/src/components/pages/production/transfer-to-laying/filter/TransferToLayingFilter.ts new file mode 100644 index 00000000..bc402d29 --- /dev/null +++ b/src/components/pages/production/transfer-to-laying/filter/TransferToLayingFilter.ts @@ -0,0 +1,33 @@ +import * as yup from 'yup'; + +export type TransferToLayingFilterType = { + startDate: string | null; + endDate: string | null; + flockSource: number[]; + flockDestination: number[]; + status: string[]; +}; + +export const TransferToLayingFilterSchema = yup.object({ + startDate: yup.string().optional().nullable(), + endDate: yup + .string() + .optional() + .nullable() + .test( + 'is-greater-than-start', + 'Tanggal akhir tidak boleh masa lampau', + function (value) { + const { startDate } = this.parent; + if (!startDate || !value) return true; + return new Date(value) >= new Date(startDate); + } + ), + flockSource: yup.array().optional().nullable(), + flockDestination: yup.array().optional().nullable(), + status: yup.array().optional().nullable(), +}); + +export type TransferToLayingFilterValues = yup.InferType< + typeof TransferToLayingFilterSchema +>; From 22b1102454b00d79fb72c853d82fb3572d742a74 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 11:40:42 +0700 Subject: [PATCH 08/41] refactor(FE): Refactor ExpensesTable to use ExpensesFilterModal --- .../pages/expense/ExpensesTable.tsx | 406 ++++++++---------- .../pages/expense/filter/ExpensesFilter.ts | 28 ++ .../expense/filter/ExpensesFilterModal.tsx | 206 +++++++++ 3 files changed, 423 insertions(+), 217 deletions(-) create mode 100644 src/components/pages/expense/filter/ExpensesFilter.ts create mode 100644 src/components/pages/expense/filter/ExpensesFilterModal.tsx diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index e141ad67..5875bc0c 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -16,10 +16,6 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Button from '@/components/Button'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; -import SelectInput, { - OptionType, - useSelect, -} from '@/components/input/SelectInput'; import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; @@ -27,17 +23,15 @@ import RealizationStatusBadge from '@/components/pages/expense/RealizationStatus import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge'; import CheckboxInput from '@/components/input/CheckboxInput'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; -import DateInput from '@/components/input/DateInput'; import RequirePermission from '@/components/helper/RequirePermission'; +import ButtonFilter from '@/components/helper/ButtonFilter'; +import ExpensesFilterModal from '@/components/pages/expense/filter/ExpensesFilterModal'; import { Expense } from '@/types/api/expense'; import { ExpenseApi } from '@/services/api/expense'; import { cn, formatCurrency, formatDate } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import { LocationApi, SupplierApi } from '@/services/api/master-data'; -import { Location } from '@/types/api/master-data/location'; -import { Supplier } from '@/types/api/master-data/supplier'; import { BaseApiResponse } from '@/types/api/api-general'; const RowOptionsMenu = ({ @@ -179,6 +173,9 @@ const ExpensesTable = () => { const approveModal = useModal(); const rejectModal = useModal(); + // ===== FILTER MODAL STATE ===== + const filterModal = useModal(); + const [selectedExpense, setSelectedExpense] = useState( undefined ); @@ -535,51 +532,32 @@ const ExpensesTable = () => { setIsRejectLoading(false); }; - const { - setInputValue: setLocationInputValue, - options: locationOptions, - isLoadingOptions: isLoadingLocationOptions, - } = useSelect(LocationApi.basePath, 'id', 'name'); - - const [selectedLocation, setSelectedLocation] = useState( - null - ); - - const locationChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedLocation(val as OptionType); - updateFilter( - 'locationId', - val ? ((val as OptionType).value as string) : '' - ); - }; - - const { - setInputValue: setVendorInputValue, - options: vendorOptions, - isLoadingOptions: isLoadingVendorOptions, - } = useSelect(SupplierApi.basePath, 'id', 'name'); - - const [selectedVendor, setSelectedVendor] = useState(null); - - const vendorChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedVendor(val as OptionType); - updateFilter('vendorId', val ? ((val as OptionType).value as string) : ''); - }; - const searchChangeHandler: ChangeEventHandler = (e) => { updateFilter('search', e.target.value); }; - const transactionDateChangeHandler: ChangeEventHandler = ( - e - ) => { - updateFilter('transactionDate', e.target.value); + // ===== FILTER MODAL HANDLERS ===== + const handleFilterModalOpen = () => { + filterModal.openModal(); }; - const realizationDateChangeHandler: ChangeEventHandler = ( - e - ) => { - updateFilter('realizationDate', e.target.value); + const handleFilterSubmit = (values: { + transaction_date?: string | null; + realization_date?: string | null; + location_id?: string | null; + vendor_id?: string | null; + }) => { + updateFilter('transactionDate', values.transaction_date || ''); + updateFilter('realizationDate', values.realization_date || ''); + updateFilter('locationId', values.location_id || ''); + updateFilter('vendorId', values.vendor_id || ''); + }; + + const handleFilterReset = () => { + updateFilter('transactionDate', ''); + updateFilter('realizationDate', ''); + updateFilter('locationId', ''); + updateFilter('vendorId', ''); }; // track sorting @@ -595,188 +573,176 @@ const ExpensesTable = () => { return ( <> -
-
-
-
-
- +
+
+ {/* Action Buttons */} +
+ + + + + {selectedRowIds.length > 0 && ( + <> +
+ + - {selectedRowIds.length > 0 && ( - <> - - - + + + - - - + + + - - - + + + + + )} +
- - - - - )} -
-
+ {/* Search and Filter */} +
+ + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', + }} + /> -
- - - - - - - - - -
+
- - data={isResponseSuccess(expenses) ? expenses?.data : []} - columns={expensesColumns} - pageSize={tableFilterState.pageSize} - page={isResponseSuccess(expenses) ? expenses?.meta?.page : 0} - totalItems={ - isResponseSuccess(expenses) ? expenses?.meta?.total_results : 0 - } - onPageChange={setPage} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - enableRowSelection={tableEnableRowSelectionHandler} - className={{ - containerClassName: cn({ - 'mb-20': - isResponseSuccess(expenses) && expenses?.data?.length === 0, - }), - tableWrapperClassName: 'overflow-x-auto min-h-full!', - tableClassName: 'font-inter w-full table-auto min-h-full!', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', - bodyRowClassName: 'border-b border-b-gray-200', - bodyColumnClassName: - 'px-6 py-3 last:flex last:flex-row last:justify-end', - }} - /> + {/* Table Section */} +
+ + data={isResponseSuccess(expenses) ? expenses?.data : []} + columns={expensesColumns} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(expenses) ? expenses?.meta?.page : 0} + totalItems={ + isResponseSuccess(expenses) ? expenses?.meta?.total_results : 0 + } + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + enableRowSelection={tableEnableRowSelectionHandler} + className={{ + containerClassName: cn('p-3 mb-0', { + 'w-full': + isResponseSuccess(expenses) && expenses?.data?.length === 0, + }), + headerColumnClassName: 'text-nowrap', + }} + /> +
{ onClick: confirmationModalRejectClickHandler, }} /> + + ); }; diff --git a/src/components/pages/expense/filter/ExpensesFilter.ts b/src/components/pages/expense/filter/ExpensesFilter.ts new file mode 100644 index 00000000..8ee14a90 --- /dev/null +++ b/src/components/pages/expense/filter/ExpensesFilter.ts @@ -0,0 +1,28 @@ +import * as yup from 'yup'; + +export type ExpensesFilterType = { + transaction_date: string | null; + realization_date: string | null; + location_id: string | null; + vendor_id: string | null; +}; + +export const ExpensesFilterSchema = yup.object({ + transaction_date: yup.string().nullable(), + realization_date: yup + .string() + .nullable() + .test( + 'is-greater-or-equal-transaction', + 'Tanggal realisasi tidak boleh sebelum tanggal transaksi', + function (value) { + const { transaction_date } = this.parent; + if (!transaction_date || !value) return true; + return new Date(value) >= new Date(transaction_date); + } + ), + location_id: yup.string().nullable(), + vendor_id: yup.string().nullable(), +}); + +export type ExpensesFilterValues = yup.InferType; diff --git a/src/components/pages/expense/filter/ExpensesFilterModal.tsx b/src/components/pages/expense/filter/ExpensesFilterModal.tsx new file mode 100644 index 00000000..99f5a75a --- /dev/null +++ b/src/components/pages/expense/filter/ExpensesFilterModal.tsx @@ -0,0 +1,206 @@ +'use client'; + +import { RefObject } from 'react'; +import { useFormik } from 'formik'; + +import { Icon } from '@iconify/react'; +import Modal from '@/components/Modal'; +import Button from '@/components/Button'; +import DateInput from '@/components/input/DateInput'; +import SelectInput from '@/components/input/SelectInput'; + +import { OptionType, useSelect } from '@/components/input/SelectInput'; +import { LocationApi, SupplierApi } from '@/services/api/master-data'; +import { Location } from '@/types/api/master-data/location'; +import { Supplier } from '@/types/api/master-data/supplier'; +import { + ExpensesFilterSchema, + ExpensesFilterValues, +} from '@/components/pages/expense/filter/ExpensesFilter'; + +interface ExpensesFilterModalProps { + ref: RefObject; + initialValues?: ExpensesFilterValues; + onSubmit?: (values: Partial) => void; + onReset?: () => void; +} + +const ExpensesFilterModal = ({ + ref, + initialValues, + onSubmit, + onReset, +}: ExpensesFilterModalProps) => { + const closeModalHandler = () => { + ref.current?.close(); + }; + + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + } = useSelect(LocationApi.basePath, 'id', 'name'); + + const { + setInputValue: setVendorInputValue, + options: vendorOptions, + isLoadingOptions: isLoadingVendorOptions, + } = useSelect(SupplierApi.basePath, 'id', 'name'); + + const formik = useFormik({ + initialValues: initialValues || { + transaction_date: null, + realization_date: null, + location_id: null, + vendor_id: null, + }, + validationSchema: ExpensesFilterSchema, + onSubmit: async (values) => { + onSubmit?.(values); + closeModalHandler(); + }, + onReset: () => { + onReset?.(); + closeModalHandler(); + }, + }); + + const locationValue = formik.values.location_id + ? locationOptions.find( + (opt) => String(opt.value) === formik.values.location_id + ) || null + : null; + + const vendorValue = formik.values.vendor_id + ? vendorOptions.find( + (opt) => String(opt.value) === formik.values.vendor_id + ) || null + : null; + + const locationChangeHandler = (val: OptionType | OptionType[] | null) => { + const locationId = + val && !Array.isArray(val) ? (String(val.value) as string) : null; + formik.setFieldValue('location_id', locationId); + }; + + const vendorChangeHandler = (val: OptionType | OptionType[] | null) => { + const vendorId = + val && !Array.isArray(val) ? (String(val.value) as string) : null; + formik.setFieldValue('vendor_id', vendorId); + }; + + return ( + +
+ {/* Modal Header */} +
+
+ +

Filter Data

+
+ + +
+ + {/* Modal Body */} +
+ + + + {formik.touched.realization_date && + formik.errors.realization_date && ( + + {formik.errors.realization_date} + + )} + + + + +
+ + {/* Modal Footer */} +
+ + + +
+
+
+ ); +}; + +export default ExpensesFilterModal; From 4fda2f661a167b0e36a8205ef65758731e7389ae Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 11:49:41 +0700 Subject: [PATCH 09/41] refactor(FE): Remove unnecessary padding and margin classes in components --- src/app/production/recording/page.tsx | 2 +- src/components/pages/expense/ExpensesTable.tsx | 2 +- src/components/pages/production/recording/RecordingTable.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/production/recording/page.tsx b/src/app/production/recording/page.tsx index 368a59ea..9b986b49 100644 --- a/src/app/production/recording/page.tsx +++ b/src/app/production/recording/page.tsx @@ -2,7 +2,7 @@ import RecordingTable from '@/components/pages/production/recording/RecordingTab const Recording = () => { return ( -
+
); diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index 5875bc0c..ba6e4ada 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -717,7 +717,7 @@ const ExpensesTable = () => {
{/* Table Section */} -
+
data={isResponseSuccess(expenses) ? expenses?.data : []} columns={expensesColumns} diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 688df7f4..8f59d7c3 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -1245,7 +1245,7 @@ const RecordingTable = () => {
{/* Table Section */} -
+
{isLoading ? (
From 8be33b230bf97f80d512610a13bcada233c9fd28 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 11:59:17 +0700 Subject: [PATCH 10/41] refactor(FE): Refactor row options menu to use popover components --- .../pages/expense/ExpensesTable.tsx | 202 ++++++++---------- 1 file changed, 88 insertions(+), 114 deletions(-) diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index ba6e4ada..e2e86535 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -16,9 +16,8 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Button from '@/components/Button'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; -import RowDropdownOptions from '@/components/table/RowDropdownOptions'; -import RowCollapseOptions from '@/components/table/RowCollapseOptions'; -import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; import RealizationStatusBadge from '@/components/pages/expense/RealizationStatusBadge'; import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge'; import CheckboxInput from '@/components/input/CheckboxInput'; @@ -35,16 +34,21 @@ import { useTableFilter } from '@/services/hooks/useTableFilter'; import { BaseApiResponse } from '@/types/api/api-general'; const RowOptionsMenu = ({ - type = 'dropdown', + popoverPosition = 'bottom', props, deleteClickHandler, }: { - type: 'dropdown' | 'collapse'; + popoverPosition: 'bottom' | 'top'; props: CellContext; - approveClickHandler: () => void; - rejectClickHandler: () => void; deleteClickHandler: () => void; }) => { + const popoverId = `expense#${props.row.original.id}`; + const popoverAnchorName = `--anchor-expense#${props.row.original.id}`; + + const closePopover = () => { + document.getElementById(popoverId)?.hidePopover(); + }; + const showEditButton = props.row.original.latest_approval ? props.row.original.latest_approval.step_number !== 6 && (props.row.original.latest_approval.step_number === 1 || @@ -53,81 +57,95 @@ const RowOptionsMenu = ({ props.row.original.latest_approval.step_number === 4) : false; - // TODO: apply RBAC const showRealizationButton = props.row.original.latest_approval ? props.row.original.latest_approval.action !== 'REJECTED' && props.row.original.latest_approval.step_number === 4 : false; return ( - -
- - - +
+ + + - {showEditButton && ( - + +
+ - )} - {showRealizationButton && ( - + {showEditButton && ( + + + + )} + + {showRealizationButton && ( + + + + )} + + - )} - - - - -
- +
+ +
); }; @@ -337,31 +355,7 @@ const ExpensesTable = () => { const currentRowRelativeIndex = currentPageRows.findIndex((r) => r.id === props.row.id) + 1; - const isLast2Rows = currentRowRelativeIndex > currentPageSize - 3; - - const approveClickHandler = () => { - setSelectedExpense(props.row.original); - - // Set row selection - setRowSelection({ - [String(props.row.original.id)]: true, - }); - - setApprovalNotes(''); - approveModal.openModal(); - }; - - const rejectClickHandler = () => { - setSelectedExpense(props.row.original); - - // Set row selection - setRowSelection({ - [String(props.row.original.id)]: true, - }); - - setApprovalNotes(''); - rejectModal.openModal(); - }; + const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; const deleteClickHandler = () => { setSelectedExpense(props.row.original); @@ -369,31 +363,11 @@ const ExpensesTable = () => { }; return ( - <> - {currentPageSize > 3 && ( - - - - )} - - {currentPageSize <= 3 && ( - - - - )} - + ); }, }, From 0af7b172a014b0c54f22e32a6fbc2f9b70de92b3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 12:08:31 +0700 Subject: [PATCH 11/41] refactor(FE): Refactor PurchaseTable to use Popover for row options menu --- .../pages/purchase/PurchaseTable.tsx | 252 ++++++++---------- 1 file changed, 115 insertions(+), 137 deletions(-) diff --git a/src/components/pages/purchase/PurchaseTable.tsx b/src/components/pages/purchase/PurchaseTable.tsx index 733165f8..87992ad2 100644 --- a/src/components/pages/purchase/PurchaseTable.tsx +++ b/src/components/pages/purchase/PurchaseTable.tsx @@ -12,10 +12,9 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Button from '@/components/Button'; 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 RowDropdownOptions from '@/components/table/RowDropdownOptions'; -import RowCollapseOptions from '@/components/table/RowCollapseOptions'; -import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import RequirePermission from '@/components/helper/RequirePermission'; import StatusBadge from '@/components/helper/StatusBadge'; @@ -69,59 +68,72 @@ const getStatusBadgeColor = (status: string): Color => { return statusBadgeColorMap[status] || 'neutral'; }; -// ===== INTERFACES ===== -interface RowOptionsMenuProps { - type: 'dropdown' | 'collapse'; - props: CellContext; - deleteClickHandler: () => void; -} - +// ===== ROW OPTIONS MENU ===== const RowOptionsMenu = ({ - type = 'dropdown', + popoverPosition = 'bottom', props, deleteClickHandler, -}: RowOptionsMenuProps) => { +}: { + popoverPosition: 'bottom' | 'top'; + props: CellContext; + deleteClickHandler: () => void; +}) => { + const popoverId = `purchase#${props.row.original.id}`; + const popoverAnchorName = `--anchor-purchase#${props.row.original.id}`; + + const closePopover = () => { + document.getElementById(popoverId)?.hidePopover(); + }; + return ( - - - - +
+ + + - {/**/} - {/* */} - {/* Edit*/} - {/**/} + +
+ + + - - - - + + + +
+
+
); }; @@ -346,27 +358,11 @@ const PurchaseTable = () => { }; return ( - <> - {currentPageSize > 2 && ( - - - - )} - - {currentPageSize <= 2 && ( - - - - )} - + ); }, }, @@ -405,22 +401,22 @@ const PurchaseTable = () => { return ( <> -
-
-
-
- - - -
+
+
+
+ + + +
+
{ }} />
- -
- -
- - data={ - isResponseSuccess(purchaseRequests) ? purchaseRequests?.data : [] - } - columns={purchaseColumns} - pageSize={tableFilterState.pageSize} - page={ - isResponseSuccess(purchaseRequests) - ? purchaseRequests?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(purchaseRequests) - ? purchaseRequests?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn({ - 'mb-20': - isResponseSuccess(purchaseRequests) && - purchaseRequests?.data?.length === 0, - }), - tableWrapperClassName: 'overflow-x-auto min-h-full!', - tableClassName: 'font-inter w-full table-auto min-h-full!', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', - bodyRowClassName: 'border-b border-b-gray-200', - bodyColumnClassName: - 'px-6 py-3 last:flex last:flex-row last:justify-end', - }} - /> + {/* Table Section */} +
+ + data={ + isResponseSuccess(purchaseRequests) ? purchaseRequests?.data : [] + } + columns={purchaseColumns} + pageSize={tableFilterState.pageSize} + page={ + isResponseSuccess(purchaseRequests) + ? purchaseRequests?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(purchaseRequests) + ? purchaseRequests?.meta?.total_results + : 0 + } + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn('p-3', { + 'w-full mb-20': + isResponseSuccess(purchaseRequests) && + purchaseRequests?.data?.length === 0, + }), + headerColumnClassName: 'text-nowrap', + }} + /> +
{/* ===== MODAL COMPONENTS ===== */} From a75d84556a658e4dc39c5e07e58e6eba69cc8392 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 15:56:12 +0700 Subject: [PATCH 12/41] refactor(FE): Refactor date validation to use shared state and cleanup --- src/components/pages/finance/FinanceTable.tsx | 31 +++--- .../TransferToLayingFilterModal.tsx | 102 ++++++++++++++++-- .../production/uniformity/UniformityTable.tsx | 95 ++++++++++++++-- .../finance/filter/DebtSupplierFilter.ts | 14 ++- .../report/finance/tab/CustomerPaymentTab.tsx | 9 +- .../report/finance/tab/DebtSupplierTab.tsx | 78 +++++++++++++- .../tab/PurchasesPerSupplierTab.tsx | 9 +- .../marketing/tab/DailyMarketingTab.tsx | 89 ++++++++++++--- 8 files changed, 371 insertions(+), 56 deletions(-) diff --git a/src/components/pages/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx index f83fa469..10d959b9 100644 --- a/src/components/pages/finance/FinanceTable.tsx +++ b/src/components/pages/finance/FinanceTable.tsx @@ -189,6 +189,7 @@ const FinanceTable = () => { const [selectedFinance, setSelectedFinance] = useState(null); const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [dateErrorShown, setDateErrorShown] = useState(false); + const [hasDateError, setHasDateError] = useState(false); // ===== Formik for Filter ===== const filterFormik = useFormik({ @@ -335,10 +336,7 @@ const FinanceTable = () => { const endDateObj = new Date(endDate); if (endDateObj < startDate) { - filterFormik.setFieldError( - 'end_date', - 'Tanggal akhir tidak boleh masa lampau' - ); + setHasDateError(true); if (!dateErrorShown) { toast.error('Tanggal akhir tidak boleh masa lampau', { duration: Infinity, @@ -346,12 +344,14 @@ const FinanceTable = () => { setDateErrorShown(true); } } else { - filterFormik.setFieldError('end_date', undefined); + setHasDateError(false); if (dateErrorShown) { toast.dismiss(); setDateErrorShown(false); } } + } else { + setHasDateError(false); } }; @@ -366,10 +366,7 @@ const FinanceTable = () => { const endDate = new Date(value); if (endDate < startDateObj) { - filterFormik.setFieldError( - 'end_date', - 'Tanggal akhir tidak boleh masa lampau' - ); + setHasDateError(true); if (!dateErrorShown) { toast.error('Tanggal akhir tidak boleh masa lampau', { duration: Infinity, @@ -380,7 +377,7 @@ const FinanceTable = () => { } } - filterFormik.setFieldError('end_date', undefined); + setHasDateError(false); if (dateErrorShown) { toast.dismiss(); setDateErrorShown(false); @@ -661,22 +658,20 @@ const FinanceTable = () => { name='start_date' label='Periode Tanggal (Mulai)' value={filterFormik.values.start_date} + errorMessage={filterFormik.errors.start_date} onChange={startDateChangeHandler} - errorMessage={ - filterFormik.errors.end_date - ? filterFormik.errors.end_date - : undefined + isError={ + filterFormik.touched.start_date && Boolean(filterFormik.errors.start_date) } /> { + return () => { + if (dateErrorShown) { + toast.dismiss(); + } + }; + }, [dateErrorShown]); + + // ===== CLEANUP TOAST WHEN MODAL CLOSES ===== + useEffect(() => { + const dialogElement = ref.current; + const handleModalClose = () => { + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }; + + dialogElement?.addEventListener('close', handleModalClose); + + return () => { + dialogElement?.removeEventListener('close', handleModalClose); + }; + }, [ref, dateErrorShown]); + // Flock Source const { setInputValue: setFlockSourceInputValue, @@ -138,24 +169,77 @@ const TransferToLayingFilterModal = ({ name='startDate' placeholder='Tanggal Awal' value={formik.values.startDate || ''} - onChange={formik.handleChange} + errorMessage={formik.errors.startDate} + onChange={(e) => { + const value = e.target.value; + formik.setFieldValue('startDate', value); + + if (value && formik.values.endDate) { + const startDate = new Date(value); + const endDateObj = new Date(formik.values.endDate); + + if (endDateObj < startDate) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + } else { + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + } + } else { + setHasDateError(false); + } + }} onBlur={formik.handleBlur} + isError={ + formik.touched.startDate && Boolean(formik.errors.startDate) + } />
{ + const value = e.target.value; + formik.setFieldValue('endDate', value); + + if (value && formik.values.startDate) { + const startDateObj = new Date(formik.values.startDate); + const endDate = new Date(value); + + if (endDate < startDateObj) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + return; + } + } + + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }} onBlur={formik.handleBlur} - isError={formik.touched.endDate && !!formik.errors.endDate} + isError={ + (formik.touched.endDate && Boolean(formik.errors.endDate)) || hasDateError + } />
- {formik.touched.endDate && formik.errors.endDate && ( - - {formik.errors.endDate} - - )} { useState(''); const [, setFilterErrors] = useState>({}); + // ===== DATE ERROR STATE ===== + const [dateErrorShown, setDateErrorShown] = useState(false); + const [hasDateError, setHasDateError] = useState(false); + const { setInputValue: setFilterLocationInputValue, options: filterLocationOptions, @@ -792,6 +796,23 @@ const UniformityTable = () => { } }, [uniformities, rowSelection]); + useEffect(() => { + return () => { + if (dateErrorShown) { + toast.dismiss(); + } + }; + }, [dateErrorShown]); + + useEffect(() => { + return () => { + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }; + }, [filterModal.open, dateErrorShown]); + // ===== TABLE COLUMNS DEFINITION ===== const uniformityColumns: ColumnDef[] = useMemo( () => [ @@ -1245,9 +1266,38 @@ const UniformityTable = () => { placeholder='Tanggal Mulai' value={filterFormik.values.start_date} errorMessage={filterFormik.errors.start_date} - onChange={(e) => - filterFormik.setFieldValue('start_date', e.target.value) - } + onChange={(e) => { + const value = e.target.value; + filterFormik.setFieldValue('start_date', value); + + if (value && filterFormik.values.end_date) { + const startDate = new Date(value); + const endDateObj = new Date( + filterFormik.values.end_date + ); + + if (endDateObj < startDate) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error( + 'Tanggal akhir tidak boleh masa lampau', + { + duration: Infinity, + } + ); + setDateErrorShown(true); + } + } else { + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + } + } else { + setHasDateError(false); + } + }} isError={ filterFormik.touched.start_date && Boolean(filterFormik.errors.start_date) @@ -1259,13 +1309,38 @@ const UniformityTable = () => { placeholder='Tanggal Akhir' value={filterFormik.values.end_date} errorMessage={filterFormik.errors.end_date} - onChange={(e) => - filterFormik.setFieldValue('end_date', e.target.value) - } - isError={ - filterFormik.touched.end_date && - Boolean(filterFormik.errors.end_date) - } + onChange={(e) => { + const value = e.target.value; + filterFormik.setFieldValue('end_date', value); + + if (value && filterFormik.values.start_date) { + const startDateObj = new Date( + filterFormik.values.start_date + ); + const endDate = new Date(value); + + if (endDate < startDateObj) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error( + 'Tanggal akhir tidak boleh masa lampau', + { + duration: Infinity, + } + ); + setDateErrorShown(true); + } + return; + } + } + + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }} + isError={hasDateError} />
diff --git a/src/components/pages/report/finance/filter/DebtSupplierFilter.ts b/src/components/pages/report/finance/filter/DebtSupplierFilter.ts index 1c1c2fac..0ecd299e 100644 --- a/src/components/pages/report/finance/filter/DebtSupplierFilter.ts +++ b/src/components/pages/report/finance/filter/DebtSupplierFilter.ts @@ -11,7 +11,19 @@ export type DebtSupplierFilterType = { export const DebtSupplierFilterSchema: yup.ObjectSchema = yup.object({ startDate: yup.string().optional().notRequired(), - endDate: yup.string().optional().notRequired(), + endDate: yup + .string() + .optional() + .notRequired() + .test( + 'is-greater-than-start', + 'Tanggal akhir tidak boleh masa lampau', + function (value) { + const startDate = this.parent.startDate; + if (!startDate || !value) return true; + return new Date(value) >= new Date(startDate); + } + ), supplierIds: yup .array() .of( diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 1e89ca43..4b596155 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -844,19 +844,26 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index 56bf3259..d5341854 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -87,6 +87,10 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { }); const [isSubmitted, setIsSubmitted] = useState(false); + // ===== DATE ERROR STATE ===== + const [dateErrorShown, setDateErrorShown] = useState(false); + const [hasDateError, setHasDateError] = useState(false); + const filterModal = useModal(); const { @@ -106,6 +110,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { const handleFilterModalOpen = () => { filterModal.openModal(); + formik.validateForm(); }; // ===== FORMIK SETUP ===== @@ -349,6 +354,23 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { }; }, [tabId, clearTabActions]); + useEffect(() => { + return () => { + if (dateErrorShown) { + toast.dismiss(); + } + }; + }, [dateErrorShown]); + + useEffect(() => { + return () => { + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }; + }, [filterModal.open, dateErrorShown]); + const getTableColumns = (supplier?: DebtSupplier): ColumnDef[] => [ { id: 'no', @@ -723,7 +745,31 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { name='startDate' value={formik.values.startDate || ''} onChange={(e) => { - formik.setFieldValue('startDate', e.target.value || null); + const value = e.target.value; + formik.setFieldValue('startDate', value || null); + + if (value && formik.values.endDate) { + const startDate = new Date(value); + const endDateObj = new Date(formik.values.endDate); + + if (endDateObj < startDate) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + } else { + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + } + } else { + setHasDateError(false); + } }} className={{ wrapper: 'w-full' }} isError={ @@ -737,10 +783,36 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { name='endDate' value={formik.values.endDate || ''} onChange={(e) => { - formik.setFieldValue('endDate', e.target.value || null); + const value = e.target.value; + formik.setFieldValue('endDate', value || null); + + if (value && formik.values.startDate) { + const startDateObj = new Date(formik.values.startDate); + const endDate = new Date(value); + + if (endDate < startDateObj) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + return; + } + } + + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } }} className={{ wrapper: 'w-full' }} - isError={formik.touched.endDate && !!formik.errors.endDate} + isError={ + (formik.touched.endDate && !!formik.errors.endDate) || + hasDateError + } errorMessage={formik.errors.endDate} isNestedModal /> diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 6023a123..d0fb8d47 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -842,18 +842,25 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index 219a380d..0f68d3b0 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -80,6 +80,10 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { // ===== FILTER STATE ===== const [filterParams, setFilterParams] = useState({}); + // ===== DATE ERROR STATE ===== + const [dateErrorShown, setDateErrorShown] = useState(false); + const [hasDateError, setHasDateError] = useState(false); + const filterModal = useModal(); // ===== OPTIONS ===== @@ -448,6 +452,23 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { }; }, [tabId, clearTabActions]); + useEffectHook(() => { + return () => { + if (dateErrorShown) { + toast.dismiss(); + } + }; + }, [dateErrorShown]); + + useEffectHook(() => { + return () => { + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }; + }, [filterModal.open, dateErrorShown]); + const getTableColumns = (): ColumnDef[] => { const tableColumns: ColumnDef[] = [ { @@ -791,34 +812,76 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { placeholder='Pilih Tanggal Awal' value={formik.values.start_date || ''} onChange={(e) => { - formik.setFieldValue('start_date', e.target.value || null); + const value = e.target.value; + formik.setFieldValue('start_date', value || null); + + if (value && formik.values.end_date) { + const startDate = new Date(value); + const endDateObj = new Date(formik.values.end_date); + + if (endDateObj < startDate) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + } else { + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + } + } else { + setHasDateError(false); + } }} className={{ wrapper: 'w-full' }} + errorMessage={formik.errors.start_date} isError={ !!formik.errors.start_date && formik.touched.start_date } /> - {formik.errors.start_date && formik.touched.start_date && ( -
- {formik.errors.start_date} -
- )} { - formik.setFieldValue('end_date', e.target.value || null); + const value = e.target.value; + formik.setFieldValue('end_date', value || null); + + if (value && formik.values.start_date) { + const startDateObj = new Date(formik.values.start_date); + const endDate = new Date(value); + + if (endDate < startDateObj) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + return; + } + } + + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } }} className={{ wrapper: 'w-full' }} - isError={!!formik.errors.end_date && formik.touched.end_date} + errorMessage={formik.errors.end_date} + isError={ + (formik.errors.end_date && formik.touched.end_date) || + hasDateError + } /> - {formik.errors.end_date && formik.touched.end_date && ( -
- {formik.errors.end_date} -
- )}
From a89e83af29ba3ad9064c156f27ebd6b3eb2e0cfe Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 15:57:25 +0700 Subject: [PATCH 13/41] refactor(FE): Fix multiline formatting for isError conditions --- src/components/pages/finance/FinanceTable.tsx | 7 +++++-- .../transfer-to-laying/TransferToLayingFilterModal.tsx | 3 ++- .../pages/report/finance/tab/CustomerPaymentTab.tsx | 7 +++++-- .../report/logistic-stock/tab/PurchasesPerSupplierTab.tsx | 7 +++++-- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/components/pages/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx index 10d959b9..b30308a5 100644 --- a/src/components/pages/finance/FinanceTable.tsx +++ b/src/components/pages/finance/FinanceTable.tsx @@ -661,7 +661,8 @@ const FinanceTable = () => { errorMessage={filterFormik.errors.start_date} onChange={startDateChangeHandler} isError={ - filterFormik.touched.start_date && Boolean(filterFormik.errors.start_date) + filterFormik.touched.start_date && + Boolean(filterFormik.errors.start_date) } /> { errorMessage={filterFormik.errors.end_date} onChange={endDateChangeHandler} isError={ - (filterFormik.touched.end_date && Boolean(filterFormik.errors.end_date)) || hasDateError + (filterFormik.touched.end_date && + Boolean(filterFormik.errors.end_date)) || + hasDateError } />
diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 4b596155..2e4051ce 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -849,7 +849,8 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { className={{ wrapper: 'w-full' }} isNestedModal isError={ - formik.touched.start_date && Boolean(formik.errors.start_date) + formik.touched.start_date && + Boolean(formik.errors.start_date) } />
@@ -862,7 +863,9 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { className={{ wrapper: 'w-full' }} isNestedModal isError={ - (formik.touched.end_date && Boolean(formik.errors.end_date)) || hasDateError + (formik.touched.end_date && + Boolean(formik.errors.end_date)) || + hasDateError } />
diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index d0fb8d47..5eaa67e1 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -847,7 +847,8 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { className={{ wrapper: 'w-full' }} isNestedModal isError={ - formik.touched.start_date && Boolean(formik.errors.start_date) + formik.touched.start_date && + Boolean(formik.errors.start_date) } />
@@ -859,7 +860,9 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { className={{ wrapper: 'w-full' }} isNestedModal isError={ - (formik.touched.end_date && Boolean(formik.errors.end_date)) || hasDateError + (formik.touched.end_date && + Boolean(formik.errors.end_date)) || + hasDateError } />
From 0031a65f97c2d61f7134623e70d1c43e2fc30b57 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 16:30:31 +0700 Subject: [PATCH 14/41] refactor(FE): Ensure filter modal closes on reset across components --- src/components/pages/closing/ClosingsTable.tsx | 1 + src/components/pages/production/uniformity/UniformityTable.tsx | 1 + src/components/pages/report/expense/tab/ReportExpenseTab.tsx | 3 ++- src/components/pages/report/finance/tab/CustomerPaymentTab.tsx | 1 + src/components/pages/report/finance/tab/DebtSupplierTab.tsx | 1 + .../report/logistic-stock/tab/PurchasesPerSupplierTab.tsx | 1 + .../pages/report/marketing/tab/DailyMarketingTab.tsx | 1 + src/components/pages/report/marketing/tab/HppPerKandangTab.tsx | 1 + .../tab/ProductionResultProjectFlockKandangTab.tsx | 1 + 9 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/pages/closing/ClosingsTable.tsx b/src/components/pages/closing/ClosingsTable.tsx index fc5984b5..7885c75c 100644 --- a/src/components/pages/closing/ClosingsTable.tsx +++ b/src/components/pages/closing/ClosingsTable.tsx @@ -159,6 +159,7 @@ const ClosingsTable = () => { onReset: () => { updateFilter('location_id', ''); updateFilter('project_status', ''); + filterModal.closeModal(); }, }); diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index 615e4941..c420ec6c 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -493,6 +493,7 @@ const UniformityTable = () => { updateFilter('kandang_id', ''); filterFormik.resetForm(); + filterModal.closeModal(); }, [filterFormik, updateFilter]); const selectedRowIds = useMemo(() => { diff --git a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx index b9296f89..971536c5 100644 --- a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx +++ b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx @@ -15,7 +15,7 @@ import { import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge'; import RealizationStatusBadge from '@/components/pages/expense/RealizationStatusBadge'; import Table from '@/components/Table'; -import { cn, formatCurrency, formatDate } from '@/lib/helper'; +import { formatCurrency, formatDate } from '@/lib/helper'; import { ReportExpense } from '@/types/api/report/report-expense'; import { ReportExpenseApi } from '@/services/api/report'; import { isResponseSuccess } from '@/lib/api-helper'; @@ -145,6 +145,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { setFilterParams({}); setIsSubmitted(false); setPage(1); + filterModal.closeModal(); }, }); diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 2e4051ce..26577109 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -119,6 +119,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { toast.dismiss(); setDateErrorShown(false); } + filterModal.closeModal(); }, }); diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index d5341854..fb02e959 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -142,6 +142,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { filter_by: undefined, }); setIsSubmitted(false); + filterModal.closeModal(); }, }); diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 5eaa67e1..afcae9a4 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -147,6 +147,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { toast.dismiss(); setDateErrorShown(false); } + filterModal.closeModal(); }, }); diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index 0f68d3b0..0be358d8 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -142,6 +142,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { onReset: () => { setFilterParams({}); setIsSubmitted(false); + filterModal.closeModal(); }, }); diff --git a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx index 953b4a88..80d9da2e 100644 --- a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx @@ -137,6 +137,7 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { toast.dismiss(); setDateErrorShown(false); } + filterModal.closeModal(); }, }); diff --git a/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx index 20dddff1..9e844ad3 100644 --- a/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx +++ b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx @@ -238,6 +238,7 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { setFilterParams({}); setIsSubmitted(false); setPage(1); + filterModal.closeModal(); }, }); From 5a679017227baeb73a939478490298ce12da01fb Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 26 Feb 2026 09:11:05 +0700 Subject: [PATCH 15/41] feat(FE): Add transaction type and subtype options to constants --- src/config/constant.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/config/constant.ts b/src/config/constant.ts index 66d0af7d..9db7b6cd 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -545,3 +545,20 @@ export const MARKETING_DATE_FILTER_TYPE_OPTIONS = [ value: 'so_date', }, ]; + +export const TRANSACTION_TYPE_OPTIONS = [ + { label: 'Pembelian', value: 'PEMBELIAN' }, + { label: 'Penjualan', value: 'PENJUALAN' }, + { label: 'Biaya', value: 'BIAYA' }, +]; + +export const TRANSACTION_SUBTYPE_OPTIONS = { + PEMBELIAN: [{ label: 'Pembelian', value: 'PURCHASE_IN' }], + PENJUALAN: [{ label: 'Penjualan', value: 'MARKETING_OUT' }], + RECORDING: [ + { label: 'Recording Stock Out', value: 'RECORDING_STOCK_OUT' }, + { label: 'Recording Depletion Out', value: 'RECORDING_DEPLETION_OUT' }, + { label: 'Recording Depletion In', value: 'RECORDING_DEPLETION_IN' }, + { label: 'Recording Egg In', value: 'RECORDING_EGG_IN' }, + ], +}; From 88b9c890e510219d3fc52c2f7d8ff97fbf6c0883 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 26 Feb 2026 10:10:59 +0700 Subject: [PATCH 16/41] refactor(FE): Refactor inventory adjustment form --- .../form/InventoryAdjustmentForm.schema.ts | 145 ++- .../form/InventoryAdjustmentForm.tsx | 896 ++++++++++++++---- src/config/constant.ts | 6 +- src/types/api/inventory/adjustment.d.ts | 34 +- 4 files changed, 823 insertions(+), 258 deletions(-) diff --git a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema.ts b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema.ts index 42ecf48d..8858cf25 100644 --- a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema.ts +++ b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema.ts @@ -1,55 +1,102 @@ import * as Yup from 'yup'; -import { OptionType } from '@/components/input/SelectInput'; -export const InventoryAdjustmentFormSchema = Yup.object({ - product_category: Yup.mixed() - .nullable() - .test( - 'is-valid-option', - 'Kategori Produk wajib diisi!', - (value) => value !== null && value !== undefined - ), +export type InventoryAdjustmentFormSchemaType = { + location: { + value: number; + label: string; + } | null; + location_id: number; + project_flock: { + value: number; + label: string; + } | null; + project_flock_id: number; + kandang: { + value: number; + label: string; + } | null; + kandang_id: number; + project_flock_kandang: { + value: number; + label: string; + } | null; + project_flock_kandang_id: number; + product: { + value: number; + label: string; + } | null; + product_id: number; + transaction_type: string; + transaction_subtype: string; + qty: number | string; + price: number | string; + notes: string; +}; - product_category_id: Yup.number().nullable(), - - product: Yup.mixed() - .nullable() - .test( - 'is-valid-option', - 'Produk wajib diisi!', - (value) => value !== null && value !== undefined - ), - - product_id: Yup.number() - .nullable() - .required('Produk wajib diisi!') - .min(1, 'Produk wajib diisi!'), - - warehouse: Yup.mixed() - .nullable() - .test( - 'is-valid-option', - 'Warehouse wajib diisi!', - (value) => value !== null && value !== undefined - ), - - warehouse_id: Yup.number() - .nullable() - .required('Warehouse wajib diisi!') - .min(1, 'Warehouse wajib diisi!'), - - transaction_type: Yup.string() - .oneOf(['increase', 'decrease'], 'Tipe transaksi tidak valid') - .nullable() - .required('Tipe transaksi wajib diisi'), - - quantity: Yup.number() - .typeError('Kuantitas harus berupa angka') - .min(1, 'Minimal kuantitas adalah 1') - .required('Kuantitas wajib diisi'), - - note: Yup.string().required('Catatan wajib diisi!'), -}); +export const InventoryAdjustmentFormSchema: Yup.ObjectSchema = + Yup.object({ + location: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + location_id: Yup.number() + .min(1, 'Lokasi wajib diisi!') + .required('Lokasi wajib diisi!') + .typeError('Lokasi wajib diisi!'), + project_flock: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + project_flock_id: Yup.number() + .min(1, 'Project flock wajib diisi!') + .required('Project flock wajib diisi!') + .typeError('Project flock wajib diisi!'), + kandang: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + kandang_id: Yup.number() + .min(1, 'Kandang wajib diisi!') + .required('Kandang wajib diisi!') + .typeError('Kandang wajib diisi!'), + project_flock_kandang: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + project_flock_kandang_id: Yup.number() + .default(0) + .typeError('Project Flock Kandang wajib diisi!') + .test( + 'is-valid-project-flock-kandang', + 'Project Flock Kandang wajib diisi!', + (value) => value !== undefined && value !== null && value > 0 + ) + .required('Project Flock Kandang wajib diisi!'), + product: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + product_id: Yup.number() + .min(1, 'Produk wajib diisi!') + .required('Produk wajib diisi!') + .typeError('Produk wajib diisi!'), + transaction_type: Yup.string() + .oneOf( + ['PEMBELIAN', 'PENJUALAN', 'BIAYA', 'RECORDING'], + 'Tipe transaksi tidak valid' + ) + .required('Tipe transaksi wajib diisi'), + transaction_subtype: Yup.string().required('Sub tipe transaksi wajib diisi'), + qty: Yup.number() + .typeError('Kuantitas harus berupa angka') + .min(1, 'Minimal kuantitas adalah 1') + .required('Kuantitas wajib diisi'), + price: Yup.number() + .typeError('Harga harus berupa angka') + .min(0, 'Minimal harga adalah 0') + .required('Harga wajib diisi'), + notes: Yup.string().required('Catatan wajib diisi!'), + }); export type InventoryAdjustmentFormValues = Yup.InferType< typeof InventoryAdjustmentFormSchema diff --git a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx index 612fbb20..31b64bed 100644 --- a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx +++ b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx @@ -1,6 +1,6 @@ 'use client'; -import { isResponseError } from '@/lib/api-helper'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { InventoryAdjustmentApi } from '@/services/api/inventory'; import { CreateInventoryAdjustmentPayload, @@ -14,11 +14,11 @@ import { InventoryAdjustmentFormSchema, InventoryAdjustmentFormValues, } from '@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema'; +import { LocationApi, ProductApi } from '@/services/api/master-data'; import { - ProductApi, - ProductCategoryApi, - WarehouseApi, -} from '@/services/api/master-data'; + ProjectFlockApi, + ProjectFlockKandangApi, +} from '@/services/api/production'; import Button from '@/components/Button'; import { Icon } from '@iconify/react'; import SelectInput, { @@ -26,13 +26,22 @@ import SelectInput, { useSelect, } from '@/components/input/SelectInput'; import TextInput from '@/components/input/TextInput'; -import { RadioGroup } from '@/components/input/RadioInput'; import TextArea from '@/components/input/TextArea'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import AlertErrorList from '@/components/helper/form/FormErrors'; -import { ProductCategory } from '@/types/api/master-data/product-category'; +import { Location } from '@/types/api/master-data/location'; +import { ProjectFlock } from '@/types/api/production/project-flock'; +import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; +import { Kandang } from '@/types/api/master-data/kandang'; import { Product } from '@/types/api/master-data/product'; -import { Warehouse } from '@/types/api/master-data/warehouse'; +import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock'; +import { BaseApiResponse } from '@/types/api/api-general'; +import useSWR from 'swr'; +import { + TRANSACTION_TYPE_OPTIONS, + TRANSACTION_SUBTYPE_OPTIONS, +} from '@/config/constant'; +import NumberInput from '@/components/input/NumberInput'; interface InventoryAdjustmentFormProps { type?: 'add' | 'edit' | 'detail'; @@ -49,8 +58,19 @@ const InventoryAdjustmentForm = ({ InventoryAdjustmentFormErrorMessage, setInventoryAdjustmentFormErrorMessage, ] = useState(''); - const [disabledProduct, setDisabledProduct] = useState(true); - const [quantityLabel, setQuantityLabel] = useState('Tambah Stok'); + const [quantityLabel, setQuantityLabel] = useState('Kuantitas'); + + // Selected States untuk cascading selects + const [selectedLocation, setSelectedLocation] = useState( + null + ); + const [selectedProjectFlock, setSelectedProjectFlock] = + useState(null); + const [selectedKandang, setSelectedKandang] = useState( + null + ); + const [selectedProjectFlockLocationId, setSelectedProjectFlockLocationId] = + useState(''); // Submit Handler const createInventoryAdjustmentHandler = useCallback( @@ -71,34 +91,258 @@ const InventoryAdjustmentForm = ({ [router] ); - const formikInitialValues = useMemo< - Partial - >(() => { - return { - product_id: initialValues?.product_warehouse?.product_id ?? 0, - warehouse_id: initialValues?.product_warehouse?.warehouse_id ?? 0, - product_category: undefined, - product: undefined, - warehouse: undefined, - quantity: initialValues?.increase ?? initialValues?.decrease ?? 0, - transaction_type: undefined, - note: initialValues?.note ?? '', - }; - }, [initialValues]); + // API Data Fetching + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + loadMore: loadMoreLocations, + } = useSelect(LocationApi.basePath, 'id', 'name'); + + const { + setInputValue: setProjectFlockInputValue, + options: projectFlockOptions, + rawData: projectFlocksRawData, + isLoadingOptions: isLoadingProjectFlockOptions, + loadMore: loadMoreProjectFlocks, + } = useSelect( + ProjectFlockApi.basePath, + 'id', + 'flock_name', + 'search', + { + location_id: selectedProjectFlockLocationId, + } + ); + + // Lookup URL untuk mendapatkan project_flock_kandang_id + const projectFlockKandangLookupUrl = useMemo(() => { + if (!selectedProjectFlock || !selectedKandang) return null; + const params = new URLSearchParams({ + project_flock_id: selectedProjectFlock.value.toString(), + kandang_id: selectedKandang.value.toString(), + }); + return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`; + }, [selectedProjectFlock, selectedKandang]); + + const { data: projectFlockKandangLookupData } = useSWR( + projectFlockKandangLookupUrl, + projectFlockKandangLookupUrl + ? () => + ProjectFlockApi.getAllFetcher( + projectFlockKandangLookupUrl + ) as Promise> + : null + ); + + const projectFlockKandangLookup = + projectFlockKandangLookupData?.status === 'success' + ? projectFlockKandangLookupData.data + : undefined; + + // Fetch project_flock_kandang detail untuk edit mode saja (tidak perlu untuk detail) + const projectFlockKandangDetailUrl = useMemo(() => { + if (type !== 'edit' || !initialValues?.project_flock_kandang_id) + return null; + return `${ProjectFlockKandangApi.basePath}/${initialValues.project_flock_kandang_id}`; + }, [type, initialValues?.project_flock_kandang_id]); + + const { data: projectFlockKandangDetailData } = useSWR( + projectFlockKandangDetailUrl, + projectFlockKandangDetailUrl + ? () => + ProjectFlockKandangApi.getAllFetcher( + projectFlockKandangDetailUrl + ) as Promise> + : null + ); + + const projectFlockKandangDetail = + projectFlockKandangDetailData?.status === 'success' + ? projectFlockKandangDetailData.data + : undefined; + + // Fetch approved project flock kandangs untuk filter kandang options + const approvedProjectFlockKandangsUrl = useMemo(() => { + const params = new URLSearchParams({ + step_name: 'Disetujui', + limit: '100', + }); + return `${ProjectFlockKandangApi.basePath}?${params.toString()}`; + }, []); + + const { data: approvedProjectFlockKandangsData } = useSWR( + approvedProjectFlockKandangsUrl, + ProjectFlockKandangApi.getAllFetcher + ); + + const approvedProjectFlockKandangs = useMemo(() => { + if (!isResponseSuccess(approvedProjectFlockKandangsData)) return []; + return approvedProjectFlockKandangsData.data; + }, [approvedProjectFlockKandangsData]); + + // Product select dengan filter project_flock_kandang_id - hanya fetch jika project_flock_kandang_id ada + const productUrl = useMemo(() => { + if (!projectFlockKandangLookup?.project_flock_kandang_id) return null; + const params = new URLSearchParams({ + project_flock_kandang_id: + projectFlockKandangLookup.project_flock_kandang_id.toString(), + page: '1', + limit: '10', + }); + return `${ProductApi.basePath}?${params.toString()}`; + }, [projectFlockKandangLookup?.project_flock_kandang_id]); + + const { data: productData, isLoading: isLoadingProductOptions } = useSWR( + productUrl, + productUrl ? ProductApi.getAllFetcher : null + ); + + const productOptions = useMemo(() => { + if (!isResponseSuccess(productData)) return []; + return productData.data.map((p: Product) => ({ + value: p.id, + label: p.name, + })); + }, [productData]); + + const setProductInputValue = useCallback((value: string) => { + // Implementasi search jika diperlukan + }, []); + + const loadMoreProducts = useCallback(() => { + // Implementasi load more jika diperlukan + }, []); + + // Kandang options dari project flock data (filtered by approved status untuk add mode) + const kandangOptions = useMemo(() => { + let options: OptionType[] = []; + + if (selectedProjectFlock && isResponseSuccess(projectFlocksRawData)) { + const data = projectFlocksRawData.data as ProjectFlock[]; + const selectedProjectFlockData = data.find( + (pf) => pf.id === selectedProjectFlock.value + ); + + if (selectedProjectFlockData?.kandangs) { + // Get approved kandang ids untuk project flock yang dipilih + const approvedKandangIds = approvedProjectFlockKandangs + .filter((pfk) => pfk.project_flock_id === selectedProjectFlock.value) + .map((pfk) => pfk.kandang_id); + + const kandangOptions = selectedProjectFlockData.kandangs + .filter((kandang: Kandang) => { + // Untuk add mode, hanya tampilkan kandang yang approved + if (type === 'add') { + return approvedKandangIds.includes(kandang.id); + } + return true; + }) + .map((kandang: Kandang) => ({ + value: kandang.id, + label: kandang.name || '', + })); + options = options.concat(kandangOptions); + } + } + + if (projectFlockKandangDetail && type === 'edit') { + const currentKandang = projectFlockKandangDetail.kandang; + if ( + currentKandang && + !options.find((opt) => opt.value === currentKandang.id) + ) { + options.push({ + value: currentKandang.id, + label: currentKandang.name || '', + }); + } + } + + return options; + }, [ + selectedProjectFlock, + projectFlocksRawData, + projectFlockKandangDetail, + type, + approvedProjectFlockKandangs, + ]); + + // Enhanced options untuk edit/detail + const enhancedLocationOptions = useMemo(() => { + const options = [...locationOptions]; + + if (projectFlockKandangDetail && (type === 'edit' || type === 'detail')) { + const currentLocation = projectFlockKandangDetail.project_flock.location; + if ( + currentLocation && + !options.find((opt) => opt.value === currentLocation.id) + ) { + options.push({ + value: currentLocation.id, + label: currentLocation.name || '', + }); + } + } + + return options; + }, [locationOptions, projectFlockKandangDetail, type]); + + const enhancedProjectFlockOptions = useMemo(() => { + const options = [...projectFlockOptions]; + + if (projectFlockKandangDetail && (type === 'edit' || type === 'detail')) { + const currentProjectFlock = projectFlockKandangDetail.project_flock; + if ( + currentProjectFlock && + !options.find((opt) => opt.value === currentProjectFlock.id) + ) { + options.push({ + value: currentProjectFlock.id, + label: currentProjectFlock.flock_name || '', + }); + } + } + + return options; + }, [projectFlockOptions, projectFlockKandangDetail, type]); + + // Formik Initial Values + const formikInitialValues = useMemo>( + () => ({ + location: null, + location_id: 0, + project_flock: null, + project_flock_id: 0, + kandang: null, + kandang_id: 0, + project_flock_kandang: null, + project_flock_kandang_id: 0, + product: null, + product_id: 0, + transaction_type: '', + transaction_subtype: '', + qty: '', + price: '', + notes: '', + }), + [] + ); // Formik const formik = useFormik({ - enableReinitialize: true, + enableReinitialize: false, initialValues: formikInitialValues as InventoryAdjustmentFormValues, validationSchema: InventoryAdjustmentFormSchema, onSubmit: async (values) => { setInventoryAdjustmentFormErrorMessage(''); const payload: CreateInventoryAdjustmentPayload = { - product_id: values.product_id as number, - warehouse_id: values.warehouse_id as number, - quantity: values.quantity as number, - transaction_type: values.transaction_type as string, - note: values.note, + project_flock_kandang_id: values.project_flock_kandang_id, + product_id: values.product_id, + transaction_subtype: values.transaction_subtype, + qty: Number(values.qty), + price: Number(values.price), + notes: values.notes, }; switch (type) { @@ -109,111 +353,318 @@ const InventoryAdjustmentForm = ({ }, }); - // Fetch Data - const { - setInputValue: setProductCategoryInputValue, - options: productCategoryOptions, - isLoadingOptions: isLoadingProductCategoryOptions, - loadMore: loadMoreProductCategories, - } = useSelect(ProductCategoryApi.basePath, 'id', 'name'); + // Transaction subtype options berdasarkan transaction_type + const transactionSubtypeOptions = useMemo(() => { + const transactionType = formik.values.transaction_type; - const { - setInputValue: setProductInputValue, - options: productOptions, - isLoadingOptions: isLoadingProductOptions, - loadMore: loadMoreProducts, - } = useSelect(ProductApi.basePath, 'id', 'name', 'search', { - product_category_id: formik.values.product_category_id - ? String(formik.values.product_category_id) - : '', - }); + if (transactionType === 'RECORDING') { + return TRANSACTION_SUBTYPE_OPTIONS.RECORDING; + } - const { - setInputValue: setWarehouseInputValue, - options: warehouseOptions, - isLoadingOptions: isLoadingWarehouseOptions, - loadMore: loadMoreWarehouses, - } = useSelect(WarehouseApi.basePath, 'id', 'name'); + return []; + }, [formik.values.transaction_type]); - // Options Handler - const productCategoryChangeHandler = ( - val: OptionType | OptionType[] | null - ) => { - formik.setFieldTouched('product_category_id', true); - formik.setFieldValue('product_category_id', (val as OptionType)?.value); + // Cek apakah subtype readonly (untuk PEMBELIAN/PENJUALAN) + const isTransactionSubtypeReadonly = useMemo(() => { + const transactionType = formik.values.transaction_type; + return transactionType === 'PEMBELIAN' || transactionType === 'PENJUALAN'; + }, [formik.values.transaction_type]); - formik.setFieldValue('product_category', val); + // Update quantity label berdasarkan transaction_subtype + useEffect(() => { + const subtype = formik.values.transaction_subtype; + if ( + subtype === 'RECORDING_STOCK_OUT' || + subtype === 'RECORDING_DEPLETION_OUT' || + subtype === 'MARKETING_OUT' + ) { + setQuantityLabel('Kurangi Stok'); + } else if ( + subtype === 'RECORDING_STOCK_IN' || + subtype === 'RECORDING_DEPLETION_IN' || + subtype === 'RECORDING_EGG_IN' || + subtype === 'PURCHASE_IN' + ) { + setQuantityLabel('Tambah Stok'); + } else { + setQuantityLabel('Kuantitas'); + } + }, [formik.values.transaction_subtype]); - const disabled = (val as OptionType)?.value == null; - setDisabledProduct(disabled); - formik.setFieldValue('product_id', 0); + // Event Handlers + const locationChangeHandler = (val: OptionType | OptionType[] | null) => { + const location = val as OptionType | null; + const locationId = location ? Number(location.value) : 0; + + formik.setFieldTouched('location', true); + formik.setFieldValue('location', location); + formik.setFieldTouched('location_id', true); + formik.setFieldValue('location_id', locationId); + + setSelectedLocation(location); + setSelectedProjectFlockLocationId( + location ? location.value.toString() : '' + ); + + // Reset dependent fields + setSelectedProjectFlock(null); + setSelectedKandang(null); + formik.setFieldValue('project_flock', null); + formik.setFieldValue('project_flock_id', 0); + formik.setFieldValue('kandang', null); + formik.setFieldValue('kandang_id', 0); + formik.setFieldValue('project_flock_kandang', null); + formik.setFieldValue('project_flock_kandang_id', 0); formik.setFieldValue('product', null); - formik.setFieldTouched('product', false); - formik.setFieldTouched('product_id', false); + formik.setFieldValue('product_id', 0); + }; + + const projectFlockChangeHandler = (val: OptionType | OptionType[] | null) => { + const projectFlock = val as OptionType | null; + const projectFlockId = Number(projectFlock?.value); + + formik.setFieldTouched('project_flock', true); + formik.setFieldValue('project_flock', projectFlock); + formik.setFieldTouched('project_flock_id', true); + formik.setFieldValue('project_flock_id', projectFlockId); + + setSelectedProjectFlock(projectFlock); + setSelectedKandang(null); + + // Reset dependent fields + formik.setFieldValue('kandang', null); + formik.setFieldValue('kandang_id', 0); + formik.setFieldValue('project_flock_kandang', null); + formik.setFieldValue('project_flock_kandang_id', 0); + formik.setFieldValue('product', null); + formik.setFieldValue('product_id', 0); + }; + + const kandangChangeHandler = (val: OptionType | OptionType[] | null) => { + const kandang = val as OptionType | null; + const kandangId = Number(kandang?.value); + + formik.setFieldTouched('kandang', true); + formik.setFieldValue('kandang', kandang); + formik.setFieldTouched('kandang_id', true); + formik.setFieldValue('kandang_id', kandangId); + + setSelectedKandang(kandang); + + // Reset product karena kandang berubah + formik.setFieldValue('product', null); + formik.setFieldValue('product_id', 0); + formik.setFieldTouched('project_flock_kandang', true); + formik.setFieldTouched('project_flock_kandang_id', true); }; const productChangeHandler = (val: OptionType | OptionType[] | null) => { + formik.setFieldTouched('product', true); formik.setFieldValue('product', val); - formik.setFieldTouched('product_id', true); - formik.setFieldValue('product_id', (val as OptionType)?.value); + formik.setFieldValue('product_id', (val as OptionType)?.value ?? 0); }; - const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => { - formik.setFieldValue('warehouse', val); + const transactionTypeChangeHandler = ( + val: OptionType | OptionType[] | null + ) => { + const selectedType = (val as OptionType)?.value as string; - formik.setFieldTouched('warehouse_id', true); - formik.setFieldValue('warehouse_id', (val as OptionType)?.value); + formik.setFieldTouched('transaction_type', true); + formik.setFieldValue('transaction_type', selectedType); + + // Reset transaction_subtype + formik.setFieldValue('transaction_subtype', ''); + + // Auto-fill transaction_subtype untuk PEMBELIAN dan PENJUALAN + if (selectedType === 'PEMBELIAN') { + formik.setFieldValue( + 'transaction_subtype', + TRANSACTION_SUBTYPE_OPTIONS.PEMBELIAN.value + ); + } else if (selectedType === 'PENJUALAN') { + formik.setFieldValue( + 'transaction_subtype', + TRANSACTION_SUBTYPE_OPTIONS.PENJUALAN.value + ); + } + }; + + const transactionSubtypeChangeHandler = ( + val: OptionType | OptionType[] | null + ) => { + const selectedSubtype = (val as OptionType)?.value as string; + formik.setFieldTouched('transaction_subtype', true); + formik.setFieldValue('transaction_subtype', selectedSubtype); }; const resetHandler = () => { formik.resetForm(); - setQuantityLabel('Tambah Stok'); - productCategoryChangeHandler(null); - productChangeHandler(null); - warehouseChangeHandler(null); + setQuantityLabel('Kuantitas'); + setSelectedLocation(null); + setSelectedProjectFlock(null); + setSelectedKandang(null); + setSelectedProjectFlockLocationId(''); }; - const { setValues: formikSetValues } = formik; - - // Effect + // Effect - Set project_flock_kandang_id dari lookup useEffect(() => { - if (initialValues?.product_warehouse?.product?.id) { - setDisabledProduct(false); - formik.setFieldValue( - 'product_id', - initialValues.product_warehouse.product.id - ); - formik.setFieldValue('product', { - value: initialValues.product_warehouse.product.id, - label: initialValues.product_warehouse.product.name, - }); - formik.setFieldValue( - 'warehouse_id', - initialValues.product_warehouse.warehouse.id - ); - formik.setFieldValue('warehouse', { - value: initialValues.product_warehouse.warehouse.id, - label: initialValues.product_warehouse.warehouse.name, - }); - formik.setFieldValue( - 'quantity', - initialValues.product_warehouse.quantity - ); - formik.setFieldValue('note', initialValues.note); + if (projectFlockKandangLookup?.project_flock_kandang_id) { + const projectFlockKandangId = + projectFlockKandangLookup.project_flock_kandang_id; + + if (formik.values.project_flock_kandang_id !== projectFlockKandangId) { + formik.setFieldValue('project_flock_kandang_id', projectFlockKandangId); + formik.setFieldValue('project_flock_kandang', { + value: projectFlockKandangId, + label: `${projectFlockKandangLookup.project_flock.flock_name} - ${projectFlockKandangLookup.kandang.name}`, + }); + } } - }, [formik, initialValues, setQuantityLabel, setDisabledProduct]); - useEffect(() => { - formikSetValues(formikInitialValues as InventoryAdjustmentFormValues); - }, [formikSetValues, formikInitialValues]); + }, [projectFlockKandangLookup, formik.values.project_flock_kandang_id]); - // Utils Function - const formatNumber = (value: string) => { - const numericValue = value.replace(/[^0-9.]/g, ''); - const [integer, decimal] = numericValue.split('.'); - const formattedInteger = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ','); - return decimal ? `${formattedInteger}.${decimal}` : formattedInteger; - }; + // Effect - Set initial values untuk edit mode (dengan projectFlockKandangDetail) + useEffect(() => { + if (initialValues && type === 'edit') { + const transactionSubtype = initialValues.transaction_subtype; + + // Determine transaction_type dari transaction_subtype + let transactionType = ''; + if (transactionSubtype === 'PURCHASE_IN') { + transactionType = 'PEMBELIAN'; + } else if (transactionSubtype === 'MARKETING_OUT') { + transactionType = 'PENJUALAN'; + } else if (transactionSubtype?.startsWith('RECORDING')) { + transactionType = 'RECORDING'; + } + + // Set lokasi + if (initialValues.location) { + const locationOption = { + value: initialValues.location.id, + label: initialValues.location.name, + }; + setSelectedLocation(locationOption); + setSelectedProjectFlockLocationId(initialValues.location.id.toString()); + } + + // Set project flock + if (initialValues.project_flock) { + const projectFlockOption = { + value: initialValues.project_flock.id, + label: initialValues.project_flock.flock_name, + }; + setSelectedProjectFlock(projectFlockOption); + } + + // Set kandang dari project_flock_kandang jika ada (hanya untuk edit mode) + if (projectFlockKandangDetail) { + const kandangOption = { + value: projectFlockKandangDetail.kandang.id, + label: projectFlockKandangDetail.kandang.name || '', + }; + setSelectedKandang(kandangOption); + } + + formik.setValues({ + location: initialValues.location + ? { + value: initialValues.location.id, + label: initialValues.location.name, + } + : null, + location_id: initialValues.location?.id ?? 0, + project_flock: initialValues.project_flock + ? { + value: initialValues.project_flock.id, + label: initialValues.project_flock.flock_name, + } + : null, + project_flock_id: initialValues.project_flock?.id ?? 0, + kandang: projectFlockKandangDetail?.kandang + ? { + value: projectFlockKandangDetail.kandang.id, + label: projectFlockKandangDetail.kandang.name || '', + } + : null, + kandang_id: projectFlockKandangDetail?.kandang?.id ?? 0, + project_flock_kandang: initialValues.project_flock_kandang_id + ? { + value: initialValues.project_flock_kandang_id, + label: `${initialValues.project_flock?.flock_name || ''} - ${projectFlockKandangDetail?.kandang?.name || ''}`, + } + : null, + project_flock_kandang_id: initialValues.project_flock_kandang_id ?? 0, + product: initialValues.product_warehouse?.product + ? { + value: initialValues.product_warehouse.product.id, + label: initialValues.product_warehouse.product.name, + } + : null, + product_id: initialValues.product_warehouse?.product?.id ?? 0, + transaction_type: transactionType, + transaction_subtype: transactionSubtype, + qty: initialValues.qty ?? '', + price: initialValues.price ?? '', + notes: initialValues.notes ?? '', + }); + } + }, [formik.setValues, initialValues, projectFlockKandangDetail, type]); + + // Effect - Set initial values untuk detail mode (tanpa projectFlockKandangDetail) + useEffect(() => { + if (initialValues && type === 'detail') { + const transactionSubtype = initialValues.transaction_subtype; + + // Determine transaction_type dari transaction_subtype + let transactionType = ''; + if (transactionSubtype === 'PURCHASE_IN') { + transactionType = 'PEMBELIAN'; + } else if (transactionSubtype === 'MARKETING_OUT') { + transactionType = 'PENJUALAN'; + } else if (transactionSubtype?.startsWith('RECORDING')) { + transactionType = 'RECORDING'; + } + + formik.setValues({ + location: initialValues.location + ? { + value: initialValues.location.id, + label: initialValues.location.name, + } + : null, + location_id: initialValues.location?.id ?? 0, + project_flock: initialValues.project_flock + ? { + value: initialValues.project_flock.id, + label: initialValues.project_flock.flock_name, + } + : null, + project_flock_id: initialValues.project_flock?.id ?? 0, + kandang: null, // Tidak perlu kandang untuk detail mode + kandang_id: 0, + project_flock_kandang: initialValues.project_flock_kandang_id + ? { + value: initialValues.project_flock_kandang_id, + label: `${initialValues.project_flock?.flock_name || ''} - Kandang`, + } + : null, + project_flock_kandang_id: initialValues.project_flock_kandang_id ?? 0, + product: initialValues.product_warehouse?.product + ? { + value: initialValues.product_warehouse.product.id, + label: initialValues.product_warehouse.product.name, + } + : null, + product_id: initialValues.product_warehouse?.product?.id ?? 0, + transaction_type: transactionType, + transaction_subtype: transactionSubtype, + qty: initialValues.qty ?? '', + price: initialValues.price ?? '', + notes: initialValues.notes ?? '', + }); + } + }, [formik.setValues, initialValues, type]); // ===== Formik Error List ===== const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); @@ -244,25 +695,60 @@ const InventoryAdjustmentForm = ({ className='w-full mt-8 flex flex-col gap-6' >
- {/* Select Input Product Category */} + {/* Select Input Location */} + {/* Select Input Project Flock */} + + + {/* Select Input Kandang */} + + {/* Select Input Product */} - {/* Select Input Warehouse */} + {/* Select Input Transaction Type */} - - {/* Radio Button Flag Stock */} - { - formik.handleChange(e); - setQuantityLabel( - e.target.value === 'increase' ? 'Tambah Stok' : 'Kurangi Stok' - ); - }} - onBlur={formik.handleBlur} + value={ + formik.values.transaction_type + ? { + value: formik.values.transaction_type, + label: + TRANSACTION_TYPE_OPTIONS.find( + (opt) => opt.value === formik.values.transaction_type + )?.label || '', + } + : null + } + onChange={transactionTypeChangeHandler} + options={TRANSACTION_TYPE_OPTIONS} isError={ formik.touched.transaction_type && Boolean(formik.errors.transaction_type) } errorMessage={formik.errors.transaction_type as string} - color='primary' - required - bottomLabel={ - formik.values.transaction_type == undefined - ? 'Pilih salah satu tipe transaksi' - : undefined - } - disabled={type === 'detail'} + isDisabled={type === 'detail'} + placeholder='Pilih Tipe Transaksi' + isClearable /> - {/* Number Input Stock */} - + opt.value === + formik.values.transaction_subtype + )?.label || '', + } + : null + } + onChange={transactionSubtypeChangeHandler} + options={transactionSubtypeOptions} + isError={ + formik.touched.transaction_subtype && + Boolean(formik.errors.transaction_subtype) + } + errorMessage={formik.errors.transaction_subtype as string} + isDisabled={ + type === 'detail' || + isTransactionSubtypeReadonly || + formik.values.transaction_type === '' + } + placeholder={ + formik.values.transaction_type === '' + ? 'Pilih Tipe Transaksi terlebih dahulu' + : isTransactionSubtypeReadonly + ? 'Otomatis terisi' + : 'Pilih Sub Tipe Transaksi' + } + isClearable + /> + + {/* Number Input Quantity */} + { - const rawValue = e.target.value.replace(/,/g, ''); - const numericValue = parseFloat(rawValue); - if (!isNaN(numericValue)) { - formik.setFieldValue('quantity', numericValue); - } else { - formik.setFieldValue('quantity', 0); - } - }} + name='qty' + value={formik.values.qty} + onChange={formik.handleChange} onBlur={formik.handleBlur} - isError={ - formik.touched.quantity && Boolean(formik.errors.quantity) - } - errorMessage={formik.errors.quantity as string} + isError={formik.touched.qty && Boolean(formik.errors.qty)} + errorMessage={formik.errors.qty as string} readOnly={type === 'detail'} /> - {/* Text Area Input Reason */} -