From 59b0eeea2b8a8ceafb14c56dbdcfe855056d5183 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 31 Oct 2025 00:02:04 +0700 Subject: [PATCH] feat(FE-170): add egg handling and validation to daily recording form --- .../recording/form/RecordingForm.tsx | 543 +++++++++++++++--- 1 file changed, 455 insertions(+), 88 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 4502800f..5722e726 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -15,14 +15,21 @@ import { FormActions } from '@/components/helper/form/FormActions'; import { RecordingApi } from '@/services/api/production'; import { CreateGrowingRecordingPayload, + CreateLayingRecordingPayload, + UpdateGrowingRecordingPayload, + UpdateLayingRecordingPayload, Recording, } from '@/types/api/production/recording'; import { type BaseApiResponse } from '@/types/api/api-general'; import { RecordingGrowingFormSchema, + RecordingLayingFormSchema, RecordingGrowingFormValues, + RecordingLayingFormValues, getRecordingGrowingFormInitialValues, + getRecordingLayingFormInitialValues, UpdateRecordingGrowingFormSchema, + UpdateRecordingLayingFormSchema, } from './RecordingForm.schema'; import { useRecordingFormHandlers } from './useRecordingFormHandlers'; import { ProjectFlockApi } from '@/services/api/production'; @@ -49,6 +56,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const [selectedBodyWeights, setSelectedBodyWeights] = useState([]); const [selectedStocks, setSelectedStocks] = useState([]); const [selectedDepletions, setSelectedDepletions] = useState([]); + const [selectedEggs, setSelectedEggs] = useState([]); const [editingAverageIndex] = useState(null); const [manuallyEditedRows, setManuallyEditedRows] = useState>( @@ -219,6 +227,20 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const { data: depletionProductsData, isLoading: isLoadingDepletionProducts } = useSWR(depletionProductsUrl, ProductWarehouseApi.getAllFetcher); + const eggProductsUrl = useMemo(() => { + if (!selectedLocation) return null; + const params = new URLSearchParams({ + search: '', + location_id: selectedLocation.value.toString(), + }); + return `${ProductWarehouseApi.basePath}?${params.toString()}`; + }, [selectedLocation]); + + const { data: eggProductsData, isLoading: isLoadingEggProducts } = useSWR( + eggProductsUrl, + ProductWarehouseApi.getAllFetcher + ); + // ===== DATA PROCESSING ===== const locationOptions = useMemo(() => { if (!isResponseSuccess(locations)) return []; @@ -358,6 +380,27 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { return options; }, [depletionProductsData]); + const eggProducts = useMemo(() => { + const options: OptionType[] = []; + if (isResponseSuccess(eggProductsData)) { + eggProductsData.data.forEach((product) => { + const productName = product.product.name; + + if ( + productName.toLowerCase().includes('telur') || + productName.toLowerCase().includes('egg') + ) { + options.push({ + value: product.id, + label: product.product.name, + }); + } + }); + } + + return options; + }, [eggProductsData]); + // ===== FORM HANDLERS ===== const { deleteModal, @@ -369,46 +412,109 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { confirmationModalDeleteClickHandler, } = useRecordingFormHandlers(initialValues?.id); - const formikInitialValues = useMemo( - () => getRecordingGrowingFormInitialValues(initialValues), - [initialValues] - ); + const isLayingCategory = + projectFlockKandangLookup?.project_flock?.category === 'LAYING'; - const formik = useFormik({ + const formikInitialValues = useMemo(() => { + if (isLayingCategory) { + return getRecordingLayingFormInitialValues( + initialValues + ) as RecordingLayingFormValues; + } + return getRecordingGrowingFormInitialValues(initialValues); + }, [initialValues, isLayingCategory]); + + const formik = useFormik< + RecordingGrowingFormValues | RecordingLayingFormValues + >({ initialValues: formikInitialValues, - validationSchema: - type === 'edit' + validationSchema: (() => { + if (isLayingCategory) { + return type === 'edit' + ? UpdateRecordingLayingFormSchema + : RecordingLayingFormSchema; + } + return type === 'edit' ? UpdateRecordingGrowingFormSchema - : RecordingGrowingFormSchema, + : RecordingGrowingFormSchema; + })(), validateOnChange: true, validateOnBlur: true, onSubmit: async (values) => { - const payload: CreateGrowingRecordingPayload = { - project_flock_kandangs_id: values.project_flock_kandangs_id, - body_weights: (values.body_weights ?? []).map((bw) => ({ - avg_weight: - typeof bw.avg_weight === 'number' - ? bw.avg_weight - : parseFloat(String(bw.avg_weight)) || 0, - qty: bw.qty || 0, - })), - stocks: (values.stocks ?? []).map((stock) => ({ - product_warehouse_id: stock.product_warehouse_id, - usage_qty: stock.usage_qty || 0, - })), - depletions: (values.depletions ?? []).map((depletion) => ({ - product_warehouse_id: depletion.product_warehouse_id, - qty: depletion.qty || 0, - })), - }; + if (isLayingCategory) { + const layingValues = values as RecordingLayingFormValues; - switch (type) { - case 'add': - await createRecordingHandler(payload); - break; - case 'edit': - await updateRecordingHandler(initialValues?.id as number, payload); - break; + const layingPayload = { + project_flock_kandangs_id: layingValues.project_flock_kandangs_id, + body_weights: (layingValues.body_weights ?? []).map((bw) => ({ + avg_weight: + typeof bw.avg_weight === 'number' + ? bw.avg_weight + : parseFloat(String(bw.avg_weight)) || 0, + qty: bw.qty || 0, + })), + stocks: (layingValues.stocks ?? []).map((stock) => ({ + product_warehouse_id: stock.product_warehouse_id, + usage_qty: stock.usage_qty || 0, + })), + depletions: (layingValues.depletions ?? []).map((depletion) => ({ + product_warehouse_id: depletion.product_warehouse_id, + qty: depletion.qty || 0, + })), + eggs: (layingValues.eggs ?? []).map((egg) => ({ + product_warehouse_id: egg.product_warehouse_id, + qty: egg.qty || 0, + })), + }; + + switch (type) { + case 'add': + await createRecordingHandler( + layingPayload as CreateLayingRecordingPayload + ); + break; + case 'edit': + await updateRecordingHandler( + initialValues?.id as number, + layingPayload as UpdateLayingRecordingPayload + ); + break; + } + } else { + const growingValues = values as RecordingGrowingFormValues; + + const growingPayload = { + project_flock_kandangs_id: growingValues.project_flock_kandangs_id, + body_weights: (growingValues.body_weights ?? []).map((bw) => ({ + avg_weight: + typeof bw.avg_weight === 'number' + ? bw.avg_weight + : parseFloat(String(bw.avg_weight)) || 0, + qty: bw.qty || 0, + })), + stocks: (growingValues.stocks ?? []).map((stock) => ({ + product_warehouse_id: stock.product_warehouse_id, + usage_qty: stock.usage_qty || 0, + })), + depletions: (growingValues.depletions ?? []).map((depletion) => ({ + product_warehouse_id: depletion.product_warehouse_id, + qty: depletion.qty || 0, + })), + }; + + switch (type) { + case 'add': + await createRecordingHandler( + growingPayload as CreateGrowingRecordingPayload + ); + break; + case 'edit': + await updateRecordingHandler( + initialValues?.id as number, + growingPayload as UpdateGrowingRecordingPayload + ); + break; + } } }, }); @@ -436,7 +542,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const getAvailableStock = useCallback( (productWarehouseId: number) => { - if (type === 'detail') return 0; + if ((type as 'add' | 'edit' | 'detail') === 'detail') return 0; if (!isResponseSuccess(stockProducts)) return 0; const productWarehouse = stockProducts.data.find( (pw) => pw.id === productWarehouseId @@ -448,7 +554,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const getStockUsageError = useCallback( (stockIdx: number) => { - if (type === 'detail') return null; + if ((type as 'add' | 'edit' | 'detail') === 'detail') return null; const stock = formik.values.stocks?.[stockIdx]; if (!stock || !stock.product_warehouse_id) return null; const availableStock = getAvailableStock(stock.product_warehouse_id); @@ -463,7 +569,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const getStockUsageAdornment = useCallback( (stockIdx: number) => { - if (type === 'detail') return null; + if ((type as 'add' | 'edit' | 'detail') === 'detail') return null; const stock = formik.values.stocks?.[stockIdx]; if (!stock || !stock.product_warehouse_id) return null; const availableStock = getAvailableStock(stock.product_warehouse_id); @@ -566,7 +672,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ); const hasExceededStock = useMemo(() => { - if (type === 'detail') return false; + if ((type as 'add' | 'edit' | 'detail') === 'detail') return false; return ( formik.values.stocks?.some((stock, idx) => { return getStockUsageError(idx) !== null; @@ -574,40 +680,35 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ); }, [formik.values.stocks, getStockUsageError, type]); - const isRepeaterInputError = < - T extends 'body_weights' | 'stocks' | 'depletions', - >( - arrayName: T, - column: T extends 'body_weights' - ? keyof RecordingGrowingFormValues['body_weights'][0] - : T extends 'stocks' - ? keyof RecordingGrowingFormValues['stocks'][0] - : T extends 'depletions' - ? keyof RecordingGrowingFormValues['depletions'][0] - : never, + const isRepeaterInputError = ( + arrayName: 'body_weights' | 'stocks' | 'depletions' | 'eggs', + column: string, idx: number ) => { - if ( - !formik.touched[arrayName] || - !Array.isArray(formik.touched[arrayName]) - ) { + const touched = formik.touched as Record; + const errors = formik.errors as Record; + + if (!touched[arrayName] || !Array.isArray(touched[arrayName])) { return { isError: false, errorMessage: '', }; } - const touchedField = formik.touched[arrayName]?.[idx]?.[column as string]; - const errorField = formik.errors[arrayName]?.[idx] as Record< + const touchedField = (touched[arrayName] as unknown[])?.[idx] as Record< string, - string + unknown + >; + const errorField = (errors[arrayName] as unknown[])?.[idx] as Record< + string, + unknown >; return { - isError: touchedField && Boolean(errorField?.[column as string]), + isError: touchedField && Boolean(errorField?.[column]), errorMessage: - touchedField && errorField?.[column as string] - ? errorField[column as string] + touchedField && errorField?.[column] + ? (errorField[column] as string) : '', }; }; @@ -899,7 +1000,51 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { setSelectedDepletions([]); }; + // Eggs Handlers + const addEgg = () => { + const newEggs = [ + ...((formik.values as RecordingLayingFormValues).eggs || []), + { + product_warehouse_id: 0, + qty: 0, + }, + ]; + formik.setFieldValue('eggs', newEggs); + }; + + const handleEggQtyChangeWrapper = useCallback( + (idx: number) => (e: React.ChangeEvent) => { + const value = parseFloat(e.target.value) || 0; + formik.setFieldValue(`eggs.${idx}.qty`, value); + }, + [formik] + ); + + const removeEgg = (idx: number) => { + const updatedEggs = ( + formik.values as RecordingLayingFormValues + ).eggs?.filter((_, i) => i !== idx); + formik.setFieldValue('eggs', updatedEggs); + }; + + const removeSelectedEggs = () => { + const updatedEggs = ( + formik.values as RecordingLayingFormValues + ).eggs?.filter((_, idx) => !selectedEggs.includes(idx)); + formik.setFieldValue('eggs', updatedEggs); + setSelectedEggs([]); + }; + // ===== EFFECTS ===== + useEffect(() => { + if (isLayingCategory && (type as 'add' | 'edit' | 'detail') !== 'detail') { + const layingValues = formik.values as RecordingLayingFormValues; + if (!layingValues.eggs || layingValues.eggs.length === 0) { + formik.setFieldValue('eggs', [{ product_warehouse_id: 0, qty: 0 }]); + } + } + }, [isLayingCategory, type, formik]); + useEffect(() => { if (formik.values.body_weights && editingAverageIndex === null) { const updatedBodyWeights = formik.values.body_weights.map( @@ -976,7 +1121,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { : 'grid grid-cols-3 gap-4' } > - {type === 'detail' ? null : ( + {(type as 'add' | 'edit' | 'detail') === 'detail' ? null : ( <> { )} - {type === 'detail' && formik.values.project_flock_kandang && ( -
- -
- {formik.values.project_flock_kandang.label} + {(type as 'add' | 'edit' | 'detail') === 'detail' && + formik.values.project_flock_kandang && ( +
+ +
+ {formik.values.project_flock_kandang.label} +
-
- )} + )}
@@ -1060,7 +1206,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { - {type !== 'detail' && ( + {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( - {type !== 'detail' && } + {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( + + )} {formik.values.body_weights?.map((bw, idx) => ( - {type !== 'detail' && ( + {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( - {type !== 'detail' && ( + {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
{ * ActionAction
{ />
- {type !== 'detail' && ( + {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
{selectedBodyWeights.length > 0 && (
- {type !== 'detail' && ( + {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
{selectedStocks.length > 0 && (
- {type !== 'detail' && ( + {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
{selectedDepletions.length > 0 && ( + )} + +
+ )} + + )} + {/* Action buttons */} type={type} @@ -1750,7 +2117,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { /> {/* Approve Confirmation Modal */} - {type === 'detail' && ( + {(type as 'add' | 'edit' | 'detail') === 'detail' && ( { )} {/* Reject Confirmation Modal */} - {type === 'detail' && ( + {(type as 'add' | 'edit' | 'detail') === 'detail' && (