diff --git a/src/components/pages/dashboard/DashboardProduction.tsx b/src/components/pages/dashboard/DashboardProduction.tsx index a0f7e471..81254442 100644 --- a/src/components/pages/dashboard/DashboardProduction.tsx +++ b/src/components/pages/dashboard/DashboardProduction.tsx @@ -5,12 +5,12 @@ import { Icon } from '@iconify/react'; import Modal, { useModal } from '@/components/Modal'; import DateInput from '@/components/input/DateInput'; import { OptionType, useSelect } from '@/components/input/SelectInput'; -import { useState, useEffect, useRef, useCallback } from 'react'; +import { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import useSWR from 'swr'; import { DashboardApi } from '@/services/api/dashboard'; import { useFormik } from 'formik'; import { ProjectFlockApi } from '@/services/api/production'; -import { KandangApi, LocationApi } from '@/services/api/master-data'; +import { LocationApi } from '@/services/api/master-data'; import { generateDashboardPDF } from '@/components/pages/dashboard/export/DashboardPDF'; import { DashboardFilterType, @@ -42,6 +42,7 @@ import { cn } from '@/lib/helper'; import DashboardExportStats, { DashboardExportStatsRef, } from '@/components/pages/dashboard/export/DashboardExportStats'; +import { ProjectFlock } from '@/types/api/production/project-flock'; // Helper function to normalize values to array const normalizeToArray = ( @@ -71,6 +72,7 @@ const DashboardProduction = () => { const [selectedLocationIds, setSelectedLocationIds] = useState( normalizeToArray(filterValues.location) ); + const [kandangInputValue, setRawKandangInputValue] = useState(''); const [exporting, setExporting] = useState(false); const allChartsRef = useRef(null); const allStatsRef = useRef(null); @@ -114,23 +116,25 @@ const DashboardProduction = () => { options: flockOptions, isLoadingOptions: isLoadingFlockOptions, loadMore: loadMoreFlock, - } = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', '', { - location_id: selectedLocationIds ? selectedLocationIds.toString() : '', - }); + rawData: projectFlocksRawData, + } = useSelect( + ProjectFlockApi.basePath, + 'id', + 'flock_name', + '', + { + location_id: + selectedLocationIds.length > 0 ? selectedLocationIds.toString() : '', + } + ); + const { setInputValue: setInputValueLocation, options: locationOptions, isLoadingOptions: isLoadingLocationOptions, loadMore: loadMoreLocation, } = useSelect(LocationApi.basePath, 'id', 'name'); - const { - setInputValue: setInputValueKandang, - options: kandangOptions, - isLoadingOptions: isLoadingKandangOptions, - loadMore: loadMoreKandang, - } = useSelect(KandangApi.basePath, 'id', 'name', '', { - location_id: selectedLocationIds ? selectedLocationIds.toString() : '', - }); + const comparisonTypeOptions = [ { value: 'FARM', label: 'Farm' }, { value: 'FLOCK', label: 'Flock' }, @@ -161,12 +165,68 @@ const DashboardProduction = () => { const { resetForm } = formik; + const selectedFlockIds = useMemo( + () => normalizeToArray(formik.values.flock), + [formik.values.flock] + ); + + const derivedKandangOptions = useMemo(() => { + if (!isResponseSuccess(projectFlocksRawData)) return []; + + const availableProjectFlocks = projectFlocksRawData.data.filter( + (projectFlock) => + selectedFlockIds.length === 0 || + selectedFlockIds.includes(projectFlock.id) + ); + + const kandangMap = new Map>(); + + availableProjectFlocks.forEach((projectFlock) => { + projectFlock.kandangs?.forEach((kandang) => { + if (!kandangMap.has(kandang.id)) { + kandangMap.set(kandang.id, { + value: kandang.id, + label: kandang.name, + }); + } + }); + }); + + const normalizedSearch = kandangInputValue.trim().toLowerCase(); + const allOptions = Array.from(kandangMap.values()); + + if (!normalizedSearch) return allOptions; + + return allOptions.filter((option) => + option.label.toLowerCase().includes(normalizedSearch) + ); + }, [projectFlocksRawData, selectedFlockIds, kandangInputValue]); + + const kandangSelect = useMemo( + () => ({ + setInputValue: setRawKandangInputValue, + options: derivedKandangOptions, + isLoadingOptions: isLoadingFlockOptions, + loadMore: loadMoreFlock, + }), + [derivedKandangOptions, isLoadingFlockOptions, loadMoreFlock] + ); + + const { + setInputValue: setInputValueKandang, + options: kandangOptions, + isLoadingOptions: isLoadingKandangOptions, + loadMore: loadMoreKandang, + } = kandangSelect; + const handleResetFilter = useCallback(() => { resetForm(); resetFilterValues(); // Clear stored filter values setAnalysisMode('OVERVIEW'); setSelectedLocationIds([]); - }, [resetForm, resetFilterValues]); + setRawKandangInputValue(''); + filterModal.closeModal(); + }, [filterModal, resetForm, resetFilterValues]); // ===== Formik Error List ===== const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); @@ -460,6 +520,7 @@ const DashboardProduction = () => { formik.setFieldValue('kandang', []); formik.setFieldValue('comparisonType', ''); setSelectedLocationIds([]); + setRawKandangInputValue(''); }} color='primary' className={{ @@ -505,6 +566,7 @@ const DashboardProduction = () => { className={{ select: 'rounded-lg text-sm border-base-content/10', }} + isClearable={true} /> )} @@ -530,6 +592,7 @@ const DashboardProduction = () => { // Reset dependent fields when location changes formik.setFieldValue('flock', []); formik.setFieldValue('kandang', []); + setRawKandangInputValue(''); }} errorMessage={formik.errors.location as string} options={locationOptions} @@ -541,6 +604,7 @@ const DashboardProduction = () => { className={{ select: 'rounded-lg text-sm border-base-content/10', }} + isClearable={true} /> ) : ( { // Reset dependent fields when location changes formik.setFieldValue('flock', []); formik.setFieldValue('kandang', []); + setRawKandangInputValue(''); }} errorMessage={formik.errors.location as string} options={locationOptions} @@ -572,6 +637,7 @@ const DashboardProduction = () => { className={{ select: 'rounded-lg text-sm border-base-content/10', }} + isClearable={true} /> )} @@ -596,9 +662,11 @@ const DashboardProduction = () => { | null | undefined } - onChange={(selected) => - formik.setFieldValue('flock', selected) - } + onChange={(selected) => { + formik.setFieldValue('flock', selected); + formik.setFieldValue('kandang', []); + setInputValueKandang(''); + }} errorMessage={formik.errors.flock as string} onInputChange={setInputValueFlock} onMenuScrollToBottom={loadMoreFlock} @@ -611,6 +679,7 @@ const DashboardProduction = () => { className={{ select: 'rounded-lg text-sm border-base-content/10', }} + isClearable={true} /> ) : ( { | null | undefined } - onChange={(selected) => - formik.setFieldValue('flock', selected) - } + onChange={(selected) => { + formik.setFieldValue('flock', selected); + formik.setFieldValue('kandang', []); + setInputValueKandang(''); + }} errorMessage={formik.errors.flock as string} onInputChange={setInputValueFlock} onMenuScrollToBottom={loadMoreFlock} @@ -637,6 +708,7 @@ const DashboardProduction = () => { className={{ select: 'rounded-lg text-sm border-base-content/10', }} + isClearable={true} /> )} @@ -675,6 +747,7 @@ const DashboardProduction = () => { className={{ select: 'rounded-lg text-sm border-base-content/10', }} + isClearable={true} /> ) : ( { className={{ select: 'rounded-lg text-sm border-base-content/10', }} + isClearable={true} /> )} diff --git a/src/components/pages/expense/form/ExpenseRequestForm.tsx b/src/components/pages/expense/form/ExpenseRequestForm.tsx index adc825c2..b50798c2 100644 --- a/src/components/pages/expense/form/ExpenseRequestForm.tsx +++ b/src/components/pages/expense/form/ExpenseRequestForm.tsx @@ -178,12 +178,14 @@ const ExpenseRequestForm = ({ setInputValue: setLocationInputValue, options: locationOptions, isLoadingOptions: isLoadingLocationOptions, + loadMore: loadMoreLocations, } = useSelect(LocationApi.basePath, 'id', 'name'); const { setInputValue: setVendorInputValue, options: supplierOptions, isLoadingOptions: isLoadingVendorOptions, + loadMore: loadMoreSuppliers, } = useSelect(SupplierApi.basePath, 'id', 'name'); const categoryChangeHandler = (val: OptionType | OptionType[] | null) => { @@ -408,6 +410,7 @@ const ExpenseRequestForm = ({ options={locationOptions} onInputChange={setLocationInputValue} isLoading={isLoadingLocationOptions} + onMenuScrollToBottom={loadMoreLocations} isError={ formik.touched.location_id && Boolean(formik.errors.location_id) } @@ -452,6 +455,7 @@ const ExpenseRequestForm = ({ options={supplierOptions} onInputChange={setVendorInputValue} isLoading={isLoadingVendorOptions} + onMenuScrollToBottom={loadMoreSuppliers} isError={ formik.touched.supplier_id && Boolean(formik.errors.supplier_id) } diff --git a/src/components/pages/marketing/MarketingFilter.tsx b/src/components/pages/marketing/MarketingFilter.tsx index 3f56854e..624c573c 100644 --- a/src/components/pages/marketing/MarketingFilter.tsx +++ b/src/components/pages/marketing/MarketingFilter.tsx @@ -10,6 +10,10 @@ import SelectInput, { useSelect, } from '@/components/input/SelectInput'; import { MARKETING_APPROVAL_LINE } from '@/config/approval-line'; +import { + MarketingFilterFormValues, + MarketingFilterSchema, +} from '@/components/pages/marketing/filter/MarketingFilter'; import { MarketingFilter } from '@/types/api/marketing/marketing'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import { MarketingApi } from '@/services/api/marketing/marketing'; @@ -70,8 +74,8 @@ const MarketingFilterModal = ({ limit: 'limit', }); - const uniqueCustomersOptions = useMemo(() => { - const seen = new Set(); + const salesCustomerOptions = useMemo(() => { + const seen = new Set(); return customersOptions.filter((customer) => { if (seen.has(customer.value)) return false; seen.add(customer.value); @@ -87,23 +91,19 @@ const MarketingFilterModal = ({ { value: 'DITOLAK', label: 'Ditolak' }, ]; - const formik = useFormik<{ - product_ids: OptionType[]; - status: OptionType | null; - customer_id: OptionType | null; - }>({ + const formik = useFormik({ initialValues: { product_ids: [], status: null, - customer_id: null, + customer: null, }, + validationSchema: MarketingFilterSchema, onSubmit: async (values) => { - const formattedValues = { - ...values, + const formattedValues: MarketingFilter = { product_ids: values.product_ids.map((item) => Number(item.value)), status: values.status?.value.toString() || '', - customer_id: Number(values.customer_id?.value), + customer_id: Number(values.customer?.value), }; onSubmit?.(formattedValues); @@ -121,7 +121,10 @@ const MarketingFilterModal = ({ }; const customerChangeHandler = (val: OptionType | OptionType[] | null) => { - formik.setFieldValue('customer_id', val as OptionType); + formik.setFieldValue( + 'customer', + !Array.isArray(val) ? (val as OptionType | null) : null + ); }; const statusChangeHandler = (val: OptionType | OptionType[] | null) => { @@ -187,9 +190,9 @@ const MarketingFilterModal = ({ label='Customer' isClearable placeholder='Pilih customer' - options={uniqueCustomersOptions} + options={salesCustomerOptions} isLoading={isLoadingCustomersOptions} - value={formik.values.customer_id} + value={formik.values.customer} onChange={customerChangeHandler} onInputChange={setCustomersInputValue} onMenuScrollToBottom={loadMoreCustomers} diff --git a/src/components/pages/marketing/filter/MarketingFilter.ts b/src/components/pages/marketing/filter/MarketingFilter.ts new file mode 100644 index 00000000..4ff9a792 --- /dev/null +++ b/src/components/pages/marketing/filter/MarketingFilter.ts @@ -0,0 +1,14 @@ +import { array, mixed, object } from 'yup'; +import { OptionType } from '@/components/input/SelectInput'; + +export const MarketingFilterSchema = object({ + product_ids: array().of(mixed>().required()).required(), + status: mixed>().nullable(), + customer: mixed>().nullable(), +}); + +export type MarketingFilterFormValues = { + product_ids: OptionType[]; + status: OptionType | null; + customer: OptionType | null; +};