refactor(US-170,174): update recording types and validation schema for daily recording form

This commit is contained in:
rstubryan
2025-10-30 18:10:37 +07:00
parent ae0cca778e
commit 4cb045de6c
3 changed files with 251 additions and 419 deletions
@@ -1,16 +1,15 @@
import * as Yup from 'yup';
import { RECORDING_FLAG_OPTIONS } from '@/config/constant';
import {
Recording,
CreateRecordingPayload,
CreateGrowingRecordingPayload,
} from '@/types/api/production/recording';
export const RecordingFormSchema = Yup.object({
export const RecordingGrowingFormSchema = Yup.object({
project_flock_kandang: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
project_flock_kandang_id: Yup.number()
project_flock_kandangs_id: Yup.number()
.default(0)
.typeError('Project Flock Kandang wajib diisi!')
.test(
@@ -22,9 +21,13 @@ export const RecordingFormSchema = Yup.object({
.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';
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;
@@ -35,20 +38,15 @@ export const RecordingFormSchema = Yup.object({
body_weights: Yup.array()
.of(
Yup.object({
weight: Yup.number()
.required('Berat ayam wajib diisi!')
.min(1, 'Berat ayam minimal 1 gram!')
.typeError('Berat ayam harus berupa angka!'),
avg_weight: Yup.number()
.required('Berat ayam rata-rata wajib diisi!')
.min(1, 'Berat ayam rata-rata minimal 1 gram!')
.typeError('Berat ayam rata-rata harus berupa angka!'),
qty: Yup.number()
.required('Jumlah ayam wajib diisi!')
.min(1, 'Jumlah ayam minimal 1 ekor!')
.typeError('Jumlah ayam harus berupa angka!')
.default(1),
average_weight: Yup.number()
.optional()
.min(0, 'Rata-rata berat tidak boleh negatif!')
.typeError('Rata-rata berat harus berupa angka!')
.default(0),
})
)
.min(1, 'Minimal harus ada 1 data bobot badan!')
@@ -60,163 +58,97 @@ export const RecordingFormSchema = Yup.object({
.required('Produk wajib diisi!')
.min(1, 'Produk wajib diisi!')
.typeError('Produk harus berupa angka!'),
usage_amount: Yup.number()
usage_qty: Yup.number()
.required('Jumlah penggunaan wajib diisi!')
.min(0, 'Jumlah penggunaan tidak boleh negatif!')
.typeError('Jumlah penggunaan harus berupa angka!'),
notes: Yup.string().optional(),
})
)
.min(1, 'Minimal harus ada 1 data stok!')
.required('Data stok wajib diisi!'),
depletions: Yup.array()
.of(
Yup.object({
total: Yup.number()
.required('Jumlah depletions wajib diisi!')
.min(1, 'Jumlah depletions minimal 1!')
.typeError('Jumlah depletions harus berupa angka!'),
notes: Yup.string()
.required('Kondisi depletions wajib diisi!')
.oneOf(
RECORDING_FLAG_OPTIONS.map((option) => option.value),
'Kondisi depletions tidak valid!'
)
.typeError('Kondisi depletions harus berupa teks!')
.min(1, 'Kondisi depletions wajib diisi!'),
})
)
.min(1, 'Minimal harus ada 1 data depletions!')
.required('Data depletions wajib diisi!'),
});
export const UpdateRecordingFormSchema = Yup.object({
project_flock_kandang: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
project_flock_kandang_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!'),
body_weights: Yup.array()
.of(
Yup.object({
weight: Yup.number()
.required('Berat ayam wajib diisi!')
.min(1, 'Berat ayam minimal 1 gram!')
.typeError('Berat ayam harus berupa angka!'),
qty: Yup.number()
.required('Jumlah ayam wajib diisi!')
.min(1, 'Jumlah ayam minimal 1 ekor!')
.typeError('Jumlah ayam harus berupa angka!')
.default(1),
average_weight: Yup.number()
.optional()
.min(0, 'Rata-rata berat tidak boleh negatif!')
.typeError('Rata-rata berat harus berupa angka!')
.default(0),
})
)
.min(1, 'Minimal harus ada 1 data bobot badan!')
.required('Data bobot badan wajib diisi!'),
stocks: Yup.array()
.of(
Yup.object({
product_warehouse_id: Yup.number()
.required('Produk wajib diisi!')
.min(1, 'Produk wajib diisi!')
.typeError('Produk harus berupa angka!'),
usage_amount: Yup.number()
.required('Jumlah penggunaan wajib diisi!')
.min(0, 'Jumlah penggunaan tidak boleh negatif!')
.typeError('Jumlah penggunaan harus berupa angka!'),
notes: Yup.string().optional(),
})
)
.min(1, 'Minimal harus ada 1 data stok!')
.required('Data stok wajib diisi!'),
depletions: Yup.array()
.of(
Yup.object({
total: 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!'),
notes: Yup.string()
.required('Kondisi depletions wajib diisi!')
.oneOf(
RECORDING_FLAG_OPTIONS.map((option) => option.value),
'Kondisi depletions tidak valid!'
)
.typeError('Kondisi depletions harus berupa teks!')
.min(1, 'Kondisi depletions wajib diisi!'),
})
)
.min(1, 'Minimal harus ada 1 data depletions!')
.required('Data depletions wajib diisi!'),
});
export type RecordingFormValues = Yup.InferType<typeof RecordingFormSchema>;
export const UpdateRecordingGrowingFormSchema =
RecordingGrowingFormSchema.shape({
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!'),
});
export type RecordingGrowingFormValues = Yup.InferType<
typeof RecordingGrowingFormSchema
>;
type RecordingFormData = Partial<Recording> & {
body_weights?: CreateRecordingPayload['body_weights'];
stocks?: CreateRecordingPayload['stocks'];
depletions?: CreateRecordingPayload['depletions'];
body_weights?: CreateGrowingRecordingPayload['body_weights'];
stocks?: CreateGrowingRecordingPayload['stocks'];
depletions?: CreateGrowingRecordingPayload['depletions'];
};
export const getRecordingFormInitialValues = (
export const getRecordingGrowingFormInitialValues = (
initialValues?: RecordingFormData
): RecordingFormValues => ({
project_flock_kandang: initialValues?.project_flock_kandang_id
): RecordingGrowingFormValues => ({
project_flock_kandang: initialValues?.project_flock_kandangs_id
? {
value: initialValues.project_flock_kandang_id,
label: `Project Flock #${initialValues.project_flock_kandang_id}`,
value: initialValues.project_flock_kandangs_id,
label: `Project Flock #${initialValues.project_flock_kandangs_id}`,
}
: null,
project_flock_kandang_id: initialValues?.project_flock_kandang_id ?? 0,
project_flock_kandangs_id: initialValues?.project_flock_kandangs_id ?? 0,
body_weights: initialValues?.body_weights?.map(
(bw: NonNullable<CreateRecordingPayload['body_weights']>[0]) => ({
weight: bw.weight,
(bw: NonNullable<CreateGrowingRecordingPayload['body_weights']>[0]) => ({
avg_weight: bw.avg_weight,
qty: bw.qty,
average_weight: bw.qty > 0 ? Math.round(bw.weight / bw.qty) : 0,
})
) ?? [
{
weight: 0,
avg_weight: 0,
qty: 0,
average_weight: 0,
},
],
stocks: initialValues?.stocks?.map(
(stock: NonNullable<CreateRecordingPayload['stocks']>[0]) => ({
(stock: NonNullable<CreateGrowingRecordingPayload['stocks']>[0]) => ({
product_warehouse_id: stock.product_warehouse_id,
usage_amount: stock.usage_amount,
notes: stock.notes,
usage_qty: stock.usage_qty,
})
) ?? [
{
product_warehouse_id: 0,
usage_amount: 0,
notes: '',
usage_qty: 0,
},
],
depletions: initialValues?.depletions?.map(
(depletion: NonNullable<CreateRecordingPayload['depletions']>[0]) => ({
(
depletion: NonNullable<CreateGrowingRecordingPayload['depletions']>[0]
) => ({
product_warehouse_id: depletion.product_warehouse_id,
total: depletion.total,
notes: depletion.notes,
qty: depletion.qty,
})
) ?? [
{
product_warehouse_id: 0,
total: 0,
notes: '',
qty: 0,
},
],
});
@@ -1,6 +1,6 @@
'use client';
import { useMemo, useState, useEffect, useCallback } from 'react';
import { useMemo, useState, useCallback } from 'react';
import { useFormik } from 'formik';
import useSWR from 'swr';
import { Icon } from '@iconify/react';
@@ -14,22 +14,21 @@ import { FormHeader } from '@/components/helper/form/FormHeader';
import { FormActions } from '@/components/helper/form/FormActions';
import { RecordingApi } from '@/services/api/production';
import {
CreateRecordingPayload,
CreateGrowingRecordingPayload,
Recording,
} from '@/types/api/production/recording';
import { type BaseApiResponse } from '@/types/api/api-general';
import {
RecordingFormSchema,
RecordingFormValues,
getRecordingFormInitialValues,
UpdateRecordingFormSchema,
RecordingGrowingFormSchema,
RecordingGrowingFormValues,
getRecordingGrowingFormInitialValues,
UpdateRecordingGrowingFormSchema,
} from './RecordingForm.schema';
import { useRecordingFormHandlers } from './useRecordingFormHandlers';
import { ProjectFlockApi } from '@/services/api/production';
import { LocationApi } from '@/services/api/master-data';
import { ProductWarehouseApi } from '@/services/api/inventory';
import { isResponseSuccess } from '@/lib/api-helper';
import { RECORDING_FLAG_OPTIONS } from '@/config/constant';
import { PeriodFlock } from '@/types/api/production/project-flock';
import { useModal } from '@/components/Modal';
import toast from 'react-hot-toast';
@@ -47,13 +46,6 @@ 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
@@ -132,7 +124,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const recordedIds = new Set<number>();
todayRecordings.forEach((recording) => {
const recordingDate = recording.record_date?.split('T')[0];
const recordingDate = recording.record_datetime?.split('T')[0];
const isRecordedToday = recordingDate === today;
@@ -143,7 +135,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
isResponseSuccess(projectFlocks)
) {
const flockIndex = projectFlocks.data.findIndex(
(pf) => pf.id === recording.project_flock_kandang_id
(pf) => pf.id === recording.project_flock_kandangs_id
);
if (
flockIndex !== undefined &&
@@ -165,7 +157,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
}
if (isRecordedToday && (isCorrectPeriod || !flockPeriodsData)) {
recordedIds.add(recording.project_flock_kandang_id);
recordedIds.add(recording.project_flock_kandangs_id);
}
});
@@ -283,45 +275,36 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
confirmationModalDeleteClickHandler,
} = useRecordingFormHandlers(initialValues?.id);
const formikInitialValues = useMemo<RecordingFormValues>(
() => getRecordingFormInitialValues(initialValues),
const formikInitialValues = useMemo<RecordingGrowingFormValues>(
() => getRecordingGrowingFormInitialValues(initialValues),
[initialValues]
);
const formik = useFormik<RecordingFormValues>({
const formik = useFormik<RecordingGrowingFormValues>({
initialValues: formikInitialValues,
validationSchema:
type === 'edit' ? UpdateRecordingFormSchema : RecordingFormSchema,
type === 'edit'
? UpdateRecordingGrowingFormSchema
: RecordingGrowingFormSchema,
validateOnChange: true,
validateOnBlur: true,
onSubmit: async (values) => {
const payload: CreateRecordingPayload = {
project_flock_kandang_id: values.project_flock_kandang_id,
const payload: CreateGrowingRecordingPayload = {
project_flock_kandangs_id: values.project_flock_kandangs_id,
body_weights: (values.body_weights ?? []).map((bw) => ({
weight:
typeof bw.weight === 'number'
? bw.weight
: parseFloat(String(bw.weight)) || 0,
qty:
typeof bw.qty === 'number'
? bw.qty
: parseFloat(String(bw.qty)) || 0,
avg_weight:
typeof bw.avg_weight === 'number'
? bw.avg_weight
: parseFloat(String(bw.avg_weight)) || 0,
qty: bw.qty || 0,
})),
stocks: (values.stocks ?? []).map((stock) => ({
product_warehouse_id: stock.product_warehouse_id,
usage_amount:
typeof stock.usage_amount === 'number'
? stock.usage_amount
: parseFloat(String(stock.usage_amount)) || 0,
notes: stock.notes || '',
usage_qty: stock.usage_qty || 0,
})),
depletions: (values.depletions ?? []).map((depletion) => ({
product_warehouse_id: 1,
total:
typeof depletion.total === 'number'
? depletion.total
: parseFloat(String(depletion.total)) || 0,
notes: depletion.notes,
product_warehouse_id: depletion.product_warehouse_id,
qty: depletion.qty || 0,
})),
};
@@ -375,7 +358,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const stock = formik.values.stocks?.[stockIdx];
if (!stock || !stock.product_warehouse_id) return null;
const availableStock = getAvailableStock(stock.product_warehouse_id);
const requestedUsage = Number(stock.usage_amount) || 0;
const requestedUsage = Number(stock.usage_qty) || 0;
if (requestedUsage > availableStock) {
return `Jumlah pakai melebihi stok tersedia! Maksimal: ${availableStock.toLocaleString('en-US')}`;
}
@@ -390,7 +373,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const stock = formik.values.stocks?.[stockIdx];
if (!stock || !stock.product_warehouse_id) return null;
const availableStock = getAvailableStock(stock.product_warehouse_id);
const requestedUsage = Number(stock.usage_amount) || 0;
const requestedUsage = Number(stock.usage_qty) || 0;
const remainingStock = availableStock - requestedUsage;
if (requestedUsage > 0) {
return (
@@ -432,7 +415,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
color={color}
size='sm'
className={{
badge: 'whitespace-nowrap font-semibold text-xs px-2 py-0.5',
badge: 'whitespace-nowrap font-semibold text-xs px-2',
}}
>
Periode {projectFlock.period}
@@ -461,7 +444,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
color='info'
size='sm'
className={{
badge: 'whitespace-nowrap font-semibold text-xs px-2 py-0.5',
badge: 'whitespace-nowrap font-semibold text-xs px-2',
}}
>
PAKAN
@@ -476,7 +459,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
color='secondary'
size='sm'
className={{
badge: 'whitespace-nowrap font-semibold text-xs px-2 py-0.5',
badge: 'whitespace-nowrap font-semibold text-xs px-2',
}}
>
OVK
@@ -503,11 +486,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
>(
arrayName: T,
column: T extends 'body_weights'
? keyof RecordingFormValues['body_weights'][0]
? keyof RecordingGrowingFormValues['body_weights'][0]
: T extends 'stocks'
? keyof RecordingFormValues['stocks'][0]
? keyof RecordingGrowingFormValues['stocks'][0]
: T extends 'depletions'
? keyof RecordingFormValues['depletions'][0]
? keyof RecordingGrowingFormValues['depletions'][0]
: never,
idx: number
) => {
@@ -540,7 +523,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedLocation(val as OptionType);
formik.setFieldValue('project_flock_kandang', null);
formik.setFieldValue('project_flock_kandang_id', 0);
formik.setFieldValue('project_flock_kandangs_id', 0);
};
const projectFlockKandangChangeHandler = (
@@ -557,9 +540,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
formik.setFieldTouched('project_flock_kandang', true);
formik.setFieldValue('project_flock_kandang', val);
formik.setFieldTouched('project_flock_kandang_id', true);
formik.setFieldTouched('project_flock_kandangs_id', true);
formik.setFieldValue(
'project_flock_kandang_id',
'project_flock_kandangs_id',
(val as OptionType)?.value || 0
);
};
@@ -629,81 +612,25 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const newBodyWeights = [
...(formik.values.body_weights || []),
{
weight: 0,
avg_weight: 0,
qty: 1,
average_weight: 0,
},
];
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 averageWeight = parseFloat((value / qty).toFixed(2));
formik.setFieldValue(
`body_weights.${idx}.average_weight`,
averageWeight
);
} else {
formik.setFieldValue(`body_weights.${idx}.average_weight`, 0);
}
}
const handleAvgWeightChange = (idx: number, value: number) => {
formik.setFieldValue(`body_weights.${idx}.avg_weight`, value);
};
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 averageWeight = parseFloat((weight / value).toFixed(2));
formik.setFieldValue(
`body_weights.${idx}.average_weight`,
averageWeight
);
} else {
formik.setFieldValue(`body_weights.${idx}.average_weight`, 0);
}
}
};
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);
}
}
};
const handleWeightChangeWrapper =
const handleAvgWeightChangeWrapper =
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseFloat(e.target.value) || 0;
handleWeightChange(idx, value);
handleAvgWeightChange(idx, value);
};
const handleQtyChangeWrapper =
@@ -712,19 +639,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
handleQtyChange(idx, value);
};
const handleAverageWeightChangeWrapper =
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
setEditingAverageIndex(idx);
setManuallyEditedRows((prev) => new Set(prev).add(idx));
const value = parseFloat(e.target.value) || 0;
handleAverageWeightChange(idx, value);
};
const handleAverageWeightBlur = (idx: number) => {
setEditingAverageIndex(null);
};
const removeBodyWeight = (idx: number) => {
const updatedBodyWeights = formik.values.body_weights?.filter(
(_, i) => i !== idx
@@ -746,17 +660,16 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
...(formik.values.stocks || []),
{
product_warehouse_id: 0,
usage_amount: 0,
notes: '',
usage_qty: 0,
},
];
formik.setFieldValue('stocks', newStocks);
};
const handleStockUsageAmountChangeWrapper = useCallback(
const handleStockUsageQtyChangeWrapper = useCallback(
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseFloat(e.target.value) || 0;
formik.setFieldValue(`stocks.${idx}.usage_amount`, value);
formik.setFieldValue(`stocks.${idx}.usage_qty`, value);
},
[formik]
);
@@ -779,17 +692,17 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const newDepletions = [
...(formik.values.depletions || []),
{
total: 0,
notes: '',
product_warehouse_id: 0,
qty: 0,
},
];
formik.setFieldValue('depletions', newDepletions);
};
const handleDepletionTotalChangeWrapper = useCallback(
const handleDepletionQtyChangeWrapper = useCallback(
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseFloat(e.target.value) || 0;
formik.setFieldValue(`depletions.${idx}.total`, value);
formik.setFieldValue(`depletions.${idx}.qty`, value);
},
[formik]
);
@@ -809,41 +722,6 @@ 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,
average_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.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),
editingAverageIndex,
manuallyEditedRows,
]);
return (
<>
<section className='w-full'>
@@ -889,6 +767,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<div>
<SelectInput
required
key={`project-flock-${formik.values.project_flock_kandangs_id}`}
label='Project Flock'
value={formik.values.project_flock_kandang ?? undefined}
onChange={projectFlockKandangChangeHandler}
@@ -896,11 +775,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
onInputChange={setProjectFlockSearchValue}
isLoading={isLoadingProjectFlocks}
isError={
formik.touched.project_flock_kandang_id &&
Boolean(formik.errors.project_flock_kandang_id)
formik.touched.project_flock_kandangs_id &&
Boolean(formik.errors.project_flock_kandangs_id)
}
errorMessage={
formik.errors.project_flock_kandang_id as string
formik.errors.project_flock_kandangs_id as string
}
isDisabled={type === 'detail' || !selectedLocation}
placeholder={
@@ -911,9 +790,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
isClearable
isSearchable
startAdornment={
formik.values.project_flock_kandang_id
formik.values.project_flock_kandangs_id
? getProjectFlockBadgeAdornment(
formik.values.project_flock_kandang_id
formik.values.project_flock_kandangs_id
)
: undefined
}
@@ -964,7 +843,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</th>
)}
<th>
Berat Ayam (gram)
Rata-rata Berat Ayam (gram)
<span
className='tooltip tooltip-error tooltip-bottom z-[9999]'
data-tip='required'
@@ -981,19 +860,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<span className='text-error'>*</span>
</span>
</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>
{type !== 'detail' && <th>Action</th>}
</tr>
</thead>
@@ -1029,9 +895,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<td>
<NumberInput
required
name={`body_weights.${idx}.weight`}
value={bw.weight}
onChange={handleWeightChangeWrapper(idx)}
name={`body_weights.${idx}.avg_weight`}
value={bw.avg_weight}
onChange={handleAvgWeightChangeWrapper(idx)}
onBlur={formik.handleBlur}
decimalScale={2}
allowNegative={false}
@@ -1039,12 +905,18 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
decimalSeparator='.'
inputSuffix='gram'
isError={
isRepeaterInputError('body_weights', 'weight', idx)
.isError
isRepeaterInputError(
'body_weights',
'avg_weight',
idx
).isError
}
errorMessage={
isRepeaterInputError('body_weights', 'weight', idx)
.errorMessage
isRepeaterInputError(
'body_weights',
'avg_weight',
idx
).errorMessage
}
readOnly={type === 'detail'}
className={{
@@ -1077,43 +949,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
}}
/>
</td>
<td>
<NumberInput
name={`body_weights.${idx}.average_weight`}
value={bw.average_weight || 0}
onChange={handleAverageWeightChangeWrapper(idx)}
onBlur={(e) => {
handleAverageWeightBlur(idx);
formik.handleBlur(e);
}}
decimalScale={2}
allowNegative={false}
thousandSeparator=','
decimalSeparator='.'
inputSuffix='gram'
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>
{type !== 'detail' && (
<td>
<div className='flex justify-center'>
<div className='flex items-center'>
<Button
type='button'
color='error'
@@ -1249,6 +1087,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<td>
<SelectInput
required
key={`stock-product-${idx}-${stock.product_warehouse_id}`}
value={
unifiedStockProducts.find(
(product) =>
@@ -1261,21 +1100,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
`stocks.${idx}.product_warehouse_id`,
option?.value || 0
);
if (
option?.value &&
isResponseSuccess(stockProducts)
) {
const selectedProduct = stockProducts.data.find(
(product) => product.id === option.value
);
if (selectedProduct) {
formik.setFieldValue(
`stocks.${idx}.notes`,
selectedProduct.product.name
);
}
}
}}
options={unifiedStockProducts}
placeholder='Pilih Produk'
@@ -1313,27 +1137,21 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<div className='flex flex-col gap-1'>
<NumberInput
required
name={`stocks.${idx}.usage_amount`}
value={stock.usage_amount}
onChange={handleStockUsageAmountChangeWrapper(idx)}
name={`stocks.${idx}.usage_qty`}
value={stock.usage_qty}
onChange={handleStockUsageQtyChangeWrapper(idx)}
onBlur={formik.handleBlur}
decimalScale={0}
allowNegative={false}
thousandSeparator=','
decimalSeparator='.'
isError={
isRepeaterInputError(
'stocks',
'usage_amount',
idx
).isError || Boolean(getStockUsageError(idx))
isRepeaterInputError('stocks', 'usage_qty', idx)
.isError || Boolean(getStockUsageError(idx))
}
errorMessage={
isRepeaterInputError(
'stocks',
'usage_amount',
idx
).errorMessage ||
isRepeaterInputError('stocks', 'usage_qty', idx)
.errorMessage ||
getStockUsageError(idx) ||
undefined
}
@@ -1348,7 +1166,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</td>
{type !== 'detail' && (
<td>
<div className='flex justify-center'>
<div className='flex items-center'>
<Button
type='button'
color='error'
@@ -1446,7 +1264,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</span>
</th>
<th>
Total
Jumlah
<span
className='tooltip tooltip-error tooltip-bottom z-[9999]'
data-tip='required'
@@ -1489,64 +1307,72 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<td>
<SelectInput
value={
RECORDING_FLAG_OPTIONS.find(
(option) => option.value === depletion.notes
unifiedStockProducts.find(
(product) =>
product.value === depletion.product_warehouse_id
) || null
}
onChange={(selectedOption) => {
const option = selectedOption as OptionType | null;
formik.setFieldValue(
`depletions.${idx}.notes`,
option?.value || ''
`depletions.${idx}.product_warehouse_id`,
option?.value || 0
);
}}
options={RECORDING_FLAG_OPTIONS}
options={unifiedStockProducts}
placeholder='Pilih Kondisi'
isLoading={isLoadingStockProducts}
isError={
isRepeaterInputError('depletions', 'notes', idx)
.isError
isRepeaterInputError(
'depletions',
'product_warehouse_id',
idx
).isError
}
errorMessage={
isRepeaterInputError('depletions', 'notes', idx)
.errorMessage
isRepeaterInputError(
'depletions',
'product_warehouse_id',
idx
).errorMessage
}
isDisabled={type === 'detail'}
className={{
wrapper: 'w-full min-w-32',
wrapper: 'w-full min-w-48',
}}
isSearchable={false}
isSearchable
isClearable={type !== 'detail'}
/>
</td>
<td>
<NumberInput
required
name={`depletions.${idx}.total`}
value={depletion.total}
onChange={handleDepletionTotalChangeWrapper(idx)}
name={`depletions.${idx}.qty`}
value={depletion.qty}
onChange={handleDepletionQtyChangeWrapper(idx)}
onBlur={formik.handleBlur}
decimalScale={0}
allowNegative={false}
thousandSeparator=','
decimalSeparator='.'
isError={
isRepeaterInputError('depletions', 'total', idx)
isRepeaterInputError('depletions', 'qty', idx)
.isError
}
errorMessage={
isRepeaterInputError('depletions', 'total', idx)
isRepeaterInputError('depletions', 'qty', idx)
.errorMessage
}
readOnly={type === 'detail'}
className={{
wrapper: 'w-full min-w-24',
}}
placeholder='Total'
placeholder='Jumlah'
/>
</td>
{type !== 'detail' && (
<td>
<div className='flex justify-center'>
<div className='flex items-center'>
<Button
type='button'
color='error'
@@ -1594,7 +1420,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</Card>
{/* Action buttons */}
<FormActions<RecordingFormValues>
<FormActions<RecordingGrowingFormValues>
type={type}
formik={formik}
editUrl={
+90 -16
View File
@@ -1,46 +1,120 @@
import { BaseMetadata, User } from '@/types/api/api-general';
export type ProductionMetrics = {
total_depletion: number;
total_depletion_qty: number;
cum_depletion_rate: number;
daily_gain: number;
avg_daily_gain: number;
cum_intake: number;
fcr_value: number;
total_chick: number;
total_chick_qty: number;
daily_depletion_rate: number;
cum_depletion: number;
};
export type BaseRecording = {
id: number;
project_flock_kandang_id: number;
project_flock_kandangs_id: number;
record_datetime: string;
record_date: string;
status: number;
ontime: boolean;
day: number;
created_user: User;
created_by: User;
} & ProductionMetrics;
export type Recording = BaseMetadata & BaseRecording;
export type RecordingBW = {
id: number;
recording_id: number;
avg_weight: number;
qty: number;
total_weight: number;
};
export type CreateRecordingPayload = {
project_flock_kandang_id: number;
export type RecordingDepletion = {
id: number;
recording_id: number;
product_warehouse_id: number;
qty: number;
};
export type RecordingStock = {
id: number;
recording_id: number;
product_warehouse_id: number;
usage_qty: number;
pending_qty: number;
};
export type RecordingEgg = {
id: number;
recording_id: number;
product_warehouse_id: number;
qty: number;
created_by: User;
};
export type GradingEgg = {
id: number;
recording_egg_id: number;
qty: number;
grade: string;
created_by: User;
};
export type Recording = BaseMetadata &
BaseRecording & {
recording_bws?: RecordingBW[];
recording_depletions?: RecordingDepletion[];
recording_stocks?: RecordingStock[];
recording_eggs?: RecordingEgg[];
grading_eggs?: GradingEgg[];
};
export type CreateGrowingRecordingPayload = {
project_flock_kandangs_id: number;
body_weights: {
weight: number;
avg_weight: number;
qty: number;
}[];
stocks?: {
product_warehouse_id: number;
usage_amount: number;
notes: string;
usage_qty: number;
pending_qty?: number;
}[];
depletions?: {
product_warehouse_id?: number;
total: number;
notes: string;
product_warehouse_id: number;
qty: number;
}[];
};
export type CreateLayingRecordingPayload = {
project_flock_kandangs_id: number;
body_weights: {
avg_weight: number;
qty: number;
}[];
stocks?: {
product_warehouse_id: number;
usage_qty: number;
pending_qty?: number;
}[];
depletions?: {
product_warehouse_id: number;
qty: number;
}[];
eggs: {
product_warehouse_id: number;
qty: number;
grading?: {
grade: string;
qty: number;
}[];
}[];
};
export type CreateRecordingPayload =
| CreateGrowingRecordingPayload
| CreateLayingRecordingPayload;
export type UpdateGrowingRecordingPayload = CreateGrowingRecordingPayload;
export type UpdateLayingRecordingPayload = CreateLayingRecordingPayload;
export type UpdateRecordingPayload = CreateRecordingPayload;