diff --git a/src/components/pages/inventory/movement/form/MovementForm.schema.ts b/src/components/pages/inventory/movement/form/MovementForm.schema.ts index 9b2c8557..048b1bd2 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!') @@ -127,13 +133,13 @@ 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() - .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!'), @@ -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!') @@ -161,6 +170,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 +180,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', @@ -226,41 +237,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 d92b14de..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( @@ -761,8 +764,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]); @@ -791,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 ( <>
@@ -810,10 +857,29 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
+ {movementFormErrorMessage && ( +
+ + {movementFormErrorMessage} +
+ )} + + {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} + {/* Top card - Movement details */} { Produk * @@ -1106,7 +1172,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { Qty * @@ -1119,7 +1185,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { {formik.values.products?.map((product, idx) => ( {type !== 'detail' && ( - + { Produk * @@ -1320,7 +1386,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { Qty * @@ -1329,7 +1395,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { Supplier * @@ -1338,7 +1404,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { Plat Nomor * @@ -1348,7 +1414,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { Biaya Pengiriman (Rp.) * @@ -1357,7 +1423,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { Biaya Per Item (Rp.) * @@ -1366,7 +1432,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { Nama Sopir * @@ -1379,7 +1445,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { {formik.values.deliveries?.map((delivery, idx) => ( {type !== 'detail' && ( - + { 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; } @@ -1747,7 +1813,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { disabled={ hasInvalidQty || hasExceededStock || - !formik.isValid || formik.isSubmitting || (formik.values.source_warehouse_id === formik.values.destination_warehouse_id && @@ -1760,17 +1825,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { )} - - {movementFormErrorMessage && ( -
- - {movementFormErrorMessage} -
- )}
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..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,10 +169,29 @@ const ProductCategoryForm = ({
+ {formErrorMessage && ( +
+ + {formErrorMessage} +
+ )} + + {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} +
Submit @@ -244,17 +282,6 @@ const ProductCategoryForm = ({
)} - - {formErrorMessage && ( -
- - {formErrorMessage} -
- )}
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..9dcf713e 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(), - 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() - .required('Satuan wajib diisi!') - .typeError('Satuan wajib diisi!'), + .min(1, 'Satuan wajib dipilih!') + .required('Satuan wajib dipilih!') + .typeError('Satuan wajib dipilih!'), product_category: Yup.object({ - value: Yup.number().min(1).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() - .required('Kategori produk wajib diisi!') - .typeError('Kategori produk wajib diisi!'), + .min(1, 'Kategori produk wajib dipilih!') + .required('Kategori produk wajib dipilih!') + .typeError('Kategori produk wajib dipilih!'), 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 5b304419..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,11 +239,30 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
-
+ {productFormErrorMessage && ( +
+ + {productFormErrorMessage} +
+ )} + + {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} + +
{ 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' && ( @@ -463,7 +515,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { type='submit' color='primary' isLoading={formik.isSubmitting} - disabled={!formik.isValid || formik.isSubmitting} + disabled={formik.isSubmitting} className='px-4' > Submit @@ -471,16 +523,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
)}
- {productFormErrorMessage && ( -
- - {productFormErrorMessage} -
- )}
{type !== 'add' && ( diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 3a6f8071..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, @@ -60,7 +62,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'; @@ -92,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] = @@ -758,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 ( @@ -1323,9 +1341,28 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { )}
+ {recordingFormErrorMessage && ( +
+ + {recordingFormErrorMessage} +
+ )} + + {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} + {/* 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 @@ -2534,9 +2569,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 @@ -2544,16 +2577,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { )} - {recordingFormErrorMessage && ( -
- - {recordingFormErrorMessage} -
- )} 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 0c0c3f70..9caa98a9 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 ( - + ); }; @@ -374,6 +353,7 @@ const UniformityTable = () => { if (filterEndDate) { queryParams.append('end_date', filterEndDate); } + queryParams.append('with_chart', 'true'); } const tableQueryString = getTableFilterQueryString(); @@ -433,6 +413,7 @@ const UniformityTable = () => { ); const handleResetFilters = useCallback(() => { + setIsSubmitted(false); setFilterLocation(null); setFilterProjectFlock(null); setFilterKandang(null); @@ -896,7 +877,10 @@ const UniformityTable = () => {
- +
= ({ const setExpandedDrawerContent = useUiStore( (s) => s.setExpandedDrawerContent ); + const [shouldFetchDetails, setShouldFetchDetails] = useState(false); + const [hasFetchedDetails, setHasFetchedDetails] = useState(false); + + const { data: uniformityDetailResponse, isLoading } = useSWR( + shouldFetchDetails + ? `uniformity-detail-${initialValues.id}-with-details` + : null, + () => UniformityApi.getUniformityDetail(initialValues.id, true) + ); + + const uniformity_details = useMemo(() => { + if (shouldFetchDetails && isResponseSuccess(uniformityDetailResponse)) { + return uniformityDetailResponse.data.uniformity_details; + } + return initialValues.uniformity_details; + }, [shouldFetchDetails, uniformityDetailResponse, initialValues]); const handleApprove = () => { router.push(`/production/uniformity?action=approve&id=${initialValues.id}`); @@ -43,12 +62,15 @@ const UniformityDetail: React.FC = ({ }; const handleViewUniformityDetails = () => { + if (!uniformity_details || uniformity_details.length === 0) { + setShouldFetchDetails(true); + return; + } + setExpandedDrawerContent( ); @@ -58,6 +80,28 @@ const UniformityDetail: React.FC = ({ }, 0); }; + useEffect(() => { + if ( + shouldFetchDetails && + uniformity_details && + uniformity_details.length > 0 && + !hasFetchedDetails + ) { + setExpandedDrawerContent( + + ); + + setHasFetchedDetails(true); + setTimeout(() => { + setExpandedDrawerOpen(true); + }, 0); + } + }, [shouldFetchDetails, uniformity_details, hasFetchedDetails]); + useEffect(() => { return () => { setExpandedDrawerOpen(false); @@ -154,12 +198,22 @@ const UniformityDetail: React.FC = ({ return (
{valueMap[id]} - +
@@ -173,6 +227,92 @@ const UniformityDetail: React.FC = ({ [initialValues] ); + const samplingTableData: DetailOptionType[] = useMemo(() => { + if (!initialValues.sampling) return []; + + return [ + { + id: 'sampling-size', + label: 'Sampling size', + value: `${formatNumber(initialValues.sampling.chick_qty_of_weight)} of Birds`, + }, + { + id: 'mean-weight', + label: 'Mean Weight', + value: `${initialValues.sampling.mean_weight} g`, + }, + { + id: 'min-limit', + label: 'Min Limit (-10%)', + value: `${initialValues.sampling.mean_down} g`, + }, + { + id: 'max-limit', + label: 'Max Limit (+10%)', + value: `${initialValues.sampling.mean_up} g`, + }, + ]; + }, [initialValues.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 (!initialValues.result) return []; + + return [ + { + id: 'ideal-birds', + label: 'Ideal Birds', + value: `${formatNumber(initialValues.result.uniform_qty)} of Birds`, + }, + { + id: 'outside-range', + label: 'Outside Range', + value: `${formatNumber(initialValues.result.outside_qty)} of Birds`, + }, + { + id: 'uniformity', + label: 'Uniformity', + value: `${initialValues.result.uniformity} %`, + }, + { + id: 'cv', + label: 'CV', + value: `${initialValues.result.cv} %`, + }, + ]; + }, [initialValues.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}, + }, + ], + [] + ); + return (
{/* Header */} @@ -185,7 +325,7 @@ const UniformityDetail: React.FC = ({ {/* Form Section */}
-
+
{initialValues ? (
{/* Info Umum */} @@ -200,23 +340,55 @@ const UniformityDetail: React.FC = ({ paginationClassName: 'hidden', }} /> - - {/* Approve/Reject Buttons */} - {initialValues.result && - initialValues.latest_approval?.step_name === 'CREATED' ? ( - <> -
- -
- - -
-
- - ) : 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..05d21535 100644 --- a/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx +++ b/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx @@ -1,152 +1,39 @@ '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'; 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, 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; - sampling: UniformitySampling; - result: UniformityResult; uniformity_details?: UniformityDetailItem[]; uniformityId: number; } const UniformityDetailsPreview = ({ info_umum, - uniformity_details: initialUniformityDetails, - sampling, - result, - 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 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,55 +116,10 @@ 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 ? ( -
- -
- ) : null} - {/* Body Weight Details */} - {uniformity_details && uniformity_details.length > 0 && ( + {uniformity_details && uniformity_details.length > 0 ? (
data={tableData} @@ -286,6 +128,17 @@ const UniformityDetailsPreview = ({ className={{ containerClassName: 'mb-5' }} />
+ ) : ( +
+ +

No data available

+

Body weight details not found

+
)}
) : ( 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 bbca72f8..54b4ee2b 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) => { @@ -339,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; } @@ -454,7 +473,7 @@ const UniformityForm = ({

Informasi Umum

-
+ {uniformityFormErrorMessage && (
)} + {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} + {formik.isSubmitting ? ( diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index d39b2e1a..2b18afee 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'; @@ -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,7 +54,9 @@ const PurchaseOrderAcceptApprovalForm = ({ const searchParams = useSearchParams(); const [purchaseOrderFormErrorMessage, setPurchaseOrderFormErrorMessage] = useState(''); + const [formErrorList, setFormErrorList] = useState([]); const [key, setKey] = useState(0); + const fileInputRef = useRef(null); const isRejected = initialValues?.latest_approval?.action === 'REJECTED'; @@ -67,7 +71,6 @@ const PurchaseOrderAcceptApprovalForm = ({ | 'purchase_item_id' | 'received_date' | 'travel_number' - | 'travel_document_path' | 'vehicle_number' | 'expedition_vendor_id' | 'received_qty' @@ -180,7 +183,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: @@ -210,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) { @@ -235,6 +253,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, @@ -242,7 +263,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 ? { @@ -250,7 +270,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 || '', }; @@ -259,20 +279,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) => { @@ -349,7 +355,7 @@ const PurchaseOrderAcceptApprovalForm = ({ return (
@@ -358,6 +364,24 @@ const PurchaseOrderAcceptApprovalForm = ({ ? 'Konfirmasi Penerimaan Produk' : 'Edit Penerimaan Produk'} + {purchaseOrderFormErrorMessage && ( +
+ + {purchaseOrderFormErrorMessage} +
+ )} + + {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )}
@@ -510,33 +534,6 @@ const PurchaseOrderAcceptApprovalForm = ({ }} /> -
- - 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', - }} - /> - { 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; } @@ -726,6 +724,10 @@ const PurchaseOrderAcceptApprovalForm = ({ onClick={() => { if (type === 'add') { formik.resetForm(); + formik.setFieldValue('travel_documents', []); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } } setPurchaseOrderFormErrorMessage(''); onCancel?.(); @@ -741,27 +743,13 @@ const PurchaseOrderAcceptApprovalForm = ({ className='px-4' isLoading={formik.isSubmitting} disabled={ - !formik.isValid || - formik.isSubmitting || - hasQuantityExceededErrors || - isRejected + formik.isSubmitting || hasQuantityExceededErrors || isRejected } > Submit - - {purchaseOrderFormErrorMessage && ( -
- - {purchaseOrderFormErrorMessage} -
- )} ); diff --git a/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts b/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts index 07a868a3..a1b60249 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts +++ b/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts @@ -38,7 +38,6 @@ type PurchaseRequestAcceptApprovalFormSchemaType = { purchase_item_id: number; received_date: string; travel_number: string; - travel_document_path: string; vehicle_number: string; expedition_vendor?: { value: number; @@ -76,7 +75,6 @@ export type PurchaseAcceptApprovalItemSchema = { purchase_item_id: number; received_date: string; travel_number: string; - travel_document_path: string; vehicle_number: string; expedition_vendor?: { value: number; @@ -185,9 +183,6 @@ const PurchaseAcceptApprovalItemObjectSchema: 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; }) ) @@ -415,7 +410,6 @@ export const PurchaseRequestAcceptApprovalFormInitialValues: PurchaseRequestAcce purchase_item_id: 0, received_date: '', travel_number: '', - travel_document_path: '', vehicle_number: '', expedition_vendor_id: 0, received_qty: '', @@ -436,7 +430,6 @@ export const PurchaseRequestAcceptApprovalFormDefaultValues = ( purchase_item_id: item.id, received_date: '', travel_number: '', - travel_document_path: '', vehicle_number: '', expedition_vendor_id: 0, received_qty: '', @@ -447,7 +440,6 @@ export const PurchaseRequestAcceptApprovalFormDefaultValues = ( purchase_item_id: 0, received_date: '', travel_number: '', - travel_document_path: '', vehicle_number: '', expedition_vendor_id: 0, received_qty: '', diff --git a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx index 6a08e53b..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,16 +671,32 @@ const PurchaseOrderStaffApprovalForm = ({ return ( <> -
+

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

+ {purchaseOrderFormErrorMessage && ( +
+ + {purchaseOrderFormErrorMessage} +
+ )} + + {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} +
{groupedPurchaseItems.length > 0 ? (
@@ -1164,23 +1199,12 @@ const PurchaseOrderStaffApprovalForm = ({ color='primary' className='px-4' isLoading={formik.isSubmitting} - disabled={!formik.isValid || formik.isSubmitting || isRejected} + disabled={formik.isSubmitting || isRejected} > Submit
- - {purchaseOrderFormErrorMessage && ( -
- - {purchaseOrderFormErrorMessage} -
- )}
diff --git a/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx index 396ce7bb..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,10 +506,29 @@ const PurchaseRequestForm = ({
+ {purchaseRequestFormErrorMessage && ( +
+ + {purchaseRequestFormErrorMessage} +
+ )} + + {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} + {/* Basic Info Card */} Submit @@ -935,17 +973,6 @@ const PurchaseRequestForm = ({ )} - - {purchaseRequestFormErrorMessage && ( -
- - {purchaseRequestFormErrorMessage} -
- )} diff --git a/src/types/api/production/uniformity.d.ts b/src/types/api/production/uniformity.d.ts index 0863c08a..239de467 100644 --- a/src/types/api/production/uniformity.d.ts +++ b/src/types/api/production/uniformity.d.ts @@ -1,6 +1,68 @@ import { BaseMetadata } from '@/types/api/api-general'; import { BaseApproval } from '@/types/api/approval/approval'; +// ==================== CHART DATA TYPES ==================== +export type WeightDistributionRange = { + range: string; + min_weight: number; + max_weight: number; + bird_count: number; + is_ideal_range: boolean; +}; + +export type IdealRange = { + min_weight: number; + max_weight: number; + total_ideal_birds: number; +}; + +export type StatisticsData = { + min_weight: number; + max_weight: number; + average_weight: number; + total_birds_measured: number; +}; + +export type WeekBarChartData = { + has_data: boolean; + weight_distribution: WeightDistributionRange[]; + ideal_range: IdealRange; + statistics: StatisticsData; +}; + +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 +83,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; }; diff --git a/src/types/api/purchase/purchase.d.ts b/src/types/api/purchase/purchase.d.ts index d355c2f8..34798ac3 100644 --- a/src/types/api/purchase/purchase.d.ts +++ b/src/types/api/purchase/purchase.d.ts @@ -120,7 +120,6 @@ export type CreateAcceptApprovalRequestPayload = { purchase_item_id: number; received_date: string; travel_number: string; - travel_document_path: string; vehicle_number: string; expedition_vendor_id: number; received_qty: number;