refactor(FE-137): integrate approve and reject functionality in RecordingForm with loading states and modal confirmations

This commit is contained in:
rstubryan
2025-10-24 20:44:15 +07:00
parent 81003eac63
commit 9c5dc0dbb5
@@ -45,45 +45,41 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const [selectedStocks, setSelectedStocks] = useState<number[]>([]);
const [selectedDepletions, setSelectedDepletions] = useState<number[]>([]);
// Track which average weight field is being edited to prevent auto-calculation override
const [editingAverageIndex, setEditingAverageIndex] = useState<number | null>(null);
// Track which rows have been manually edited to prevent auto-calculation override
const [manuallyEditedRows, setManuallyEditedRows] = useState<Set<number>>(new Set());
// State for Location search and selection
const [locationSearchValue, setLocationSearchValue] = useState('');
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(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<Recording[]>
>('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<Recording[]>
>('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<RecordingFormValues>(
() => 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 (
<span className='text-sm text-gray-600 whitespace-nowrap'>
@@ -388,7 +297,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</span>
);
}
return (
<span className='text-sm text-gray-600 whitespace-nowrap'>
(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<HTMLInputElement>) => {
// 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<HTMLInputElement>) => {
// 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<HTMLInputElement>) => {
// 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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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<Recording[]>
>('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<Recording[]>
>('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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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) => {
</div>
</Card>
{/* Body Weights Table */}
<Card
title='Bobot Badan'
@@ -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