From 9c5dc0dbb53229bc85e9b3e6bee85c5903932f78 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 24 Oct 2025 20:44:15 +0700 Subject: [PATCH] refactor(FE-137): integrate approve and reject functionality in RecordingForm with loading states and modal confirmations --- .../recording/form/RecordingForm.tsx | 792 ++++++++---------- 1 file changed, 357 insertions(+), 435 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 2e1a5952..ca4a696d 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -45,45 +45,41 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const [selectedStocks, setSelectedStocks] = useState([]); const [selectedDepletions, setSelectedDepletions] = useState([]); - // Track which average weight field is being edited to prevent auto-calculation override const [editingAverageIndex, setEditingAverageIndex] = useState(null); - - // Track which rows have been manually edited to prevent auto-calculation override const [manuallyEditedRows, setManuallyEditedRows] = useState>(new Set()); - // State for Location search and selection const [locationSearchValue, setLocationSearchValue] = useState(''); const [selectedLocation, setSelectedLocation] = useState(null); - - // State for Project Flock search const [projectFlockSearchValue, setProjectFlockSearchValue] = useState(''); - // Fetch Locations data + const [isApproveLoading, setIsApproveLoading] = useState(false); + const [isRejectLoading, setIsRejectLoading] = useState(false); + + const approveModal = useModal(); + const rejectModal = useModal(); + + // ===== API DATA FETCHING ===== const locationsUrl = `${LocationApi.basePath}?${new URLSearchParams({ search: locationSearchValue || '', }).toString()}`; - const { data: locations, isLoading: isLoadingLocations } = useSWR( locationsUrl, LocationApi.getAllFetcher ); - // Fetch Project Flocks data with location filter const projectFlocksUrl = `${ProjectFlockApi.basePath}?${new URLSearchParams({ search: projectFlockSearchValue || '', ...(selectedLocation ? { location_id: selectedLocation.value.toString() } : {}), }).toString()}`; - const { data: projectFlocks, isLoading: isLoadingProjectFlocks } = useSWR( projectFlocksUrl, ProjectFlockApi.getAllFetcher ); - // Fetch Products with location filter (both PAKAN and OVK) - using selectedLocation for now const stockProductsUrl = useMemo(() => { if (!selectedLocation) return null; const params = new URLSearchParams({ - flags: 'PAKAN,OVK', // Fetch both flags in one request + flags: 'PAKAN,OVK', search: '', location_id: selectedLocation.value.toString(), }); @@ -95,22 +91,19 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ProductWarehouseApi.getAllFetcher ); - // Extract location options from locations data + // ===== DATA PROCESSING ===== const locationOptions = useMemo(() => { if (!isResponseSuccess(locations)) return []; - return locations?.data.map((location) => ({ value: location.id, label: location.name, })) || []; }, [locations]); - // Extract kandang options from project_flocks data const projectFlockKandangOptions = useMemo(() => { if (!isResponseSuccess(projectFlocks)) return []; const options: OptionType[] = []; - projectFlocks?.data.forEach((projectFlock) => { projectFlock.kandangs.forEach((kandang) => { options.push({ @@ -119,11 +112,68 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }); }); }); - return options; }, [projectFlocks]); + const unifiedStockProducts = useMemo(() => { + const options: OptionType[] = []; + if (isResponseSuccess(stockProducts)) { + stockProducts.data.forEach((product) => { + const warehouse = product.warehouse; + const stockText = product.quantity.toLocaleString('id-ID'); + const hasPakanFlag = product.product.flags?.includes('PAKAN'); + const hasOvkFlag = product.product.flags?.includes('OVK'); + + if (hasPakanFlag) { + options.push({ + value: product.id, + label: `[PAKAN] ${product.product.name} - ${warehouse?.name || ''} (${stockText})` + }); + } + + if (hasOvkFlag) { + options.push({ + value: product.id, + label: `[OVK] ${product.product.name} - ${warehouse?.name || ''} (${stockText})` + }); + } + }); + } + + if (initialValues && 'stocks' in initialValues && initialValues.stocks && type !== 'add') { + const initialValuesWithStocks = initialValues as Recording & { + stocks?: Array<{ + product_warehouse_id: number; + usage_amount: number; + notes: string; + product_warehouse?: { + id: number; + product_id: number; + product_name: string; + warehouse_id: number; + warehouse_name: string; + }; + }>; + }; + + initialValuesWithStocks.stocks?.forEach((stock) => { + if (stock.product_warehouse && stock.product_warehouse.product_name) { + const existingOption = options.find(opt => opt.value === stock.product_warehouse_id); + if (!existingOption) { + options.push({ + value: stock.product_warehouse_id, + label: `${stock.product_warehouse.product_name} - ${stock.product_warehouse.warehouse_name}` + }); + } + } + }); + } + + return options; + }, [stockProducts, initialValues, type]); + + // ===== FORM HANDLERS ===== const { deleteModal, recordingFormErrorMessage, @@ -134,73 +184,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { confirmationModalDeleteClickHandler, } = useRecordingFormHandlers(initialValues?.id); - const [isApproveLoading, setIsApproveLoading] = useState(false); - const [isRejectLoading, setIsRejectLoading] = useState(false); - - // Modals for approve/reject actions - const approveModal = useModal(); - const rejectModal = useModal(); - - // Approve handler - const approveHandler = async () => { - setIsApproveLoading(true); - - const approveResponse = await RecordingApi.customRequest< - BaseApiResponse - >('approvals', { - method: 'POST', - payload: { - action: 'APPROVED', - approvable_ids: [initialValues?.id as number], - notes: 'Approved via Form', - }, - }); - - if (isResponseSuccess(approveResponse)) { - toast.success('Recording berhasil disetujui!'); - approveModal.closeModal(); - // Optional: redirect or refresh data - if (typeof window !== 'undefined') { - window.location.href = '/production/recording'; - } - } else { - toast.error(approveResponse?.message as string || 'Gagal menyetujui recording'); - approveModal.closeModal(); - } - - setIsApproveLoading(false); - }; - - // Reject handler - const rejectHandler = async () => { - setIsRejectLoading(true); - - const rejectResponse = await RecordingApi.customRequest< - BaseApiResponse - >('approvals', { - method: 'POST', - payload: { - action: 'REJECTED', - approvable_ids: [initialValues?.id as number], - notes: 'Rejected via Form', - }, - }); - - if (isResponseSuccess(rejectResponse)) { - toast.success('Recording berhasil ditolak!'); - rejectModal.closeModal(); - // Optional: redirect or refresh data - if (typeof window !== 'undefined') { - window.location.href = '/production/recording'; - } - } else { - toast.error(rejectResponse?.message as string || 'Gagal menolak recording'); - rejectModal.closeModal(); - } - - setIsRejectLoading(false); - }; - const formikInitialValues = useMemo( () => getRecordingFormInitialValues(initialValues), [initialValues] @@ -254,12 +237,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }, }); - // Get location from selected project flock for stock filtering - const getProjectFlockLocation = useCallback((): OptionType | null => { + // ===== HELPER FUNCTIONS ===== + useCallback((): OptionType | null => { if (!formik.values.project_flock_kandang || !isResponseSuccess(projectFlocks)) { - return selectedLocation; // Fallback to manual location selection + return selectedLocation; } - const kandangId = formik.values.project_flock_kandang.value; for (const projectFlock of projectFlocks.data) { const kandang = projectFlock.kandangs.find(k => k.id === kandangId); @@ -270,82 +252,16 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }; } } - - return selectedLocation; // Fallback to manual location selection + return selectedLocation; }, [formik.values.project_flock_kandang, projectFlocks, selectedLocation]); - // EVENT HANDLERS - Select Inputs - const locationChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedLocation(val as OptionType); - - // Reset project flock selection when location changes - formik.setFieldValue('project_flock_kandang', null); - formik.setFieldValue('project_flock_kandang_id', 0); - }; - - const projectFlockKandangChangeHandler = (val: OptionType | OptionType[] | null) => { - formik.setFieldTouched('project_flock_kandang', true); - formik.setFieldValue('project_flock_kandang', val); - formik.setFieldTouched('project_flock_kandang_id', true); - formik.setFieldValue('project_flock_kandang_id', (val as OptionType)?.value || 0); - }; - - // EVENT HANDLERS - Date Time - const recordDateTimeChangeHandler = (datetime: Date | null) => { - formik.setFieldValue('record_datetime', datetime, false); - }; - - // Auto-calculate average weight when weight or qty changes (but not when editing average weight manually) - useEffect(() => { - // Only run auto-calculation if no field is being edited - if (formik.values.body_weights && editingAverageIndex === null) { - const updatedBodyWeights = formik.values.body_weights.map((weight, idx) => { - // Skip the field that's being edited or has been manually edited - if (idx === editingAverageIndex || manuallyEditedRows.has(idx)) { - return weight; - } - - return { - ...weight, - average_weight: - weight.qty > 0 && weight.weight > 0 - ? parseFloat((weight.weight / weight.qty).toFixed(2)) - : 0, - }; - }); - - // Only update if values are different to avoid infinite loops - const hasChanges = updatedBodyWeights.some( - (updated, idx) => - idx !== editingAverageIndex && // Skip the field being edited - !manuallyEditedRows.has(idx) && // Skip manually edited rows - updated.average_weight !== - (formik.values.body_weights[idx]?.average_weight || 0) - ); - - if (hasChanges) { - // Use false to prevent triggering validation and other side effects - formik.setFieldValue('body_weights', updatedBodyWeights, false); - } - } - }, [ - formik.values.body_weights?.map((w) => w.weight), - formik.values.body_weights?.map((w) => w.qty), - editingAverageIndex, // Include editing index in dependencies - manuallyEditedRows, // Include manually edited rows in dependencies - ]); - - // Stock validation functions - Following MovementForm pattern const getAvailableStock = useCallback( (productWarehouseId: number) => { if (type === 'detail') return 0; - if (!isResponseSuccess(stockProducts)) return 0; - const productWarehouse = stockProducts.data.find( (pw) => pw.id === productWarehouseId ); - return productWarehouse?.quantity ?? 0; }, [stockProducts, type] @@ -354,17 +270,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const getStockUsageError = useCallback( (stockIdx: number) => { if (type === 'detail') return null; - const stock = formik.values.stocks?.[stockIdx]; if (!stock || !stock.product_warehouse_id) return null; - const availableStock = getAvailableStock(stock.product_warehouse_id); const requestedUsage = Number(stock.usage_amount) || 0; - if (requestedUsage > availableStock) { return `Jumlah pakai melebihi stok tersedia! Maksimal: ${availableStock.toLocaleString('id-ID')}`; } - return null; }, [formik.values.stocks, getAvailableStock, type] @@ -373,14 +285,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const getStockUsageAdornment = useCallback( (stockIdx: number) => { if (type === 'detail') return null; - const stock = formik.values.stocks?.[stockIdx]; if (!stock || !stock.product_warehouse_id) return null; - const availableStock = getAvailableStock(stock.product_warehouse_id); const requestedUsage = Number(stock.usage_amount) || 0; const remainingStock = availableStock - requestedUsage; - if (requestedUsage > 0) { return ( @@ -388,7 +297,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ); } - return ( (tersedia: {availableStock.toLocaleString('id-ID')}) @@ -400,7 +308,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const hasExceededStock = useMemo(() => { if (type === 'detail') return false; - return ( formik.values.stocks?.some((stock, idx) => { return getStockUsageError(idx) !== null; @@ -408,273 +315,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ); }, [formik.values.stocks, getStockUsageError, type]); - // EVENT HANDLERS - Body Weights - const addBodyWeight = () => { - const newBodyWeights = [ - ...(formik.values.body_weights || []), - { - weight: 0, - qty: 1, - average_weight: 0, - }, - ]; - formik.setFieldValue('body_weights', newBodyWeights); - }; - - // Handle calculation when weight changes - const handleWeightChange = (idx: number, value: number) => { - formik.setFieldValue(`body_weights.${idx}.weight`, value); - - // Reset manual edit flag when weight changes (user wants auto-calculation) - setManuallyEditedRows(prev => { - const newSet = new Set(prev); - newSet.delete(idx); - return newSet; - }); - - const currentWeight = formik.values.body_weights?.[idx]; - if (currentWeight) { - const qty = currentWeight.qty; - if (qty > 0 && value > 0) { - const averageWeight = parseFloat((value / qty).toFixed(2)); - formik.setFieldValue(`body_weights.${idx}.average_weight`, averageWeight); - } else { - formik.setFieldValue(`body_weights.${idx}.average_weight`, 0); - } - } - }; - - // Handle calculation when qty changes - const handleQtyChange = (idx: number, value: number) => { - formik.setFieldValue(`body_weights.${idx}.qty`, value); - - // Reset manual edit flag when qty changes (user wants auto-calculation) - setManuallyEditedRows(prev => { - const newSet = new Set(prev); - newSet.delete(idx); - return newSet; - }); - - const currentWeight = formik.values.body_weights?.[idx]; - if (currentWeight) { - const weight = currentWeight.weight; - if (value > 0 && weight > 0) { - const averageWeight = parseFloat((weight / value).toFixed(2)); - formik.setFieldValue(`body_weights.${idx}.average_weight`, averageWeight); - } else { - formik.setFieldValue(`body_weights.${idx}.average_weight`, 0); - } - } - }; - - // Handle calculation when average_weight changes - const handleAverageWeightChange = (idx: number, value: number) => { - formik.setFieldValue(`body_weights.${idx}.average_weight`, value); - - const currentWeight = formik.values.body_weights?.[idx]; - if (currentWeight) { - const qty = currentWeight.qty; - if (qty > 0 && value > 0) { - const totalWeight = value * qty; - formik.setFieldValue(`body_weights.${idx}.weight`, totalWeight); - } else { - formik.setFieldValue(`body_weights.${idx}.weight`, 0); - } - } - }; - - // Create wrapper handlers that match NumberInput's onChange signature - const handleWeightChangeWrapper = (idx: number) => (e: React.ChangeEvent) => { - // Parse the value more carefully to handle decimal numbers properly - const rawValue = e.target.value.replace(/[^\d,.-]/g, ''); - // Convert comma thousand separator to nothing, but keep decimal point - const normalizedValue = rawValue.replace(/,/g, ''); - const value = parseFloat(normalizedValue) || 0; - handleWeightChange(idx, value); - }; - - const handleQtyChangeWrapper = (idx: number) => (e: React.ChangeEvent) => { - // Parse the value more carefully to handle decimal numbers properly - const rawValue = e.target.value.replace(/[^\d,.-]/g, ''); - // Convert comma thousand separator to nothing, but keep decimal point - const normalizedValue = rawValue.replace(/,/g, ''); - const value = parseFloat(normalizedValue) || 0; - handleQtyChange(idx, value); - }; - - const handleAverageWeightChangeWrapper = (idx: number) => (e: React.ChangeEvent) => { - // Set focus state to prevent auto-calculation override - setEditingAverageIndex(idx); - - // Mark this row as manually edited - setManuallyEditedRows(prev => new Set(prev).add(idx)); - - // Parse the value more carefully to handle decimal numbers properly - const rawValue = e.target.value.replace(/[^\d,.-]/g, ''); - // Convert comma thousand separator to nothing, but keep decimal point - const normalizedValue = rawValue.replace(/,/g, ''); - const value = parseFloat(normalizedValue) || 0; - handleAverageWeightChange(idx, value); - }; - - const handleAverageWeightBlur = (idx: number) => { - // Clear focus state when user leaves the field to re-enable auto-calculation - setEditingAverageIndex(null); - }; - - const removeBodyWeight = (idx: number) => { - const updatedBodyWeights = formik.values.body_weights?.filter( - (_, i) => i !== idx - ); - formik.setFieldValue('body_weights', updatedBodyWeights); - }; - - const removeSelectedBodyWeights = () => { - const updatedBodyWeights = formik.values.body_weights?.filter( - (_, idx) => !selectedBodyWeights.includes(idx) - ); - formik.setFieldValue('body_weights', updatedBodyWeights); - setSelectedBodyWeights([]); - }; - - // EVENT HANDLERS - Stocks - const addStock = () => { - const newStocks = [ - ...(formik.values.stocks || []), - { - product_warehouse_id: 0, - usage_amount: 0, - notes: '', - }, - ]; - formik.setFieldValue('stocks', newStocks); - }; - - // Memoized unified products for stock selection - const unifiedStockProducts = useMemo(() => { - const options: OptionType[] = []; - - // Add products from API stockProducts - if (isResponseSuccess(stockProducts)) { - stockProducts.data.forEach((product) => { - const warehouse = product.warehouse; - const stockText = product.quantity.toLocaleString('id-ID'); - - // Check if product has any of the flags - const hasPakanFlag = product.product.flags?.includes('PAKAN'); - const hasOvkFlag = product.product.flags?.includes('OVK'); - - // Add products with warehouse and location grouping in label (similar to projectFlockKandangOptions pattern) - if (hasPakanFlag) { - options.push({ - value: product.id, - label: `[PAKAN] ${product.product.name} - ${warehouse?.name || ''} (${stockText})` - }); - } - - if (hasOvkFlag) { - options.push({ - value: product.id, - label: `[OVK] ${product.product.name} - ${warehouse?.name || ''} (${stockText})` - }); - } - }); - } - - // Add existing stock products from initialValues (for detail/edit mode) - if (initialValues && 'stocks' in initialValues && initialValues.stocks && type !== 'add') { - const initialValuesWithStocks = initialValues as Recording & { - stocks?: Array<{ - product_warehouse_id: number; - usage_amount: number; - notes: string; - product_warehouse?: { - id: number; - product_id: number; - product_name: string; - warehouse_id: number; - warehouse_name: string; - }; - }>; - }; - - initialValuesWithStocks.stocks?.forEach((stock) => { - if (stock.product_warehouse && stock.product_warehouse.product_name) { - const existingOption = options.find(opt => opt.value === stock.product_warehouse_id); - if (!existingOption) { - options.push({ - value: stock.product_warehouse_id, - label: `${stock.product_warehouse.product_name} - ${stock.product_warehouse.warehouse_name}` - }); - } - } - }); - } - - return options; - }, [stockProducts, getProjectFlockLocation(), initialValues, type]); - - - // Handle stock usage amount change - simplified following MovementForm pattern - const handleStockUsageAmountChangeWrapper = useCallback( - (idx: number) => (e: React.ChangeEvent) => { - const value = parseInt(e.target.value.replace(/[^\d.-]/g, '')) || 0; - formik.setFieldValue(`stocks.${idx}.usage_amount`, value); - }, - [formik] - ); - - // Unified Stock remove handlers - const removeStock = (idx: number) => { - const updatedStocks = formik.values.stocks?.filter((_, i) => i !== idx); - formik.setFieldValue('stocks', updatedStocks); - }; - - const removeSelectedStocks = () => { - const updatedStocks = formik.values.stocks?.filter( - (_, idx) => !selectedStocks.includes(idx) - ); - formik.setFieldValue('stocks', updatedStocks); - setSelectedStocks([]); - }; - - // EVENT HANDLERS - Depletions - const addDepletion = () => { - const newDepletions = [ - ...(formik.values.depletions || []), - { - total: 0, - notes: '', - }, - ]; - formik.setFieldValue('depletions', newDepletions); - }; - - // Handle depletion total change - const handleDepletionTotalChangeWrapper = useCallback( - (idx: number) => (e: React.ChangeEvent) => { - const value = parseInt(e.target.value.replace(/[^\d.-]/g, '')) || 0; - formik.setFieldValue(`depletions.${idx}.total`, value); - }, - [formik] - ); - - const removeDepletion = (idx: number) => { - const updatedDepletions = formik.values.depletions?.filter( - (_, i) => i !== idx - ); - formik.setFieldValue('depletions', updatedDepletions); - }; - - const removeSelectedDepletions = () => { - const updatedDepletions = formik.values.depletions?.filter( - (_, idx) => !selectedDepletions.includes(idx) - ); - formik.setFieldValue('depletions', updatedDepletions); - setSelectedDepletions([]); - }; - - // HELPER FUNCTIONS const isRepeaterInputError = < T extends 'body_weights' | 'stocks' | 'depletions', >( @@ -713,6 +353,290 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }; }; + // ===== EVENT HANDLERS ===== + const locationChangeHandler = (val: OptionType | OptionType[] | null) => { + setSelectedLocation(val as OptionType); + formik.setFieldValue('project_flock_kandang', null); + formik.setFieldValue('project_flock_kandang_id', 0); + }; + + const projectFlockKandangChangeHandler = (val: OptionType | OptionType[] | null) => { + formik.setFieldTouched('project_flock_kandang', true); + formik.setFieldValue('project_flock_kandang', val); + formik.setFieldTouched('project_flock_kandang_id', true); + formik.setFieldValue('project_flock_kandang_id', (val as OptionType)?.value || 0); + }; + + const approveHandler = async () => { + setIsApproveLoading(true); + + const approveResponse = await RecordingApi.customRequest< + BaseApiResponse + >('approvals', { + method: 'POST', + payload: { + action: 'APPROVED', + approvable_ids: [initialValues?.id as number], + notes: 'Approved via Form', + }, + }); + + if (isResponseSuccess(approveResponse)) { + toast.success('Recording berhasil disetujui!'); + approveModal.closeModal(); + if (typeof window !== 'undefined') { + window.location.href = '/production/recording'; + } + } else { + toast.error(approveResponse?.message as string || 'Gagal menyetujui recording'); + approveModal.closeModal(); + } + + setIsApproveLoading(false); + }; + + const rejectHandler = async () => { + setIsRejectLoading(true); + + const rejectResponse = await RecordingApi.customRequest< + BaseApiResponse + >('approvals', { + method: 'POST', + payload: { + action: 'REJECTED', + approvable_ids: [initialValues?.id as number], + notes: 'Rejected via Form', + }, + }); + + if (isResponseSuccess(rejectResponse)) { + toast.success('Recording berhasil ditolak!'); + rejectModal.closeModal(); + if (typeof window !== 'undefined') { + window.location.href = '/production/recording'; + } + } else { + toast.error(rejectResponse?.message as string || 'Gagal menolak recording'); + rejectModal.closeModal(); + } + + setIsRejectLoading(false); + }; + + // Body Weights Handlers + const addBodyWeight = () => { + const newBodyWeights = [ + ...(formik.values.body_weights || []), + { + weight: 0, + qty: 1, + average_weight: 0, + }, + ]; + formik.setFieldValue('body_weights', newBodyWeights); + }; + + const handleWeightChange = (idx: number, value: number) => { + formik.setFieldValue(`body_weights.${idx}.weight`, value); + + setManuallyEditedRows(prev => { + const newSet = new Set(prev); + newSet.delete(idx); + return newSet; + }); + + const currentWeight = formik.values.body_weights?.[idx]; + if (currentWeight) { + const qty = currentWeight.qty; + if (qty > 0 && value > 0) { + const averageWeight = parseFloat((value / qty).toFixed(2)); + formik.setFieldValue(`body_weights.${idx}.average_weight`, averageWeight); + } else { + formik.setFieldValue(`body_weights.${idx}.average_weight`, 0); + } + } + }; + + const handleQtyChange = (idx: number, value: number) => { + formik.setFieldValue(`body_weights.${idx}.qty`, value); + + setManuallyEditedRows(prev => { + const newSet = new Set(prev); + newSet.delete(idx); + return newSet; + }); + + const currentWeight = formik.values.body_weights?.[idx]; + if (currentWeight) { + const weight = currentWeight.weight; + if (value > 0 && weight > 0) { + const averageWeight = parseFloat((weight / value).toFixed(2)); + formik.setFieldValue(`body_weights.${idx}.average_weight`, averageWeight); + } else { + formik.setFieldValue(`body_weights.${idx}.average_weight`, 0); + } + } + }; + + const handleAverageWeightChange = (idx: number, value: number) => { + formik.setFieldValue(`body_weights.${idx}.average_weight`, value); + + const currentWeight = formik.values.body_weights?.[idx]; + if (currentWeight) { + const qty = currentWeight.qty; + if (qty > 0 && value > 0) { + const totalWeight = value * qty; + formik.setFieldValue(`body_weights.${idx}.weight`, totalWeight); + } else { + formik.setFieldValue(`body_weights.${idx}.weight`, 0); + } + } + }; + + const handleWeightChangeWrapper = (idx: number) => (e: React.ChangeEvent) => { + const rawValue = e.target.value.replace(/[^\d,.-]/g, ''); + const normalizedValue = rawValue.replace(/,/g, ''); + const value = parseFloat(normalizedValue) || 0; + handleWeightChange(idx, value); + }; + + const handleQtyChangeWrapper = (idx: number) => (e: React.ChangeEvent) => { + const rawValue = e.target.value.replace(/[^\d,.-]/g, ''); + const normalizedValue = rawValue.replace(/,/g, ''); + const value = parseFloat(normalizedValue) || 0; + handleQtyChange(idx, value); + }; + + const handleAverageWeightChangeWrapper = (idx: number) => (e: React.ChangeEvent) => { + setEditingAverageIndex(idx); + setManuallyEditedRows(prev => new Set(prev).add(idx)); + + const rawValue = e.target.value.replace(/[^\d,.-]/g, ''); + const normalizedValue = rawValue.replace(/,/g, ''); + const value = parseFloat(normalizedValue) || 0; + handleAverageWeightChange(idx, value); + }; + + const handleAverageWeightBlur = (idx: number) => { + setEditingAverageIndex(null); + }; + + const removeBodyWeight = (idx: number) => { + const updatedBodyWeights = formik.values.body_weights?.filter( + (_, i) => i !== idx + ); + formik.setFieldValue('body_weights', updatedBodyWeights); + }; + + const removeSelectedBodyWeights = () => { + const updatedBodyWeights = formik.values.body_weights?.filter( + (_, idx) => !selectedBodyWeights.includes(idx) + ); + formik.setFieldValue('body_weights', updatedBodyWeights); + setSelectedBodyWeights([]); + }; + + // Stocks Handlers + const addStock = () => { + const newStocks = [ + ...(formik.values.stocks || []), + { + product_warehouse_id: 0, + usage_amount: 0, + notes: '', + }, + ]; + formik.setFieldValue('stocks', newStocks); + }; + + const handleStockUsageAmountChangeWrapper = useCallback( + (idx: number) => (e: React.ChangeEvent) => { + const value = parseInt(e.target.value.replace(/[^\d.-]/g, '')) || 0; + formik.setFieldValue(`stocks.${idx}.usage_amount`, value); + }, + [formik] + ); + + const removeStock = (idx: number) => { + const updatedStocks = formik.values.stocks?.filter((_, i) => i !== idx); + formik.setFieldValue('stocks', updatedStocks); + }; + + const removeSelectedStocks = () => { + const updatedStocks = formik.values.stocks?.filter( + (_, idx) => !selectedStocks.includes(idx) + ); + formik.setFieldValue('stocks', updatedStocks); + setSelectedStocks([]); + }; + + // Depletions Handlers + const addDepletion = () => { + const newDepletions = [ + ...(formik.values.depletions || []), + { + total: 0, + notes: '', + }, + ]; + formik.setFieldValue('depletions', newDepletions); + }; + + const handleDepletionTotalChangeWrapper = useCallback( + (idx: number) => (e: React.ChangeEvent) => { + const value = parseInt(e.target.value.replace(/[^\d.-]/g, '')) || 0; + formik.setFieldValue(`depletions.${idx}.total`, value); + }, + [formik] + ); + + const removeDepletion = (idx: number) => { + const updatedDepletions = formik.values.depletions?.filter( + (_, i) => i !== idx + ); + formik.setFieldValue('depletions', updatedDepletions); + }; + + const removeSelectedDepletions = () => { + const updatedDepletions = formik.values.depletions?.filter( + (_, idx) => !selectedDepletions.includes(idx) + ); + formik.setFieldValue('depletions', updatedDepletions); + setSelectedDepletions([]); + }; + + // ===== EFFECTS ===== + useEffect(() => { + if (formik.values.body_weights && editingAverageIndex === null) { + const updatedBodyWeights = formik.values.body_weights.map((weight, idx) => { + if (idx === editingAverageIndex || manuallyEditedRows.has(idx)) { + return weight; + } + return { + ...weight, + average_weight: + weight.qty > 0 && weight.weight > 0 + ? parseFloat((weight.weight / weight.qty).toFixed(2)) + : 0, + }; + }); + const hasChanges = updatedBodyWeights.some( + (updated, idx) => + idx !== editingAverageIndex && + !manuallyEditedRows.has(idx) && + updated.average_weight !== + (formik.values.body_weights[idx]?.average_weight || 0) + ); + if (hasChanges) { + formik.setFieldValue('body_weights', updatedBodyWeights, false); + } + } + }, [ + formik.values.body_weights?.map((w) => w.weight), + formik.values.body_weights?.map((w) => w.qty), + editingAverageIndex, + manuallyEditedRows, + ]); return ( <> @@ -774,7 +698,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { - {/* Body Weights Table */} { data-tip='Otomatis dihitung: Total Berat รท Jumlah Ayam' > - + {type !== 'detail' && Action} @@ -1096,7 +1019,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { option?.value || 0 ); - // Auto-populate notes with product name by finding it in stockProducts data if (option?.value && isResponseSuccess(stockProducts)) { const selectedProduct = stockProducts.data.find( (product) => product.id === option.value @@ -1486,4 +1408,4 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ); }; -export default RecordingForm; +export default RecordingForm; \ No newline at end of file