From 309a9ecc865afa4abe430895214c5a67ebe8615f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 7 Jan 2026 11:38:28 +0700 Subject: [PATCH 01/46] refactor(FE): Increase upload file size limit to 5 MB --- .../pages/inventory/movement/form/MovementForm.schema.ts | 4 ++-- src/components/pages/inventory/movement/form/MovementForm.tsx | 4 ++-- .../purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx | 4 ++-- .../pages/purchase/form/order/PurchaseOrderForm.schema.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/pages/inventory/movement/form/MovementForm.schema.ts b/src/components/pages/inventory/movement/form/MovementForm.schema.ts index 9b2c8557..ea6b473c 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.schema.ts +++ b/src/components/pages/inventory/movement/form/MovementForm.schema.ts @@ -131,9 +131,9 @@ const DeliveryObjectSchema: Yup.ObjectSchema = Yup.object({ document_index: Yup.number().optional(), document: Yup.mixed() .nullable() - .test('fileSize', 'Ukuran dokumen maksimal 2 MB', (value) => { + .test('fileSize', 'Ukuran dokumen maksimal 5 MB', (value) => { 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 true; }), driver_name: Yup.string().required('Nama sopir wajib diisi!'), diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index d92b14de..1559838a 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -1589,8 +1589,8 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { onChange={(e) => { const file = e.target.files?.[0]; if (file) { - if (file.size > 2 * 1024 * 1024) { - toast.error('Ukuran dokumen maksimal 2 MB!'); + if (file.size > 5 * 1024 * 1024) { + toast.error('Ukuran dokumen maksimal 5 MB!'); e.target.value = ''; return; } diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index d39b2e1a..ef90e06a 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -690,11 +690,11 @@ const PurchaseOrderAcceptApprovalForm = ({ onChange={(e) => { const files = Array.from(e.target.files || []); const invalidFiles = files.filter( - (file) => file.size > 2 * 1024 * 1024 + (file) => file.size > 5 * 1024 * 1024 ); if (invalidFiles.length > 0) { - toast.error('Ukuran dokumen maksimal 2 MB!'); + toast.error('Ukuran dokumen maksimal 5 MB!'); e.target.value = ''; return; } diff --git a/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts b/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts index 07a868a3..06ed6346 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts +++ b/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts @@ -395,9 +395,9 @@ export const PurchaseRequestAcceptApprovalFormSchema: Yup.ObjectSchema() .required('Dokumen surat jalan wajib diupload!') - .test('fileSize', 'Ukuran dokumen maksimal 2 MB', (value) => { + .test('fileSize', 'Ukuran dokumen maksimal 5 MB', (value) => { 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 true; }) ) From 324b9b14ef1aa08b035a905ed540848cb2f237c5 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 7 Jan 2026 14:07:02 +0700 Subject: [PATCH 02/46] 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 03/46] 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 04/46] 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 05/46] 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 06/46] 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 07/46] 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 08/46] 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 09/46] 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 10/46] 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 11/46] 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 12/46] 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 d5b4111ae45d60dbff7e77fcc8316c3d3368cc35 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 7 Jan 2026 15:46:58 +0700 Subject: [PATCH 13/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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 965dc01e864fd2b60cc356260b94369bd95559a7 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 8 Jan 2026 09:27:51 +0700 Subject: [PATCH 20/46] 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 21/46] 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 22/46] 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 23/46] 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 24/46] 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 25/46] 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 27/46] 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 28/46] 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 29/46] 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 30/46] 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 31/46] 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 32/46] 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 33/46] 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 34/46] 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 35/46] 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; } From 3b9599d16940042740a20f598f189c9952478f04 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 8 Jan 2026 13:51:28 +0700 Subject: [PATCH 36/46] refactor(FE): Set peer flag in package-lock.json --- package-lock.json | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 19a7623b..39f99f0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4506,6 +4506,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4516,6 +4517,7 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4597,6 +4599,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -5120,6 +5123,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5825,7 +5829,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/d3-array": { "version": "3.2.4", @@ -6201,7 +6206,8 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -6462,6 +6468,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6635,6 +6642,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -8152,6 +8160,7 @@ "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz", "integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "fast-png": "^6.2.0", @@ -9371,6 +9380,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -9401,6 +9411,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -9468,7 +9479,8 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-number-format": { "version": "5.4.4", @@ -9485,6 +9497,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -9653,7 +9666,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -10519,6 +10533,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -10686,6 +10701,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" From c894f26d18205a9e8e96f6388905eac595773a34 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 8 Jan 2026 13:52:23 +0700 Subject: [PATCH 37/46] refactor(FE): Handle ideal/outside ranges in uniformity tooltip --- .../production/uniformity/UniformityChart.tsx | 5 +- .../uniformity/chart/UniformityBarChart.tsx | 102 ++++++++++++++++-- src/types/api/production/uniformity.d.ts | 2 + 3 files changed, 99 insertions(+), 10 deletions(-) diff --git a/src/components/pages/production/uniformity/UniformityChart.tsx b/src/components/pages/production/uniformity/UniformityChart.tsx index 52e7a24b..302d5468 100644 --- a/src/components/pages/production/uniformity/UniformityChart.tsx +++ b/src/components/pages/production/uniformity/UniformityChart.tsx @@ -39,9 +39,8 @@ const UniformityChart = ({ name: range.range, uv: range.bird_count, isIdeal: range.is_ideal_range, - idealCount: range.is_ideal_range - ? weekData.ideal_range.total_ideal_birds - : undefined, + idealRange: range.ideal_range, + outsideRange: range.outside_range, })); }, [chartData]); diff --git a/src/components/pages/production/uniformity/chart/UniformityBarChart.tsx b/src/components/pages/production/uniformity/chart/UniformityBarChart.tsx index 9f1f8656..88d0dc59 100644 --- a/src/components/pages/production/uniformity/chart/UniformityBarChart.tsx +++ b/src/components/pages/production/uniformity/chart/UniformityBarChart.tsx @@ -27,7 +27,8 @@ interface BarChartData { name: string; uv: number; isIdeal?: boolean; - idealCount?: number; + idealRange?: string; + outsideRange?: string; } interface UniformityBarChartProps { @@ -40,30 +41,117 @@ function CustomTooltip({ payload, label, active }: CustomTooltipProps) { const chartData = data.payload as BarChartData; const labelStr = String(label); - if (chartData.isIdeal && chartData.idealCount !== undefined) { + // If the range has both ideal and outside ranges (like 340-344) + if (chartData.idealRange && chartData.outsideRange) { + return ( +
+

Uniformity 2025

+
+
+
+
+ Ideal +
+ + {chartData.idealRange} + +
+
+
+
+ Outside +
+ + {chartData.outsideRange} + +
+
+
+ Total Birds: + {payload[0].value} +
+
+
{labelStr}
+
+
+ ); + } + + // If the range has only ideal range + if (chartData.idealRange) { return (

Uniformity 2025

- {chartData.idealCount} of Birds + Ideal
- {labelStr} + {chartData.idealRange} +
+
+
+ Birds: + {payload[0].value} +
+
+
+ {labelStr}
); } + // If the range has only outside range + if (chartData.outsideRange) { + return ( +
+

Uniformity 2025

+
+
+
+ Outside +
+ + {chartData.outsideRange} + +
+
+
+ Birds: + {payload[0].value} +
+
+
+ {labelStr} +
+
+ ); + } + + // Fallback for backward compatibility return (

Uniformity 2025

-
- {payload[0].value} of Birds +
+ + {chartData.isIdeal ? 'Ideal' : 'Outside'} + +
+ {labelStr} +
+
+
+ Birds: + {payload[0].value}
- {labelStr}
); diff --git a/src/types/api/production/uniformity.d.ts b/src/types/api/production/uniformity.d.ts index 239de467..825607d8 100644 --- a/src/types/api/production/uniformity.d.ts +++ b/src/types/api/production/uniformity.d.ts @@ -8,6 +8,8 @@ export type WeightDistributionRange = { max_weight: number; bird_count: number; is_ideal_range: boolean; + ideal_range?: string; + outside_range?: string; }; export type IdealRange = { From badb1e141af1acf7ca36ca5c04c1459a1bd4219e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 9 Jan 2026 08:29:21 +0700 Subject: [PATCH 38/46] refactor(FE): Deduplicate delivery documents by filename --- .../inventory/movement/form/MovementForm.tsx | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index 62a23595..cd01cd96 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -189,12 +189,45 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { return; } const documents: File[] = []; + const documentNameToIndex = new Map(); + let sequentialDocumentIndex = 0; + const deliveriesPayload = values.deliveries.map((d) => { - let documentIndex = 0; + let documentIndex = -1; if (d.document && d.document instanceof File) { - documents.push(d.document); - documentIndex = documents.length - 1; + const fileName = d.document.name; + + if (documentNameToIndex.has(fileName)) { + documentIndex = documentNameToIndex.get(fileName)!; + } else { + documents.push(d.document); + documentIndex = sequentialDocumentIndex; + documentNameToIndex.set(fileName, documentIndex); + sequentialDocumentIndex++; + } + } else if (d.document_path) { + const pathFileName = + d.document_path.split('/').pop() || d.document_path; + + if (documentNameToIndex.has(pathFileName)) { + documentIndex = documentNameToIndex.get(pathFileName)!; + } else { + documentIndex = sequentialDocumentIndex; + documentNameToIndex.set(pathFileName, documentIndex); + sequentialDocumentIndex++; + } + } else if (d.document && !(d.document instanceof File)) { + const existingDocFileName = + d.document.path.split('/').pop() || d.document.path; + + if (documentNameToIndex.has(existingDocFileName)) { + documentIndex = documentNameToIndex.get(existingDocFileName)!; + } else { + documentIndex = sequentialDocumentIndex; + documentNameToIndex.set(existingDocFileName, documentIndex); + sequentialDocumentIndex++; + } } return { From 4fdfe63dc9fdfe7ec1c50662618b4710be696b77 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 9 Jan 2026 09:24:46 +0700 Subject: [PATCH 39/46] refactor(FE): Remove document_path from movement payload --- src/components/pages/inventory/movement/form/MovementForm.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index cd01cd96..d9aef6cd 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -235,7 +235,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { delivery_cost_per_item: parseInt((d.delivery_cost_per_item || '').toString()) || 0, document_index: documentIndex, - document_path: d.document_path, driver_name: d.driver_name, vehicle_plate: d.vehicle_plate, supplier_id: d.supplier_id, From 3ce30115f8563a249024e1c388a20543a6979310 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 9 Jan 2026 09:30:51 +0700 Subject: [PATCH 40/46] refactor(FE): Add validation and error messages to filter modal --- .../production/uniformity/UniformityTable.tsx | 177 +++++++++++++----- 1 file changed, 127 insertions(+), 50 deletions(-) diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index 9caa98a9..ff57a2ff 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -230,6 +230,7 @@ const UniformityTable = () => { const [filterStartDate, setFilterStartDate] = useState(''); const [filterEndDate, setFilterEndDate] = useState(''); const [projectFlockSearchValue, setProjectFlockSearchValue] = useState(''); + const [filterErrors, setFilterErrors] = useState>({}); const { setInputValue: setFilterLocationInputValue, @@ -423,9 +424,38 @@ const UniformityTable = () => { }, []); const handleApplyFilters = useCallback(() => { - setIsSubmitted(true); - filterModal.closeModal(); - }, [filterModal]); + const errors: Record = {}; + + if (!filterStartDate) { + errors.start_date = 'Tanggal mulai wajib diisi'; + } + if (!filterEndDate) { + errors.end_date = 'Tanggal akhir wajib diisi'; + } + if (!filterLocation) { + errors.location = 'Lokasi wajib dipilih'; + } + if (!filterProjectFlock) { + errors.project_flock = 'Project Flock wajib dipilih'; + } + if (!filterKandang) { + errors.kandang = 'Kandang wajib dipilih'; + } + + setFilterErrors(errors); + + if (Object.keys(errors).length === 0) { + setIsSubmitted(true); + filterModal.closeModal(); + } + }, [ + filterModal, + filterStartDate, + filterEndDate, + filterLocation, + filterProjectFlock, + filterKandang, + ]); const selectedRowIds = useMemo(() => { return Object.keys(rowSelection) @@ -1124,58 +1154,105 @@ const UniformityTable = () => {
- setFilterStartDate(e.target.value)} - className={{ wrapper: 'w-full' }} - /> +
+ { + setFilterStartDate(e.target.value); + setFilterErrors((prev) => ({ ...prev, start_date: '' })); + }} + className={{ wrapper: 'w-full' }} + /> + {filterErrors.start_date && ( +

+ {filterErrors.start_date} +

+ )} +
- setFilterEndDate(e.target.value)} - className={{ wrapper: 'w-full' }} - /> +
+ { + setFilterEndDate(e.target.value); + setFilterErrors((prev) => ({ ...prev, end_date: '' })); + }} + className={{ wrapper: 'w-full' }} + /> + {filterErrors.end_date && ( +

+ {filterErrors.end_date} +

+ )} +
- +
+ { + handleFilterLocationChange(value); + setFilterErrors((prev) => ({ ...prev, location: '' })); + }} + options={filterLocationOptions} + onInputChange={setFilterLocationInputValue} + isLoading={isLoadingFilterLocations} + className={{ wrapper: 'w-full' }} + /> + {filterErrors.location && ( +

+ {filterErrors.location} +

+ )} +
- +
+ { + handleFilterProjectFlockChange(value); + setFilterErrors((prev) => ({ ...prev, project_flock: '' })); + }} + options={filterProjectFlockOptions} + onInputChange={setProjectFlockSearchValue} + isLoading={isLoadingFilterProjectFlocks} + isDisabled={!filterLocation} + className={{ wrapper: 'w-full' }} + /> + {filterErrors.project_flock && ( +

+ {filterErrors.project_flock} +

+ )} +
- +
+ { + handleFilterKandangChange(value); + setFilterErrors((prev) => ({ ...prev, kandang: '' })); + }} + options={filterKandangOptions} + isDisabled={!filterProjectFlock} + className={{ wrapper: 'w-full' }} + /> + {filterErrors.kandang && ( +

+ {filterErrors.kandang} +

+ )} +
{/* Action Buttons */} From 88e3ec7bbc6a7772030d9755e62535ff9f892ad7 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 9 Jan 2026 09:48:54 +0700 Subject: [PATCH 41/46] refactor(FE): Reduce default query limit to 100 --- src/components/pages/production/uniformity/UniformityTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index ff57a2ff..6afba6dc 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -644,7 +644,7 @@ const UniformityTable = () => { if (filterEndDate) { queryParams.append('end_date', filterEndDate); } - queryParams.append('limit', '10000'); + queryParams.append('limit', '100'); queryParams.append('page', '1'); const queryString = queryParams.toString(); From 97c16ce59685337d0269edbf9c5f81feba6eebd4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 9 Jan 2026 10:46:05 +0700 Subject: [PATCH 42/46] refactor(FE): Initialize and use current gauge week index --- .../production/uniformity/UniformityChart.tsx | 47 +++++++++++++------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/src/components/pages/production/uniformity/UniformityChart.tsx b/src/components/pages/production/uniformity/UniformityChart.tsx index 302d5468..6ddf50d3 100644 --- a/src/components/pages/production/uniformity/UniformityChart.tsx +++ b/src/components/pages/production/uniformity/UniformityChart.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useState, useEffect } 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'; @@ -22,13 +22,27 @@ const UniformityChart = ({ return uniformityData.chart_data; }, [uniformityData]); + useEffect(() => { + if (uniformityData?.chart_data?.gauge_chart?.week_info) { + const { current_week_index } = + uniformityData.chart_data.gauge_chart.week_info; + setCurrentWeekIndex(current_week_index); + } + }, [uniformityData]); + const barChartData = useMemo(() => { - if (!chartData?.bar_chart) { + if (!chartData?.bar_chart || !chartData?.gauge_chart) { return []; } - const { bar_chart } = chartData; - const currentWeekStr = String(bar_chart.current_week); + const { bar_chart, gauge_chart } = chartData; + const currentWeekData = gauge_chart.available_weeks[currentWeekIndex]; + + if (!currentWeekData || !currentWeekData.has_data) { + return []; + } + + const currentWeekStr = String(currentWeekData.week); const weekData = bar_chart.all_weeks[currentWeekStr]; if (!weekData || !weekData.has_data) { @@ -42,7 +56,7 @@ const UniformityChart = ({ idealRange: range.ideal_range, outsideRange: range.outside_range, })); - }, [chartData]); + }, [chartData, currentWeekIndex]); const gaugeChartData = useMemo(() => { if (!chartData?.gauge_chart || !uniformityData) return undefined; @@ -54,28 +68,33 @@ const UniformityChart = ({ return undefined; } + const hasPrevWeek = currentWeekIndex > 0; + const hasNextWeek = + currentWeekIndex < gauge_chart.available_weeks.length - 1; + return { value: currentWeekData.uniformity_percentage, label: 'Uniformity', 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, + hasPrevWeek, + hasNextWeek, }; }, [chartData, currentWeekIndex, uniformityData]); const handleWeekChange = (direction: 'prev' | 'next') => { if (!chartData?.gauge_chart) return; - const { available_weeks, week_info } = chartData.gauge_chart; + const { available_weeks } = 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) - ); + if (direction === 'prev' && currentWeekIndex > 0) { + setCurrentWeekIndex((prev) => prev - 1); + } else if ( + direction === 'next' && + currentWeekIndex < available_weeks.length - 1 + ) { + setCurrentWeekIndex((prev) => prev + 1); } }; From 69d7f65b7628dc4a273ca9330134ef3927b60166 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 9 Jan 2026 12:31:53 +0700 Subject: [PATCH 43/46] refactor(FE): Disable sales tab and related data fetching --- src/app/closing/detail/page.tsx | 13 +++++++------ src/components/pages/closing/ClosingDetail.tsx | 10 +++++----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx index 62f3fa20..f3a78d9d 100644 --- a/src/app/closing/detail/page.tsx +++ b/src/app/closing/detail/page.tsx @@ -19,10 +19,10 @@ const ClosingDetailPage = () => { (id: number) => ClosingApi.getGeneralInfo(id) ); - const { data: salesData, isLoading: isLoadingSales } = useSWR( - closingId ? `sales-${closingId}` : null, - () => ClosingApi.getPenjualan(Number(closingId)) - ); + // const { data: salesData, isLoading: isLoadingSales } = useSWR( + // closingId ? `sales-${closingId}` : null, + // () => ClosingApi.getPenjualan(Number(closingId)) + // ); const { data: hppEkspedisiData, isLoading: isLoadingHppEkspedisi } = useSWR( closingId ? `hpp-ekspedisi-${closingId}` : null, @@ -44,7 +44,8 @@ const ClosingDetailPage = () => { return; } - const isLoading = isLoadingClosing || isLoadingSales || isLoadingHppEkspedisi; + const isLoading = isLoadingClosing || isLoadingHppEkspedisi; + // const isLoading = isLoadingClosing || isLoadingSales || isLoadingHppEkspedisi; return (
@@ -54,7 +55,7 @@ const ClosingDetailPage = () => { = ({ /> ), }, - { - id: 'penjualan', - label: 'Penjualan', - content: , - }, + // { + // id: 'penjualan', + // label: 'Penjualan', + // content: , + // }, { id: 'overhead', label: 'Overhead', From 4fc689898f826caae9ad9cde94d6f5e51fc735d2 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 9 Jan 2026 12:33:21 +0700 Subject: [PATCH 44/46] refactor(FE): Use optional chaining for summary_hpp fields --- .../pages/closing/ClosingFinanceTable.tsx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/pages/closing/ClosingFinanceTable.tsx b/src/components/pages/closing/ClosingFinanceTable.tsx index 9d0c92d6..b0b22a08 100644 --- a/src/components/pages/closing/ClosingFinanceTable.tsx +++ b/src/components/pages/closing/ClosingFinanceTable.tsx @@ -217,8 +217,8 @@ const ClosingFinanceTable = ({ return props.column.id === 'budgeting_rp_per_bird' && isResponseSuccess(finance) ? formatCurrency( - finance.data.hpp_purchases.summary_hpp.budgeting - .rp_per_bird || 0 + finance.data.hpp_purchases.summary_hpp?.budgeting + ?.rp_per_bird || 0 ) : '-'; }, @@ -233,8 +233,8 @@ const ClosingFinanceTable = ({ return props.column.id === 'budgeting_rp_per_kg' && isResponseSuccess(finance) ? formatCurrency( - finance.data.hpp_purchases.summary_hpp.budgeting - .rp_per_kg || 0 + finance.data.hpp_purchases.summary_hpp?.budgeting + ?.rp_per_kg || 0 ) : '-'; }, @@ -249,8 +249,8 @@ const ClosingFinanceTable = ({ return props.column.id === 'budgeting_amount' && isResponseSuccess(finance) ? formatCurrency( - finance.data.hpp_purchases.summary_hpp.budgeting - .amount || 0 + finance.data.hpp_purchases.summary_hpp?.budgeting + ?.amount || 0 ) : '-'; }, @@ -271,8 +271,8 @@ const ClosingFinanceTable = ({ return props.column.id === 'realization_rp_per_bird' && isResponseSuccess(finance) ? formatCurrency( - finance.data.hpp_purchases.summary_hpp.realization - .rp_per_bird || 0 + finance.data.hpp_purchases.summary_hpp + ?.realization?.rp_per_bird || 0 ) : '-'; }, @@ -287,8 +287,8 @@ const ClosingFinanceTable = ({ return props.column.id === 'realization_rp_per_kg' && isResponseSuccess(finance) ? formatCurrency( - finance.data.hpp_purchases.summary_hpp.realization - .rp_per_kg || 0 + finance.data.hpp_purchases.summary_hpp + ?.realization?.rp_per_kg || 0 ) : '-'; }, @@ -303,8 +303,8 @@ const ClosingFinanceTable = ({ return props.column.id === 'realization_amount' && isResponseSuccess(finance) ? formatCurrency( - finance.data.hpp_purchases.summary_hpp.realization - .amount || 0 + finance.data.hpp_purchases.summary_hpp + ?.realization?.amount || 0 ) : '-'; }, From 7951754722d9e66195de90ec4ff0dc91823daac1 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 9 Jan 2026 12:46:19 +0700 Subject: [PATCH 45/46] refactor(FE): Generate HPP table from static rows with defaults --- .../pages/closing/ClosingFinanceTable.tsx | 136 +++++++++++++++--- 1 file changed, 118 insertions(+), 18 deletions(-) diff --git a/src/components/pages/closing/ClosingFinanceTable.tsx b/src/components/pages/closing/ClosingFinanceTable.tsx index b0b22a08..8c39b3fb 100644 --- a/src/components/pages/closing/ClosingFinanceTable.tsx +++ b/src/components/pages/closing/ClosingFinanceTable.tsx @@ -23,6 +23,14 @@ type HppTableRow = type?: never; budgeting?: never; realization?: never; + } + | { + type: string; + group_name: string; + group_index: number; + isGroupHeader: false; + budgeting?: { rp_per_bird: number; rp_per_kg: number; amount: number }; + realization?: { rp_per_bird: number; rp_per_kg: number; amount: number }; }; type ProfitLossTableRow = @@ -52,25 +60,117 @@ const ClosingFinanceTable = ({ () => ClosingApi.getFinance(projectFlockId) ); - const hppTableData: HppTableRow[] = isResponseSuccess(finance) - ? finance.data.hpp_purchases.hpp.flatMap((hpp, groupIndex) => [ - // Group header row - { - group_name: hpp.group_name, - group_index: groupIndex, - isGroupHeader: true as const, - }, - // Data rows - ...hpp.data.map((item) => ({ - group_name: hpp.group_name, - group_index: groupIndex, - type: item.type, - budgeting: item.budgeting, - realization: item.realization, + const staticHppRows: Array<{ + group_name: string; + type: string; + group_index: number; + }> = [ + { + group_name: 'HPP dan Pengeluaran', + type: 'Pembelian PAKAN', + group_index: 0, + }, + { + group_name: 'HPP dan Pengeluaran', + type: 'Pembelian STARTER', + group_index: 0, + }, + { + group_name: 'HPP dan Pengeluaran', + type: 'Pembelian DOC', + group_index: 0, + }, + { + group_name: 'HPP dan Pengeluaran', + type: 'Pembelian PULLET', + group_index: 0, + }, + { + group_name: 'HPP dan Pengeluaran', + type: 'Pembelian LAYER', + group_index: 0, + }, + { + group_name: 'HPP dan Bahan Baku', + type: 'Pengeluaran Overhead', + group_index: 1, + }, + { + group_name: 'HPP dan Bahan Baku', + type: 'Beban Ekspedisi', + group_index: 1, + }, + ]; + + const hppTableData: HppTableRow[] = [ + { + group_name: 'HPP dan Pengeluaran', + group_index: 0, + isGroupHeader: true as const, + }, + ...staticHppRows + .filter((row) => row.group_index === 0) + .map((staticRow) => { + const apiData = isResponseSuccess(finance) + ? finance.data.hpp_purchases.hpp + .find((g) => g.group_name === staticRow.group_name) + ?.data.find((d) => d.type === staticRow.type) + : null; + + return { + group_name: staticRow.group_name, + group_index: staticRow.group_index, + type: staticRow.type, + budgeting: apiData?.budgeting || { + rp_per_bird: 0, + rp_per_kg: 0, + amount: 0, + }, + realization: apiData?.realization || { + rp_per_bird: 0, + rp_per_kg: 0, + amount: 0, + }, isGroupHeader: false as const, - })), - ]) - : []; + }; + }), + { + group_name: 'HPP dan Bahan Baku', + group_index: 1, + isGroupHeader: true as const, + }, + ...staticHppRows + .filter((row) => row.group_index === 1) + .map((staticRow) => { + const apiData = isResponseSuccess(finance) + ? finance.data.hpp_purchases.hpp + .find((g) => g.group_name === staticRow.group_name) + ?.data.find((d) => d.type === staticRow.type) + : null; + + return { + group_name: staticRow.group_name, + group_index: staticRow.group_index, + type: staticRow.type, + budgeting: apiData?.budgeting || { + rp_per_bird: 0, + rp_per_kg: 0, + amount: 0, + }, + realization: apiData?.realization || { + rp_per_bird: 0, + rp_per_kg: 0, + amount: 0, + }, + isGroupHeader: false as const, + }; + }), + { + group_name: 'HPP', + group_index: 2, + isGroupHeader: true as const, + }, + ]; const profitLossTableData: ProfitLossTableRow[] = isResponseSuccess(finance) ? [ From f38cebc0d9ca66ae95991ad6985e44c077124288 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 9 Jan 2026 13:02:20 +0700 Subject: [PATCH 46/46] refactor(FE): Limit Location select options to 100 --- .../pages/production/uniformity/UniformityTable.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index 6afba6dc..63c446ac 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -236,7 +236,9 @@ const UniformityTable = () => { setInputValue: setFilterLocationInputValue, options: filterLocationOptions, isLoadingOptions: isLoadingFilterLocations, - } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); + } = useSelect(LocationApi.basePath, 'id', 'name', 'search', { + limit: '100', + }); // ===== FETCH PROJECT FLOCKS DATA FOR FILTER ===== const filterProjectFlocksUrl = useMemo(() => { @@ -308,6 +310,7 @@ const UniformityTable = () => { project_flock_id: filterProjectFlock.value.toString(), kandang_id: filterKandang.value.toString(), withpopulation: Boolean(true).toString(), + limit: '100', }); return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`; }, [filterProjectFlock, filterKandang]);