refactor(FE-170,174): restructure schema and validation for body weights, stocks, and depletions; improve handling of input values

This commit is contained in:
rstubryan
2025-11-02 23:04:54 +07:00
parent 4afeded080
commit e116311dc2
2 changed files with 207 additions and 144 deletions
@@ -7,107 +7,165 @@ import {
CreateGradingPayload, CreateGradingPayload,
} from '@/types/api/production/recording'; } from '@/types/api/production/recording';
export const RecordingGrowingFormSchema = Yup.object({ type RecordingGrowingFormSchemaType = {
project_flock_kandang: Yup.object({ project_flock_kandang: {
value: Yup.number().min(1).required(), value: number;
label: Yup.string().required(), label: string;
}).nullable(), } | null;
project_flock_kandangs_id: Yup.number() project_flock_kandangs_id: number;
.default(0) body_weights: {
.typeError('Project Flock Kandang wajib diisi!') weight: number | string;
.test( avg_weight: number | string;
'is-valid-project-flock-kandang', qty: number | string;
'Project Flock Kandang wajib diisi!', }[];
(value) => value !== undefined && value !== null && value > 0 stocks: {
) product_warehouse_id: number;
.required('Project Flock Kandang wajib diisi!') usage_qty: number | string;
.test( }[];
'not-already-recorded', depletions: {
'Project Flock ini sudah direcord hari ini!', product_warehouse_id: number;
function (value) { qty: number | string;
const recordedProjectFlockIds = this.options.context }[];
?.recordedProjectFlockIds as Set<number>; };
const formType = this.options.context?.type as
| 'add' type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & {
| 'edit' eggs: {
| 'detail'; product_warehouse_id: number;
if (formType !== 'add') return true; qty: number | string;
if (value && recordedProjectFlockIds?.has(value)) { }[];
return false; };
}
return true; type RecordingGradingFormSchemaType = {
} recording_egg_id: number;
), eggs_grading: {
body_weights: Yup.array() grade: string;
.of( qty: number | string;
Yup.object({ }[];
weight: Yup.number() };
.required('Berat ayam total wajib diisi!')
.min(1, 'Berat ayam total minimal 1 gram!') export type BodyWeightSchema = {
.typeError('Berat ayam total harus berupa angka!'), weight: number | string;
avg_weight: Yup.number() avg_weight: number | string;
.required('Berat ayam rata-rata wajib diisi!') qty: number | string;
.min(1, 'Berat ayam rata-rata minimal 1 gram!') };
.typeError('Berat ayam rata-rata harus berupa angka!'),
qty: Yup.number() export type StockSchema = {
.required('Jumlah ayam wajib diisi!') product_warehouse_id: number;
.min(1, 'Jumlah ayam minimal 1 ekor!') usage_qty: number | string;
.typeError('Jumlah ayam harus berupa angka!') };
.default(1),
}) export type DepletionSchema = {
) product_warehouse_id: number;
.min(1, 'Minimal harus ada 1 data bobot badan!') qty: number | string;
.required('Data bobot badan wajib diisi!'), };
stocks: Yup.array()
.of( export type EggSchema = {
Yup.object({ product_warehouse_id: number;
product_warehouse_id: Yup.number() qty: number | string;
.required('Produk wajib diisi!') };
.min(1, 'Produk wajib diisi!')
.typeError('Produk harus berupa angka!'), const BodyWeightObjectSchema: Yup.ObjectSchema<BodyWeightSchema> = Yup.object({
usage_qty: Yup.number() weight: Yup.number()
.required('Jumlah penggunaan wajib diisi!') .required('Berat ayam total wajib diisi!')
.min(1, 'Jumlah penggunaan tidak boleh 0!') .min(1, 'Berat ayam total minimal 1 gram!')
.typeError('Jumlah penggunaan harus berupa angka!'), .typeError('Berat ayam total harus berupa angka!'),
}) avg_weight: Yup.number()
) .required('Berat ayam rata-rata wajib diisi!')
.min(1, 'Minimal harus ada 1 data stok!') .min(1, 'Berat ayam rata-rata minimal 1 gram!')
.required('Data stok wajib diisi!'), .typeError('Berat ayam rata-rata harus berupa angka!'),
depletions: Yup.array() qty: Yup.number()
.of( .required('Jumlah ayam wajib diisi!')
Yup.object({ .min(1, 'Jumlah ayam minimal 1 ekor!')
product_warehouse_id: Yup.number() .typeError('Jumlah ayam harus berupa angka!'),
.required('Produk depletions wajib diisi!')
.min(1, 'Produk depletions wajib diisi!')
.typeError('Produk depletions harus berupa angka!'),
qty: Yup.number()
.required('Jumlah depletions wajib diisi!')
.min(1, 'Jumlah depletions minimal 1!')
.typeError('Jumlah depletions harus berupa angka!'),
})
)
.min(1, 'Minimal harus ada 1 data depletions!')
.required('Data depletions wajib diisi!'),
}); });
export const RecordingLayingFormSchema = RecordingGrowingFormSchema.shape({ const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
eggs: Yup.array() product_warehouse_id: Yup.number()
.of( .required('Produk wajib diisi!')
Yup.object({ .min(1, 'Produk wajib diisi!')
product_warehouse_id: Yup.number() .typeError('Produk harus berupa angka!'),
.required('Produk telur wajib diisi!') usage_qty: Yup.number()
.min(1, 'Produk telur wajib diisi!') .required('Jumlah penggunaan wajib diisi!')
.typeError('Produk telur harus berupa angka!'), .min(1, 'Jumlah penggunaan tidak boleh 0!')
qty: Yup.number() .typeError('Jumlah penggunaan harus berupa angka!'),
.required('Jumlah telur wajib diisi!')
.min(1, 'Jumlah telur tidak boleh 0!')
.typeError('Jumlah telur harus berupa angka!'),
})
)
.min(1, 'Minimal harus ada 1 data telur!')
.required('Data telur wajib diisi!'),
}); });
const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({
product_warehouse_id: Yup.number()
.required('Produk depletions wajib diisi!')
.min(1, 'Produk depletions wajib diisi!')
.typeError('Produk depletions harus berupa angka!'),
qty: Yup.number()
.required('Jumlah depletions wajib diisi!')
.min(1, 'Jumlah depletions minimal 1!')
.typeError('Jumlah depletions harus berupa angka!'),
});
const EggObjectSchema: Yup.ObjectSchema<EggSchema> = Yup.object({
product_warehouse_id: Yup.number()
.required('Produk telur wajib diisi!')
.min(1, 'Produk telur wajib diisi!')
.typeError('Produk telur harus berupa angka!'),
qty: Yup.number()
.required('Jumlah telur wajib diisi!')
.min(1, 'Jumlah telur tidak boleh 0!')
.typeError('Jumlah telur harus berupa angka!'),
});
export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSchemaType> =
Yup.object({
project_flock_kandang: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
project_flock_kandangs_id: Yup.number()
.default(0)
.typeError('Project Flock Kandang wajib diisi!')
.test(
'is-valid-project-flock-kandang',
'Project Flock Kandang wajib diisi!',
(value) => value !== undefined && value !== null && value > 0
)
.required('Project Flock Kandang wajib diisi!')
.test(
'not-already-recorded',
'Project Flock ini sudah direcord hari ini!',
function (value) {
const recordedProjectFlockIds = this.options.context
?.recordedProjectFlockIds as Set<number>;
const formType = this.options.context?.type as
| 'add'
| 'edit'
| 'detail';
if (formType !== 'add') return true;
if (value && recordedProjectFlockIds?.has(value)) {
return false;
}
return true;
}
),
body_weights: Yup.array()
.of(BodyWeightObjectSchema)
.min(1, 'Minimal harus ada 1 data bobot badan!')
.required('Data bobot badan wajib diisi!'),
stocks: Yup.array()
.of(StockObjectSchema)
.min(1, 'Minimal harus ada 1 data stok!')
.required('Data stok wajib diisi!'),
depletions: Yup.array()
.of(DepletionObjectSchema)
.min(1, 'Minimal harus ada 1 data depletions!')
.required('Data depletions wajib diisi!'),
});
export const RecordingLayingFormSchema: Yup.ObjectSchema<RecordingLayingFormSchemaType> =
RecordingGrowingFormSchema.shape({
eggs: Yup.array()
.of(EggObjectSchema)
.min(1, 'Minimal harus ada 1 data telur!')
.required('Data telur wajib diisi!'),
});
export const UpdateRecordingGrowingFormSchema = export const UpdateRecordingGrowingFormSchema =
RecordingGrowingFormSchema.shape({ RecordingGrowingFormSchema.shape({
project_flock_kandangs_id: Yup.number() project_flock_kandangs_id: Yup.number()
@@ -133,26 +191,27 @@ export const UpdateRecordingLayingFormSchema = RecordingLayingFormSchema.shape({
.required('Project Flock Kandang wajib diisi!'), .required('Project Flock Kandang wajib diisi!'),
}); });
export const RecordingGradingFormSchema = Yup.object({ export const RecordingGradingFormSchema: Yup.ObjectSchema<RecordingGradingFormSchemaType> =
recording_egg_id: Yup.number() Yup.object({
.required('Recording Egg ID wajib diisi!') recording_egg_id: Yup.number()
.min(1, 'Recording Egg ID minimal 1!') .required('Recording Egg ID wajib diisi!')
.typeError('Recording Egg ID harus berupa angka!'), .min(1, 'Recording Egg ID minimal 1!')
eggs_grading: Yup.array() .typeError('Recording Egg ID harus berupa angka!'),
.of( eggs_grading: Yup.array()
Yup.object({ .of(
grade: Yup.string() Yup.object({
.required('Grade telur wajib diisi!') grade: Yup.string()
.typeError('Grade telur harus berupa string!'), .required('Grade telur wajib diisi!')
qty: Yup.number() .typeError('Grade telur harus berupa string!'),
.required('Jumlah telur wajib diisi!') qty: Yup.number()
.min(1, 'Jumlah telur minimal 1!') .required('Jumlah telur wajib diisi!')
.typeError('Jumlah telur harus berupa angka!'), .min(1, 'Jumlah telur minimal 1!')
}) .typeError('Jumlah telur harus berupa angka!'),
) })
.min(1, 'Minimal harus ada 1 data grading telur!') )
.required('Data grading telur wajib diisi!'), .min(1, 'Minimal harus ada 1 data grading telur!')
}); .required('Data grading telur wajib diisi!'),
});
export const UpdateRecordingGradingFormSchema = export const UpdateRecordingGradingFormSchema =
RecordingGradingFormSchema.shape({ RecordingGradingFormSchema.shape({
@@ -199,9 +258,9 @@ export const getRecordingGrowingFormInitialValues = (
}) })
) ?? [ ) ?? [
{ {
weight: 0, weight: '',
avg_weight: 0, avg_weight: '',
qty: 0, qty: '',
}, },
], ],
stocks: initialValues?.stocks?.map( stocks: initialValues?.stocks?.map(
@@ -212,7 +271,7 @@ export const getRecordingGrowingFormInitialValues = (
) ?? [ ) ?? [
{ {
product_warehouse_id: 0, product_warehouse_id: 0,
usage_qty: 0, usage_qty: '',
}, },
], ],
depletions: initialValues?.depletions?.map( depletions: initialValues?.depletions?.map(
@@ -225,7 +284,7 @@ export const getRecordingGrowingFormInitialValues = (
) ?? [ ) ?? [
{ {
product_warehouse_id: 0, product_warehouse_id: 0,
qty: 0, qty: '',
}, },
], ],
}); });
@@ -241,7 +300,7 @@ export const getRecordingLayingFormInitialValues = (
})) ?? [ })) ?? [
{ {
product_warehouse_id: 0, product_warehouse_id: 0,
qty: 0, qty: '',
}, },
], ],
}); });
@@ -256,7 +315,7 @@ export const getRecordingGradingFormInitialValues = (
})) ?? [ })) ?? [
{ {
grade: '', grade: '',
qty: 0, qty: '',
}, },
], ],
}); });
@@ -773,7 +773,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const currentWeight = formik.values.body_weights?.[idx]; const currentWeight = formik.values.body_weights?.[idx];
if (currentWeight) { if (currentWeight) {
const qty = currentWeight.qty; const qty = Number(currentWeight.qty) || 0;
if (qty > 0 && value > 0) { if (qty > 0 && value > 0) {
const avgWeight = parseFloat((value / qty).toFixed(2)); const avgWeight = parseFloat((value / qty).toFixed(2));
formik.setFieldValue(`body_weights.${idx}.avg_weight`, avgWeight); formik.setFieldValue(`body_weights.${idx}.avg_weight`, avgWeight);
@@ -794,7 +794,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const currentWeight = formik.values.body_weights?.[idx]; const currentWeight = formik.values.body_weights?.[idx];
if (currentWeight) { if (currentWeight) {
const qty = currentWeight.qty; const qty = Number(currentWeight.qty) || 0;
if (qty > 0 && value > 0) { if (qty > 0 && value > 0) {
const totalWeight = value * qty; const totalWeight = value * qty;
formik.setFieldValue(`body_weights.${idx}.weight`, totalWeight); formik.setFieldValue(`body_weights.${idx}.weight`, totalWeight);
@@ -815,7 +815,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const currentWeight = formik.values.body_weights?.[idx]; const currentWeight = formik.values.body_weights?.[idx];
if (currentWeight) { if (currentWeight) {
const weight = currentWeight.weight; const weight = Number(currentWeight.weight) || 0;
if (value > 0 && weight > 0) { if (value > 0 && weight > 0) {
const avgWeight = parseFloat((weight / value).toFixed(2)); const avgWeight = parseFloat((weight / value).toFixed(2));
formik.setFieldValue(`body_weights.${idx}.avg_weight`, avgWeight); formik.setFieldValue(`body_weights.${idx}.avg_weight`, avgWeight);
@@ -864,7 +864,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
...(formik.values.stocks || []), ...(formik.values.stocks || []),
{ {
product_warehouse_id: 0, product_warehouse_id: 0,
usage_qty: 0, usage_qty: '',
}, },
]; ];
formik.setFieldValue('stocks', newStocks); formik.setFieldValue('stocks', newStocks);
@@ -897,7 +897,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
...(formik.values.depletions || []), ...(formik.values.depletions || []),
{ {
product_warehouse_id: 0, product_warehouse_id: 0,
qty: 0, qty: '',
}, },
]; ];
formik.setFieldValue('depletions', newDepletions); formik.setFieldValue('depletions', newDepletions);
@@ -932,7 +932,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
...((formik.values as RecordingLayingFormValues).eggs || []), ...((formik.values as RecordingLayingFormValues).eggs || []),
{ {
product_warehouse_id: 0, product_warehouse_id: 0,
qty: 0, qty: '',
}, },
]; ];
formik.setFieldValue('eggs', newEggs); formik.setFieldValue('eggs', newEggs);
@@ -966,7 +966,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
if (isLayingCategory && (type as 'add' | 'edit' | 'detail') !== 'detail') { if (isLayingCategory && (type as 'add' | 'edit' | 'detail') !== 'detail') {
const layingValues = formik.values as RecordingLayingFormValues; const layingValues = formik.values as RecordingLayingFormValues;
if (!layingValues.eggs || layingValues.eggs.length === 0) { if (!layingValues.eggs || layingValues.eggs.length === 0) {
formik.setFieldValue('eggs', [{ product_warehouse_id: 0, qty: 0 }]); formik.setFieldValue('eggs', [{ product_warehouse_id: 0, qty: '' }]);
} }
} }
}, [isLayingCategory, type, formik]); }, [isLayingCategory, type, formik]);
@@ -998,11 +998,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
if (idx === editingAverageIndex || manuallyEditedRows.has(idx)) { if (idx === editingAverageIndex || manuallyEditedRows.has(idx)) {
return weight; return weight;
} }
const qty = Number(weight.qty) || 0;
const weightValue = Number(weight.weight) || 0;
return { return {
...weight, ...weight,
avg_weight: avg_weight:
weight.qty > 0 && weight.weight > 0 qty > 0 && weightValue > 0
? parseFloat((weight.weight / weight.qty).toFixed(2)) ? parseFloat((weightValue / qty).toFixed(2))
: 0, : 0,
}; };
} }
@@ -1303,7 +1305,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<NumberInput <NumberInput
required required
name={`body_weights.${idx}.weight`} name={`body_weights.${idx}.weight`}
value={bw.weight} value={bw.weight ?? ''}
onChange={handleWeightChangeWrapper(idx)} onChange={handleWeightChangeWrapper(idx)}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
decimalScale={2} decimalScale={2}
@@ -1311,6 +1313,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
thousandSeparator=',' thousandSeparator=','
decimalSeparator='.' decimalSeparator='.'
inputSuffix='gram' inputSuffix='gram'
placeholder='Masukkan berat total...'
isError={ isError={
isRepeaterInputError('body_weights', 'weight', idx) isRepeaterInputError('body_weights', 'weight', idx)
.isError .isError
@@ -1329,13 +1332,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<NumberInput <NumberInput
required required
name={`body_weights.${idx}.qty`} name={`body_weights.${idx}.qty`}
value={bw.qty} value={bw.qty ?? ''}
onChange={handleQtyChangeWrapper(idx)} onChange={handleQtyChangeWrapper(idx)}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
decimalScale={0} decimalScale={0}
allowNegative={false} allowNegative={false}
thousandSeparator=',' thousandSeparator=','
decimalSeparator='.' decimalSeparator='.'
placeholder='Masukkan jumlah ayam...'
isError={ isError={
isRepeaterInputError('body_weights', 'qty', idx) isRepeaterInputError('body_weights', 'qty', idx)
.isError .isError
@@ -1354,7 +1358,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<NumberInput <NumberInput
required required
name={`body_weights.${idx}.avg_weight`} name={`body_weights.${idx}.avg_weight`}
value={bw.avg_weight} value={bw.avg_weight ?? ''}
onChange={handleAvgWeightChangeWrapper(idx)} onChange={handleAvgWeightChangeWrapper(idx)}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
decimalScale={2} decimalScale={2}
@@ -1362,6 +1366,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
thousandSeparator=',' thousandSeparator=','
decimalSeparator='.' decimalSeparator='.'
inputSuffix='gram' inputSuffix='gram'
placeholder='Masukkan berat rata-rata...'
isError={ isError={
isRepeaterInputError( isRepeaterInputError(
'body_weights', 'body_weights',
@@ -1574,7 +1579,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<NumberInput <NumberInput
required required
name={`stocks.${idx}.usage_qty`} name={`stocks.${idx}.usage_qty`}
value={stock.usage_qty} value={stock.usage_qty ?? ''}
onChange={handleStockUsageQtyChangeWrapper(idx)} onChange={handleStockUsageQtyChangeWrapper(idx)}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
decimalScale={0} decimalScale={0}
@@ -1595,7 +1600,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
className={{ className={{
wrapper: 'w-full min-w-24', wrapper: 'w-full min-w-24',
}} }}
placeholder='Jumlah Pakai' placeholder='Masukkan jumlah pakai'
/> />
{(type as 'add' | 'edit' | 'detail') !== 'detail' && {(type as 'add' | 'edit' | 'detail') !== 'detail' &&
getStockUsageAdornment(idx)} getStockUsageAdornment(idx)}
@@ -1787,7 +1792,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<NumberInput <NumberInput
required required
name={`depletions.${idx}.qty`} name={`depletions.${idx}.qty`}
value={depletion.qty} value={depletion.qty ?? ''}
onChange={handleDepletionQtyChangeWrapper(idx)} onChange={handleDepletionQtyChangeWrapper(idx)}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
decimalScale={0} decimalScale={0}
@@ -1806,7 +1811,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
className={{ className={{
wrapper: 'w-full min-w-24', wrapper: 'w-full min-w-24',
}} }}
placeholder='Jumlah' placeholder='Masukkan jumlah deplesi'
/> />
</td> </td>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
@@ -1998,7 +2003,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<NumberInput <NumberInput
required required
name={`eggs.${idx}.qty`} name={`eggs.${idx}.qty`}
value={egg.qty} value={egg.qty ?? ''}
onChange={handleEggQtyChangeWrapper(idx)} onChange={handleEggQtyChangeWrapper(idx)}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
decimalScale={0} decimalScale={0}
@@ -2017,7 +2022,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
className={{ className={{
wrapper: 'w-full min-w-24', wrapper: 'w-full min-w-24',
}} }}
placeholder='Jumlah' placeholder='Masukkan jumlah telur'
/> />
</div> </div>
</td> </td>
@@ -2191,10 +2196,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
toast.success( toast.success(
'Recording berhasil disimpan! Mengalihkan ke form Grading...' 'Recording berhasil disimpan! Mengalihkan ke form Grading...'
); );
// Redirect ke grading form setelah submit berhasil
setTimeout(() => { setTimeout(() => {
router.push( router.push(
'/production/recording/grading/add?recording_id=new' '/production/recording/grading/add?recording_id=1'
); );
}, 1000); }, 1000);
} }