diff --git a/src/components/pages/closing/ClosingDetailTabs.tsx b/src/components/pages/closing/ClosingDetailTabs.tsx index dc8bd6f8..12651313 100644 --- a/src/components/pages/closing/ClosingDetailTabs.tsx +++ b/src/components/pages/closing/ClosingDetailTabs.tsx @@ -18,7 +18,7 @@ import HppExpeditionClosingTab from '@/components/pages/closing/tab/HppExpeditio import ClosingKandangList from '@/components/pages/closing/ClosingKandangList'; import { ProjectFlock } from '@/types/api/production/project-flock'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; -import { useClosingTabStore } from '@/stores/closing/closing-tab.store'; +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; interface ClosingDetailProps { id: number; initialValue?: ClosingGeneralInformation; @@ -33,7 +33,7 @@ const ClosingDetail: React.FC = ({ kandangData, }) => { const [activeTabId, setActiveTabId] = useState('sapronak'); - const tabActions = useClosingTabStore((state) => state.tabActions); + const tabActions = useTabActionsStore((state) => state.tabActions); const closingDetailTabs = useMemo(() => { const validTabs = [ diff --git a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx index aae37d8e..c10976eb 100644 --- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx +++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx @@ -236,6 +236,25 @@ const DeliveryOrderProductForm = ({ }); }; + // Handler untuk onChange - auto calculation real-time untuk field yang mempengaruhi total_price (total_peti, weight_per_convertion, price_per_convertion, sisa_berat, price_sisa_berat, price_per_qty, qty) + const handleFieldChange = ( + field: string, + value: number | string, + callback?: () => void + ) => { + formik.setFieldValue(field, value); + + setTimeout(() => { + handleMarketingCalculation(field, { + values: { ...formik.values, [field]: value }, + setFieldValue: formik.setFieldValue, + hasSisaBerat, + }); + }, 0); + + if (callback) callback(); + }; + // Handler khusus untuk toggle sisa berat - langsung pakai nilai baru const handleSisaBeratToggle = (newHasSisaBerat: boolean) => { setHasSisaBerat(newHasSisaBerat); @@ -520,13 +539,11 @@ const DeliveryOrderProductForm = ({ } per ${formik.values.convertion_unit?.value}`} value={formik.values.weight_per_convertion ?? ''} onChange={(e) => { - formik.setFieldValue( - 'weight_per_convertion', - Number(e.target.value) + const value = Number(e.target.value); + handleFieldChange('weight_per_convertion', value, () => + setCurrentInput(e.target.name) ); - setCurrentInput(e.target.name); }} - onBlur={() => handleBlurField('weight_per_convertion')} /> @@ -564,10 +581,11 @@ const DeliveryOrderProductForm = ({ name='total_peti' value={formik.values.total_peti ?? undefined} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('total_peti', value, () => + setCurrentInput(e.target.name) + ); }} - onBlur={() => handleBlurField('total_peti')} isError={ formik.touched.total_peti && Boolean(formik.errors.total_peti) } @@ -592,10 +610,11 @@ const DeliveryOrderProductForm = ({ name='avg_weight' value={formik.values.avg_weight} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('avg_weight', value, () => + setCurrentInput('avg_weight') + ); }} - onBlur={() => handleBlurField('avg_weight')} isError={ formik.touched.avg_weight && Boolean(formik.errors.avg_weight) @@ -613,10 +632,11 @@ const DeliveryOrderProductForm = ({ name='total_weight' value={formik.values.total_weight} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('total_weight', value, () => + setCurrentInput('total_weight') + ); }} - onBlur={() => handleBlurField('total_weight')} isError={ formik.touched.total_weight && Boolean(formik.errors.total_weight) @@ -638,10 +658,11 @@ const DeliveryOrderProductForm = ({ name='qty' value={formik.values.qty} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('qty', value, () => + setCurrentInput(e.target.name) + ); }} - onBlur={() => handleBlurField('qty')} isError={Boolean(formik.errors.qty)} errorMessage={formik.errors.qty} placeholder='Masukan Kuantitas' @@ -677,10 +698,11 @@ const DeliveryOrderProductForm = ({ name='price_per_convertion' value={formik.values.price_per_convertion ?? undefined} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('price_per_convertion', value, () => + setCurrentInput(e.target.name) + ); }} - onBlur={() => handleBlurField('price_per_convertion')} isError={ formik.touched.price_per_convertion && Boolean(formik.errors.price_per_convertion) @@ -699,10 +721,11 @@ const DeliveryOrderProductForm = ({ name='price_per_qty' value={formik.values.price_per_qty ?? undefined} onChange={(e) => { - formik.setFieldValue('price_per_qty', Number(e.target.value)); - setCurrentInput('price_per_qty'); + const value = Number(e.target.value); + handleFieldChange('price_per_qty', value, () => + setCurrentInput('price_per_qty') + ); }} - onBlur={() => handleBlurField('price_per_qty')} isError={ formik.touched.price_per_qty && Boolean(formik.errors.price_per_qty) @@ -721,10 +744,11 @@ const DeliveryOrderProductForm = ({ name='unit_price' value={formik.values.unit_price} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('unit_price', value, () => + setCurrentInput(e.target.name) + ); }} - onBlur={() => handleBlurField('unit_price')} isError={Boolean(formik.errors.unit_price)} errorMessage={formik.errors.unit_price} placeholder='Masukan Harga Satuan' @@ -760,10 +784,11 @@ const DeliveryOrderProductForm = ({ name='sisa_berat' value={formik.values.sisa_berat ?? undefined} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('sisa_berat', value, () => + setCurrentInput(e.target.name) + ); }} - onBlur={() => handleBlurField('sisa_berat')} isError={ formik.touched.sisa_berat && Boolean(formik.errors.sisa_berat) } @@ -776,10 +801,11 @@ const DeliveryOrderProductForm = ({ name='price_sisa_berat' value={formik.values.price_sisa_berat ?? undefined} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('price_sisa_berat', value, () => + setCurrentInput(e.target.name) + ); }} - onBlur={() => handleBlurField('price_sisa_berat')} isError={ formik.touched.price_sisa_berat && Boolean(formik.errors.price_sisa_berat) @@ -797,10 +823,11 @@ const DeliveryOrderProductForm = ({ name='total_price' value={formik.values.total_price} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('total_price', value, () => + setCurrentInput('total_price') + ); }} - onBlur={() => handleBlurField('total_price')} isError={ formik.touched.total_price && Boolean(formik.errors.total_price) } diff --git a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx index 70965071..8da873e5 100644 --- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx +++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx @@ -250,6 +250,25 @@ const SalesOrderProductForm = ({ }); }; + // Handler untuk onChange - auto calculation real-time untuk field yang mempengaruhi total_price (total_peti, weight_per_convertion, price_per_convertion, sisa_berat, price_sisa_berat, price_per_qty, qty) + const handleFieldChange = ( + field: string, + value: number | string, + callback?: () => void + ) => { + formik.setFieldValue(field, value); + + setTimeout(() => { + handleMarketingCalculation(field, { + values: { ...formik.values, [field]: value }, + setFieldValue: formik.setFieldValue, + hasSisaBerat, + }); + }, 0); + + if (callback) callback(); + }; + // Handler khusus untuk toggle sisa berat - langsung pakai nilai baru const handleSisaBeratToggle = (newHasSisaBerat: boolean) => { setHasSisaBerat(newHasSisaBerat); @@ -475,13 +494,11 @@ const SalesOrderProductForm = ({ } per ${formik.values.convertion_unit?.value}`} value={formik.values.weight_per_convertion ?? ''} onChange={(e) => { - formik.setFieldValue( - 'weight_per_convertion', - Number(e.target.value) + const value = Number(e.target.value); + handleFieldChange('weight_per_convertion', value, () => + setCurrentInput(e.target.name) ); - setCurrentInput(e.target.name); }} - onBlur={() => handleBlurField('weight_per_convertion')} /> @@ -519,10 +536,11 @@ const SalesOrderProductForm = ({ name='total_peti' value={formik.values.total_peti ?? undefined} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('total_peti', value, () => + setCurrentInput(e.target.name) + ); }} - onBlur={() => handleBlurField('total_peti')} isError={ formik.touched.total_peti && Boolean(formik.errors.total_peti) } @@ -547,10 +565,11 @@ const SalesOrderProductForm = ({ name='avg_weight' value={formik.values.avg_weight} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('avg_weight', value, () => + setCurrentInput('avg_weight') + ); }} - onBlur={() => handleBlurField('avg_weight')} isError={ formik.touched.avg_weight && Boolean(formik.errors.avg_weight) @@ -568,10 +587,11 @@ const SalesOrderProductForm = ({ name='total_weight' value={formik.values.total_weight} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('total_weight', value, () => + setCurrentInput('total_weight') + ); }} - onBlur={() => handleBlurField('total_weight')} isError={ formik.touched.total_weight && Boolean(formik.errors.total_weight) @@ -593,10 +613,11 @@ const SalesOrderProductForm = ({ name='qty' value={formik.values.qty} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('qty', value, () => + setCurrentInput(e.target.name) + ); }} - onBlur={() => handleBlurField('qty')} isError={formik.touched.qty && Boolean(formik.errors.qty)} errorMessage={formik.errors.qty} placeholder='Masukan Kuantitas' @@ -630,10 +651,11 @@ const SalesOrderProductForm = ({ name='price_per_convertion' value={formik.values.price_per_convertion ?? undefined} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('price_per_convertion', value, () => + setCurrentInput(e.target.name) + ); }} - onBlur={() => handleBlurField('price_per_convertion')} isError={ formik.touched.price_per_convertion && Boolean(formik.errors.price_per_convertion) @@ -652,10 +674,11 @@ const SalesOrderProductForm = ({ name='price_per_qty' value={formik.values.price_per_qty ?? undefined} onChange={(e) => { - formik.setFieldValue('price_per_qty', Number(e.target.value)); - setCurrentInput('price_per_qty'); + const value = Number(e.target.value); + handleFieldChange('price_per_qty', value, () => + setCurrentInput('price_per_qty') + ); }} - onBlur={() => handleBlurField('price_per_qty')} isError={ formik.touched.price_per_qty && Boolean(formik.errors.price_per_qty) @@ -674,10 +697,11 @@ const SalesOrderProductForm = ({ name='unit_price' value={formik.values.unit_price} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('unit_price', value, () => + setCurrentInput(e.target.name) + ); }} - onBlur={() => handleBlurField('unit_price')} isError={ formik.touched.unit_price && Boolean(formik.errors.unit_price) } @@ -715,10 +739,11 @@ const SalesOrderProductForm = ({ name='sisa_berat' value={formik.values.sisa_berat ?? undefined} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('sisa_berat', value, () => + setCurrentInput(e.target.name) + ); }} - onBlur={() => handleBlurField('sisa_berat')} isError={ formik.touched.sisa_berat && Boolean(formik.errors.sisa_berat) } @@ -731,10 +756,11 @@ const SalesOrderProductForm = ({ name='price_sisa_berat' value={formik.values.price_sisa_berat ?? undefined} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('price_sisa_berat', value, () => + setCurrentInput(e.target.name) + ); }} - onBlur={() => handleBlurField('price_sisa_berat')} isError={ formik.touched.price_sisa_berat && Boolean(formik.errors.price_sisa_berat) @@ -752,10 +778,11 @@ const SalesOrderProductForm = ({ name='total_price' value={formik.values.total_price} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('total_price', value, () => + setCurrentInput('total_price') + ); }} - onBlur={() => handleBlurField('total_price')} isError={ formik.touched.total_price && Boolean(formik.errors.total_price) } diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index 7a8ba4e1..dbf1804b 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -3,7 +3,10 @@ import Button from '@/components/Button'; import CheckboxInput from '@/components/input/CheckboxInput'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; -import { OptionType, useSelect } from '@/components/input/SelectInput'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; @@ -22,6 +25,7 @@ import { useRouter } from 'next/navigation'; import { ChangeEventHandler, useEffect, useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import useSWR from 'swr'; +import { useFormik } from 'formik'; import RequirePermission from '@/components/helper/RequirePermission'; import StatusBadge from '@/components/helper/StatusBadge'; @@ -32,6 +36,12 @@ import { useProjectFlockStore } from '@/stores/production/project-flock/project- import { ProjectFlockFormValues } from './form/ProjectFlockForm.schema'; import { useChickinStore } from '@/stores/production/chickin/chickin.store'; import { useProjectFlockClosingStore } from '@/stores/production/project-flock-closing/project-flock-closing.store'; +import { + ProjectFlockFilterSchema, + ProjectFlockFilterType, +} from './filter/ProjectFlockFilter'; +import Modal from '@/components/Modal'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; const RowOptionsMenu = ({ props, @@ -154,19 +164,21 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { } = useTableFilter({ initial: { search: '', - areaFilter: '', - locationFilter: '', - kandangFilter: '', - periodFilter: '', + area_id: '', + location_id: '', + kandang_id: '', + category: '', + period: '', }, paramMap: { page: 'page', pageSize: 'limit', search: 'search', - areaFilter: 'area_id', - locationFilter: 'location_id', - kandangFilter: 'kandang_id', - periodFilter: 'period', + area_id: 'area_id', + location_id: 'location_id', + kandang_id: 'kandang_id', + category: 'category', + period: 'period', }, }); const router = useRouter(); @@ -207,6 +219,190 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { setClosingLoading, } = useProjectFlockClosingStore(); + // ===== FILTER MODAL STATE ===== + const filterModal = useModal(); + + // ===== FILTER DEPENDENCIES STATE ===== + const [filterAreaId, setFilterAreaId] = useState( + undefined + ); + const [filterLocationId, setFilterLocationId] = useState( + undefined + ); + + // ===== FORMIK SETUP FOR FILTER ===== + const formik = useFormik({ + initialValues: { + area_id: null, + location_id: null, + kandang_id: null, + category: null, + period: null, + }, + validationSchema: ProjectFlockFilterSchema, + onSubmit: (values, { setSubmitting }) => { + updateFilter('area_id', values.area_id || ''); + updateFilter('location_id', values.location_id || ''); + updateFilter('kandang_id', values.kandang_id || ''); + updateFilter('category', values.category || ''); + updateFilter('period', values.period || ''); + filterModal.closeModal(); + setSubmitting(false); + }, + onReset: () => { + updateFilter('area_id', ''); + updateFilter('location_id', ''); + updateFilter('kandang_id', ''); + updateFilter('category', ''); + updateFilter('period', ''); + setFilterAreaId(undefined); + setFilterLocationId(undefined); + filterModal.closeModal(); + }, + }); + + // ===== FILTER OPTIONS ===== + const { + setInputValue: setAreaInputValue, + options: areaOptions, + isLoadingOptions: isLoadingAreaOptions, + loadMore: loadMoreAreas, + } = useSelect(AreaApi.basePath, 'id', 'name'); + + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + loadMore: loadMoreLocations, + } = useSelect(LocationApi.basePath, 'id', 'name', 'search', { + area_id: filterAreaId || '', + }); + + const { + setInputValue: setKandangInputValue, + options: kandangOptions, + isLoadingOptions: isLoadingKandangOptions, + loadMore: loadMoreKandangs, + } = useSelect(KandangApi.basePath, 'id', 'name', 'search', { + area_id: filterAreaId || '', + location_id: filterLocationId || '', + }); + + const categoryOptions = useMemo( + () => [ + { value: 'GROWING', label: 'Growing' }, + { value: 'LAYING', label: 'Laying' }, + ], + [] + ); + + const periodOptions = useMemo( + () => [ + { value: '1', label: 'Periode 1' }, + { value: '2', label: 'Periode 2' }, + ], + [] + ); + + // ===== FILTER HELPERS ===== + const areaValue = useMemo(() => { + if (!formik.values.area_id) return null; + return ( + areaOptions.find((opt) => String(opt.value) === formik.values.area_id) || + null + ); + }, [formik.values.area_id, areaOptions]); + + const locationValue = useMemo(() => { + if (!formik.values.location_id) return null; + return ( + locationOptions.find( + (opt) => String(opt.value) === formik.values.location_id + ) || null + ); + }, [formik.values.location_id, locationOptions]); + + const kandangValue = useMemo(() => { + if (!formik.values.kandang_id) return null; + return ( + kandangOptions.find( + (opt) => String(opt.value) === formik.values.kandang_id + ) || null + ); + }, [formik.values.kandang_id, kandangOptions]); + + const categoryValue = useMemo(() => { + if (!formik.values.category) return null; + return ( + categoryOptions.find((opt) => opt.value === formik.values.category) || + null + ); + }, [formik.values.category, categoryOptions]); + + const periodValue = useMemo(() => { + if (!formik.values.period) return null; + return ( + periodOptions.find((opt) => opt.value === formik.values.period) || null + ); + }, [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; + setFilterAreaId(areaId); + if (!areaId) { + setFilterLocationId(undefined); + formik.setFieldValue('location_id', null); + formik.setFieldValue('kandang_id', null); + } + }; + + const handleFilterLocationChange = (location: OptionType | null) => { + const locationId = location?.value ? String(location.value) : undefined; + setFilterLocationId(locationId); + if (!locationId) { + formik.setFieldValue('kandang_id', null); + } + }; + + // ===== HANDLE FILTER MODAL OPEN ===== + const handleFilterModalOpen = () => { + const areaId = tableFilterState.area_id || null; + const locationId = tableFilterState.location_id || null; + + formik.setValues({ + area_id: areaId, + location_id: locationId, + kandang_id: tableFilterState.kandang_id || null, + category: tableFilterState.category || null, + period: tableFilterState.period || null, + }); + + setFilterAreaId(areaId || undefined); + setFilterLocationId(locationId || undefined); + + filterModal.openModal(); + }; + // ===== Fetch Data ===== const { data: projectFlocks, @@ -768,26 +964,21 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { void }) => { }} /> + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ { + if (!Array.isArray(val)) { + const areaValue = val?.value ? String(val.value) : null; + formik.setFieldValue('area_id', areaValue); + handleFilterAreaChange(val || null); + } + }} + onInputChange={setAreaInputValue} + isLoading={isLoadingAreaOptions} + isClearable + onMenuScrollToBottom={loadMoreAreas} + className={{ wrapper: 'w-full' }} + /> + + { + if (!Array.isArray(val)) { + const locationValue = val?.value ? String(val.value) : null; + formik.setFieldValue('location_id', locationValue); + handleFilterLocationChange(val || null); + } + }} + onInputChange={setLocationInputValue} + isLoading={isLoadingLocationOptions} + isClearable + onMenuScrollToBottom={loadMoreLocations} + className={{ wrapper: 'w-full' }} + /> + + { + if (!Array.isArray(val)) { + formik.setFieldValue( + 'kandang_id', + val?.value ? String(val.value) : null + ); + } + }} + onInputChange={setKandangInputValue} + isLoading={isLoadingKandangOptions} + isClearable + onMenuScrollToBottom={loadMoreKandangs} + className={{ wrapper: 'w-full' }} + /> + + { + if (!Array.isArray(val)) { + formik.setFieldValue('category', val?.value || null); + } + }} + className={{ wrapper: 'w-full' }} + isClearable={true} + /> + + { + if (!Array.isArray(val)) { + formik.setFieldValue('period', val?.value || null); + } + }} + className={{ wrapper: 'w-full' }} + isClearable + /> +
+ + {/* Modal Footer */} +
+ + +
+
+
+ {/* Project Flock Closing Modal */} ( + ProjectFlockApi.basePath, + 'id', + 'flock_name', + 'search', + selectedKandangId ? { kandang_id: `[${selectedKandangId}]` } : undefined + ); + const selectedKandang = projectFlock.kandangs?.find( (kandang) => kandang.id === Number(selectedKandangId) ); + // Cek apakah ada project aktif di kandang yang sama (selain project saat ini) + // Hanya boleh 1 kandang aktif, jadi jika ada project lain yang aktif, tidak bisa di-unclose baik kategori growing maupun laying + const hasActiveProjectWithSameKandang = isResponseSuccess(projectFlockRawData) + ? projectFlockRawData.data.some((pf) => + pf.kandangs?.some( + (k) => + k.id === Number(selectedKandangId) && + pf.id !== projectFlock.id && + k.status !== 'NON_ACTIVE' + ) + ) + : false; + + const isCloseButtonDisabled = + !selectedKandangId || + projectFlock?.approval?.step_number == 1 || + (selectedKandang?.status === 'NON_ACTIVE' && + hasActiveProjectWithSameKandang); + const { data: projectFlockApprovalResponse } = useSWR( projectFlock.id ? ['approval-project-flock', projectFlock.id] : undefined, ([, id]) => ProjectFlockApi.getApprovalLineHistory(Number(id)) @@ -419,10 +447,7 @@ const ProjectFlockDetail = ({ className='w-full px-2 py-1 text-sm' variant='outline' color='error' - disabled={ - !selectedKandangId || - projectFlock?.approval?.step_number == 1 - } + disabled={isCloseButtonDisabled} > {selectedKandang?.status === 'NON_ACTIVE' ? ( <> diff --git a/src/components/pages/production/project-flock/filter/ProjectFlockFilter.ts b/src/components/pages/production/project-flock/filter/ProjectFlockFilter.ts new file mode 100644 index 00000000..a31d6680 --- /dev/null +++ b/src/components/pages/production/project-flock/filter/ProjectFlockFilter.ts @@ -0,0 +1,21 @@ +import * as yup from 'yup'; + +export type ProjectFlockFilterType = { + area_id: string | null; + location_id: string | null; + kandang_id: string | null; + category: string | null; + period: string | null; +}; + +export const ProjectFlockFilterSchema = yup.object({ + area_id: yup.string().nullable(), + location_id: yup.string().nullable(), + kandang_id: yup.string().nullable(), + category: yup.string().nullable(), + period: yup.string().nullable(), +}); + +export type ProjectFlockFilterValues = yup.InferType< + typeof ProjectFlockFilterSchema +>; diff --git a/src/components/pages/report/expense/ReportExpenseTabs.tsx b/src/components/pages/report/expense/ReportExpenseTabs.tsx index 704d1f6f..0bde153d 100644 --- a/src/components/pages/report/expense/ReportExpenseTabs.tsx +++ b/src/components/pages/report/expense/ReportExpenseTabs.tsx @@ -3,12 +3,12 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; -import { useReportTabStore } from '@/stores/report/report-tab.store'; +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import ReportExpenseTab from './tab/ReportExpenseTab'; const ReportExpenseTabs = () => { const [activeTabId, setActiveTabId] = useState('1'); - const tabActions = useReportTabStore((state) => state.tabActions); + const tabActions = useTabActionsStore((state) => state.tabActions); const tabs = [ { diff --git a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx index 2581ec5c..121338b7 100644 --- a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx +++ b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx @@ -19,7 +19,7 @@ import { cn, 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'; -import { useReportTabStore } from '@/stores/report/report-tab.store'; +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import Modal, { useModal } from '@/components/Modal'; import Pagination from '@/components/Pagination'; import ReportExpenseSkeleton from '@/components/pages/report/expense/skeleton/ReportExpenseSkeleton'; @@ -305,8 +305,8 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { ]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useReportTabStore((state) => state.setTabActions); - const clearTabActions = useReportTabStore((state) => state.clearTabActions); + const setTabActions = useTabActionsStore((state) => state.setTabActions); + const clearTabActions = useTabActionsStore((state) => state.clearTabActions); useEffect(() => { setTabActions( diff --git a/src/components/pages/report/finance/FinanceTabs.tsx b/src/components/pages/report/finance/FinanceTabs.tsx index de924f62..5c49ed3c 100644 --- a/src/components/pages/report/finance/FinanceTabs.tsx +++ b/src/components/pages/report/finance/FinanceTabs.tsx @@ -4,11 +4,11 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; import CustomerPaymentTab from '@/components/pages/report/finance/tab/CustomerPaymentTab'; import DebtSupplierTab from '@/components/pages/report/finance/tab/DebtSupplierTab'; -import { useReportTabStore } from '@/stores/report/report-tab.store'; +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; const FinanceTabs = () => { const [activeTabId, setActiveTabId] = useState('1'); - const tabActions = useReportTabStore((state) => state.tabActions); + const tabActions = useTabActionsStore((state) => state.tabActions); const tabs = [ { diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 1c546058..9ee90fae 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -34,7 +34,7 @@ import { } from '@/components/pages/report/finance/filter/CustomerPaymentFilter'; import { generateCustomerPaymentExcel } from '@/components/pages/report/finance/export/CustomerPaymentExportXLSX'; import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/export/CustomerPaymentExportPDF'; -import { useReportTabStore } from '@/stores/report/report-tab.store'; +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'; @@ -373,8 +373,8 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { }, [customerPaymentExport, filterParams, customerOptions]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useReportTabStore((state) => state.setTabActions); - const clearTabActions = useReportTabStore((state) => state.clearTabActions); + const setTabActions = useTabActionsStore((state) => state.setTabActions); + const clearTabActions = useTabActionsStore((state) => state.clearTabActions); useEffect(() => { setTabActions( diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index 635bece8..d1b7425d 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -31,7 +31,7 @@ import { Color } from '@/types/theme'; import { Supplier } from '@/types/api/master-data/supplier'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; -import { useReportTabStore } from '@/stores/report/report-tab.store'; +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import StatusBadge from '@/components/helper/StatusBadge'; import DebtSupplierSkeleton from '@/components/pages/report/finance/skeleton/DebtSupplierSkeleton'; @@ -265,8 +265,8 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { }, [debtSupplierExport]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useReportTabStore((state) => state.setTabActions); - const clearTabActions = useReportTabStore((state) => state.clearTabActions); + const setTabActions = useTabActionsStore((state) => state.setTabActions); + const clearTabActions = useTabActionsStore((state) => state.clearTabActions); useEffect(() => { setTabActions( diff --git a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx index 1e3f4109..5e292afa 100644 --- a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx +++ b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx @@ -3,11 +3,11 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; import PurchasesPerSupplierTab from '@/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab'; -import { useReportTabStore } from '@/stores/report/report-tab.store'; +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; const LogisticStockTabs = () => { const [activeTabId, setActiveTabId] = useState('1'); - const tabActions = useReportTabStore((state) => state.tabActions); + const tabActions = useTabActionsStore((state) => state.tabActions); const tabs = [ { diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 87c5ee8d..6f1599bc 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -30,7 +30,7 @@ import { } from '@/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; -import { useReportTabStore } from '@/stores/report/report-tab.store'; +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import PurchasePerSupplierSkeleton from '@/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton'; interface PurchasesPerSupplierTabProps { @@ -479,8 +479,8 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { ]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useReportTabStore((state) => state.setTabActions); - const clearTabActions = useReportTabStore((state) => state.clearTabActions); + const setTabActions = useTabActionsStore((state) => state.setTabActions); + const clearTabActions = useTabActionsStore((state) => state.clearTabActions); useEffect(() => { setTabActions( diff --git a/src/components/pages/report/marketing/MarketingTabs.tsx b/src/components/pages/report/marketing/MarketingTabs.tsx index 8a02a0c2..87139574 100644 --- a/src/components/pages/report/marketing/MarketingTabs.tsx +++ b/src/components/pages/report/marketing/MarketingTabs.tsx @@ -4,11 +4,11 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; import DailyMarketingReportContent from '@/components/pages/report/marketing/tab/DailyMarketingTab'; import HppPerKandangTab from '@/components/pages/report/marketing/tab/HppPerKandangTab'; -import { useReportTabStore } from '@/stores/report/report-tab.store'; +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; const MarketingReportContent = () => { const [activeTabId, setActiveTabId] = useState('1'); - const tabActions = useReportTabStore((state) => state.tabActions); + const tabActions = useTabActionsStore((state) => state.tabActions); const tabs = [ { diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index a336b671..eda89750 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -37,7 +37,7 @@ import { import SelectInput from '@/components/input/SelectInput'; import Modal, { useModal } from '@/components/Modal'; import { cn } from '@/lib/helper'; -import { useReportTabStore } from '@/stores/report/report-tab.store'; +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import DailyMarketingReportSkeleton from '@/components/pages/report/marketing/skeleton/DailyMarketingSkeleton'; import { useEffect as useEffectHook } from 'react'; import { httpClient } from '@/services/http/client'; @@ -390,8 +390,8 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { }, [dailyMarketingsExport, summaryTotal]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useReportTabStore((state) => state.setTabActions); - const clearTabActions = useReportTabStore((state) => state.clearTabActions); + const setTabActions = useTabActionsStore((state) => state.setTabActions); + const clearTabActions = useTabActionsStore((state) => state.clearTabActions); useEffectHook(() => { setTabActions( diff --git a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx index 991c6546..dc487003 100644 --- a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx @@ -31,7 +31,7 @@ import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; import Modal, { useModal } from '@/components/Modal'; import { cn } from '@/lib/helper'; -import { useReportTabStore } from '@/stores/report/report-tab.store'; +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import HppPerKandangSkeleton from '@/components/pages/report/marketing/skeleton/HppPerKandangSkeleton'; import { useEffect as useEffectHook } from 'react'; @@ -110,7 +110,7 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { weight_max: null, period: null, sort_by: null, - show_unrecorded: null, + show_unrecorded: false, }, validationSchema: HppPerKandangFilterSchema, onSubmit: (values, { setSubmitting }) => { @@ -122,8 +122,7 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { weight_max: values.weight_max || undefined, period: values.period || undefined, sort_by: values.sort_by || undefined, - show_unrecorded: - values.show_unrecorded !== null ? values.show_unrecorded : undefined, + show_unrecorded: values.show_unrecorded ?? undefined, }); filterModal.closeModal(); setIsSubmitted(true); @@ -480,8 +479,8 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { ]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useReportTabStore((state) => state.setTabActions); - const clearTabActions = useReportTabStore((state) => state.clearTabActions); + const setTabActions = useTabActionsStore((state) => state.setTabActions); + const clearTabActions = useTabActionsStore((state) => state.clearTabActions); useEffectHook(() => { setTabActions( @@ -1026,7 +1025,11 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { if (!Array.isArray(val)) { formik.setFieldValue( 'show_unrecorded', - val?.value === 'true' || null + val?.value === 'true' + ? true + : val?.value === 'false' + ? false + : null ); } }} diff --git a/src/components/pages/report/production-result/ProductionResultTabs.tsx b/src/components/pages/report/production-result/ProductionResultTabs.tsx index 6f5e4410..8e335cf2 100644 --- a/src/components/pages/report/production-result/ProductionResultTabs.tsx +++ b/src/components/pages/report/production-result/ProductionResultTabs.tsx @@ -3,11 +3,10 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; import ProductionResultTab from '@/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab'; -import { useReportTabStore } from '@/stores/report/report-tab.store'; - +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; const ProductionResultTabs = () => { const [activeTabId, setActiveTabId] = useState('1'); - const tabActions = useReportTabStore((state) => state.tabActions); + const tabActions = useTabActionsStore((state) => state.tabActions); const tabs = [ { diff --git a/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx index 9ac5faf6..cbefadfe 100644 --- a/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx +++ b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx @@ -34,7 +34,7 @@ import { ColumnDef } from '@tanstack/react-table'; import { ProductionResult } from '@/types/api/report/production-result'; import ProductionResultReportPDF from '../export/ProductionResultExportPDF'; import { pdf } from '@react-pdf/renderer'; -import { useReportTabStore } from '@/stores/report/report-tab.store'; +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import Modal, { useModal } from '@/components/Modal'; import { cn, formatNumber } from '@/lib/helper'; import Pagination from '@/components/Pagination'; @@ -532,8 +532,8 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { }, [filterParams]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useReportTabStore((state) => state.setTabActions); - const clearTabActions = useReportTabStore((state) => state.clearTabActions); + const setTabActions = useTabActionsStore((state) => state.setTabActions); + const clearTabActions = useTabActionsStore((state) => state.clearTabActions); useEffect(() => { setTabActions( diff --git a/src/services/api/report/marketing-sale.ts b/src/services/api/report/marketing-sale.ts index 92c59b2c..f474adb7 100644 --- a/src/services/api/report/marketing-sale.ts +++ b/src/services/api/report/marketing-sale.ts @@ -35,7 +35,7 @@ export class MarketingSaleReportService extends BaseApiService< weight_max: weight_max, period: period, sort_by: sort_by, - show_unrecorded: show_unrecorded, + show_unrecorded: show_unrecorded ?? false, page: page, limit: limit, }, diff --git a/src/stores/closing/closing-tab.store.ts b/src/stores/closing/closing-tab.store.ts deleted file mode 100644 index 1f81c26a..00000000 --- a/src/stores/closing/closing-tab.store.ts +++ /dev/null @@ -1,21 +0,0 @@ -'use client'; - -import { create } from 'zustand'; -import { devtools } from 'zustand/middleware'; -import { - createClosingTabSlice, - ClosingTabSlice, -} from '@/stores/closing/slices/closing-tab.slice'; - -export type ClosingTabStore = ClosingTabSlice; - -export const useClosingTabStore = create()( - devtools( - (...args) => ({ - ...createClosingTabSlice(...args), - }), - { - name: 'ClosingTabStore', - } - ) -); diff --git a/src/stores/closing/slices/closing-tab.slice.ts b/src/stores/closing/slices/closing-tab.slice.ts deleted file mode 100644 index cd47bbdc..00000000 --- a/src/stores/closing/slices/closing-tab.slice.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ReactNode } from 'react'; -import { StateCreator } from 'zustand'; - -export type ClosingTabSlice = { - // State - actions per tab ID - tabActions: Record; - - // Actions - setTabActions: (tabId: string, actions: ReactNode) => void; - clearTabActions: (tabId: string) => void; - clearAllTabActions: () => void; -}; - -export const createClosingTabSlice: StateCreator< - ClosingTabSlice, - [], - [], - ClosingTabSlice -> = (set) => ({ - tabActions: {}, - - setTabActions: (tabId, actions) => - set((state) => ({ - tabActions: { - ...state.tabActions, - [tabId]: actions, - }, - })), - - clearTabActions: (tabId) => - set((state) => { - const { [tabId]: _, ...rest } = state.tabActions; - return { tabActions: rest }; - }), - - clearAllTabActions: () => set({ tabActions: {} }), -}); diff --git a/src/stores/report/report-tab.store.ts b/src/stores/report/report-tab.store.ts deleted file mode 100644 index aad47d17..00000000 --- a/src/stores/report/report-tab.store.ts +++ /dev/null @@ -1,21 +0,0 @@ -'use client'; - -import { create } from 'zustand'; -import { devtools } from 'zustand/middleware'; -import { - createReportTabSlice, - ReportTabSlice, -} from '@/stores/report/slices/report-tab.slice'; - -export type ReportTabStore = ReportTabSlice; - -export const useReportTabStore = create()( - devtools( - (...args) => ({ - ...createReportTabSlice(...args), - }), - { - name: 'ReportTabStore', - } - ) -); diff --git a/src/stores/report/slices/report-tab.slice.ts b/src/stores/report/slices/report-tab.slice.ts deleted file mode 100644 index 6582eaed..00000000 --- a/src/stores/report/slices/report-tab.slice.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ReactNode } from 'react'; -import { StateCreator } from 'zustand'; - -export type ReportTabSlice = { - // State - actions per tab ID - tabActions: Record; - - // Actions - setTabActions: (tabId: string, actions: ReactNode) => void; - clearTabActions: (tabId: string) => void; - clearAllTabActions: () => void; -}; - -export const createReportTabSlice: StateCreator< - ReportTabSlice, - [], - [], - ReportTabSlice -> = (set) => ({ - tabActions: {}, - - setTabActions: (tabId, actions) => - set((state) => ({ - tabActions: { - ...state.tabActions, - [tabId]: actions, - }, - })), - - clearTabActions: (tabId) => - set((state) => { - const { [tabId]: _, ...rest } = state.tabActions; - return { tabActions: rest }; - }), - - clearAllTabActions: () => set({ tabActions: {} }), -}); diff --git a/src/stores/tab-actions/tab-actions.store.ts b/src/stores/tab-actions/tab-actions.store.ts new file mode 100644 index 00000000..15ccf186 --- /dev/null +++ b/src/stores/tab-actions/tab-actions.store.ts @@ -0,0 +1,42 @@ +'use client'; + +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { ReactNode } from 'react'; + +export type TabActionsSlice = { + // State - actions per tab ID + tabActions: Record; + + // Actions + setTabActions: (tabId: string, actions: ReactNode) => void; + clearTabActions: (tabId: string) => void; + clearAllTabActions: () => void; +}; + +export const useTabActionsStore = create()( + devtools( + (set) => ({ + tabActions: {}, + + setTabActions: (tabId, actions) => + set((state) => ({ + tabActions: { + ...state.tabActions, + [tabId]: actions, + }, + })), + + clearTabActions: (tabId) => + set((state) => { + const { [tabId]: _, ...rest } = state.tabActions; + return { tabActions: rest }; + }), + + clearAllTabActions: () => set({ tabActions: {} }), + }), + { + name: 'TabActionsStore', + } + ) +); diff --git a/src/types/api/production/project-flock.d.ts b/src/types/api/production/project-flock.d.ts index 172c24b5..204e7b49 100644 --- a/src/types/api/production/project-flock.d.ts +++ b/src/types/api/production/project-flock.d.ts @@ -23,6 +23,7 @@ export type BaseProjectFlock = { kandang_ids: number[]; kandangs: (Kandang & { project_flock_kandang_id: number; + closed_at?: string; })[]; project_budgets?: ProjectFlockBudget[]; approval: BaseApproval;