From 324b9b14ef1aa08b035a905ed540848cb2f237c5 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 7 Jan 2026 14:07:02 +0700 Subject: [PATCH 01/37] refactor(FE): Move form error above fields --- .../form/ProductCategoryForm.tsx | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/components/pages/master-data/product-category/form/ProductCategoryForm.tsx b/src/components/pages/master-data/product-category/form/ProductCategoryForm.tsx index 7acd3a01..dc34ac5e 100644 --- a/src/components/pages/master-data/product-category/form/ProductCategoryForm.tsx +++ b/src/components/pages/master-data/product-category/form/ProductCategoryForm.tsx @@ -154,6 +154,16 @@ const ProductCategoryForm = ({ onReset={formik.handleReset} className='w-full mt-8 flex flex-col gap-6' > + {formErrorMessage && ( +
+ + {formErrorMessage} +
+ )}
Submit @@ -244,17 +254,6 @@ const ProductCategoryForm = ({
)} - - {formErrorMessage && ( -
- - {formErrorMessage} -
- )} From e6172be81e773f235952624487b5a77b3e8506fa Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 7 Jan 2026 14:08:04 +0700 Subject: [PATCH 02/37] refactor(FE): Allow submit when invalid and move error alert --- .../master-data/product/form/ProductForm.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/pages/master-data/product/form/ProductForm.tsx b/src/components/pages/master-data/product/form/ProductForm.tsx index 5b304419..2a009200 100644 --- a/src/components/pages/master-data/product/form/ProductForm.tsx +++ b/src/components/pages/master-data/product/form/ProductForm.tsx @@ -224,6 +224,16 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { onReset={formik.handleReset} className='w-full mt-8 flex flex-col gap-6' > + {productFormErrorMessage && ( +
+ + {productFormErrorMessage} +
+ )}
{ type='submit' color='primary' isLoading={formik.isSubmitting} - disabled={!formik.isValid || formik.isSubmitting} + disabled={formik.isSubmitting} className='px-4' > Submit @@ -471,16 +481,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
)} - {productFormErrorMessage && ( -
- - {productFormErrorMessage} -
- )} {type !== 'add' && ( From 8b7ed9e46bdb8267ed14fb434bc2ddbeb3f96f99 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 7 Jan 2026 14:09:47 +0700 Subject: [PATCH 03/37] refactor(FE): Move error alert above form --- .../recording/form/RecordingForm.tsx | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 3a6f8071..ee76f407 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -60,7 +60,6 @@ import { GROWING_RECORDING_APPROVAL_LINE, LAYING_RECORDING_APPROVAL_LINE, } from '@/config/approval-line'; -import Table from '@/components/Table'; interface RecordingFormProps { type?: 'add' | 'edit' | 'detail'; @@ -1326,6 +1325,16 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { onSubmit={formik.handleSubmit} className='w-full mt-8 flex flex-col gap-6' > + {recordingFormErrorMessage && ( +
+ + {recordingFormErrorMessage} +
+ )} {/* Basic Info Card */} {(type === 'add' || type === 'edit') && ( { color='primary' className='px-4' isLoading={formik.isSubmitting} - disabled={ - hasExceededStock || !formik.isValid || formik.isSubmitting - } + disabled={hasExceededStock || formik.isSubmitting} > Submit @@ -2544,16 +2551,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { )} - {recordingFormErrorMessage && ( -
- - {recordingFormErrorMessage} -
- )} From d049f6c34f98332729bb4bca2c3ca24a22a0c716 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 7 Jan 2026 14:21:37 +0700 Subject: [PATCH 04/37] refactor(FE): Tighten product form validation and layout --- .../product/form/ProductForm.schema.ts | 12 +- .../master-data/product/form/ProductForm.tsx | 362 +++++++++--------- 2 files changed, 195 insertions(+), 179 deletions(-) diff --git a/src/components/pages/master-data/product/form/ProductForm.schema.ts b/src/components/pages/master-data/product/form/ProductForm.schema.ts index 37881636..e4852792 100644 --- a/src/components/pages/master-data/product/form/ProductForm.schema.ts +++ b/src/components/pages/master-data/product/form/ProductForm.schema.ts @@ -29,36 +29,38 @@ export const ProductFormSchema: Yup.ObjectSchema = sku: Yup.string().required('SKU wajib diisi!'), uom: Yup.object({ - value: Yup.number().min(1).required(), + value: Yup.number().min(1, 'Satuan wajib dipilih!').required(), label: Yup.string().required(), }) .nullable() .required('Satuan wajib diisi!'), uom_id: Yup.number() + .min(1, 'Satuan wajib dipilih!') .required('Satuan wajib diisi!') .typeError('Satuan wajib diisi!'), product_category: Yup.object({ - value: Yup.number().min(1).required(), + value: Yup.number().min(1, 'Kategori produk wajib dipilih!').required(), label: Yup.string().required(), }) .nullable() .required('Kategori produk wajib diisi!'), product_category_id: Yup.number() + .min(1, 'Kategori produk wajib dipilih!') .required('Kategori produk wajib diisi!') .typeError('Kategori produk wajib diisi!'), product_price: Yup.number() .required('Harga produk wajib diisi!') .typeError('Harga produk wajib diisi!') - .min(0, 'Harga produk tidak boleh kurang dari 0!'), + .min(1, 'Harga produk tidak boleh kurang dari 1!'), selling_price: Yup.number() .required('Harga jual wajib diisi!') .typeError('Harga jual wajib diisi!') - .min(0, 'Harga jual tidak boleh kurang dari 0!'), + .min(1, 'Harga jual tidak boleh kurang dari 1!'), tax: Yup.number() .required('Pajak wajib diisi!') @@ -69,7 +71,7 @@ export const ProductFormSchema: Yup.ObjectSchema = expiry_period: Yup.number() .required('Periode kadaluarsa wajib diisi!') .typeError('Periode kadaluarsa wajib diisi!') - .min(0, 'Periode kadaluarsa tidak boleh kurang dari 0!'), + .min(1, 'Periode kadaluarsa tidak boleh kurang dari 1 hari!'), supplier_ids: Yup.array() .of(Yup.number().required().typeError('Supplier tidak valid!')) diff --git a/src/components/pages/master-data/product/form/ProductForm.tsx b/src/components/pages/master-data/product/form/ProductForm.tsx index 2a009200..200b9d39 100644 --- a/src/components/pages/master-data/product/form/ProductForm.tsx +++ b/src/components/pages/master-data/product/form/ProductForm.tsx @@ -234,7 +234,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { {productFormErrorMessage} )} -
+
{ errorMessage={formik.errors.name} readOnly={type === 'detail'} /> - - - - - - - - - - (formik.values.supplier_ids || []).includes(opt.value) - )} - onChange={supplierChangeHandler} - options={supplierOptions} - onInputChange={setSupplierSelectInputValue} - isLoading={isLoadingSuppliers} - isError={ - formik.touched.supplier_ids && - Boolean(formik.errors.supplier_ids) - } - errorMessage={formik.errors.supplier_ids as string} - isDisabled={type === 'detail'} - isClearable - /> - - (formik.values.flags || []).includes(opt.value) - )} - onChange={(val) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - formik.setFieldValue( - 'flags', - arr.map((v) => (v as OptionType).value) - ); - }} - options={PRODUCT_FLAG_OPTIONS} - isError={formik.touched.flags && Boolean(formik.errors.flags)} - errorMessage={formik.errors.flags as string} - isDisabled={type === 'detail'} - isClearable - /> +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + (formik.values.supplier_ids || []).includes(opt.value) + )} + onChange={supplierChangeHandler} + options={supplierOptions} + onInputChange={setSupplierSelectInputValue} + isLoading={isLoadingSuppliers} + isError={ + formik.touched.supplier_ids && + Boolean(formik.errors.supplier_ids) + } + errorMessage={formik.errors.supplier_ids as string} + isDisabled={type === 'detail'} + isClearable + /> + + (formik.values.flags || []).includes(opt.value) + )} + onChange={(val) => { + const arr = Array.isArray(val) ? val : val ? [val] : []; + formik.setFieldValue( + 'flags', + arr.map((v) => (v as OptionType).value) + ); + }} + options={PRODUCT_FLAG_OPTIONS} + isError={formik.touched.flags && Boolean(formik.errors.flags)} + errorMessage={formik.errors.flags as string} + isDisabled={type === 'detail'} + isClearable + /> +
{type !== 'add' && ( From 59f45288411ef36befc81d2d5250d59b644f2b60 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 7 Jan 2026 14:39:43 +0700 Subject: [PATCH 05/37] refactor(FE): Stop disabling Submit when form is invalid --- .../pages/production/recording/form/RecordingForm.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index ee76f407..bd866927 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -2541,9 +2541,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { color='primary' className='px-4' isLoading={formik.isSubmitting} - disabled={ - hasExceededStock || !formik.isValid || formik.isSubmitting - } + disabled={hasExceededStock || formik.isSubmitting} > Submit From 09ae619829fba1097300aec0e04f72d097909d16 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 7 Jan 2026 14:41:54 +0700 Subject: [PATCH 06/37] refactor(FE): Stop blocking Submit on form validity --- .../purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx | 5 +---- .../purchase/form/order/PurchaseOrderStaffApprovalForm.tsx | 2 +- .../pages/purchase/form/request/PurchaseRequestForm.tsx | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index ef90e06a..aadb1b99 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -741,10 +741,7 @@ const PurchaseOrderAcceptApprovalForm = ({ className='px-4' isLoading={formik.isSubmitting} disabled={ - !formik.isValid || - formik.isSubmitting || - hasQuantityExceededErrors || - isRejected + formik.isSubmitting || hasQuantityExceededErrors || isRejected } > Submit diff --git a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx index 6a08e53b..89ba4e61 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx @@ -1164,7 +1164,7 @@ const PurchaseOrderStaffApprovalForm = ({ color='primary' className='px-4' isLoading={formik.isSubmitting} - disabled={!formik.isValid || formik.isSubmitting || isRejected} + disabled={formik.isSubmitting || isRejected} > Submit diff --git a/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx index 396ce7bb..822cc003 100644 --- a/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx +++ b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx @@ -896,7 +896,7 @@ const PurchaseRequestForm = ({ color='primary' className='px-4' isLoading={formik.isSubmitting} - disabled={!formik.isValid || formik.isSubmitting} + disabled={formik.isSubmitting} > Submit From b0a1b837d032eb0266382a06b1b84fd71ae13bae Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 7 Jan 2026 14:45:38 +0700 Subject: [PATCH 07/37] refactor(FE): Move error alert to top of purchase forms --- .../order/PurchaseOrderAcceptApprovalForm.tsx | 17 ++++++--------- .../order/PurchaseOrderStaffApprovalForm.tsx | 21 +++++++++---------- .../form/request/PurchaseRequestForm.tsx | 21 +++++++++---------- 3 files changed, 26 insertions(+), 33 deletions(-) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index aadb1b99..1f0b3fc5 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -352,6 +352,12 @@ const PurchaseOrderAcceptApprovalForm = ({ onSubmit={formik.handleSubmit} className='w-full flex flex-col gap-6' > + {purchaseOrderFormErrorMessage && ( +
+ + {purchaseOrderFormErrorMessage} +
+ )}

{type === 'add' @@ -748,17 +754,6 @@ const PurchaseOrderAcceptApprovalForm = ({

- - {purchaseOrderFormErrorMessage && ( -
- - {purchaseOrderFormErrorMessage} -
- )}
); diff --git a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx index 89ba4e61..7063ba93 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx @@ -656,6 +656,16 @@ const PurchaseOrderStaffApprovalForm = ({ onSubmit={formik.handleSubmit} className='w-full flex flex-col gap-6' > + {purchaseOrderFormErrorMessage && ( +
+ + {purchaseOrderFormErrorMessage} +
+ )}

{type === 'add' @@ -1170,17 +1180,6 @@ const PurchaseOrderStaffApprovalForm = ({

- - {purchaseOrderFormErrorMessage && ( -
- - {purchaseOrderFormErrorMessage} -
- )} diff --git a/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx index 822cc003..ef428b25 100644 --- a/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx +++ b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx @@ -491,6 +491,16 @@ const PurchaseRequestForm = ({ onReset={formik.handleReset} className='w-full mt-8 flex flex-col gap-6' > + {purchaseRequestFormErrorMessage && ( +
+ + {purchaseRequestFormErrorMessage} +
+ )} {/* Basic Info Card */} )} - - {purchaseRequestFormErrorMessage && ( -
- - {purchaseRequestFormErrorMessage} -
- )} From bfcdb9883da1cc52a9817aa54ca2fe0f1e1f3b2f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 7 Jan 2026 14:53:24 +0700 Subject: [PATCH 08/37] refactor(FE): Move purchase order error alert below heading --- .../order/PurchaseOrderAcceptApprovalForm.tsx | 17 +++++++++------- .../order/PurchaseOrderStaffApprovalForm.tsx | 20 +++++++++---------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index 1f0b3fc5..55f399e6 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -352,19 +352,22 @@ const PurchaseOrderAcceptApprovalForm = ({ onSubmit={formik.handleSubmit} className='w-full flex flex-col gap-6' > - {purchaseOrderFormErrorMessage && ( -
- - {purchaseOrderFormErrorMessage} -
- )}

{type === 'add' ? 'Konfirmasi Penerimaan Produk' : 'Edit Penerimaan Produk'}

- + {purchaseOrderFormErrorMessage && ( +
+ + {purchaseOrderFormErrorMessage} +
+ )}
diff --git a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx index 7063ba93..95ca2f50 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx @@ -656,22 +656,22 @@ const PurchaseOrderStaffApprovalForm = ({ onSubmit={formik.handleSubmit} className='w-full flex flex-col gap-6' > - {purchaseOrderFormErrorMessage && ( -
- - {purchaseOrderFormErrorMessage} -
- )}

{type === 'add' ? 'Konfirmasi Item Pembelian' : 'Edit Item Pembelian'}

+ {purchaseOrderFormErrorMessage && ( +
+ + {purchaseOrderFormErrorMessage} +
+ )}
{groupedPurchaseItems.length > 0 ? (
From 90c61cbdf6f161b55a07aa47df3da2115fe7be64 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 7 Jan 2026 15:01:57 +0700 Subject: [PATCH 09/37] refactor(FE): Only disable submit button during submission --- .../pages/production/uniformity/form/UniformityForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/production/uniformity/form/UniformityForm.tsx b/src/components/pages/production/uniformity/form/UniformityForm.tsx index bbca72f8..e7e1e075 100644 --- a/src/components/pages/production/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityForm.tsx @@ -693,7 +693,7 @@ const UniformityForm = ({ type='submit' color='primary' className='w-full' - disabled={!formik.isValid || formik.isSubmitting} + disabled={formik.isSubmitting} > {formik.isSubmitting ? ( From cf8ed9ccadd75e43b085427bac10d4276cae9539 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 7 Jan 2026 15:06:43 +0700 Subject: [PATCH 10/37] refactor(FE): Use expedition vendor id fallback in mapping --- .../purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index 55f399e6..be533059 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -235,6 +235,9 @@ const PurchaseOrderAcceptApprovalForm = ({ useEffect(() => { if (purchaseItems.length > 0 && initialValues?.items) { const updatedItems = initialValues.items.map((item) => { + const expeditionVendorId = + item.expedition_vendor_id || item.expedition_vendor?.id || 0; + return { purchase_item: null, purchase_item_id: item.id, @@ -250,7 +253,7 @@ const PurchaseOrderAcceptApprovalForm = ({ label: item.expedition_vendor.name, } : null, - expedition_vendor_id: item.expedition_vendor_id || 0, + expedition_vendor_id: expeditionVendorId, received_qty: item.total_qty || '', transport_per_item: item.transport_per_item || '', }; From 38dfeec89238091dc58b23beb726c056a9e11fc4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 7 Jan 2026 15:11:36 +0700 Subject: [PATCH 11/37] refactor(FE): Move movement error to top and remove isValid check --- .../inventory/movement/form/MovementForm.tsx | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index 1559838a..6002aa55 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -814,6 +814,16 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { onReset={formik.handleReset} className='w-full mt-8 flex flex-col gap-6' > + {movementFormErrorMessage && ( +
+ + {movementFormErrorMessage} +
+ )} {/* Top card - Movement details */} { disabled={ hasInvalidQty || hasExceededStock || - !formik.isValid || formik.isSubmitting || (formik.values.source_warehouse_id === formik.values.destination_warehouse_id && @@ -1760,17 +1769,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
)}
- - {movementFormErrorMessage && ( -
- - {movementFormErrorMessage} -
- )} From c766f537534584b6e52b231a5b8544a7b0813c4f Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 7 Jan 2026 15:16:40 +0700 Subject: [PATCH 12/37] fix(FE): fix chickins approvals status --- .../production/chickin/form/ChickinForm.tsx | 2 +- .../chickin/form/tabs/ChickLogsView.tsx | 30 +++++----- .../project-flock/form/ProjectFlockForm.tsx | 58 +++++++++++-------- .../api/production/project-flock-kandang.d.ts | 13 +---- 4 files changed, 55 insertions(+), 48 deletions(-) diff --git a/src/components/pages/production/chickin/form/ChickinForm.tsx b/src/components/pages/production/chickin/form/ChickinForm.tsx index 7d8a4c7c..8c613737 100644 --- a/src/components/pages/production/chickin/form/ChickinForm.tsx +++ b/src/components/pages/production/chickin/form/ChickinForm.tsx @@ -34,7 +34,7 @@ const ChickinFormKandang = ({ isLoading: approvalsLoading, refresh: refreshApprovals, } = useApprovalSteps({ - latestApproval: initialValues?.approval, + latestApproval: initialValues?.chickin_approval, approvalLines: CHICKINS_APPROVAL_LINE, moduleName: 'CHICKINS', moduleId: initialValues?.id.toString() ?? '', diff --git a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx index 17c76822..80846565 100644 --- a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx +++ b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx @@ -60,8 +60,9 @@ const ChickinLogsView = ({
) : ( (initialValues?.chickins || []).map((chickin, index) => { - const isApproved = chickin.usage_qty !== 0; - const isPending = chickin.pending_usage_qty !== 0; + const isApproved = + initialValues.chickin_approval?.step_number === 2; + const isPending = initialValues.chickin_approval?.step_number === 1; const quantity = isApproved ? chickin.usage_qty : isPending @@ -146,18 +147,19 @@ const ChickinLogsView = ({ }) )} - {initialValues?.approval?.step_number <= 2 && ( - - - - )} + {initialValues.chickin_approval && + initialValues?.chickin_approval?.step_number < 2 && ( + + + + )} {chickinErrorMessage && (
setChickinErrorMessage('')}> diff --git a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx index 36ea90ca..20a8aff4 100644 --- a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx +++ b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx @@ -66,6 +66,7 @@ const ProjectFlockForm = ({ useState(''); const [selectedArea, setSelectedArea] = useState(''); const [selectedLocation, setSelectedLocation] = useState(''); + const [selectedCategory, setSelectedCategory] = useState(''); const [disabledLocation, setDisabledLocation] = useState( initialValues?.location?.id ? false : true ); @@ -125,7 +126,10 @@ const ProjectFlockForm = ({ const { options: optionsProductionStandards, isLoadingOptions: isLoadingProductionStandards, - } = useSelect(ProductionStandardApi.basePath, 'id', 'name'); + } = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', { + search: '', + project_category: selectedCategory, + }); const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({ search: '', @@ -237,9 +241,19 @@ const ProjectFlockForm = ({ }; const categoryChangeHandler = (val: OptionType | OptionType[] | null) => { - formik.setFieldValue('category', (val as OptionType)?.value); + // Reset production standard when category is changed + formik.setFieldValue('production_standard_id', ''); + formik.setFieldValue('production_standard', ''); + formik.setFieldValue('category_option', val); - if (val == null) { + formik.setFieldValue('category', val ? (val as OptionType)?.value : ''); + + setSelectedCategory((val as OptionType)?.value as string); + + if (Boolean(val)) { + formik.setFieldTouched('category', false); + formik.setFieldError('category', ''); + } else { formik.setFieldTouched('category', true); } }; @@ -378,8 +392,6 @@ const ProjectFlockForm = ({ validationSchema: formType == 'add' ? ProjectFlockFormSchema : UpdateProjectFlockFormSchema, validateOnBlur: true, - // validateOnChange: true, - // validateOnMount: true, onSubmit: async (values) => { setProjectFlockFormErrorMessage(''); const payload: CreateProjectFlockPayload = { @@ -770,23 +782,6 @@ const ProjectFlockForm = ({ isClearable isDisabled={formType != 'add'} /> - { - optionChangeHandler(val, 'production_standard'); - }} - options={optionsProductionStandards} - isLoading={isLoadingProductionStandards} - isError={ - formik.touched.production_standard && - Boolean(formik.errors.production_standard) - } - errorMessage={formik.errors.production_standard as string} - isClearable - isDisabled={formType != 'add'} - /> + { + optionChangeHandler(val, 'production_standard'); + }} + options={optionsProductionStandards} + isLoading={isLoadingProductionStandards} + isError={ + formik.touched.production_standard_id && + Boolean(formik.errors.production_standard_id) + } + errorMessage={formik.errors.production_standard_id as string} + isClearable + isDisabled={formType != 'add'} + /> diff --git a/src/types/api/production/project-flock-kandang.d.ts b/src/types/api/production/project-flock-kandang.d.ts index 3a98a6e8..8c8d6273 100644 --- a/src/types/api/production/project-flock-kandang.d.ts +++ b/src/types/api/production/project-flock-kandang.d.ts @@ -10,9 +10,10 @@ export type BaseProjectFlockKandang = { kandang_id: number; kandang: Kandang; project_flock: ProjectFlock; - available_qtys?: AvailableQty[]; - chickins?: Chickin[]; approval: BaseApproval; + chickins?: Chickin[]; + available_qtys?: AvailableQty[]; + chickin_approval?: BaseApproval; }; export type AvailableQty = { @@ -56,14 +57,6 @@ export type ClosingExpense = { reference_number: string; }; -// "flag_name": "PAKAN", -// "product_warehouse_id": 14, -// "product_id": 8, -// "product_name": "281 SPECIAL STARTER", -// "product_category": "Bahan Baku", -// "uom": "Kilogram", -// "quantity": 1100 - export type StockItem = { flag_name: string; product_warehouse_id: number; From d5b4111ae45d60dbff7e77fcc8316c3d3368cc35 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 7 Jan 2026 15:46:58 +0700 Subject: [PATCH 13/37] refactor(FE): Remove travel_document_path field and handling --- .../order/PurchaseOrderAcceptApprovalForm.tsx | 44 ------------------- .../form/order/PurchaseOrderForm.schema.ts | 8 ---- src/types/api/purchase/purchase.d.ts | 1 - 3 files changed, 53 deletions(-) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index be533059..e3997bf0 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -67,7 +67,6 @@ const PurchaseOrderAcceptApprovalForm = ({ | 'purchase_item_id' | 'received_date' | 'travel_number' - | 'travel_document_path' | 'vehicle_number' | 'expedition_vendor_id' | 'received_qty' @@ -180,7 +179,6 @@ const PurchaseOrderAcceptApprovalForm = ({ purchase_item_id: formItem.purchase_item_id || 0, received_date: formItem.received_date || '', travel_number: formItem.travel_number || '', - travel_document_path: formItem.travel_document_path || '', vehicle_number: formItem.vehicle_number || '', expedition_vendor_id: formItem.expedition_vendor_id || 0, received_qty: @@ -245,7 +243,6 @@ const PurchaseOrderAcceptApprovalForm = ({ ? new Date(item.received_date).toISOString().split('T')[0] : '', travel_number: item.travel_number || '', - travel_document_path: item.travel_document_path || '', vehicle_number: item.vehicle_number || '', expedition_vendor: item.expedition_vendor ? { @@ -262,20 +259,6 @@ const PurchaseOrderAcceptApprovalForm = ({ } }, [purchaseItems, initialValues, key]); - useEffect(() => { - if ( - formik.values.travel_documents && - formik.values.travel_documents.length > 0 - ) { - const fileNames = formik.values.travel_documents - .map((file) => file.name) - .join(', '); - formik.values.items?.forEach((item, idx) => { - formik.setFieldValue(`items.${idx}.travel_document_path`, fileNames); - }); - } - }, [formik.values.travel_documents]); - // ===== HELPER FUNCTIONS ===== const getQuantityExceededError = useCallback( (idx: number, receivedQty: number) => { @@ -522,33 +505,6 @@ const PurchaseOrderAcceptApprovalForm = ({ }} /> -
{type !== 'detail' && ( - {type !== 'detail' && ( -
- - formik.setFieldValue( - `items.${idx}.travel_document_path`, - e.target.value - ) - } - onBlur={formik.handleBlur} - isError={ - isRepeaterInputError(idx, 'travel_document_path') - .isError - } - errorMessage={ - isRepeaterInputError(idx, 'travel_document_path') - .errorMessage - } - placeholder='Masukkan path dokumen' - className={{ - wrapper: 'min-w-52 md:min-w-72 lg:min-w-80', - }} - /> - Date: Wed, 7 Jan 2026 15:54:16 +0700 Subject: [PATCH 14/37] refactor(FE): Clear file input on form reset --- .../form/order/PurchaseOrderAcceptApprovalForm.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index e3997bf0..5c093895 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useFormik } from 'formik'; import { Icon } from '@iconify/react'; import { toast } from 'react-hot-toast'; @@ -53,6 +53,7 @@ const PurchaseOrderAcceptApprovalForm = ({ const [purchaseOrderFormErrorMessage, setPurchaseOrderFormErrorMessage] = useState(''); const [key, setKey] = useState(0); + const fileInputRef = useRef(null); const isRejected = initialValues?.latest_approval?.action === 'REJECTED'; @@ -655,6 +656,7 @@ const PurchaseOrderAcceptApprovalForm = ({ name='travel_documents' label='Dokumen Surat Jalan' accept='.pdf,.jpg,.jpeg,.png' + ref={fileInputRef} onChange={(e) => { const files = Array.from(e.target.files || []); const invalidFiles = files.filter( @@ -694,6 +696,10 @@ const PurchaseOrderAcceptApprovalForm = ({ onClick={() => { if (type === 'add') { formik.resetForm(); + formik.setFieldValue('travel_documents', []); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } } setPurchaseOrderFormErrorMessage(''); onCancel?.(); From c4debcecce4f5816ca51815711cd4021f464b2ba Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 7 Jan 2026 16:18:02 +0700 Subject: [PATCH 15/37] refactor(FE): Add default empty product and delivery rows --- .../movement/form/MovementForm.schema.ts | 93 ++++++++++++------- .../inventory/movement/form/MovementForm.tsx | 32 ++++++- 2 files changed, 87 insertions(+), 38 deletions(-) diff --git a/src/components/pages/inventory/movement/form/MovementForm.schema.ts b/src/components/pages/inventory/movement/form/MovementForm.schema.ts index ea6b473c..45658295 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.schema.ts +++ b/src/components/pages/inventory/movement/form/MovementForm.schema.ts @@ -226,41 +226,62 @@ export const getMovementFormInitialValues = ( } : null, destination_warehouse_id: initialValues?.destination_warehouse?.id ?? 0, - products: - initialValues?.details?.map((detail) => ({ - product: { - value: detail.product.id, - label: detail.product.name, - }, - product_id: detail.product.id, - product_qty: detail.quantity, - })) ?? [], - deliveries: - initialValues?.deliveries?.map((d) => ({ - delivery_cost: d.shipping_cost_total ?? undefined, - delivery_cost_per_item: d.shipping_cost_item ?? undefined, - document_number: d.document_number ?? '', - document: d.document ?? null, - document_path: d.document_path ?? null, - driver_name: d.driver_name ?? '', - vehicle_plate: d.vehicle_plate ?? '', - supplier: d.supplier - ? { value: d.supplier.id, label: d.supplier.name } - : null, - supplier_id: d.supplier?.id ?? 0, - products: - d.items?.map((item) => { - const productData = detailIdToProductId.get( - item.stock_transfer_detail_id - ); - return { - product: productData - ? { value: productData.id, label: productData.name } - : null, - product_id: productData?.id ?? 0, - product_qty: item.quantity, - }; - }) ?? [], - })) ?? [], + products: initialValues?.details?.map((detail) => ({ + product: { + value: detail.product.id, + label: detail.product.name, + }, + product_id: detail.product.id, + product_qty: detail.quantity, + })) ?? [ + { + product: null, + product_id: 0, + product_qty: '', + }, + ], + deliveries: initialValues?.deliveries?.map((d) => ({ + delivery_cost: d.shipping_cost_total ?? undefined, + delivery_cost_per_item: d.shipping_cost_item ?? undefined, + document: d.document ?? null, + document_path: d.document_path ?? null, + driver_name: d.driver_name ?? '', + vehicle_plate: d.vehicle_plate ?? '', + supplier: d.supplier + ? { value: d.supplier.id, label: d.supplier.name } + : null, + supplier_id: d.supplier?.id ?? 0, + products: + d.items?.map((item) => { + const productData = detailIdToProductId.get( + item.stock_transfer_detail_id + ); + return { + product: productData + ? { value: productData.id, label: productData.name } + : null, + product_id: productData?.id ?? 0, + product_qty: item.quantity, + }; + }) ?? [], + })) ?? [ + { + delivery_cost: undefined, + delivery_cost_per_item: undefined, + document: null, + document_path: null, + driver_name: '', + vehicle_plate: '', + supplier: null, + supplier_id: 0, + products: [ + { + product: null, + product_id: 0, + product_qty: '', + }, + ], + }, + ], }; }; diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index 6002aa55..df04cb53 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -761,8 +761,36 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { type !== 'edit' && type !== 'detail' ) { - formik.setFieldValue('products', []); - formik.setFieldValue('deliveries', []); + if (formik.values.products.length === 0) { + formik.setFieldValue('products', [ + { + product: null, + product_id: 0, + product_qty: '', + }, + ]); + } + if (formik.values.deliveries.length === 0) { + formik.setFieldValue('deliveries', [ + { + delivery_cost: undefined, + delivery_cost_per_item: undefined, + document: null, + document_path: null, + driver_name: '', + vehicle_plate: '', + supplier: null, + supplier_id: 0, + products: [ + { + product: null, + product_id: 0, + product_qty: '', + }, + ], + }, + ]); + } } }, [formik.values.source_warehouse_id]); From 22be41058f478f150f6e2b4b58d2cd365b9fd3d3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 7 Jan 2026 17:35:55 +0700 Subject: [PATCH 16/37] refactor(FE): Normalize Tailwind classnames in MovementForm --- .../inventory/movement/form/MovementForm.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index df04cb53..479928fa 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -1135,7 +1135,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { Produk * @@ -1144,7 +1144,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { Qty * @@ -1157,7 +1157,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { {formik.values.products?.map((product, idx) => (
+ { Produk * @@ -1358,7 +1358,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { Qty * @@ -1367,7 +1367,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { Supplier * @@ -1376,7 +1376,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { Plat Nomor * @@ -1386,7 +1386,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { Biaya Pengiriman (Rp.) * @@ -1395,7 +1395,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { Biaya Per Item (Rp.) * @@ -1404,7 +1404,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { Nama Sopir * @@ -1417,7 +1417,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { {formik.values.deliveries?.map((delivery, idx) => (
+ Date: Wed, 7 Jan 2026 17:50:44 +0700 Subject: [PATCH 17/37] refactor(FE): Require warehouse IDs to be at least 1 --- .../pages/inventory/movement/form/MovementForm.schema.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/pages/inventory/movement/form/MovementForm.schema.ts b/src/components/pages/inventory/movement/form/MovementForm.schema.ts index 45658295..b199ed71 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.schema.ts +++ b/src/components/pages/inventory/movement/form/MovementForm.schema.ts @@ -161,6 +161,7 @@ export const MovementFormSchema: Yup.ObjectSchema = }).nullable(), source_warehouse_id: Yup.number() .required('Gudang asal wajib diisi!') + .min(1, 'Gudang asal wajib diisi!') .typeError('Gudang asal wajib diisi!'), destination_warehouse: Yup.object({ value: Yup.number().min(1).required(), @@ -170,6 +171,7 @@ export const MovementFormSchema: Yup.ObjectSchema = }).nullable(), destination_warehouse_id: Yup.number() .required('Gudang tujuan wajib diisi!') + .min(1, 'Gudang tujuan wajib diisi!') .typeError('Gudang tujuan wajib diisi!') .test( 'different-warehouse', From 319afa3fafe9ffa3af2384091af0b4d8498ecf0e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 7 Jan 2026 18:01:26 +0700 Subject: [PATCH 18/37] refactor(FE): Require positive IDs in movement form schema --- .../movement/form/MovementForm.schema.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/components/pages/inventory/movement/form/MovementForm.schema.ts b/src/components/pages/inventory/movement/form/MovementForm.schema.ts index b199ed71..d5127346 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.schema.ts +++ b/src/components/pages/inventory/movement/form/MovementForm.schema.ts @@ -85,7 +85,10 @@ const ProductObjectSchema: Yup.ObjectSchema = Yup.object({ value: Yup.number().min(1).required(), label: Yup.string().required(), }).nullable(), - product_id: Yup.number().required('Produk wajib diisi!'), + product_id: Yup.number() + .required('Produk wajib diisi!') + .min(1, 'Produk wajib diisi!') + .typeError('Produk wajib diisi!'), product_qty: Yup.number() .required('Qty wajib diisi!') .min(1, 'Qty minimal 1!') @@ -97,7 +100,10 @@ const DeliveryProductObjectSchema = Yup.object({ value: Yup.number().min(1).required(), label: Yup.string().required(), }).nullable(), - product_id: Yup.number().required('Produk wajib diisi!'), + product_id: Yup.number() + .required('Produk wajib diisi!') + .min(1, 'Produk wajib diisi!') + .typeError('Produk wajib diisi!'), product_qty: Yup.number() .required('Qty wajib diisi!') .min(1, 'Qty minimal 1!') @@ -142,7 +148,10 @@ const DeliveryObjectSchema: Yup.ObjectSchema = Yup.object({ value: Yup.number().min(1).required(), label: Yup.string().required(), }).nullable(), - supplier_id: Yup.number().required('Supplier wajib diisi!'), + supplier_id: Yup.number() + .required('Supplier wajib diisi!') + .min(1, 'Supplier wajib diisi!') + .typeError('Supplier wajib diisi!'), products: Yup.array() .of(DeliveryProductObjectSchema) .min(1, 'Minimal harus ada 1 produk!') From 0d6e229ee5e4dc4d15fd7fccddf14f566023d7ce Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 7 Jan 2026 18:11:12 +0700 Subject: [PATCH 19/37] refactor(FE): Allow null for document_path in schema --- .../pages/inventory/movement/form/MovementForm.schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/inventory/movement/form/MovementForm.schema.ts b/src/components/pages/inventory/movement/form/MovementForm.schema.ts index d5127346..048b1bd2 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.schema.ts +++ b/src/components/pages/inventory/movement/form/MovementForm.schema.ts @@ -133,7 +133,7 @@ const DeliveryObjectSchema: Yup.ObjectSchema = Yup.object({ (delivery_cost !== undefined && delivery_cost > 0) ); }), - document_path: Yup.string().optional(), + document_path: Yup.string().nullable().optional(), document_index: Yup.number().optional(), document: Yup.mixed() .nullable() From 13205ca80a2beee8f4b132c5a64a6060c5368fcf Mon Sep 17 00:00:00 2001 From: randy-ar Date: Thu, 8 Jan 2026 08:59:27 +0700 Subject: [PATCH 20/37] feat(FE): adding alert errors message for project flock and fixing bug approval status in chickin --- src/components/helper/form/FormErrors.tsx | 46 ++++++++++++ src/components/pages/ApprovalSteps.tsx | 2 +- .../form/InventoryAdjustmentForm.tsx | 1 + .../production/chickin/form/ChickinForm.tsx | 7 ++ .../chickin/form/tabs/ChickLogsView.tsx | 15 +++- .../form/ProjectFlockForm.schema.ts | 6 +- .../project-flock/form/ProjectFlockForm.tsx | 29 +++++++- src/lib/formik-helper.ts | 71 +++++++++++++++++++ 8 files changed, 169 insertions(+), 8 deletions(-) create mode 100644 src/components/helper/form/FormErrors.tsx create mode 100644 src/lib/formik-helper.ts diff --git a/src/components/helper/form/FormErrors.tsx b/src/components/helper/form/FormErrors.tsx new file mode 100644 index 00000000..a351227f --- /dev/null +++ b/src/components/helper/form/FormErrors.tsx @@ -0,0 +1,46 @@ +import Alert from '@/components/Alert'; +import Button from '@/components/Button'; +import { Icon } from '@iconify/react'; + +/** + * Alert Unique Error List + * @param formErrorList - Array of error messages + * @param onClose - Function to close the alert + */ +const AlertErrorList = ({ + formErrorList, + onClose, +}: { + formErrorList: string[]; + onClose: () => void; +}) => { + return ( + +
+
+ + + Terdapat {formErrorList.length} error pada form: + +
+ +
+
    + {formErrorList.map((error, index) => ( +
  • + {error} +
  • + ))} +
+
+ ); +}; + +export default AlertErrorList; diff --git a/src/components/pages/ApprovalSteps.tsx b/src/components/pages/ApprovalSteps.tsx index 6ae7c13a..27d03da3 100644 --- a/src/components/pages/ApprovalSteps.tsx +++ b/src/components/pages/ApprovalSteps.tsx @@ -309,7 +309,7 @@ const useApprovalSteps = ({ moduleId: string; params?: { page?: number; - limit: number; + limit: number | string; search?: string; group_step_number?: boolean; }; diff --git a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx index f134369e..525b81bb 100644 --- a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx +++ b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx @@ -125,6 +125,7 @@ const InventoryAdjustmentForm = ({ const warehouseUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ search: '', + limit: '100', }).toString()}`; const { data: warehouses, isLoading: isLoadingWarehouses } = useSWR( warehouseUrl, diff --git a/src/components/pages/production/chickin/form/ChickinForm.tsx b/src/components/pages/production/chickin/form/ChickinForm.tsx index 8c613737..b5b1dc4d 100644 --- a/src/components/pages/production/chickin/form/ChickinForm.tsx +++ b/src/components/pages/production/chickin/form/ChickinForm.tsx @@ -18,6 +18,7 @@ import { Icon } from '@iconify/react'; import Badge from '@/components/Badge'; import { CHICKINS_APPROVAL_LINE } from '@/config/approval-line'; import RequirePermission from '@/components/helper/RequirePermission'; +import { BaseApproval } from '@/types/api/api-general'; const ChickinFormKandang = ({ formType = 'add', initialValues, @@ -33,11 +34,16 @@ const ChickinFormKandang = ({ approvals, isLoading: approvalsLoading, refresh: refreshApprovals, + rawDataApprovals, } = useApprovalSteps({ latestApproval: initialValues?.chickin_approval, approvalLines: CHICKINS_APPROVAL_LINE, moduleName: 'CHICKINS', moduleId: initialValues?.id.toString() ?? '', + params: { + limit: 'limit', + group_step_number: false, + }, }); const afterSubmitFormChickin = () => { @@ -180,6 +186,7 @@ const ChickinFormKandang = ({ {openChickin && ( diff --git a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx index 80846565..e800ee68 100644 --- a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx +++ b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx @@ -8,6 +8,7 @@ import PillBadge from '@/components/PillBadge'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { formatDate, formatNumber } from '@/lib/helper'; import { ChickinApi } from '@/services/api/production/chickin'; +import { BaseApproval } from '@/types/api/api-general'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; import { Icon } from '@iconify/react'; import { useState } from 'react'; @@ -16,9 +17,11 @@ import toast from 'react-hot-toast'; const ChickinLogsView = ({ initialValues, afterSubmit, + rawDataApprovals, }: { initialValues: ProjectFlockKandang; afterSubmit?: () => void; + rawDataApprovals: BaseApproval[]; }) => { const confirmModal = useModal(); const [isApproveLoading, setIsApproveLoading] = useState(false); @@ -60,9 +63,15 @@ const ChickinLogsView = ({ ) : ( (initialValues?.chickins || []).map((chickin, index) => { + const latestApproval = rawDataApprovals[0]; const isApproved = - initialValues.chickin_approval?.step_number === 2; - const isPending = initialValues.chickin_approval?.step_number === 1; + index == (initialValues?.chickins || []).length - 1 + ? latestApproval?.step_number === 2 + : true; + const isPending = + index == (initialValues?.chickins || []).length - 1 + ? latestApproval?.step_number === 1 + : false; const quantity = isApproved ? chickin.usage_qty : isPending @@ -82,7 +91,7 @@ const ChickinLogsView = ({ {/* Header with Status Badge */}
- Chick In #{index + 1} + Chick In #{index + 1} - {latestApproval?.step_number}
= diff --git a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx index 20a8aff4..7e90c94b 100644 --- a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx +++ b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx @@ -6,6 +6,8 @@ import SelectInput, { useSelect, } from '@/components/input/SelectInput'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { getUniqueFormikErrors } from '@/lib/formik-helper'; +import AlertErrorList from '@/components/helper/form/FormErrors'; import { AreaApi, FcrApi, @@ -64,6 +66,7 @@ const ProjectFlockForm = ({ const [projectFlockFormErrorMessage, setProjectFlockFormErrorMessage] = useState(''); + const [formErrorList, setFormErrorList] = useState([]); const [selectedArea, setSelectedArea] = useState(''); const [selectedLocation, setSelectedLocation] = useState(''); const [selectedCategory, setSelectedCategory] = useState(''); @@ -134,6 +137,7 @@ const ProjectFlockForm = ({ const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({ search: '', location_id: selectedLocation == '' ? '0' : selectedLocation, + limit: 'limit', }).toString()}`; const { data: kandang, @@ -638,6 +642,17 @@ const ProjectFlockForm = ({ return !isNonstockAlreadyInBudgets; }); + const handleValidateForm = async () => { + const errors = await formik.validateForm(); + + if (Object.keys(errors).length > 0) { + // Parse and display errors + const errorMessages = getUniqueFormikErrors(errors); + setFormErrorList(errorMessages); + return; // Stop submission + } + }; + return ( <>
@@ -697,7 +712,11 @@ const ProjectFlockForm = ({
{ + e.preventDefault(); + handleValidateForm(); + formik.handleSubmit(e); + }} onReset={formik.handleReset} > {/* Form Informasi Umum */} @@ -1063,6 +1082,14 @@ const ProjectFlockForm = ({
+ {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} +
{formType !== 'detail' && ( ( + errors: FormikErrors, + parentKey: string = '' +): ErrorMessage[] { + const errorList: ErrorMessage[] = []; + + Object.keys(errors).forEach((key) => { + const value = errors[key as keyof typeof errors]; + const fullKey = parentKey ? `${parentKey}.${key}` : key; + + if (typeof value === 'string') { + // Direct error message + errorList.push({ key: fullKey, message: value }); + } else if (Array.isArray(value)) { + // Array of errors + value.forEach((item, index) => { + if (typeof item === 'string') { + errorList.push({ key: `${fullKey}[${index}]`, message: item }); + } else if (item && typeof item === 'object') { + // Nested object in array + const nestedErrors = parseFormikErrors( + item as FormikErrors, + `${fullKey}[${index}]` + ); + errorList.push(...nestedErrors); + } + }); + } else if (value && typeof value === 'object') { + // Nested object + const nestedErrors = parseFormikErrors( + value as FormikErrors, + fullKey + ); + errorList.push(...nestedErrors); + } + }); + + return errorList; +} + +/** + * Get unique error messages from Formik errors + * @param errors - Formik errors object + * @returns Array of unique error messages + */ +export function getUniqueFormikErrors(errors: FormikErrors): string[] { + const errorList = parseFormikErrors(errors); + return Array.from(new Set(errorList.map((e) => e.message))); +} + +/** + * Get all error messages from Formik errors + * @param errors - Formik errors object + * @returns Array of error messages + */ +export function getAllFormikErrors(errors: FormikErrors): ErrorMessage[] { + return parseFormikErrors(errors); +} From 0ed30e184b1fc54c6031d3473eaeb6753f22f1d3 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Thu, 8 Jan 2026 09:19:55 +0700 Subject: [PATCH 21/37] feat(FE): implement alert error list in marketing module --- .../pages/marketing/form/MarketingForm.tsx | 32 +++++++++++++++- .../delivery-order/DeliverOrderProduct.tsx | 35 ++++++++++++++--- .../sales-order/SalesOrderProduct.schema.ts | 12 ++++-- .../sales-order/SalesOrderProductForm.tsx | 38 ++++++++++++++++--- 4 files changed, 99 insertions(+), 18 deletions(-) diff --git a/src/components/pages/marketing/form/MarketingForm.tsx b/src/components/pages/marketing/form/MarketingForm.tsx index 1c5322e1..51c20d8e 100644 --- a/src/components/pages/marketing/form/MarketingForm.tsx +++ b/src/components/pages/marketing/form/MarketingForm.tsx @@ -48,6 +48,8 @@ import DeliveryOrderProductForm from '@/components/pages/marketing/form/repeater import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema'; import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema'; import RequirePermission from '@/components/helper/RequirePermission'; +import { getUniqueFormikErrors } from '@/lib/formik-helper'; +import AlertErrorList from '@/components/helper/form/FormErrors'; const MemoizedSalesOrderProductTable = memo(SalesOrderProductTable); const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm); @@ -217,6 +219,7 @@ const MarketingForm = ({ const [deliveryFormState, setDeliveryFormState] = useState<'add' | 'edit'>( 'add' ); + const [formErrorList, setFormErrorList] = useState([]); const [deliveryOrderValues, setDeliveryOrderValues] = useState< DeliveryOrderProductFormValues[] >( @@ -558,11 +561,28 @@ const MarketingForm = ({ ); }, [memoSalesOrder]); + const handleValidateForm = async () => { + const errors = await formik.validateForm(); + + if (Object.keys(errors).length > 0) { + // Parse and display errors + const errorMessages = getUniqueFormikErrors(errors); + setFormErrorList(errorMessages); + return; // Stop submission + } + }; + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + handleValidateForm(); + formik.handleSubmit(); + }; + return ( <>
+ {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} + {/* Form Actions */}
+ {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} +
diff --git a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts index b2f42254..e62ed701 100644 --- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts +++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts @@ -25,15 +25,19 @@ export const SalesOrderProductSchema: Yup.ObjectSchema { const [formErrorMessage, setFormErrorMessage] = useState(''); const [currentInput, setCurrentInput] = useState(''); + const [formErrorList, setFormErrorList] = useState([]); // ============ Formik ============ const formik = useFormik({ @@ -169,15 +172,29 @@ const SalesOrderProductForm = ({ } }; + const handleValidateForm = async () => { + const errors = await formik.validateForm(); + + if (Object.keys(errors).length > 0) { + // Parse and display errors + const errorMessages = getUniqueFormikErrors(errors); + setFormErrorList(errorMessages); + return; // Stop submission + } + }; + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + handleBlurField(currentInput); + handleValidateForm(); + formik.handleSubmit(e); + }; + return ( <> { - e.preventDefault(); - handleBlurField(currentInput); - formik.handleSubmit(e); - }} + onSubmit={handleFormSubmit} onReset={handleResetForm} > {formErrorMessage && ( @@ -338,6 +355,15 @@ const SalesOrderProductForm = ({ placeholder='Masukan Total Penjualan' />
+ + {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} +
From 965dc01e864fd2b60cc356260b94369bd95559a7 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 8 Jan 2026 09:27:51 +0700 Subject: [PATCH 22/37] feat(FE): Add location, project flock, and kandang fields --- .../recording/form/RecordingForm.schema.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/components/pages/production/recording/form/RecordingForm.schema.ts b/src/components/pages/production/recording/form/RecordingForm.schema.ts index 4901b349..90abc19a 100644 --- a/src/components/pages/production/recording/form/RecordingForm.schema.ts +++ b/src/components/pages/production/recording/form/RecordingForm.schema.ts @@ -7,6 +7,21 @@ import { } from '@/types/api/production/recording'; type RecordingGrowingFormSchemaType = { + 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; @@ -85,6 +100,30 @@ const EggObjectSchema: Yup.ObjectSchema = Yup.object({ export const RecordingGrowingFormSchema: Yup.ObjectSchema = Yup.object({ + location: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + location_id: Yup.number() + .min(1, 'Location wajib diisi!') + .required('Location wajib diisi!') + .typeError('Location wajib diisi!'), + project_flock: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + project_flock_id: Yup.number() + .min(1, 'Project flock wajib diisi!') + .required('Project flock wajib diisi!') + .typeError('Project flock wajib diisi!'), + kandang: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + kandang_id: Yup.number() + .min(1, 'Kandang wajib diisi!') + .required('Kandang wajib diisi!') + .typeError('Kandang wajib diisi!'), project_flock_kandang: Yup.object({ value: Yup.number().min(1).required(), label: Yup.string().required(), @@ -179,6 +218,12 @@ type RecordingFormData = Partial & { export const getRecordingGrowingFormInitialValues = ( initialValues?: RecordingFormData ): RecordingGrowingFormValues => ({ + location: null, + location_id: 0, + project_flock: null, + project_flock_id: 0, + kandang: null, + kandang_id: 0, project_flock_kandang: initialValues?.project_flock_kandang_id ? { value: initialValues.project_flock_kandang_id, From dbdfd2c50ba246e9953801f9c676e0e155442546 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 8 Jan 2026 09:37:05 +0700 Subject: [PATCH 23/37] refactor(FE): Remove location, project_flock and kandang fields --- .../recording/form/RecordingForm.schema.ts | 45 ------------------- 1 file changed, 45 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.schema.ts b/src/components/pages/production/recording/form/RecordingForm.schema.ts index 90abc19a..4901b349 100644 --- a/src/components/pages/production/recording/form/RecordingForm.schema.ts +++ b/src/components/pages/production/recording/form/RecordingForm.schema.ts @@ -7,21 +7,6 @@ import { } from '@/types/api/production/recording'; type RecordingGrowingFormSchemaType = { - 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; @@ -100,30 +85,6 @@ const EggObjectSchema: Yup.ObjectSchema = Yup.object({ export const RecordingGrowingFormSchema: Yup.ObjectSchema = Yup.object({ - location: Yup.object({ - value: Yup.number().min(1).required(), - label: Yup.string().required(), - }).nullable(), - location_id: Yup.number() - .min(1, 'Location wajib diisi!') - .required('Location wajib diisi!') - .typeError('Location wajib diisi!'), - project_flock: Yup.object({ - value: Yup.number().min(1).required(), - label: Yup.string().required(), - }).nullable(), - project_flock_id: Yup.number() - .min(1, 'Project flock wajib diisi!') - .required('Project flock wajib diisi!') - .typeError('Project flock wajib diisi!'), - kandang: Yup.object({ - value: Yup.number().min(1).required(), - label: Yup.string().required(), - }).nullable(), - kandang_id: Yup.number() - .min(1, 'Kandang wajib diisi!') - .required('Kandang wajib diisi!') - .typeError('Kandang wajib diisi!'), project_flock_kandang: Yup.object({ value: Yup.number().min(1).required(), label: Yup.string().required(), @@ -218,12 +179,6 @@ type RecordingFormData = Partial & { export const getRecordingGrowingFormInitialValues = ( initialValues?: RecordingFormData ): RecordingGrowingFormValues => ({ - location: null, - location_id: 0, - project_flock: null, - project_flock_id: 0, - kandang: null, - kandang_id: 0, project_flock_kandang: initialValues?.project_flock_kandang_id ? { value: initialValues.project_flock_kandang_id, From 3cb6bfcf5208f0fef226cf54393d5f43306fa9c1 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 8 Jan 2026 09:47:33 +0700 Subject: [PATCH 24/37] feat(FE): Add chart data types to Uniformity API types --- src/types/api/production/uniformity.d.ts | 57 ++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/types/api/production/uniformity.d.ts b/src/types/api/production/uniformity.d.ts index 0863c08a..1788742c 100644 --- a/src/types/api/production/uniformity.d.ts +++ b/src/types/api/production/uniformity.d.ts @@ -1,6 +1,62 @@ import { BaseMetadata } from '@/types/api/api-general'; import { BaseApproval } from '@/types/api/approval/approval'; +// ==================== CHART DATA TYPES ==================== +export type WeightDistributionData = { + weight: number; + count: number; +}; + +export type StatisticsData = { + mean: number; + standardDeviation: number; + min: number; + max: number; +}; + +export type WeekBarChartData = { + has_data: boolean; + weight_distribution: WeightDistributionData[]; + ideal_range: { + min: number; + max: number; + } | null; + statistics: StatisticsData | null; +}; + +export type BarChart = { + current_week: number; + all_weeks: Record; +}; + +export type AvailableWeek = { + week: number; + uniformity_percentage: number; + ideal_count: number; + outside_ideal_count: number; + total_count: number; + has_data: boolean; +}; + +export type WeekInfo = { + total_weeks: number; + weeks_with_data: number; + current_week_index: number; + has_prev_week: boolean; + has_next_week: boolean; +}; + +export type GaugeChart = { + current_week: number; + available_weeks: AvailableWeek[]; + week_info: WeekInfo; +}; + +export type ChartData = { + bar_chart: BarChart; + gauge_chart: GaugeChart; +}; + // ==================== GET ALL RESPONSE ==================== export type Uniformity = BaseMetadata & { id: number; @@ -21,6 +77,7 @@ export type Uniformity = BaseMetadata & { standard_mean_weight: number | null; standard_uniformity: number | null; created_at: string; + chart_data?: ChartData; created_by: number; latest_approval?: BaseApproval; }; From 662dec38bc055c93d97f6dfc35c37b3b48a0c432 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 8 Jan 2026 09:51:10 +0700 Subject: [PATCH 25/37] refactor(FE): Add with_chart param to date filter --- src/components/pages/production/uniformity/UniformityTable.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index 0c0c3f70..886fe6ef 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -374,6 +374,7 @@ const UniformityTable = () => { if (filterEndDate) { queryParams.append('end_date', filterEndDate); } + queryParams.append('with_chart', 'true'); } const tableQueryString = getTableFilterQueryString(); From b3c4a438ad47cebe54b777d7307cb0c1021e7922 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 8 Jan 2026 10:17:31 +0700 Subject: [PATCH 26/37] refactor(FE): Use chart_data for Uniformity and add week nav --- .../production/uniformity/UniformityChart.tsx | 126 +++++++++--------- .../production/uniformity/UniformityTable.tsx | 32 +---- src/types/api/production/uniformity.d.ts | 32 +++-- 3 files changed, 88 insertions(+), 102 deletions(-) diff --git a/src/components/pages/production/uniformity/UniformityChart.tsx b/src/components/pages/production/uniformity/UniformityChart.tsx index 77d1608d..52e7a24b 100644 --- a/src/components/pages/production/uniformity/UniformityChart.tsx +++ b/src/components/pages/production/uniformity/UniformityChart.tsx @@ -1,93 +1,86 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import Card from '@/components/Card'; import UniformityBarChart from '@/components/pages/production/uniformity/chart/UniformityBarChart'; import UniformityGaugeChart from '@/components/pages/production/uniformity/chart/UniformityGaugeChart'; import UniformityBarChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton'; import UniformityGaugeChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton'; -import { - UniformityDetailItem, - Uniformity, -} from '@/types/api/production/uniformity'; +import { Uniformity, type ChartData } from '@/types/api/production/uniformity'; interface UniformityChartProps { uniformityData?: Uniformity | null; - uniformityDetails?: UniformityDetailItem[]; + isFiltered?: boolean; } const UniformityChart = ({ uniformityData, - uniformityDetails, + isFiltered = false, }: UniformityChartProps) => { - const defaultUniformityDetails: UniformityDetailItem[] = [ - { id: 1, weight: 61, range: 'Ideal' }, - { id: 2, weight: 62, range: 'Ideal' }, - { id: 3, weight: 63, range: 'Ideal' }, - { id: 4, weight: 64, range: 'Ideal' }, - { id: 5, weight: 65, range: 'Ideal' }, - { id: 6, weight: 66, range: 'Ideal' }, - { id: 7, weight: 67, range: 'Ideal' }, - ]; + const [currentWeekIndex, setCurrentWeekIndex] = useState(0); - const detailsToUse = uniformityDetails || defaultUniformityDetails; + const chartData = useMemo((): ChartData | undefined => { + if (!uniformityData?.chart_data) return undefined; + return uniformityData.chart_data; + }, [uniformityData]); const barChartData = useMemo(() => { - if (!uniformityData) { + if (!chartData?.bar_chart) { return []; } - if (!detailsToUse || detailsToUse.length === 0) { + const { bar_chart } = chartData; + const currentWeekStr = String(bar_chart.current_week); + const weekData = bar_chart.all_weeks[currentWeekStr]; + + if (!weekData || !weekData.has_data) { return []; } - const weights = detailsToUse.map((d) => d.weight); - const minWeight = Math.floor(Math.min(...weights) / 5) * 5; - const maxWeight = Math.ceil(Math.max(...weights) / 5) * 5; - - const rangeSize = maxWeight - minWeight < 11 ? 4 : 5; - const ranges: string[] = []; - - for (let start = minWeight; start <= maxWeight; start += rangeSize) { - const end = start + rangeSize; - ranges.push(`${start}-${end}`); - } - - const totalIdealCount = detailsToUse.filter( - (d) => d.range === 'Ideal' - ).length; - - return ranges.map((range) => { - const [minStr, maxStr] = range.split('-').map(Number); - const min = minStr; - const max = maxStr; - - const birdsInRange = detailsToUse.filter( - (d) => d.weight >= min && d.weight < max - ).length; - - const hasIdeal = detailsToUse.some( - (d) => d.range === 'Ideal' && d.weight >= min && d.weight < max - ); - - return { - name: range, - uv: birdsInRange, - isIdeal: hasIdeal, - idealCount: hasIdeal ? totalIdealCount : undefined, - }; - }); - }, [uniformityData, detailsToUse]); + return weekData.weight_distribution.map((range) => ({ + name: range.range, + uv: range.bird_count, + isIdeal: range.is_ideal_range, + idealCount: range.is_ideal_range + ? weekData.ideal_range.total_ideal_birds + : undefined, + })); + }, [chartData]); const gaugeChartData = useMemo(() => { - if (!uniformityData) return undefined; + if (!chartData?.gauge_chart || !uniformityData) return undefined; + + const { gauge_chart } = chartData; + const currentWeekData = gauge_chart.available_weeks[currentWeekIndex]; + + if (!currentWeekData || !currentWeekData.has_data) { + return undefined; + } return { - value: uniformityData.uniformity, + value: currentWeekData.uniformity_percentage, label: 'Uniformity', - week: `Week ${uniformityData.week}`, - currentValue: uniformityData.uniform_qty, - totalValue: uniformityData.chick_qty_of_weight, + week: `Week ${currentWeekData.week}`, + currentValue: currentWeekData.ideal_count, + totalValue: currentWeekData.total_count, + hasPrevWeek: gauge_chart.week_info.has_prev_week, + hasNextWeek: gauge_chart.week_info.has_next_week, }; - }, [uniformityData]); + }, [chartData, currentWeekIndex, uniformityData]); + + const handleWeekChange = (direction: 'prev' | 'next') => { + if (!chartData?.gauge_chart) return; + + const { available_weeks, week_info } = chartData.gauge_chart; + + if (direction === 'prev' && week_info.has_prev_week) { + setCurrentWeekIndex((prev) => Math.max(0, prev - 1)); + } else if (direction === 'next' && week_info.has_next_week) { + setCurrentWeekIndex((prev) => + Math.min(available_weeks.length - 1, prev + 1) + ); + } + }; + + const shouldShowEmptyState = !isFiltered; return (
@@ -100,14 +93,16 @@ const UniformityChart = ({ }} >
- {!uniformityData || barChartData.length === 0 ? ( + {shouldShowEmptyState || + !uniformityData || + barChartData.length === 0 ? ( ) : ( )}
- {!uniformityData || !gaugeChartData ? ( + {shouldShowEmptyState || !uniformityData || !gaugeChartData ? ( )} diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index 886fe6ef..08e6df51 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -151,8 +151,10 @@ const UniformityConfirmationPreview = ({ const UniformityChartWrapper = ({ uniformitySwrKey, + isFiltered, }: { uniformitySwrKey: string; + isFiltered: boolean; }) => { const { data: uniformities } = useSWR( uniformitySwrKey, @@ -166,31 +168,8 @@ const UniformityChartWrapper = ({ return null; }, [uniformities]); - const shouldFetchDetails = !!uniformityData; - const uniformityDetailSwrKey = useMemo(() => { - if (!uniformityData) return null; - return `${UniformityApi.basePath}/${uniformityData.id}?with_details=true`; - }, [uniformityData]); - - const { data: uniformityDetailResponse } = useSWR( - uniformityDetailSwrKey, - shouldFetchDetails ? UniformityApi.getAllFetcher : null - ); - - const uniformityDetails = useMemo(() => { - if (shouldFetchDetails && isResponseSuccess(uniformityDetailResponse)) { - const detailData = - uniformityDetailResponse.data as unknown as UniformityDetail; - return detailData.uniformity_details; - } - return undefined; - }, [shouldFetchDetails, uniformityDetailResponse]); - return ( - + ); }; @@ -897,7 +876,10 @@ const UniformityTable = () => {
- +
Date: Thu, 8 Jan 2026 10:29:08 +0700 Subject: [PATCH 27/37] refactor(FE): Move sampling and result tables to UniformityDetail --- .../uniformity/detail/UniformityDetail.tsx | 151 +++++++++++++++--- .../detail/UniformityDetailsPreview.tsx | 123 +------------- 2 files changed, 132 insertions(+), 142 deletions(-) diff --git a/src/components/pages/production/uniformity/detail/UniformityDetail.tsx b/src/components/pages/production/uniformity/detail/UniformityDetail.tsx index 4d0cd887..a89c15d2 100644 --- a/src/components/pages/production/uniformity/detail/UniformityDetail.tsx +++ b/src/components/pages/production/uniformity/detail/UniformityDetail.tsx @@ -11,7 +11,7 @@ import Badge from '@/components/Badge'; import Tooltip from '@/components/Tooltip'; import RequirePermission from '@/components/helper/RequirePermission'; import { UniformityDetail as UniformityDetailType } from '@/types/api/production/uniformity'; -import { formatDate } from '@/lib/helper'; +import { formatDate, formatNumber } from '@/lib/helper'; import { useUiStore } from '@/stores/ui/ui.store'; import UniformityDetailsPreview from '@/components/pages/production/uniformity/detail/UniformityDetailsPreview'; import { @@ -47,8 +47,6 @@ const UniformityDetail: React.FC = ({ ); @@ -154,7 +152,7 @@ const UniformityDetail: React.FC = ({ return (
{valueMap[id]} - + - -
- - - ) : null}
+ + {/* Sampling and Range */} + {initialValues.sampling && ( +
+

Sampling and Range

+ + data={samplingTableData} + columns={columnsSampling} + pageSize={4} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> +
+ )} + + {/* Result */} + {initialValues.result && ( +
+

Result

+ + data={resultTableData} + columns={resultColumns} + pageSize={4} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> +
+ )} + + {/* Approve/Reject Buttons */} + {initialValues.result && + initialValues.latest_approval?.step_name === 'CREATED' ? ( + <> +
+ +
+ + +
+
+ + ) : null}
) : (
diff --git a/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx b/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx index 21be03d7..35d88771 100644 --- a/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx +++ b/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx @@ -7,14 +7,10 @@ import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { useUiStore } from '@/stores/ui/ui.store'; import { UniformityDetailItem, - UniformitySampling, - UniformityResult, UniformityInfoUmum, } from '@/types/api/production/uniformity'; import Table from '@/components/Table'; import Badge from '@/components/Badge'; -import { formatNumber } from '@/lib/helper'; -import { DetailOptionType } from '@/types/api/production/uniformity'; import { getWeightStatusColor, getWeightStatusIndicatorColor, @@ -28,8 +24,6 @@ import { isResponseSuccess } from '@/lib/api-helper'; interface UniformityDetailsPreviewProps { info_umum: UniformityInfoUmum; - sampling: UniformitySampling; - result: UniformityResult; uniformity_details?: UniformityDetailItem[]; uniformityId: number; } @@ -37,8 +31,6 @@ interface UniformityDetailsPreviewProps { const UniformityDetailsPreview = ({ info_umum, uniformity_details: initialUniformityDetails, - sampling, - result, uniformityId, }: UniformityDetailsPreviewProps) => { const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen); @@ -66,87 +58,6 @@ const UniformityDetailsPreview = ({ setShouldFetchDetails(true); }; - const samplingTableData: DetailOptionType[] = useMemo(() => { - if (!sampling) return []; - - return [ - { - id: 'sampling-size', - label: 'Sampling size', - value: `${formatNumber(sampling.chick_qty_of_weight)} of Birds`, - }, - { - id: 'mean-weight', - label: 'Mean Weight', - value: `${sampling.mean_weight} g`, - }, - { - id: 'min-limit', - label: 'Min Limit (-10%)', - value: `${sampling.mean_down} g`, - }, - { - id: 'max-limit', - label: 'Max Limit (+10%)', - value: `${sampling.mean_up} g`, - }, - ]; - }, [sampling]); - - const columnsSampling: ColumnDef[] = useMemo( - () => [ - { - accessorKey: 'label', - header: 'Label', - cell: (props) => props.row.original.label, - }, - { - accessorKey: 'value', - header: 'Value', - cell: (props) => {props.row.original.value}, - }, - ], - [] - ); - - const resultTableData: DetailOptionType[] = useMemo(() => { - if (!result) return []; - - return [ - { - id: 'ideal-birds', - label: 'Ideal Birds', - value: `${formatNumber(result.uniform_qty)} of Birds`, - }, - { - id: 'outside-range', - label: 'Outside Range', - value: `${formatNumber(result.outside_qty)} of Birds`, - }, - { - id: 'uniformity', - label: 'Uniformity', - value: `${result.uniformity} %`, - }, - ]; - }, [result]); - - const resultColumns: ColumnDef[] = useMemo( - () => [ - { - accessorKey: 'label', - header: 'Label', - cell: (props) => props.row.original.label, - }, - { - accessorKey: 'value', - header: 'Value', - cell: (props) => {props.row.original.value}, - }, - ], - [] - ); - const tableData = useMemo(() => { if (!uniformity_details) return []; @@ -229,40 +140,8 @@ const UniformityDetailsPreview = ({ {/* Form Section */}
- {info_umum || sampling || result ? ( + {info_umum ? (
- {/* Sampling and Range */} - {sampling && ( -
-

Sampling and Range

- - data={samplingTableData} - columns={columnsSampling} - pageSize={4} - className={{ - containerClassName: 'mb-0', - paginationClassName: 'hidden', - }} - /> -
- )} - - {/* Result */} - {result && ( -
-

Result

- - data={resultTableData} - columns={resultColumns} - pageSize={4} - className={{ - containerClassName: 'mb-0', - paginationClassName: 'hidden', - }} - /> -
- )} - {!uniformity_details || uniformity_details.length === 0 ? (
From a4a07f2ce9c89349c2bf8758d97b6024a7d39ae2 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 8 Jan 2026 10:39:21 +0700 Subject: [PATCH 29/37] refactor(FE): Remove on-demand weight fetch and show empty state --- .../detail/UniformityDetailsPreview.tsx | 54 +++++-------------- 1 file changed, 14 insertions(+), 40 deletions(-) diff --git a/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx b/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx index 35d88771..05d21535 100644 --- a/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx +++ b/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; import { Icon } from '@iconify/react'; import { ColumnDef } from '@tanstack/react-table'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; @@ -17,10 +17,6 @@ import { getWeightStatusText, } from '@/components/pages/production/uniformity/uniformity-utils'; import { BodyWeightData } from '@/types/api/production/uniformity'; -import Button from '@/components/Button'; -import { UniformityApi } from '@/services/api/uniformity'; -import useSWR from 'swr'; -import { isResponseSuccess } from '@/lib/api-helper'; interface UniformityDetailsPreviewProps { info_umum: UniformityInfoUmum; @@ -30,34 +26,14 @@ interface UniformityDetailsPreviewProps { const UniformityDetailsPreview = ({ info_umum, - uniformity_details: initialUniformityDetails, - uniformityId, + uniformity_details, }: UniformityDetailsPreviewProps) => { const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen); - const [shouldFetchDetails, setShouldFetchDetails] = useState(false); - - const { data: uniformityDetailResponse, isLoading } = useSWR( - shouldFetchDetails - ? `uniformity-detail-${uniformityId}-with-details` - : null, - () => UniformityApi.getUniformityDetail(uniformityId, true) - ); - - const uniformity_details = useMemo(() => { - if (shouldFetchDetails && isResponseSuccess(uniformityDetailResponse)) { - return uniformityDetailResponse.data.uniformity_details; - } - return initialUniformityDetails; - }, [shouldFetchDetails, uniformityDetailResponse, initialUniformityDetails]); const handleClose = () => { setExpandedDrawerOpen(false); }; - const fetchWeightData = () => { - setShouldFetchDetails(true); - }; - const tableData = useMemo(() => { if (!uniformity_details) return []; @@ -142,21 +118,8 @@ const UniformityDetailsPreview = ({
{info_umum ? (
- {!uniformity_details || uniformity_details.length === 0 ? ( -
- -
- ) : null} - {/* Body Weight Details */} - {uniformity_details && uniformity_details.length > 0 && ( + {uniformity_details && uniformity_details.length > 0 ? (
data={tableData} @@ -165,6 +128,17 @@ const UniformityDetailsPreview = ({ className={{ containerClassName: 'mb-5' }} />
+ ) : ( +
+ +

No data available

+

Body weight details not found

+
)}
) : ( From 549e15499c8b472387716f600d399ec5c45034d7 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 8 Jan 2026 10:42:07 +0700 Subject: [PATCH 30/37] feat(FE): Add CV field and section bottom margin --- .../production/uniformity/detail/UniformityDetail.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/pages/production/uniformity/detail/UniformityDetail.tsx b/src/components/pages/production/uniformity/detail/UniformityDetail.tsx index 05572099..e8e5f1b3 100644 --- a/src/components/pages/production/uniformity/detail/UniformityDetail.tsx +++ b/src/components/pages/production/uniformity/detail/UniformityDetail.tsx @@ -289,6 +289,11 @@ const UniformityDetail: React.FC = ({ label: 'Uniformity', value: `${initialValues.result.uniformity} %`, }, + { + id: 'cv', + label: 'CV', + value: `${initialValues.result.cv} %`, + }, ]; }, [initialValues.result]); @@ -320,7 +325,7 @@ const UniformityDetail: React.FC = ({ {/* Form Section */}
-
+
{initialValues ? (
{/* Info Umum */} From 3dd4a9cebcf31b444ee453c6d8d2565036750bce Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 8 Jan 2026 10:51:47 +0700 Subject: [PATCH 31/37] refactor(FE): Reset submission flag when clearing uniformity filters --- src/components/pages/production/uniformity/UniformityTable.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index 08e6df51..9caa98a9 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -413,6 +413,7 @@ const UniformityTable = () => { ); const handleResetFilters = useCallback(() => { + setIsSubmitted(false); setFilterLocation(null); setFilterProjectFlock(null); setFilterKandang(null); From 0898892f15a34f97d0d06acaf8124095b8337a74 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 8 Jan 2026 11:11:58 +0700 Subject: [PATCH 32/37] feat(FE): Show unique form errors and improve product form --- .../form/ProductCategoryForm.tsx | 30 +++++++++++++- .../product/form/ProductForm.schema.ts | 28 ++++++------- .../master-data/product/form/ProductForm.tsx | 40 ++++++++++++++++--- 3 files changed, 77 insertions(+), 21 deletions(-) diff --git a/src/components/pages/master-data/product-category/form/ProductCategoryForm.tsx b/src/components/pages/master-data/product-category/form/ProductCategoryForm.tsx index dc34ac5e..d241a3dd 100644 --- a/src/components/pages/master-data/product-category/form/ProductCategoryForm.tsx +++ b/src/components/pages/master-data/product-category/form/ProductCategoryForm.tsx @@ -11,6 +11,8 @@ import TextInput from '@/components/input/TextInput'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import RequirePermission from '@/components/helper/RequirePermission'; +import { getUniqueFormikErrors } from '@/lib/formik-helper'; +import AlertErrorList from '@/components/helper/form/FormErrors'; import { ProductCategoryFormSchema, @@ -39,6 +41,7 @@ const ProductCategoryForm = ({ const deleteModal = useModal(); const [formErrorMessage, setFormErrorMessage] = useState(''); + const [formErrorList, setFormErrorList] = useState([]); const [isDeleteLoading, setIsDeleteLoading] = useState(false); const createProductCategoryHandler = useCallback( @@ -129,6 +132,22 @@ const ProductCategoryForm = ({ formikSetValues(formikInitialValues); }, [formikSetValues, formikInitialValues]); + const handleValidateForm = async () => { + const errors = await formik.validateForm(); + + if (Object.keys(errors).length > 0) { + const errorMessages = getUniqueFormikErrors(errors); + setFormErrorList(errorMessages); + return; + } + }; + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + handleValidateForm(); + formik.handleSubmit(e); + }; + return ( <>
@@ -150,7 +169,7 @@ const ProductCategoryForm = ({ @@ -164,6 +183,15 @@ const ProductCategoryForm = ({ {formErrorMessage}
)} + + {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} +
= sku: Yup.string().required('SKU wajib diisi!'), uom: Yup.object({ - value: Yup.number().min(1, 'Satuan wajib dipilih!').required(), - label: Yup.string().required(), - }) - .nullable() - .required('Satuan wajib diisi!'), + value: Yup.number() + .min(1, 'Satuan wajib dipilih!') + .required('Satuan wajib dipilih!'), + label: Yup.string().required('Satuan wajib dipilih!'), + }).nullable(), uom_id: Yup.number() .min(1, 'Satuan wajib dipilih!') - .required('Satuan wajib diisi!') - .typeError('Satuan wajib diisi!'), + .required('Satuan wajib dipilih!') + .typeError('Satuan wajib dipilih!'), product_category: Yup.object({ - value: Yup.number().min(1, 'Kategori produk wajib dipilih!').required(), - label: Yup.string().required(), - }) - .nullable() - .required('Kategori produk wajib diisi!'), + value: Yup.number() + .min(1, 'Kategori produk wajib dipilih!') + .required('Kategori produk wajib dipilih!'), + label: Yup.string().required('Kategori produk wajib dipilih!'), + }).nullable(), product_category_id: Yup.number() .min(1, 'Kategori produk wajib dipilih!') - .required('Kategori produk wajib diisi!') - .typeError('Kategori produk wajib diisi!'), + .required('Kategori produk wajib dipilih!') + .typeError('Kategori produk wajib dipilih!'), product_price: Yup.number() .required('Harga produk wajib diisi!') diff --git a/src/components/pages/master-data/product/form/ProductForm.tsx b/src/components/pages/master-data/product/form/ProductForm.tsx index 200b9d39..bf4cf1ee 100644 --- a/src/components/pages/master-data/product/form/ProductForm.tsx +++ b/src/components/pages/master-data/product/form/ProductForm.tsx @@ -17,6 +17,8 @@ import SelectInput, { import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import RequirePermission from '@/components/helper/RequirePermission'; +import { getUniqueFormikErrors } from '@/lib/formik-helper'; +import AlertErrorList from '@/components/helper/form/FormErrors'; import { ProductFormSchema, @@ -48,6 +50,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { const deleteModal = useModal(); const [productFormErrorMessage, setProductFormErrorMessage] = useState(''); + const [formErrorList, setFormErrorList] = useState([]); const [isDeleteLoading, setIsDeleteLoading] = useState(false); const createProductHandler = useCallback( @@ -201,6 +204,22 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { formikSetValues(formikInitialValues); }, [formikSetValues, formikInitialValues]); + const handleValidateForm = async () => { + const errors = await formik.validateForm(); + + if (Object.keys(errors).length > 0) { + const errorMessages = getUniqueFormikErrors(errors); + setFormErrorList(errorMessages); + return; + } + }; + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + handleValidateForm(); + formik.handleSubmit(e); + }; + return ( <>
@@ -220,7 +239,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { @@ -234,6 +253,15 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { {productFormErrorMessage}
)} + + {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} +
{ errorMessage={formik.errors.name} readOnly={type === 'detail'} /> -
+
{ readOnly={type === 'detail'} />
-
+
{ isClearable />
-
+
{ readOnly={type === 'detail'} />
-
+
{ readOnly={type === 'detail'} />
-
+
Date: Thu, 8 Jan 2026 12:27:26 +0700 Subject: [PATCH 33/37] refactor(FE): Show validation error list before submit --- .../inventory/movement/form/MovementForm.tsx | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index 479928fa..62a23595 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -36,6 +36,8 @@ import CheckboxInput from '@/components/input/CheckboxInput'; import Badge from '@/components/Badge'; import Card from '@/components/Card'; import { S3_PUBLIC_BASE_URL } from '@/config/constant'; +import { getUniqueFormikErrors } from '@/lib/formik-helper'; +import AlertErrorList from '@/components/helper/form/FormErrors'; interface MovementFormProps { type?: 'add' | 'edit' | 'detail'; @@ -53,6 +55,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { ] = useState(''); const [selectedProducts, setSelectedProducts] = useState([]); const [selectedDeliveries, setSelectedDeliveries] = useState([]); + const [formErrorList, setFormErrorList] = useState([]); // ===== FORM HANDLERS ===== const createMovementHandler = useCallback( @@ -819,6 +822,22 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { formik.errors.destination_warehouse_id, ]); + const handleValidateForm = async () => { + const errors = await formik.validateForm(); + + if (Object.keys(errors).length > 0) { + const errorMessages = getUniqueFormikErrors(errors); + setFormErrorList(errorMessages); + return; + } + }; + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + handleValidateForm(); + formik.handleSubmit(e); + }; + return ( <>
@@ -838,7 +857,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { @@ -852,6 +871,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { {movementFormErrorMessage}
)} + + {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} + {/* Top card - Movement details */} Date: Thu, 8 Jan 2026 12:31:25 +0700 Subject: [PATCH 34/37] refactor(FE): Display Formik validation errors in alert list --- .../recording/form/RecordingForm.tsx | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index bd866927..4a9d6c13 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -17,6 +17,7 @@ import CheckboxInput from '@/components/input/CheckboxInput'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; import { useModal } from '@/components/Modal'; +import AlertErrorList from '@/components/helper/form/FormErrors'; import { ProjectFlockKandangApi, @@ -52,6 +53,7 @@ import { import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; import { formatDate, formatNumber } from '@/lib/helper'; +import { getUniqueFormikErrors } from '@/lib/formik-helper'; import toast from 'react-hot-toast'; import ApprovalSteps, { useApprovalSteps, @@ -91,6 +93,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const [, setApprovalNotes] = useState(''); const [recordingFormErrorMessage, setRecordingFormErrorMessage] = useState(''); + const [formErrorList, setFormErrorList] = useState([]); const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [, setNewRecordingData] = useState(null); const [nextDayRecording, setNextDayRecording] = @@ -757,6 +760,22 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }, }); + const handleValidateForm = async () => { + const errors = await formik.validateForm(); + + if (Object.keys(errors).length > 0) { + const errorMessages = getUniqueFormikErrors(errors); + setFormErrorList(errorMessages); + return; + } + }; + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + handleValidateForm(); + formik.handleSubmit(e); + }; + // ===== HELPER FUNCTIONS ===== useCallback((): OptionType | null => { if ( @@ -1322,7 +1341,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { )} {recordingFormErrorMessage && ( @@ -1335,6 +1354,15 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {recordingFormErrorMessage}
)} + + {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} + {/* Basic Info Card */} {(type === 'add' || type === 'edit') && ( Date: Thu, 8 Jan 2026 12:35:16 +0700 Subject: [PATCH 35/37] refactor(FE): Display form error list in purchase forms --- .../order/PurchaseOrderAcceptApprovalForm.tsx | 30 ++++++++++++++++- .../order/PurchaseOrderStaffApprovalForm.tsx | 33 ++++++++++++++++--- .../form/request/PurchaseRequestForm.tsx | 30 ++++++++++++++++- 3 files changed, 87 insertions(+), 6 deletions(-) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index 5c093895..2b18afee 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -14,6 +14,7 @@ import SelectInput, { useSelect, } from '@/components/input/SelectInput'; import { useRouter } from 'next/navigation'; +import AlertErrorList from '@/components/helper/form/FormErrors'; import { PurchaseRequestAcceptApprovalFormDefaultValues, @@ -28,6 +29,7 @@ import { } from '@/types/api/purchase/purchase'; import DateInput from '@/components/input/DateInput'; import { formatNumber } from '@/lib/helper'; +import { getUniqueFormikErrors } from '@/lib/formik-helper'; import { Supplier } from '@/types/api/master-data/supplier'; import { SupplierApi } from '@/services/api/master-data'; @@ -52,6 +54,7 @@ const PurchaseOrderAcceptApprovalForm = ({ const searchParams = useSearchParams(); const [purchaseOrderFormErrorMessage, setPurchaseOrderFormErrorMessage] = useState(''); + const [formErrorList, setFormErrorList] = useState([]); const [key, setKey] = useState(0); const fileInputRef = useRef(null); @@ -209,6 +212,22 @@ const PurchaseOrderAcceptApprovalForm = ({ }, }); + const handleValidateForm = async () => { + const errors = await formik.validateForm(); + + if (Object.keys(errors).length > 0) { + const errorMessages = getUniqueFormikErrors(errors); + setFormErrorList(errorMessages); + return; + } + }; + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + handleValidateForm(); + formik.handleSubmit(e); + }; + // ===== API DATA FETCHING ===== const purchaseItems = useMemo(() => { if (initialValues?.items) { @@ -336,7 +355,7 @@ const PurchaseOrderAcceptApprovalForm = ({ return (
@@ -355,6 +374,15 @@ const PurchaseOrderAcceptApprovalForm = ({ {purchaseOrderFormErrorMessage}
)} + + {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} +
diff --git a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx index 95ca2f50..729b6782 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx @@ -12,10 +12,12 @@ import NumberInput from '@/components/input/NumberInput'; import SelectInput, { OptionType } from '@/components/input/SelectInput'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import { useModal } from '@/components/Modal'; +import AlertErrorList from '@/components/helper/form/FormErrors'; import { SupplierApi } from '@/services/api/master-data'; import { SupplierProducts } from '@/types/api/master-data/supplier'; import { isResponseSuccess } from '@/lib/api-helper'; +import { getUniqueFormikErrors } from '@/lib/formik-helper'; import { PurchaseRequestStaffApprovalFormDefaultValues, PurchaseRequestStaffApprovalFormInitialValues, @@ -87,6 +89,7 @@ const PurchaseOrderStaffApprovalForm = ({ const deleteModal = useModal(); const [purchaseOrderFormErrorMessage, setPurchaseOrderFormErrorMessage] = useState(''); + const [formErrorList, setFormErrorList] = useState([]); const [selectedItemForDelete, setSelectedItemForDelete] = useState< number | null >(null); @@ -415,6 +418,22 @@ const PurchaseOrderStaffApprovalForm = ({ }, }); + const handleValidateForm = async () => { + const errors = await formik.validateForm(); + + if (Object.keys(errors).length > 0) { + const errorMessages = getUniqueFormikErrors(errors); + setFormErrorList(errorMessages); + return; + } + }; + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + handleValidateForm(); + formik.handleSubmit(e); + }; + const supplierProductOptions = baseSupplierProductOptions; // ===== API DATA FETCHING ===== @@ -652,10 +671,7 @@ const PurchaseOrderStaffApprovalForm = ({ return ( <> - +

{type === 'add' @@ -672,6 +688,15 @@ const PurchaseOrderStaffApprovalForm = ({ {purchaseOrderFormErrorMessage}

)} + + {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} +
{groupedPurchaseItems.length > 0 ? (
diff --git a/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx index ef428b25..0c319f5a 100644 --- a/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx +++ b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx @@ -16,6 +16,7 @@ import SelectInput, { } from '@/components/input/SelectInput'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import { useModal } from '@/components/Modal'; +import AlertErrorList from '@/components/helper/form/FormErrors'; import { PurchaseRequestFormSchema, @@ -32,6 +33,7 @@ import { import { Supplier, SupplierProducts } from '@/types/api/master-data/supplier'; import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; import { formatNumber } from '@/lib/helper'; +import { getUniqueFormikErrors } from '@/lib/formik-helper'; import { PurchaseApi } from '@/services/api/purchase'; import Card from '@/components/Card'; @@ -59,6 +61,7 @@ const PurchaseRequestForm = ({ ); const [purchaseRequestFormErrorMessage, setPurchaseRequestFormErrorMessage] = useState(''); + const [formErrorList, setFormErrorList] = useState([]); // ===== TYPE DEFINITIONS ===== interface ProductOptionType { @@ -211,6 +214,22 @@ const PurchaseRequestForm = ({ }, }); + const handleValidateForm = async () => { + const errors = await formik.validateForm(); + + if (Object.keys(errors).length > 0) { + const errorMessages = getUniqueFormikErrors(errors); + setFormErrorList(errorMessages); + return; + } + }; + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + handleValidateForm(); + formik.handleSubmit(e); + }; + // ===== API DATA FETCHING ===== const { data: supplierData, isLoading: isLoadingProducts } = useSWR( formik.values.supplier_id && Number(formik.values.supplier_id) > 0 @@ -487,7 +506,7 @@ const PurchaseRequestForm = ({ @@ -501,6 +520,15 @@ const PurchaseRequestForm = ({ {purchaseRequestFormErrorMessage}
)} + + {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} + {/* Basic Info Card */} Date: Thu, 8 Jan 2026 12:38:07 +0700 Subject: [PATCH 36/37] refactor(FE): Validate Uniformity form and show error list --- .../uniformity/form/UniformityForm.tsx | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/components/pages/production/uniformity/form/UniformityForm.tsx b/src/components/pages/production/uniformity/form/UniformityForm.tsx index e7e1e075..9d5a0cef 100644 --- a/src/components/pages/production/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityForm.tsx @@ -43,7 +43,9 @@ import UniformityResultForm from '@/components/pages/production/uniformity/form/ import { generateUniformityTemplate } from '@/components/pages/production/uniformity/export/UniformityTemplate'; import useSWR from 'swr'; import { cn, formatNumber } from '@/lib/helper'; +import { getUniqueFormikErrors } from '@/lib/formik-helper'; import Tooltip from '@/components/Tooltip'; +import AlertErrorList from '@/components/helper/form/FormErrors'; interface UniformityFormProps { formType?: 'add' | 'edit'; @@ -77,6 +79,7 @@ const UniformityForm = ({ const [uniformityFormErrorMessage, setUniformityFormErrorMessage] = useState(''); + const [formErrorList, setFormErrorList] = useState([]); const fileInputRef = useRef(null); @@ -282,6 +285,22 @@ const UniformityForm = ({ }, }); + const handleValidateForm = async () => { + const errors = await formik.validateForm(); + + if (Object.keys(errors).length > 0) { + const errorMessages = getUniqueFormikErrors(errors); + setFormErrorList(errorMessages); + return; + } + }; + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + handleValidateForm(); + formik.handleSubmit(e); + }; + // ===== FORM HANDLERS ===== const handleLocationChange = useCallback( (val: OptionType | OptionType[] | null) => { @@ -454,7 +473,7 @@ const UniformityForm = ({

Informasi Umum

- + {uniformityFormErrorMessage && (
)} + {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} + Date: Thu, 8 Jan 2026 12:40:35 +0700 Subject: [PATCH 37/37] refactor(FE): Increase file upload limit to 5 MB --- .../pages/production/uniformity/form/UniformityForm.schema.ts | 4 ++-- .../pages/production/uniformity/form/UniformityForm.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/pages/production/uniformity/form/UniformityForm.schema.ts b/src/components/pages/production/uniformity/form/UniformityForm.schema.ts index 273e5326..037180f0 100644 --- a/src/components/pages/production/uniformity/form/UniformityForm.schema.ts +++ b/src/components/pages/production/uniformity/form/UniformityForm.schema.ts @@ -24,9 +24,9 @@ type UniformityFormSchemaType = { }; const FileSchema = Yup.mixed() - .test('documentSize', 'Ukuran file maksimal 2 MB', (value): boolean => { + .test('documentSize', 'Ukuran file maksimal 5 MB', (value): boolean => { if (!value) return true; - if (value instanceof File) return value.size <= 2 * 1024 * 1024; + if (value instanceof File) return value.size <= 5 * 1024 * 1024; return false; }) .test('documentType', 'Format file harus Excel', (value): boolean => { diff --git a/src/components/pages/production/uniformity/form/UniformityForm.tsx b/src/components/pages/production/uniformity/form/UniformityForm.tsx index 9d5a0cef..54b4ee2b 100644 --- a/src/components/pages/production/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityForm.tsx @@ -358,8 +358,8 @@ const UniformityForm = ({ return; } - if (document.size > 2 * 1024 * 1024) { - toast.error(`Ukuran file ${document.name} maksimal 2 MB!`); + if (document.size > 5 * 1024 * 1024) { + toast.error(`Ukuran file ${document.name} maksimal 5 MB!`); return; }