refactor(FE-114,136,137): update feed and vaccination fields to use IDs instead of names and add stock validation

This commit is contained in:
rstubryan
2025-10-17 13:59:28 +07:00
parent fa60f884c1
commit caf68d438f
3 changed files with 174 additions and 80 deletions
@@ -48,7 +48,7 @@ export const RecordingFormSchema = Yup.object({
feed_data: Yup.array()
.of(
Yup.object({
feed_name: Yup.string().required('Nama pakan wajib diisi!'),
feed_id: Yup.string().required('Nama pakan wajib diisi!'),
feed_qty: Yup.number()
.required('Qty pakan wajib diisi!')
.min(1, 'Qty minimal 1!')
@@ -56,7 +56,15 @@ export const RecordingFormSchema = Yup.object({
feed_stock: Yup.number()
.required('Stock pakan wajib diisi!')
.min(1, 'Stock minimal 1!')
.typeError('Stock pakan wajib diisi!'),
.typeError('Stock pakan wajib diisi!')
.test(
'is-not-exceed-qty',
'Feed stock tidak boleh melebihi feed qty yang tersedia!',
function (value) {
const { feed_qty } = this.parent;
return value === undefined || value <= feed_qty;
}
),
})
)
.min(1, 'Minimal harus ada 1 data pakan!')
@@ -80,7 +88,7 @@ export const RecordingFormSchema = Yup.object({
vaccination: Yup.array()
.of(
Yup.object({
vaccine_name: Yup.string().required('Nama vaksin wajib diisi!'),
vaccine_id: Yup.string().required('Nama vaksin wajib diisi!'),
total_stock: Yup.number()
.required('Total stock wajib diisi!')
.min(1, 'Total stock minimal 1!')
@@ -88,7 +96,15 @@ export const RecordingFormSchema = Yup.object({
used_stock: Yup.number()
.required('Jumlah stock wajib diisi!')
.min(1, 'Jumlah stock minimal 1!')
.typeError('Jumlah stock wajib diisi!'),
.typeError('Jumlah stock wajib diisi!')
.test(
'is-not-exceed-total',
'Used stock tidak boleh melebihi total stock yang tersedia!',
function (value) {
const { total_stock } = this.parent;
return value === undefined || value <= total_stock;
}
),
})
)
.min(1, 'Minimal harus ada 1 data vaksinasi!')
@@ -142,13 +158,19 @@ export const getRecordingFormInitialValues = (
recording_date: initialValues?.recording_date
? new Date(initialValues.recording_date)
: new Date(),
feed_data: initialValues?.feed_data ?? [
{
feed_name: '',
feed_qty: 0,
feed_stock: 0,
},
],
feed_data: initialValues?.feed_data
? initialValues.feed_data.map((feed) => ({
feed_id: feed.feed_name,
feed_qty: feed.feed_qty,
feed_stock: feed.feed_stock,
}))
: [
{
feed_id: '',
feed_qty: 0,
feed_stock: 0,
},
],
body_weight: initialValues?.body_weight ?? [
{
chicken_weight: 0,
@@ -156,13 +178,19 @@ export const getRecordingFormInitialValues = (
average_chicken_weight: 0,
},
],
vaccination: initialValues?.vaccination ?? [
{
vaccine_name: '',
total_stock: 0,
used_stock: 0,
},
],
vaccination: initialValues?.vaccination
? initialValues.vaccination.map((vaccine) => ({
vaccine_id: vaccine.vaccine_name,
total_stock: vaccine.total_stock,
used_stock: vaccine.used_stock,
}))
: [
{
vaccine_id: '',
total_stock: 0,
used_stock: 0,
},
],
mortality: initialValues?.mortality ?? [
{
condition: '',
@@ -23,6 +23,7 @@ import { isResponseSuccess } from '@/lib/api-helper';
import { RECORDING_FLAG_OPTIONS } from '@/config/constant';
import useSWR from 'swr';
import { KandangApi, LocationApi } from '@/services/api/master-data';
import { ProductWarehouseApi } from '@/services/api/inventory';
interface RecordingFormProps {
type?: 'add' | 'edit' | 'detail';
@@ -71,7 +72,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
? values.recording_date.toISOString()
: '',
feed_data: (values.feed_data ?? []).map((p) => ({
feed_name: p.feed_name,
feed_id: p.feed_id,
feed_qty: p.feed_qty,
feed_stock: p.feed_stock,
})),
@@ -81,7 +82,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
average_chicken_weight: b.average_chicken_weight,
})),
vaccination: (values.vaccination ?? []).map((v) => ({
vaccine_name: v.vaccine_name,
vaccine_id: v.vaccine_id,
total_stock: v.total_stock,
used_stock: v.used_stock,
})),
@@ -116,6 +117,50 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
: [];
// Pakan selection
const pakanUrl = `${ProductWarehouseApi.basePath}?${new URLSearchParams({ flag: 'PAKAN', search: '' }).toString()}`;
const { data: pakanProducts } = useSWR(
pakanUrl,
ProductWarehouseApi.getAllFetcher
);
const pakanOptions = isResponseSuccess(pakanProducts)
? pakanProducts?.data.map((product) => ({
value: product.id,
label: product.product.name,
}))
: [];
// Create stock mapping for pakan (Feed)
const pakanStockMap = useMemo(() => {
if (!isResponseSuccess(pakanProducts)) return new Map<number, number>();
const map = new Map<number, number>();
pakanProducts.data.forEach((product) => {
map.set(product.id, product.quantity);
});
return map;
}, [pakanProducts]);
// OVK selection
const ovkUrl = `${ProductWarehouseApi.basePath}?${new URLSearchParams({ flag: 'OVK', search: '' }).toString()}`;
const { data: ovkProducts } = useSWR(
ovkUrl,
ProductWarehouseApi.getAllFetcher
);
const ovkOptions = isResponseSuccess(ovkProducts)
? ovkProducts?.data.map((product) => ({
value: product.id,
label: product.product.name,
}))
: [];
// Create stock mapping for OVK (Vaccination)
const ovkStockMap = useMemo(() => {
if (!isResponseSuccess(ovkProducts)) return new Map<number, number>();
const map = new Map<number, number>();
ovkProducts.data.forEach((product) => {
map.set(product.id, product.quantity);
});
return map;
}, [ovkProducts]);
const locationsUrl = `${LocationApi.basePath}?${new URLSearchParams({ search: locationSelectInputValue ?? '' }).toString()}`;
const { data: locations, isLoading: isLoadingLocations } = useSWR(
@@ -209,7 +254,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const newFeedData = [
...(formik.values.feed_data || []),
{
feed_name: '',
feed: null,
feed_id: 0,
feed_qty: 0,
feed_stock: 0,
},
@@ -263,7 +309,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const newVaccination = [
...(formik.values.vaccination || []),
{
vaccine_name: '',
vaccine: null,
vaccine_id: 0,
total_stock: 0,
used_stock: 0,
},
@@ -327,33 +374,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<div className='flex flex-col gap-6'>
<div className='flex gap-4'>
{/*<SelectInput*/}
{/* required*/}
{/* label='Flock'*/}
{/* value={*/}
{/* formik.values.flock_id*/}
{/* ? {*/}
{/* value: formik.values.flock_id,*/}
{/* label: initialValues?.flock?.name,*/}
{/* }*/}
{/* : null*/}
{/* }*/}
{/* onChange={(val) => {*/}
{/* formik.setFieldValue(*/}
{/* 'flock_id',*/}
{/* (val as OptionType)?.value*/}
{/* );*/}
{/* }}*/}
{/* options={flockOptions}*/}
{/* onInputChange={setFlockSelectInputValue}*/}
{/* isLoading={isLoadingFlocks}*/}
{/* isError={*/}
{/* formik.touched.flock_id && Boolean(formik.errors.flock_id)*/}
{/* }*/}
{/* errorMessage={formik.errors.flock_id as string}*/}
{/* isDisabled={type === 'detail'}*/}
{/* isClearable*/}
{/*/>*/}
<SelectInput
required
label='Flock'
@@ -473,8 +493,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</th>
)}
<th>Feed Name</th>
<th>Feed Qty</th>
<th>Feed Stock</th>
<th>Feed Qty (Available Stock)</th>
<th>Feed Stock (Used)</th>
{type !== 'detail' && <th>Action</th>}
</tr>
</thead>
@@ -500,27 +520,47 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</td>
)}
<td>
<TextInput
<SelectInput
required
name={`feed_data.${idx}.feed_name`}
value={feed.feed_name}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={
pakanOptions.find(
(opt: OptionType) =>
Number(opt.value) === Number(feed.feed_id)
) ?? null
}
onChange={(val) => {
const productWarehouseId =
(val as OptionType)?.value ?? 0;
const stock =
pakanStockMap.get(
productWarehouseId as number
) ?? 0;
formik.setFieldValue(
`feed_data.${idx}.feed`,
val
);
formik.setFieldValue(
`feed_data.${idx}.feed_id`,
productWarehouseId
);
formik.setFieldValue(
`feed_data.${idx}.feed_qty`,
stock
);
}}
options={pakanOptions}
isLoading={false}
isError={
isRepeaterInputError(
'feed_data',
'feed_name',
idx
).isError
isRepeaterInputError('feed_data', 'feed_id', idx)
.isError
}
errorMessage={
isRepeaterInputError(
'feed_data',
'feed_name',
idx
).errorMessage
isRepeaterInputError('feed_data', 'feed_id', idx)
.errorMessage
}
readOnly={type === 'detail'}
isDisabled={type === 'detail'}
isClearable
className={{
wrapper: 'w-full min-w-24',
}}
@@ -542,7 +582,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
isRepeaterInputError('feed_data', 'feed_qty', idx)
.errorMessage
}
readOnly={type === 'detail'}
readOnly={true}
className={{
wrapper: 'w-full min-w-24',
}}
@@ -847,7 +887,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</th>
)}
<th>Vaccine Name</th>
<th>Total Stock</th>
<th>Total Stock (Available Stock)</th>
<th>Used Stock</th>
{type !== 'detail' && <th>Action</th>}
</tr>
@@ -874,27 +914,53 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</td>
)}
<td>
<TextInput
<SelectInput
required
name={`vaccination.${idx}.vaccine_name`}
value={vaccine.vaccine_name}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={
ovkOptions.find(
(opt: OptionType) =>
Number(opt.value) ===
Number(vaccine.vaccine_id)
) ?? null
}
onChange={(val) => {
const productWarehouseId =
(val as OptionType)?.value ?? 0;
const stock =
ovkStockMap.get(productWarehouseId as number) ??
0;
formik.setFieldValue(
`vaccination.${idx}.vaccine`,
val
);
formik.setFieldValue(
`vaccination.${idx}.vaccine_id`,
productWarehouseId
);
formik.setFieldValue(
`vaccination.${idx}.total_stock`,
stock
);
}}
options={ovkOptions}
isLoading={false}
isError={
isRepeaterInputError(
'vaccination',
'vaccine_name',
'vaccine_id',
idx
).isError
}
errorMessage={
isRepeaterInputError(
'vaccination',
'vaccine_name',
'vaccine_id',
idx
).errorMessage
}
readOnly={type === 'detail'}
isDisabled={type === 'detail'}
isClearable
className={{
wrapper: 'w-full min-w-24',
}}
@@ -922,7 +988,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
idx
).errorMessage
}
readOnly={type === 'detail'}
readOnly={true}
className={{
wrapper: 'w-full min-w-24',
}}
+2 -2
View File
@@ -38,7 +38,7 @@ export type CreateRecordingPayload = {
location_id: number;
coop_id: number;
feed_data: {
feed_name: string;
feed_id: string;
feed_qty: number;
feed_stock: number;
}[];
@@ -48,7 +48,7 @@ export type CreateRecordingPayload = {
average_chicken_weight: number;
}[];
vaccination: {
vaccine_name: string;
vaccine_id: string;
total_stock: number;
used_stock: number;
}[];