From 71a430c99cc75276a36abbcecc58711451b973e8 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Sat, 24 Jan 2026 11:20:24 +0700 Subject: [PATCH] feat: create TransferToLayingFormModal component --- .../TransferToLayingFormModal.tsx | 940 ++++++++++++++++++ 1 file changed, 940 insertions(+) create mode 100644 src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx diff --git a/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx b/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx new file mode 100644 index 00000000..9e2e61ad --- /dev/null +++ b/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx @@ -0,0 +1,940 @@ +'use client'; + +import { + FormEventHandler, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; +import useSWR, { useSWRConfig } from 'swr'; +import toast from 'react-hot-toast'; + +import { Icon } from '@iconify/react'; +import Modal, { useModal } from '@/components/Modal'; +import Button from '@/components/Button'; +import DateInput from '@/components/input/DateInput'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; +import { OptionType, useSelect } from '@/components/input/SelectInput'; +import NumberInput from '@/components/input/NumberInput'; +import TextArea from '@/components/input/TextArea'; +import AlertErrorList from '@/components/helper/form/FormErrors'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import { ProjectFlockApi } from '@/services/api/production'; +import { getIn, useFormik } from 'formik'; +import { + getFilledTransferToLayingFormInitialValues, + getTransferToLayingFormInitialValues, + TransferToLayingFormSchema, + TransferToLayingFormValues, +} from '@/components/pages/production/transfer-to-laying/form/TransferToLayingForm.schema'; +import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import StatusBadge from '@/components/helper/StatusBadge'; +import CheckboxInput from '@/components/input/CheckboxInput'; +import { ProjectFlock } from '@/types/api/production/project-flock'; +import { cn, formatNumber } from '@/lib/helper'; +import { + CreateTransferToLayingPayload, + UpdateTransferToLayingPayload, +} from '@/types/api/production/transfer-to-laying'; +import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; + +const TransferToLayingFormModal = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const modalAction = searchParams.get('action'); + const transferToLayingId = searchParams.get('id'); + + const { mutate } = useSWRConfig(); + + const refreshTransferToLayings = () => { + mutate( + (key) => + typeof key === 'string' && key.includes(TransferToLayingApi.basePath) + ); + }; + + const { data: transferToLaying, isLoading: isLoadingTransferToLaying } = + useSWR(transferToLayingId ? transferToLayingId : undefined, (id: number) => + TransferToLayingApi.getSingle(id) + ); + + /** + * Step 1: General Information + * Step 2: Select source and destination kandang + * Step 3: Enter transfered quantity + * Step 4: Submit + */ + const [step, setStep] = useState(1); + + const formModal = useModal(); + + const [formErrorMessage, setFormErrorMessage] = useState(null); + + // Flock Source + const { + setInputValue: setFlockSourceInputValue, + options: flockSourceOptions, + isLoadingOptions: isLoadingFlockSourceOptions, + loadMore: loadMoreFlockSource, + rawData: flockSourceRawData, + } = useSelect( + ProjectFlockApi.basePath, + 'id', + 'flock_name', + 'search', + { + category: 'GROWING', + } + ); + + // Flock Destination + const { + setInputValue: setFlockDestinationInputValue, + options: flockDestinationOptions, + isLoadingOptions: isLoadingFlockDestinationOptions, + loadMore: loadMoreFlockDestination, + rawData: flockDestinationRawData, + } = useSelect( + ProjectFlockApi.basePath, + 'id', + 'flock_name', + 'search', + { + category: 'LAYING', + } + ); + + const closeModalHandler = (shouldPushToRoute: boolean = true) => { + if (shouldPushToRoute) { + router.push('/production/transfer-to-laying'); + } + + formik.resetForm(); + setStep(1); + setFormErrorMessage(''); + formModal.closeModal(); + }; + + const createTransferToLayingHandler = useCallback( + async (payload: CreateTransferToLayingPayload) => { + const createTransferToLayingRes = + await TransferToLayingApi.create(payload); + + if (isResponseError(createTransferToLayingRes)) { + setFormErrorMessage(createTransferToLayingRes.message); + return; + } + + refreshTransferToLayings(); + toast.success(createTransferToLayingRes?.message as string); + router.push('/production/transfer-to-laying'); + closeModalHandler(false); + }, + [router] + ); + + const updateTransferToLayingHandler = useCallback( + async ( + transferToLayingId: number, + payload: UpdateTransferToLayingPayload + ) => { + const updateKandangRes = await TransferToLayingApi.update( + transferToLayingId, + payload + ); + + if (updateKandangRes?.status === 'error') { + setFormErrorMessage(updateKandangRes.message); + return; + } + + refreshTransferToLayings(); + toast.success(updateKandangRes?.message as string); + router.push('/production/transfer-to-laying'); + closeModalHandler(false); + }, + [router] + ); + + const [formikInitialValues, setFormikInitialValues] = useState( + getTransferToLayingFormInitialValues() + ); + + const formik = useFormik({ + initialValues: formikInitialValues, + validationSchema: TransferToLayingFormSchema, + onSubmit: async (values) => { + const transferToLayingPayload: CreateTransferToLayingPayload = { + transfer_date: values.transfer_date as string, + source_project_flock_id: values.flockSource?.value as number, + target_project_flock_id: values.flockDestination?.value as number, + totalQuantity: values.totalQuantity as number, + + source_kandangs: values.flockSourceKandangs?.map((kandang) => ({ + project_flock_kandang_id: kandang.kandang.value, + quantity: parseFloat(kandang.quantity as string), + })) as CreateTransferToLayingPayload['source_kandangs'], + + target_kandangs: values.flockDestinationKandangs?.map((kandang) => ({ + project_flock_kandang_id: kandang.kandang.value, + quantity: parseFloat(kandang.quantity as string), + })) as CreateTransferToLayingPayload['target_kandangs'], + + reason: values.reason as string, + }; + + switch (modalAction) { + case 'add': + await createTransferToLayingHandler(transferToLayingPayload); + break; + + case 'edit': + await updateTransferToLayingHandler( + Number(transferToLayingId), + transferToLayingPayload + ); + + break; + } + }, + }); + + const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); + + const selectedFlockSourceRawData = isResponseSuccess(flockSourceRawData) + ? flockSourceRawData.data.find( + (item) => item.id === formik.values.flockSource?.value + ) + : undefined; + + const selectedFlockDestinationRawData = isResponseSuccess( + flockDestinationRawData + ) + ? flockDestinationRawData.data.find( + (item) => item.id === formik.values.flockDestination?.value + ) + : undefined; + + const { + data: flockSourceKandangsAvailability, + isLoading: isLoadingFlockSourceKandangsAvailability, + } = useSWR( + formik.values.flockSource + ? String(formik.values.flockSource.value) + : undefined, + (id: string) => + TransferToLayingApi.getMappedFlockKandangsAvailability(Number(id)) + ); + + const mappedFlockSourceKandangsAvailability: { + kandang_name: string; + available_qty: number; + project_flock_kandang_id: number; + }[] = useMemo(() => { + if (!flockSourceKandangsAvailability || !selectedFlockSourceRawData) + return []; + + return selectedFlockSourceRawData + ? selectedFlockSourceRawData.kandangs.map((kandang) => { + const availability = + flockSourceKandangsAvailability[kandang.project_flock_kandang_id] + .available_qty; + + return { + kandang_name: kandang.name, + available_qty: availability, + project_flock_kandang_id: kandang.project_flock_kandang_id, + }; + }) + : []; + }, [flockSourceKandangsAvailability, selectedFlockSourceRawData]); + + const mappedFlockSourceKandangsAvailabilityInfo: { + available: number; + unavailable: number; + } = useMemo(() => { + if (!mappedFlockSourceKandangsAvailability) + return { available: 0, unavailable: 0 }; + + let countAvailable = 0; + let countUnavailable = 0; + + mappedFlockSourceKandangsAvailability.forEach((item) => { + if (item.available_qty > 0) { + countAvailable += 1; + } else { + countUnavailable += 1; + } + }); + + return { available: countAvailable, unavailable: countUnavailable }; + }, [mappedFlockSourceKandangsAvailability]); + + const mappedFlockDestinationKandangsAvailabilityInfo: { + available: number; + unavailable: number; + } = useMemo(() => { + if (!selectedFlockDestinationRawData) + return { available: 0, unavailable: 0 }; + + let countAvailable = 0; + let countUnavailable = 0; + + selectedFlockDestinationRawData?.kandangs.forEach((item) => { + // TODO: change this to real available quota later + if (item.capacity > 0) { + countAvailable += 1; + } else { + countUnavailable += 1; + } + }); + + return { available: countAvailable, unavailable: countUnavailable }; + }, [selectedFlockDestinationRawData]); + + const totalEnteredChickenForTransfer = + formik.values.flockSourceKandangs.reduce( + (acc, item) => acc + Number(item.quantity), + 0 + ); + + const totalTransferedChicken = formik.values.flockDestinationKandangs.reduce( + (acc, item) => acc + Number(item.quantity), + 0 + ); + + const totalAvailableChickenForTransfer = + totalEnteredChickenForTransfer - totalTransferedChicken; + + const isNextButtonDisabled = useMemo(() => { + if (step === 1) { + return Boolean( + !formik.values.transfer_date || + !formik.values.flockSource || + !formik.values.flockDestination + ); + } + + if (step === 2) { + return Boolean( + !formik.values.flockSourceKandangs.length || + !formik.values.flockDestinationKandangs.length + ); + } + + return true; + }, [step, formik.values]); + + const nextButtonHandler = () => { + setStep(step + 1); + }; + + const deleteEnteredKandangHandler = () => { + formik.setFieldValue('flockSourceKandangs', []); + formik.setFieldValue('flockDestinationKandangs', []); + formik.setFieldValue('totalQuantity', ''); + formik.setFieldValue('maxTotalQuantity', ''); + formik.setFieldValue('reason', ''); + formik.setFieldTouched('reason', false); + + setStep(2); + }; + + const flockSourceChangeHandler = (val: OptionType | OptionType[] | null) => { + formik.setFieldValue('flockSource', val); + formik.setFieldValue('flockSourceKandangs', []); + }; + + const flockDestinationChangeHandler = ( + val: OptionType | OptionType[] | null + ) => { + formik.setFieldValue('flockDestination', val); + formik.setFieldValue('flockDestinationKandangs', []); + }; + + useEffect(() => { + if (modalAction === 'add' || modalAction === 'edit') { + formModal.openModal(); + } + }, [modalAction]); + + useEffect(() => { + const getFilledInitialValues = async () => { + if (transferToLayingId && isResponseSuccess(transferToLaying)) { + const filledInitialValues = + await getFilledTransferToLayingFormInitialValues( + transferToLaying.data + ); + + formik.setValues(filledInitialValues); + setStep(3); + } + }; + + getFilledInitialValues(); + }, [transferToLayingId, transferToLaying]); + + useEffect(() => { + formik.setFieldValue('totalQuantity', totalTransferedChicken); + formik.setFieldValue('maxTotalQuantity', totalTransferedChicken); + }, [totalTransferedChicken]); + + return ( + +
+ {/* 1st Section */} +
+
+ + +
+ +

+ Add Transfer to Laying +

+
+ +
+

+ Informasi Umum +

+ + 2} + /> + + 2} + /> + + 2} + /> +
+ + {step >= 2 && ( + <> +
+

+ Pilih Kandang Asal +

+ +
+ + +
+ + +
+ +
+ {mappedFlockSourceKandangsAvailability.map( + (item, itemIdx) => { + const isAvailable = item.available_qty > 0; + const isChecked = formik.values.flockSourceKandangs.some( + (k) => k.kandang.value === item.project_flock_kandang_id + ); + + const flockSourceKandangCheckboxChangeHandler: FormEventHandler< + HTMLInputElement + > = (e) => { + const checked = (e.target as HTMLInputElement).checked; + if (checked) { + formik.setFieldValue('flockSourceKandangs', [ + ...formik.values.flockSourceKandangs, + { + kandang: { + value: item.project_flock_kandang_id, + label: item.kandang_name, + }, + quantity: '', + maxQuantity: item.available_qty, + }, + ]); + } else { + formik.setFieldValue( + 'flockSourceKandangs', + formik.values.flockSourceKandangs.filter( + (k) => + k.kandang.value !== + item.project_flock_kandang_id + ) + ); + } + }; + + return ( +
+
+ + + +
+ + +
+ ); + } + )} +
+
+ +
+
+

+ Pilih Kandang Tujuan +

+ {formik.touched.flockDestinationKandangs && + formik.errors.flockDestinationKandangs && + typeof formik.errors.flockDestinationKandangs === + 'string' && ( + + {formik.errors.flockDestinationKandangs} + + )} +
+ +
+ + +
+ + +
+ +
+ {selectedFlockDestinationRawData?.kandangs.map( + (item, itemIdx) => { + // TODO: change this to real available quota later + const isAvailable = item.capacity > 0; + const isChecked = + formik.values.flockDestinationKandangs.some( + (k) => + k.kandang.value === item.project_flock_kandang_id + ); + + const flockDestinationKandangCheckboxChangeHandler: FormEventHandler< + HTMLInputElement + > = (e) => { + const checked = (e.target as HTMLInputElement).checked; + if (checked) { + formik.setFieldValue('flockDestinationKandangs', [ + ...formik.values.flockDestinationKandangs, + { + kandang: { + value: item.project_flock_kandang_id, + label: item.name, + }, + quantity: '', + // TODO: change this to real available quota later + maxQuantity: item.capacity, + }, + ]); + } else { + formik.setFieldValue( + 'flockDestinationKandangs', + formik.values.flockDestinationKandangs.filter( + (k) => + k.kandang.value !== + item.project_flock_kandang_id + ) + ); + } + }; + + return ( +
+
+ + + +
+ + +
+ ); + } + )} +
+
+ + )} + +
+ {step < 3 && ( + + )} +
+
+ + {/* 2nd Section */} + {step === 3 && ( +
+
+

+ Tambah Kuantitas +

+ + +
+ +
+

+ Informasi Kandang +

+ + {/* Source Kandang */} +
+ + Kandang Asal + + + {formik.values.flockSourceKandangs.length === 0 && ( + + Belum ada kandang asal yang dipilih + + )} + + {formik.values.flockSourceKandangs.length > 0 && ( +
+ {formik.values.flockSourceKandangs.map((item, index) => { + const isInvalid = + item.quantity === '' + ? false + : Boolean( + getIn( + formik.errors, + `flockSourceKandangs[${index}].quantity` + ) + ); + + const errorMessage = getIn( + formik.errors, + `flockSourceKandangs[${index}].quantity` + ); + + return ( + + + {item.kandang.label} + + +
+
+ } + className={{ + inputPrefix: + 'py-0 px-0 pl-3 text-base-content/50 bg-transparent border-r-0', + inputPrefixSuffixWrapper: 'grid grid-cols-2', + inputWrapper: 'border-l-0 pl-5', + }} + /> + ); + })} +
+ )} +
+ + {/* Destination Kandang */} +
+ + Kandang Tujuan + +
+ + + + + {formik.values.flockDestinationKandangs.length === 0 && ( + + Belum ada kandang tujuan yang dipilih + + )} + + {formik.values.flockDestinationKandangs.length > 0 && ( +
+ {formik.values.flockDestinationKandangs.map( + (item, index) => { + const isInvalid = + item.quantity === '' + ? false + : Boolean( + getIn( + formik.errors, + `flockDestinationKandangs[${index}].quantity` + ) + ); + + const errorMessage = getIn( + formik.errors, + `flockDestinationKandangs[${index}].quantity` + ); + + return ( + + + {item.kandang.label} + + +
+
+ } + className={{ + inputPrefix: + 'py-0 px-0 pl-3 text-base-content/50 bg-transparent border-r-0', + inputPrefixSuffixWrapper: 'grid grid-cols-2', + inputWrapper: 'border-l-0 pl-5', + }} + /> + ); + } + )} +
+ )} +
+
+ +
+

+ Informasi Umum +

+ + + +