feat(FE-170,174): enhance daily recording form with weight validation and dynamic average weight calculation

This commit is contained in:
rstubryan
2025-10-30 19:09:21 +07:00
parent 4cb045de6c
commit 75348620d7
2 changed files with 161 additions and 15 deletions
@@ -38,6 +38,10 @@ export const RecordingGrowingFormSchema = Yup.object({
body_weights: Yup.array()
.of(
Yup.object({
weight: Yup.number()
.required('Berat ayam total wajib diisi!')
.min(1, 'Berat ayam total minimal 1 gram!')
.typeError('Berat ayam total harus berupa angka!'),
avg_weight: Yup.number()
.required('Berat ayam rata-rata wajib diisi!')
.min(1, 'Berat ayam rata-rata minimal 1 gram!')
@@ -118,11 +122,13 @@ export const getRecordingGrowingFormInitialValues = (
project_flock_kandangs_id: initialValues?.project_flock_kandangs_id ?? 0,
body_weights: initialValues?.body_weights?.map(
(bw: NonNullable<CreateGrowingRecordingPayload['body_weights']>[0]) => ({
weight: bw.avg_weight * bw.qty,
avg_weight: bw.avg_weight,
qty: bw.qty,
})
) ?? [
{
weight: 0,
avg_weight: 0,
qty: 0,
},
@@ -1,6 +1,6 @@
'use client';
import { useMemo, useState, useCallback } from 'react';
import { useMemo, useState, useEffect, useCallback } from 'react';
import { useFormik } from 'formik';
import useSWR from 'swr';
import { Icon } from '@iconify/react';
@@ -46,6 +46,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const [selectedStocks, setSelectedStocks] = useState<number[]>([]);
const [selectedDepletions, setSelectedDepletions] = useState<number[]>([]);
const [editingAverageIndex, setEditingAverageIndex] = useState<number | null>(
null
);
const [manuallyEditedRows, setManuallyEditedRows] = useState<Set<number>>(
new Set()
);
const [locationSearchValue, setLocationSearchValue] = useState('');
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
null
@@ -612,6 +619,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const newBodyWeights = [
...(formik.values.body_weights || []),
{
weight: 0,
avg_weight: 0,
qty: 1,
},
@@ -619,14 +627,75 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
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 avgWeight = parseFloat((value / qty).toFixed(2));
formik.setFieldValue(`body_weights.${idx}.avg_weight`, avgWeight);
} else {
formik.setFieldValue(`body_weights.${idx}.avg_weight`, 0);
}
}
};
const handleAvgWeightChange = (idx: number, value: number) => {
formik.setFieldValue(`body_weights.${idx}.avg_weight`, value);
setManuallyEditedRows((prev) => {
const newSet = new Set(prev);
newSet.add(idx);
return newSet;
});
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 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 avgWeight = parseFloat((weight / value).toFixed(2));
formik.setFieldValue(`body_weights.${idx}.avg_weight`, avgWeight);
} else {
formik.setFieldValue(`body_weights.${idx}.avg_weight`, 0);
}
}
};
const handleWeightChangeWrapper =
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseFloat(e.target.value) || 0;
handleWeightChange(idx, value);
};
const handleAvgWeightChangeWrapper =
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseFloat(e.target.value) || 0;
@@ -722,6 +791,41 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
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,
avg_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.avg_weight !==
(formik.values.body_weights[idx]?.avg_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 (
<>
<section className='w-full'>
@@ -843,7 +947,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</th>
)}
<th>
Rata-rata Berat Ayam (gram)
Berat Ayam (gram)
<span
className='tooltip tooltip-error tooltip-bottom z-[9999]'
data-tip='required'
@@ -860,6 +964,15 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<span className='text-error'>*</span>
</span>
</th>
<th>
Rata-rata Berat Ayam (gram)
<span
className='tooltip tooltip-error tooltip-bottom z-[9999]'
data-tip='required'
>
<span className='text-error'>*</span>
</span>
</th>
{type !== 'detail' && <th>Action</th>}
</tr>
</thead>
@@ -895,9 +1008,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<td>
<NumberInput
required
name={`body_weights.${idx}.avg_weight`}
value={bw.avg_weight}
onChange={handleAvgWeightChangeWrapper(idx)}
name={`body_weights.${idx}.weight`}
value={bw.weight}
onChange={handleWeightChangeWrapper(idx)}
onBlur={formik.handleBlur}
decimalScale={2}
allowNegative={false}
@@ -905,18 +1018,12 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
decimalSeparator='.'
inputSuffix='gram'
isError={
isRepeaterInputError(
'body_weights',
'avg_weight',
idx
).isError
isRepeaterInputError('body_weights', 'weight', idx)
.isError
}
errorMessage={
isRepeaterInputError(
'body_weights',
'avg_weight',
idx
).errorMessage
isRepeaterInputError('body_weights', 'weight', idx)
.errorMessage
}
readOnly={type === 'detail'}
className={{
@@ -949,6 +1056,39 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
}}
/>
</td>
<td>
<NumberInput
required
name={`body_weights.${idx}.avg_weight`}
value={bw.avg_weight}
onChange={handleAvgWeightChangeWrapper(idx)}
onBlur={formik.handleBlur}
decimalScale={2}
allowNegative={false}
thousandSeparator=','
decimalSeparator='.'
inputSuffix='gram'
isError={
isRepeaterInputError(
'body_weights',
'avg_weight',
idx
).isError
}
errorMessage={
isRepeaterInputError(
'body_weights',
'avg_weight',
idx
).errorMessage
}
readOnly={type === 'detail'}
className={{
wrapper: 'w-full min-w-32',
}}
/>
</td>
{type !== 'detail' && (
<td>
<div className='flex items-center'>