mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 21:41:57 +00:00
feat(FE-170,174): enhance daily recording form with weight validation and dynamic average weight calculation
This commit is contained in:
@@ -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'>
|
||||
|
||||
Reference in New Issue
Block a user