From 4c4c70e10f84efd4a55331a144abc9c8b2c970dd Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 17 Jan 2026 12:03:32 +0700 Subject: [PATCH 1/7] refactor(FE): Validate duplicates by selected record date --- .../recording/form/RecordingForm.schema.ts | 2 +- .../recording/form/RecordingForm.tsx | 87 ++++++++++++++++--- 2 files changed, 75 insertions(+), 14 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.schema.ts b/src/components/pages/production/recording/form/RecordingForm.schema.ts index 8ebd4aa2..18dc9823 100644 --- a/src/components/pages/production/recording/form/RecordingForm.schema.ts +++ b/src/components/pages/production/recording/form/RecordingForm.schema.ts @@ -104,7 +104,7 @@ export const RecordingGrowingFormSchema: Yup.ObjectSchema; diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index b45f72d0..3e40b404 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -56,6 +56,7 @@ import { import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; import { Kandang } from '@/types/api/master-data/kandang'; +import * as Yup from 'yup'; import { RecordingGrowingFormSchema, RecordingLayingFormSchema, @@ -231,6 +232,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const [, setNewRecordingData] = useState(null); const [nextDayRecording, setNextDayRecording] = useState(null); + const [currentRecordDate, setCurrentRecordDate] = useState( + new Date().toISOString().split('T')[0] + ); + const [duplicateErrorShown, setDuplicateErrorShown] = useState(false); const approveModal = useModal(); const rejectModal = useModal(); @@ -719,18 +724,18 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const recordedProjectFlockKandangIds = useMemo(() => { if (!isResponseSuccess(existingRecordings)) return new Set(); - const todayRecordings = existingRecordings.data; + const allRecordings = existingRecordings.data; const recordedIds = new Set(); - todayRecordings.forEach((recording) => { + allRecordings.forEach((recording) => { const recordingDate = recording.record_datetime?.split('T')[0]; - if (recordingDate === today) { + if (recordingDate === currentRecordDate) { recordedIds.add(recording.project_flock?.project_flock_kandang_id); } }); return recordedIds; - }, [existingRecordings, today]); + }, [existingRecordings, currentRecordDate]); const currentTotalChickQty = useMemo(() => { if (type === 'add' && projectFlockKandangLookup) { @@ -910,14 +915,38 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { initialValues: formikInitialValues, enableReinitialize: true, validationSchema: (() => { + let schema; if (isLayingCategory) { - return type === 'edit' - ? UpdateRecordingLayingFormSchema - : RecordingLayingFormSchema; + schema = + type === 'edit' + ? UpdateRecordingLayingFormSchema + : RecordingLayingFormSchema; + } else { + schema = + type === 'edit' + ? UpdateRecordingGrowingFormSchema + : RecordingGrowingFormSchema; } - return type === 'edit' - ? UpdateRecordingGrowingFormSchema - : RecordingGrowingFormSchema; + return schema.clone().concat( + Yup.object().shape({ + project_flock_kandang_id: Yup.number().test( + 'not-already-recorded', + 'Project Flock ini sudah direcord pada tanggal tersebut!', + function (value) { + if (type !== 'add') return true; + if (value && recordedProjectFlockKandangIds.has(value)) { + if (this.createError) { + return this.createError({ + message: `Project Flock ini sudah direcord pada tanggal ${formatDate(currentRecordDate, 'DD MMMM YYYY')}!`, + }); + } + return false; + } + return true; + } + ), + }) + ); })(), validateOnChange: true, validateOnBlur: true, @@ -1149,6 +1178,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { setSelectedLocation(location); setSelectedProjectFlock(null); setSelectedKandang(null); + if (duplicateErrorShown) { + toast.dismiss(); + setDuplicateErrorShown(false); + } setSelectedProjectFlockLocationId( location ? location.value.toString() : '' ); @@ -1159,6 +1192,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const projectFlockChangeHandler = (val: OptionType | OptionType[] | null) => { setSelectedProjectFlock(val as OptionType); setSelectedKandang(null); + if (duplicateErrorShown) { + toast.dismiss(); + setDuplicateErrorShown(false); + } formik.setFieldValue('project_flock_kandang', null); formik.setFieldValue('project_flock_kandang_id', 0); }; @@ -1166,6 +1203,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const kandangChangeHandler = (val: OptionType | OptionType[] | null) => { const kandang = val as OptionType; setSelectedKandang(kandang); + if (duplicateErrorShown) { + toast.dismiss(); + setDuplicateErrorShown(false); + } if (selectedLocation && kandang) { setStockProductsLocationId(selectedLocation.value.toString()); setStockProductsKandangId(kandang.value.toString()); @@ -1187,9 +1228,18 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const handleRecordDateChange = useCallback( (e: React.ChangeEvent) => { - formik.setFieldValue('record_date', e.target.value); + const newDate = e.target.value; + formik.setFieldValue('record_date', newDate); + setCurrentRecordDate(newDate); + if (duplicateErrorShown) { + toast.dismiss(); + setDuplicateErrorShown(false); + } + setTimeout(() => { + formik.validateField('project_flock_kandang_id'); + }, 0); }, - [formik] + [formik, duplicateErrorShown] ); useEffect(() => { @@ -1199,8 +1249,19 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { if (type === 'add') { if (recordedProjectFlockKandangIds.has(projectFlockKandangId)) { - toast.error('Project Flock Kandang ini sudah direcord hari ini!'); + if (!duplicateErrorShown) { + toast.error( + `Project Flock Kandang ini sudah direcord pada tanggal ${formatDate(currentRecordDate, 'DD MMMM YYYY')}!`, + { duration: Infinity } + ); + setDuplicateErrorShown(true); + } return; + } else { + if (duplicateErrorShown) { + toast.dismiss(); + setDuplicateErrorShown(false); + } } if ( From cd9fa31ad71415fb109284080dcf7a18502b4ed8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 17 Jan 2026 12:11:30 +0700 Subject: [PATCH 2/7] refactor(FE): Disable form fields in edit mode --- .../pages/production/recording/form/RecordingForm.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 3e40b404..106fc220 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -1660,6 +1660,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { Boolean(formik.errors.record_date) } errorMessage={formik.errors.record_date as string} + disabled={type === 'edit'} /> { placeholder='Pilih Lokasi' isClearable isSearchable + isDisabled={type === 'edit'} /> { onInputChange={setProjectFlockSearchValue} isLoading={isLoadingProjectFlocks} onMenuScrollToBottom={loadMoreProjectFlocks} - isDisabled={!selectedLocation} + isDisabled={!selectedLocation || type === 'edit'} placeholder={ selectedLocation ? 'Pilih Project Flock' @@ -1704,7 +1706,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { onChange={kandangChangeHandler} options={kandangOptions} isLoading={false} - isDisabled={!selectedProjectFlock} + isDisabled={!selectedProjectFlock || type === 'edit'} placeholder={ selectedProjectFlock ? 'Pilih Kandang' From f32b77c5527bfbda7f64eeae60db77b1b1c90ded Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 17 Jan 2026 12:24:32 +0700 Subject: [PATCH 3/7] refactor(FE): Add location, project_flock and kandang fields --- .../recording/form/RecordingForm.schema.ts | 45 ++++++++++++++++++ .../recording/form/RecordingForm.tsx | 47 ++++++++++++++++--- 2 files changed, 85 insertions(+), 7 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.schema.ts b/src/components/pages/production/recording/form/RecordingForm.schema.ts index 18dc9823..59d5ac43 100644 --- a/src/components/pages/production/recording/form/RecordingForm.schema.ts +++ b/src/components/pages/production/recording/form/RecordingForm.schema.ts @@ -8,6 +8,21 @@ import { type RecordingGrowingFormSchemaType = { record_date: string; + location?: { + value: number; + label: string; + } | null; + location_id: number; + project_flock?: { + value: number; + label: string; + } | null; + project_flock_id: number; + kandang?: { + value: number; + label: string; + } | null; + kandang_id: number; project_flock_kandang: { value: number; label: string; @@ -89,6 +104,30 @@ export const RecordingGrowingFormSchema: Yup.ObjectSchema { // ===== EVENT HANDLERS ===== const locationChangeHandler = (val: OptionType | OptionType[] | null) => { - const location = val as OptionType; + const location = val as OptionType | null; + const locationId = Number(location?.value); + + formik.setFieldTouched('location', true); + formik.setFieldValue('location', location); + formik.setFieldTouched('location_id', true); + formik.setFieldValue('location_id', locationId); + setSelectedLocation(location); setSelectedProjectFlock(null); setSelectedKandang(null); @@ -1185,23 +1192,34 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { setSelectedProjectFlockLocationId( location ? location.value.toString() : '' ); - formik.setFieldValue('project_flock_kandang', null); - formik.setFieldValue('project_flock_kandang_id', 0); }; const projectFlockChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedProjectFlock(val as OptionType); + const projectFlock = val as OptionType | null; + const projectFlockId = Number(projectFlock?.value); + + formik.setFieldTouched('project_flock', true); + formik.setFieldValue('project_flock', projectFlock); + formik.setFieldTouched('project_flock_id', true); + formik.setFieldValue('project_flock_id', projectFlockId); + + setSelectedProjectFlock(projectFlock); setSelectedKandang(null); if (duplicateErrorShown) { toast.dismiss(); setDuplicateErrorShown(false); } - formik.setFieldValue('project_flock_kandang', null); - formik.setFieldValue('project_flock_kandang_id', 0); }; const kandangChangeHandler = (val: OptionType | OptionType[] | null) => { - const kandang = val as OptionType; + const kandang = val as OptionType | null; + const kandangId = Number(kandang?.value); + + formik.setFieldTouched('kandang', true); + formik.setFieldValue('kandang', kandang); + formik.setFieldTouched('kandang_id', true); + formik.setFieldValue('kandang_id', kandangId); + setSelectedKandang(kandang); if (duplicateErrorShown) { toast.dismiss(); @@ -1676,6 +1694,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { isClearable isSearchable isDisabled={type === 'edit'} + isError={ + formik.touched.location_id && + Boolean(formik.errors.location_id) + } + errorMessage={formik.errors.location_id as string} /> { } isClearable isSearchable + isError={ + formik.touched.project_flock_id && + Boolean(formik.errors.project_flock_id) + } + errorMessage={formik.errors.project_flock_id as string} /> { } isClearable isSearchable + isError={ + formik.touched.kandang_id && + Boolean(formik.errors.kandang_id) + } + errorMessage={formik.errors.kandang_id as string} startAdornment={ projectFlockKandangLookup || projectFlockKandangDetail ? getProjectFlockBadgeAdornment() From 0cdbff69546d9c68b94c04c4ebf906d6e26d7c4a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 17 Jan 2026 12:32:55 +0700 Subject: [PATCH 4/7] refactor(FE): Validate recording date and handle null location --- .../pages/production/recording/form/RecordingForm.schema.ts | 1 + .../pages/production/recording/form/RecordingForm.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.schema.ts b/src/components/pages/production/recording/form/RecordingForm.schema.ts index 59d5ac43..82b59036 100644 --- a/src/components/pages/production/recording/form/RecordingForm.schema.ts +++ b/src/components/pages/production/recording/form/RecordingForm.schema.ts @@ -103,6 +103,7 @@ export const RecordingGrowingFormSchema: Yup.ObjectSchema { // ===== EVENT HANDLERS ===== const locationChangeHandler = (val: OptionType | OptionType[] | null) => { const location = val as OptionType | null; - const locationId = Number(location?.value); + const locationId = location ? Number(location.value) : 0; formik.setFieldTouched('location', true); formik.setFieldValue('location', location); From 1152b6d2c3b47f6b9a5fad3670ca30f2214ca42d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 17 Jan 2026 12:45:11 +0700 Subject: [PATCH 5/7] refactor(FE): Disable Submit button when duplicate error shown --- .../production/recording/form/RecordingForm.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 7f66ef6d..0995f27f 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -2990,7 +2990,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { color='primary' className='px-4' isLoading={formik.isSubmitting} - disabled={hasExceededStock || formik.isSubmitting} + disabled={ + hasExceededStock || + formik.isSubmitting || + duplicateErrorShown + } > Submit @@ -3015,7 +3019,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { color='primary' className='px-4' isLoading={formik.isSubmitting} - disabled={hasExceededStock || formik.isSubmitting} + disabled={ + hasExceededStock || + formik.isSubmitting || + duplicateErrorShown + } > Submit From f4abfd4279c9f72a6c140d43e630848f73158be5 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 17 Jan 2026 12:54:16 +0700 Subject: [PATCH 6/7] refactor(FE): Dismiss toast notifications on unmount --- .../pages/production/recording/form/RecordingForm.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 0995f27f..d7f913e8 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -237,6 +237,12 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ); const [duplicateErrorShown, setDuplicateErrorShown] = useState(false); + useEffect(() => { + return () => { + toast.dismiss(); + }; + }, []); + const approveModal = useModal(); const rejectModal = useModal(); const deleteModal = useModal(); From d19b1e885e74769bc2758d8681f18d46ebd0f7af Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 17 Jan 2026 12:59:41 +0700 Subject: [PATCH 7/7] refactor(FE): Compute item total on qty change and limit reset --- .../order/PurchaseOrderStaffApprovalForm.tsx | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx index 729b6782..a232347d 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx @@ -633,8 +633,18 @@ const PurchaseOrderStaffApprovalForm = ({ formik.setFieldValue(`items.${idx}.qty`, numValue); - formik.setFieldValue(`items.${idx}.price`, ''); - formik.setFieldValue(`items.${idx}.total_price`, ''); + if ( + formItem.price !== '' && + formItem.price !== undefined && + formItem.price !== null && + numValue !== '' && + numValue > 0 + ) { + const calculatedTotal = Number(formItem.price) * Number(numValue); + formik.setFieldValue(`items.${idx}.total_price`, calculatedTotal); + } else if (numValue === '') { + formik.setFieldValue(`items.${idx}.total_price`, ''); + } } if (field === 'price' || field === 'total_price') { @@ -1184,8 +1194,10 @@ const PurchaseOrderStaffApprovalForm = ({ color='warning' className='px-4' onClick={() => { - formik.setValues(formikInitialValues); - formik.resetForm(); + if (type === 'add') { + formik.setValues(formikInitialValues); + formik.resetForm(); + } setPurchaseOrderFormErrorMessage(''); onCancel?.(); onModalClose?.();