mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-24 15:25:46 +00:00
feat(FE-114): add average weight calculation and input handling in RecordingForm
This commit is contained in:
@@ -39,6 +39,11 @@ export const RecordingFormSchema = Yup.object({
|
|||||||
.min(1, 'Jumlah ayam minimal 1 ekor!')
|
.min(1, 'Jumlah ayam minimal 1 ekor!')
|
||||||
.typeError('Jumlah ayam harus berupa angka!')
|
.typeError('Jumlah ayam harus berupa angka!')
|
||||||
.default(1),
|
.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(),
|
notes: Yup.string().optional(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -123,12 +128,14 @@ export const getRecordingFormInitialValues = (
|
|||||||
(bw: NonNullable<CreateRecordingPayload['body_weights']>[0]) => ({
|
(bw: NonNullable<CreateRecordingPayload['body_weights']>[0]) => ({
|
||||||
weight: bw.weight,
|
weight: bw.weight,
|
||||||
qty: bw.qty,
|
qty: bw.qty,
|
||||||
|
average_weight: bw.qty > 0 ? Math.round(bw.weight / bw.qty) : 0,
|
||||||
notes: bw.notes || '',
|
notes: bw.notes || '',
|
||||||
})
|
})
|
||||||
) ?? [
|
) ?? [
|
||||||
{
|
{
|
||||||
weight: 0,
|
weight: 0,
|
||||||
qty: 1,
|
qty: 1,
|
||||||
|
average_weight: 0,
|
||||||
notes: '',
|
notes: '',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Icon } from '@iconify/react';
|
|||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
|
|
||||||
import TextInput from '@/components/input/TextInput';
|
import TextInput from '@/components/input/TextInput';
|
||||||
|
import NumberInput from '@/components/input/NumberInput';
|
||||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
@@ -135,6 +136,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
? bw.qty
|
? bw.qty
|
||||||
: parseFloat(String(bw.qty)) || 0,
|
: parseFloat(String(bw.qty)) || 0,
|
||||||
notes: bw.notes,
|
notes: bw.notes,
|
||||||
|
// average_weight is not included in payload as it's calculated field only
|
||||||
})),
|
})),
|
||||||
stocks: (values.stocks ?? []).map((stock) => ({
|
stocks: (values.stocks ?? []).map((stock) => ({
|
||||||
product_warehouse_id: stock.product_warehouse_id,
|
product_warehouse_id: stock.product_warehouse_id,
|
||||||
@@ -229,6 +231,33 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
}
|
}
|
||||||
}, [formik.values.record_datetime]);
|
}, [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
|
// EVENT HANDLERS - Body Weights
|
||||||
const addBodyWeight = () => {
|
const addBodyWeight = () => {
|
||||||
const newBodyWeights = [
|
const newBodyWeights = [
|
||||||
@@ -237,11 +266,76 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
weight: 0,
|
weight: 0,
|
||||||
qty: 1,
|
qty: 1,
|
||||||
notes: '',
|
notes: '',
|
||||||
|
average_weight: 0,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
formik.setFieldValue('body_weights', newBodyWeights);
|
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 removeBodyWeight = (idx: number) => {
|
||||||
const updatedBodyWeights = formik.values.body_weights?.filter(
|
const updatedBodyWeights = formik.values.body_weights?.filter(
|
||||||
(_, i) => i !== idx
|
(_, i) => i !== idx
|
||||||
@@ -353,6 +447,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className='w-full'>
|
<section className='w-full'>
|
||||||
@@ -514,6 +609,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-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>
|
<th>Catatan</th>
|
||||||
{type !== 'detail' && <th>Action</th>}
|
{type !== 'detail' && <th>Action</th>}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -546,17 +650,18 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
<td>
|
<td>
|
||||||
<TextInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
name={`body_weights.${idx}.weight`}
|
name={`body_weights.${idx}.weight`}
|
||||||
type='number'
|
value={bw.weight}
|
||||||
value={
|
onChange={handleWeightChangeWrapper(idx)}
|
||||||
typeof bw.weight === 'number'
|
|
||||||
? bw.weight.toString()
|
|
||||||
: bw.weight
|
|
||||||
}
|
|
||||||
onChange={formik.handleChange}
|
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
|
maskType='weight'
|
||||||
|
weightUnit='gram'
|
||||||
|
decimals={2}
|
||||||
|
min={0}
|
||||||
|
thousandSeparator=','
|
||||||
|
decimalSeparator='.'
|
||||||
isError={
|
isError={
|
||||||
isRepeaterInputError('body_weights', 'weight', idx)
|
isRepeaterInputError('body_weights', 'weight', idx)
|
||||||
.isError
|
.isError
|
||||||
@@ -569,21 +674,20 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full min-w-32',
|
wrapper: 'w-full min-w-32',
|
||||||
}}
|
}}
|
||||||
placeholder='Berat ayam (gram)'
|
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<TextInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
name={`body_weights.${idx}.qty`}
|
name={`body_weights.${idx}.qty`}
|
||||||
type='number'
|
value={bw.qty}
|
||||||
value={
|
onChange={handleQtyChangeWrapper(idx)}
|
||||||
typeof bw.qty === 'number'
|
|
||||||
? bw.qty.toString()
|
|
||||||
: bw.qty
|
|
||||||
}
|
|
||||||
onChange={formik.handleChange}
|
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
|
maskType='number'
|
||||||
|
decimals={0}
|
||||||
|
min={0}
|
||||||
|
thousandSeparator=','
|
||||||
|
decimalSeparator='.'
|
||||||
isError={
|
isError={
|
||||||
isRepeaterInputError('body_weights', 'qty', idx)
|
isRepeaterInputError('body_weights', 'qty', idx)
|
||||||
.isError
|
.isError
|
||||||
@@ -596,7 +700,32 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full min-w-24',
|
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>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
Reference in New Issue
Block a user