feat(FE-114): add average weight calculation and input handling in RecordingForm

This commit is contained in:
rstubryan
2025-10-23 21:54:06 +07:00
parent 71df86c8df
commit ef249fee12
2 changed files with 154 additions and 18 deletions
@@ -39,6 +39,11 @@ export const RecordingFormSchema = Yup.object({
.min(1, 'Jumlah ayam minimal 1 ekor!')
.typeError('Jumlah ayam harus berupa angka!')
.default(1),
average_weight: Yup.number()
.optional()
.min(0, 'Rata-rata berat tidak boleh negatif!')
.typeError('Rata-rata berat harus berupa angka!')
.default(0),
notes: Yup.string().optional(),
})
)
@@ -123,12 +128,14 @@ export const getRecordingFormInitialValues = (
(bw: NonNullable<CreateRecordingPayload['body_weights']>[0]) => ({
weight: bw.weight,
qty: bw.qty,
average_weight: bw.qty > 0 ? Math.round(bw.weight / bw.qty) : 0,
notes: bw.notes || '',
})
) ?? [
{
weight: 0,
qty: 1,
average_weight: 0,
notes: '',
},
],
@@ -7,6 +7,7 @@ import { Icon } from '@iconify/react';
import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput';
import NumberInput from '@/components/input/NumberInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import CheckboxInput from '@/components/input/CheckboxInput';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
@@ -135,6 +136,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
? bw.qty
: parseFloat(String(bw.qty)) || 0,
notes: bw.notes,
// average_weight is not included in payload as it's calculated field only
})),
stocks: (values.stocks ?? []).map((stock) => ({
product_warehouse_id: stock.product_warehouse_id,
@@ -229,6 +231,33 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
}
}, [formik.values.record_datetime]);
// Auto-calculate average weight when weight or qty changes
useEffect(() => {
if (formik.values.body_weights) {
const updatedBodyWeights = formik.values.body_weights.map((weight) => ({
...weight,
average_weight:
weight.qty > 0 && weight.weight > 0
? Math.round(weight.weight / weight.qty)
: 0,
}));
// Only update if values are different to avoid infinite loops
const hasChanges = updatedBodyWeights.some(
(updated, 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),
]);
// EVENT HANDLERS - Body Weights
const addBodyWeight = () => {
const newBodyWeights = [
@@ -237,11 +266,76 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
weight: 0,
qty: 1,
notes: '',
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);
const currentWeight = formik.values.body_weights?.[idx];
if (currentWeight) {
const qty = currentWeight.qty;
if (qty > 0 && value > 0) {
const averageWeight = Math.round(value / qty);
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);
const currentWeight = formik.values.body_weights?.[idx];
if (currentWeight) {
const weight = currentWeight.weight;
if (value > 0 && weight > 0) {
const averageWeight = Math.round(weight / value);
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>) => {
const value = parseFloat(e.target.value.replace(/[^\d,.-]/g, '').replace(/,/g, '')) || 0;
handleWeightChange(idx, value);
};
const handleQtyChangeWrapper = (idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseFloat(e.target.value.replace(/[^\d,.-]/g, '').replace(/,/g, '')) || 0;
handleQtyChange(idx, value);
};
const handleAverageWeightChangeWrapper = (idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseFloat(e.target.value.replace(/[^\d,.-]/g, '').replace(/,/g, '')) || 0;
handleAverageWeightChange(idx, value);
};
const removeBodyWeight = (idx: number) => {
const updatedBodyWeights = formik.values.body_weights?.filter(
(_, i) => i !== idx
@@ -353,6 +447,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
};
};
return (
<>
<section className='w-full'>
@@ -514,6 +609,15 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<span className='text-error'>*</span>
</span>
</th>
<th>
Rata-rata Berat Ayam (gram)
<span
className='tooltip tooltip-info tooltip-bottom z-[9999]'
data-tip='Otomatis dihitung: Total Berat ÷ Jumlah Ayam'
>
<Icon icon="material-symbols:info-outline" width={16} height={16} />
</span>
</th>
<th>Catatan</th>
{type !== 'detail' && <th>Action</th>}
</tr>
@@ -546,17 +650,18 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</td>
)}
<td>
<TextInput
<NumberInput
required
name={`body_weights.${idx}.weight`}
type='number'
value={
typeof bw.weight === 'number'
? bw.weight.toString()
: bw.weight
}
onChange={formik.handleChange}
value={bw.weight}
onChange={handleWeightChangeWrapper(idx)}
onBlur={formik.handleBlur}
maskType='weight'
weightUnit='gram'
decimals={2}
min={0}
thousandSeparator=','
decimalSeparator='.'
isError={
isRepeaterInputError('body_weights', 'weight', idx)
.isError
@@ -569,21 +674,20 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
className={{
wrapper: 'w-full min-w-32',
}}
placeholder='Berat ayam (gram)'
/>
</td>
<td>
<TextInput
<NumberInput
required
name={`body_weights.${idx}.qty`}
type='number'
value={
typeof bw.qty === 'number'
? bw.qty.toString()
: bw.qty
}
onChange={formik.handleChange}
value={bw.qty}
onChange={handleQtyChangeWrapper(idx)}
onBlur={formik.handleBlur}
maskType='number'
decimals={0}
min={0}
thousandSeparator=','
decimalSeparator='.'
isError={
isRepeaterInputError('body_weights', 'qty', idx)
.isError
@@ -596,7 +700,32 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
className={{
wrapper: 'w-full min-w-24',
}}
placeholder='Jumlah ayam'
/>
</td>
<td>
<NumberInput
name={`body_weights.${idx}.average_weight`}
value={bw.average_weight || 0}
onChange={handleAverageWeightChangeWrapper(idx)}
onBlur={formik.handleBlur}
maskType='weight'
weightUnit='gram'
decimals={2}
min={0}
thousandSeparator=','
decimalSeparator='.'
isError={
isRepeaterInputError('body_weights', 'average_weight', idx)
.isError
}
errorMessage={
isRepeaterInputError('body_weights', 'average_weight', idx)
.errorMessage
}
readOnly={type === 'detail'}
className={{
wrapper: 'w-full min-w-32',
}}
/>
</td>
<td>