From caf68d438ff88be2b5b5debd33ea2982df6119f4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 17 Oct 2025 13:59:28 +0700 Subject: [PATCH] refactor(FE-114,136,137): update feed and vaccination fields to use IDs instead of names and add stock validation --- .../recording/form/RecordingForm.schema.ts | 64 ++++-- .../flock/recording/form/RecordingForm.tsx | 186 ++++++++++++------ src/types/api/flock/recording.d.ts | 4 +- 3 files changed, 174 insertions(+), 80 deletions(-) diff --git a/src/components/pages/flock/recording/form/RecordingForm.schema.ts b/src/components/pages/flock/recording/form/RecordingForm.schema.ts index 319482e7..ed5507eb 100644 --- a/src/components/pages/flock/recording/form/RecordingForm.schema.ts +++ b/src/components/pages/flock/recording/form/RecordingForm.schema.ts @@ -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: '', diff --git a/src/components/pages/flock/recording/form/RecordingForm.tsx b/src/components/pages/flock/recording/form/RecordingForm.tsx index d6e80a76..e2bd0d94 100644 --- a/src/components/pages/flock/recording/form/RecordingForm.tsx +++ b/src/components/pages/flock/recording/form/RecordingForm.tsx @@ -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(); + const map = new Map(); + 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(); + const map = new Map(); + 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) => {
- {/* {*/} - {/* 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*/} - {/*/>*/} { )} Feed Name - Feed Qty - Feed Stock + Feed Qty (Available Stock) + Feed Stock (Used) {type !== 'detail' && Action} @@ -500,27 +520,47 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { )} - + 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) => { )} Vaccine Name - Total Stock + Total Stock (Available Stock) Used Stock {type !== 'detail' && Action} @@ -874,27 +914,53 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { )} - + 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', }} diff --git a/src/types/api/flock/recording.d.ts b/src/types/api/flock/recording.d.ts index 58fed128..d4bf90ee 100644 --- a/src/types/api/flock/recording.d.ts +++ b/src/types/api/flock/recording.d.ts @@ -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; }[];