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; +}; diff --git a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx index c04d09c4..f045621e 100644 --- a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx +++ b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx @@ -33,18 +33,18 @@ import { generateReportExpensePDF } from '../export/ReportExpenseExportPDF'; import { generateReportExpenseExcel } from '../export/ReportExpenseExportXLSX'; import toast from 'react-hot-toast'; import { - KandangApi, LocationApi, NonstockApi, SupplierApi, } from '@/services/api/master-data'; import { Supplier } from '@/types/api/master-data/supplier'; -import { Kandang } from '@/types/api/master-data/kandang'; import { Nonstock } from '@/types/api/master-data/nonstock'; 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'; +import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; +import { ProjectFlockKandangApi } from '@/services/api/production/project-flock-kandang'; interface ReportExpenseTabProps { tabId: string; @@ -136,7 +136,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { options: locationOptions, isLoadingOptions: isLoadingLocations, loadMore: loadMoreLocations, - } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); + } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); const { setInputValue: setSupplierInputValue, @@ -146,14 +146,14 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { } = useSelect(SupplierApi.basePath, 'id', 'name', 'search'); const { - setInputValue: setKandangInputValue, - options: kandangOptions, - isLoadingOptions: isLoadingKandangs, - loadMore: loadMoreKandangs, - } = useSelect( - KandangApi.basePath, + setInputValue: setProjectFlockKandangInputValue, + options: projectFlockKandangOptions, + isLoadingOptions: isLoadingProjectFlockKandangs, + loadMore: loadMoreProjectFlockKandangs, + } = useSelect( + ProjectFlockKandangApi.basePath, 'id', - 'name', + 'name_with_period', 'search', formik.values.location_id?.value ? { location_id: String(formik.values.location_id.value) } @@ -643,14 +643,14 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { { formik.setFieldValue('kandang_id', val); }} - onInputChange={setKandangInputValue} - onMenuScrollToBottom={loadMoreKandangs} + onInputChange={setProjectFlockKandangInputValue} + onMenuScrollToBottom={loadMoreProjectFlockKandangs} isClearable isDisabled={!formik.values.location_id} className={{ wrapper: 'w-full' }} diff --git a/src/components/pages/report/production-result/filter/ProductionResultFilter.ts b/src/components/pages/report/production-result/filter/ProductionResultFilter.ts index 6df3759e..4c1afdd0 100644 --- a/src/components/pages/report/production-result/filter/ProductionResultFilter.ts +++ b/src/components/pages/report/production-result/filter/ProductionResultFilter.ts @@ -43,15 +43,7 @@ export const ProductionResultFilterSchema = yup.object({ } return !!value; }), - kandang_id: yup - .mixed() - .required('Kandang wajib dipilih') - .test('is-not-empty', 'Kandang wajib dipilih', (value) => { - if (Array.isArray(value)) { - return value.length > 0; - } - return !!value; - }), + kandang_id: yup.mixed().nullable(), }) as yup.ObjectSchema; export type ProductionResultFilterValues = yup.InferType< diff --git a/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx index eb2c629c..8f5fbdc9 100644 --- a/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx +++ b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx @@ -46,6 +46,7 @@ import Modal, { useModal } from '@/components/Modal'; import { formatNumber } from '@/lib/helper'; import Pagination from '@/components/Pagination'; import ProductionResultSkeleton from '@/components/pages/report/production-result/skeleton/ProductionResultSkeleton'; +import { ProjectFlock } from '@/types/api/production/project-flock'; interface ProductionResultTabProps { tabId: string; @@ -238,6 +239,17 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { ? String(values.kandang_id.value) : undefined, }); + + const selectedProjectFlockKandangRawData = isResponseSuccess( + projectFlockKandangsRawData + ) + ? projectFlockKandangsRawData.data.find( + (item) => item.id === values.kandang_id?.value + ) + : undefined; + + setSelectedProjectFlockKandang(selectedProjectFlockKandangRawData); + filterModal.closeModal(); setIsSubmitted(true); setPage(1); @@ -255,6 +267,9 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { formik.validateForm(); }; + const [selectedProjectFlockKandang, setSelectedProjectFlockKandang] = + useState(); + // ===== OPTIONS ===== const { setInputValue: setAreaInputValue, @@ -279,7 +294,7 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { options: projectFlockOptions, isLoadingOptions: isLoadingProjectFlocks, loadMore: loadMoreProjectFlocks, - } = useSelect( + } = useSelect( ProjectFlockApi.basePath, 'id', 'flock_name', @@ -300,10 +315,11 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { options: projectFlockKandangOptions, isLoadingOptions: isLoadingProjectFlockKandangs, loadMore: loadMoreProjectFlockKandangs, - } = useSelect( + rawData: projectFlockKandangsRawData, + } = useSelect( ProjectFlockKandangApi.basePath, 'id', - 'kandang.name', + 'name_with_period', 'search', { area_id: formik.values.area_id?.value @@ -359,13 +375,15 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { ([url]: string[]) => httpClient>(url) ); - const projectFlockKandangs = useMemo( - () => - isResponseSuccess(projectFlockKandangsData) - ? projectFlockKandangsData.data - : null, - [projectFlockKandangsData] - ); + const projectFlockKandangs = useMemo(() => { + if (selectedProjectFlockKandang) { + return [selectedProjectFlockKandang]; + } + + return isResponseSuccess(projectFlockKandangsData) + ? projectFlockKandangsData.data + : null; + }, [projectFlockKandangsData, selectedProjectFlockKandang]); const projectFlockKandangMetadata = useMemo( () => @@ -804,7 +822,6 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { />