mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-24 23:35:45 +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()
|
body_weights: Yup.array()
|
||||||
.of(
|
.of(
|
||||||
Yup.object({
|
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()
|
avg_weight: Yup.number()
|
||||||
.required('Berat ayam rata-rata wajib diisi!')
|
.required('Berat ayam rata-rata wajib diisi!')
|
||||||
.min(1, 'Berat ayam rata-rata minimal 1 gram!')
|
.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,
|
project_flock_kandangs_id: initialValues?.project_flock_kandangs_id ?? 0,
|
||||||
body_weights: initialValues?.body_weights?.map(
|
body_weights: initialValues?.body_weights?.map(
|
||||||
(bw: NonNullable<CreateGrowingRecordingPayload['body_weights']>[0]) => ({
|
(bw: NonNullable<CreateGrowingRecordingPayload['body_weights']>[0]) => ({
|
||||||
|
weight: bw.avg_weight * bw.qty,
|
||||||
avg_weight: bw.avg_weight,
|
avg_weight: bw.avg_weight,
|
||||||
qty: bw.qty,
|
qty: bw.qty,
|
||||||
})
|
})
|
||||||
) ?? [
|
) ?? [
|
||||||
{
|
{
|
||||||
|
weight: 0,
|
||||||
avg_weight: 0,
|
avg_weight: 0,
|
||||||
qty: 0,
|
qty: 0,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMemo, useState, useCallback } from 'react';
|
import { useMemo, useState, useEffect, useCallback } from 'react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
@@ -46,6 +46,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
const [selectedStocks, setSelectedStocks] = useState<number[]>([]);
|
const [selectedStocks, setSelectedStocks] = useState<number[]>([]);
|
||||||
const [selectedDepletions, setSelectedDepletions] = 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 [locationSearchValue, setLocationSearchValue] = useState('');
|
||||||
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
||||||
null
|
null
|
||||||
@@ -612,6 +619,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
const newBodyWeights = [
|
const newBodyWeights = [
|
||||||
...(formik.values.body_weights || []),
|
...(formik.values.body_weights || []),
|
||||||
{
|
{
|
||||||
|
weight: 0,
|
||||||
avg_weight: 0,
|
avg_weight: 0,
|
||||||
qty: 1,
|
qty: 1,
|
||||||
},
|
},
|
||||||
@@ -619,14 +627,75 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
formik.setFieldValue('body_weights', newBodyWeights);
|
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) => {
|
const handleAvgWeightChange = (idx: number, value: number) => {
|
||||||
formik.setFieldValue(`body_weights.${idx}.avg_weight`, value);
|
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) => {
|
const handleQtyChange = (idx: number, value: number) => {
|
||||||
formik.setFieldValue(`body_weights.${idx}.qty`, value);
|
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 =
|
const handleAvgWeightChangeWrapper =
|
||||||
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = parseFloat(e.target.value) || 0;
|
const value = parseFloat(e.target.value) || 0;
|
||||||
@@ -722,6 +791,41 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
setSelectedDepletions([]);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className='w-full'>
|
<section className='w-full'>
|
||||||
@@ -843,7 +947,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
<th>
|
<th>
|
||||||
Rata-rata Berat Ayam (gram)
|
Berat Ayam (gram)
|
||||||
<span
|
<span
|
||||||
className='tooltip tooltip-error tooltip-bottom z-[9999]'
|
className='tooltip tooltip-error tooltip-bottom z-[9999]'
|
||||||
data-tip='required'
|
data-tip='required'
|
||||||
@@ -860,6 +964,15 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
<span className='text-error'>*</span>
|
<span className='text-error'>*</span>
|
||||||
</span>
|
</span>
|
||||||
</th>
|
</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>}
|
{type !== 'detail' && <th>Action</th>}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -895,9 +1008,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
<td>
|
<td>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
name={`body_weights.${idx}.avg_weight`}
|
name={`body_weights.${idx}.weight`}
|
||||||
value={bw.avg_weight}
|
value={bw.weight}
|
||||||
onChange={handleAvgWeightChangeWrapper(idx)}
|
onChange={handleWeightChangeWrapper(idx)}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
decimalScale={2}
|
decimalScale={2}
|
||||||
allowNegative={false}
|
allowNegative={false}
|
||||||
@@ -905,18 +1018,12 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
decimalSeparator='.'
|
decimalSeparator='.'
|
||||||
inputSuffix='gram'
|
inputSuffix='gram'
|
||||||
isError={
|
isError={
|
||||||
isRepeaterInputError(
|
isRepeaterInputError('body_weights', 'weight', idx)
|
||||||
'body_weights',
|
.isError
|
||||||
'avg_weight',
|
|
||||||
idx
|
|
||||||
).isError
|
|
||||||
}
|
}
|
||||||
errorMessage={
|
errorMessage={
|
||||||
isRepeaterInputError(
|
isRepeaterInputError('body_weights', 'weight', idx)
|
||||||
'body_weights',
|
.errorMessage
|
||||||
'avg_weight',
|
|
||||||
idx
|
|
||||||
).errorMessage
|
|
||||||
}
|
}
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
className={{
|
className={{
|
||||||
@@ -949,6 +1056,39 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</td>
|
</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' && (
|
{type !== 'detail' && (
|
||||||
<td>
|
<td>
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
|
|||||||
Reference in New Issue
Block a user