Merge branch 'feat/FE/refactor-submission-form' into 'development'

[FEAT/FE] Refactor Submission Form (Alert Error and Submit Button) & Adjust Uniformity Page

See merge request mbugroup/lti-web-client!145
This commit is contained in:
Rivaldi A N S
2026-01-08 06:13:10 +00:00
18 changed files with 997 additions and 690 deletions
@@ -85,7 +85,10 @@ const ProductObjectSchema: Yup.ObjectSchema<ProductSchema> = Yup.object({
value: Yup.number().min(1).required(), value: Yup.number().min(1).required(),
label: Yup.string().required(), label: Yup.string().required(),
}).nullable(), }).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() product_qty: Yup.number()
.required('Qty wajib diisi!') .required('Qty wajib diisi!')
.min(1, 'Qty minimal 1!') .min(1, 'Qty minimal 1!')
@@ -97,7 +100,10 @@ const DeliveryProductObjectSchema = Yup.object({
value: Yup.number().min(1).required(), value: Yup.number().min(1).required(),
label: Yup.string().required(), label: Yup.string().required(),
}).nullable(), }).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() product_qty: Yup.number()
.required('Qty wajib diisi!') .required('Qty wajib diisi!')
.min(1, 'Qty minimal 1!') .min(1, 'Qty minimal 1!')
@@ -127,13 +133,13 @@ const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
(delivery_cost !== undefined && delivery_cost > 0) (delivery_cost !== undefined && delivery_cost > 0)
); );
}), }),
document_path: Yup.string().optional(), document_path: Yup.string().nullable().optional(),
document_index: Yup.number().optional(), document_index: Yup.number().optional(),
document: Yup.mixed<File | MovementDocument>() document: Yup.mixed<File | MovementDocument>()
.nullable() .nullable()
.test('fileSize', 'Ukuran dokumen maksimal 2 MB', (value) => { .test('fileSize', 'Ukuran dokumen maksimal 5 MB', (value) => {
if (!value) return true; 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; return true;
}), }),
driver_name: Yup.string().required('Nama sopir wajib diisi!'), driver_name: Yup.string().required('Nama sopir wajib diisi!'),
@@ -142,7 +148,10 @@ const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
value: Yup.number().min(1).required(), value: Yup.number().min(1).required(),
label: Yup.string().required(), label: Yup.string().required(),
}).nullable(), }).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() products: Yup.array()
.of(DeliveryProductObjectSchema) .of(DeliveryProductObjectSchema)
.min(1, 'Minimal harus ada 1 produk!') .min(1, 'Minimal harus ada 1 produk!')
@@ -161,6 +170,7 @@ export const MovementFormSchema: Yup.ObjectSchema<MovementFormSchemaType> =
}).nullable(), }).nullable(),
source_warehouse_id: Yup.number() source_warehouse_id: Yup.number()
.required('Gudang asal wajib diisi!') .required('Gudang asal wajib diisi!')
.min(1, 'Gudang asal wajib diisi!')
.typeError('Gudang asal wajib diisi!'), .typeError('Gudang asal wajib diisi!'),
destination_warehouse: Yup.object({ destination_warehouse: Yup.object({
value: Yup.number().min(1).required(), value: Yup.number().min(1).required(),
@@ -170,6 +180,7 @@ export const MovementFormSchema: Yup.ObjectSchema<MovementFormSchemaType> =
}).nullable(), }).nullable(),
destination_warehouse_id: Yup.number() destination_warehouse_id: Yup.number()
.required('Gudang tujuan wajib diisi!') .required('Gudang tujuan wajib diisi!')
.min(1, 'Gudang tujuan wajib diisi!')
.typeError('Gudang tujuan wajib diisi!') .typeError('Gudang tujuan wajib diisi!')
.test( .test(
'different-warehouse', 'different-warehouse',
@@ -226,41 +237,62 @@ export const getMovementFormInitialValues = (
} }
: null, : null,
destination_warehouse_id: initialValues?.destination_warehouse?.id ?? 0, destination_warehouse_id: initialValues?.destination_warehouse?.id ?? 0,
products: products: initialValues?.details?.map((detail) => ({
initialValues?.details?.map((detail) => ({ product: {
product: { value: detail.product.id,
value: detail.product.id, label: detail.product.name,
label: detail.product.name, },
}, product_id: detail.product.id,
product_id: detail.product.id, product_qty: detail.quantity,
product_qty: detail.quantity, })) ?? [
})) ?? [], {
deliveries: product: null,
initialValues?.deliveries?.map((d) => ({ product_id: 0,
delivery_cost: d.shipping_cost_total ?? undefined, product_qty: '',
delivery_cost_per_item: d.shipping_cost_item ?? undefined, },
document_number: d.document_number ?? '', ],
document: d.document ?? null, deliveries: initialValues?.deliveries?.map((d) => ({
document_path: d.document_path ?? null, delivery_cost: d.shipping_cost_total ?? undefined,
driver_name: d.driver_name ?? '', delivery_cost_per_item: d.shipping_cost_item ?? undefined,
vehicle_plate: d.vehicle_plate ?? '', document: d.document ?? null,
supplier: d.supplier document_path: d.document_path ?? null,
? { value: d.supplier.id, label: d.supplier.name } driver_name: d.driver_name ?? '',
: null, vehicle_plate: d.vehicle_plate ?? '',
supplier_id: d.supplier?.id ?? 0, supplier: d.supplier
products: ? { value: d.supplier.id, label: d.supplier.name }
d.items?.map((item) => { : null,
const productData = detailIdToProductId.get( supplier_id: d.supplier?.id ?? 0,
item.stock_transfer_detail_id products:
); d.items?.map((item) => {
return { const productData = detailIdToProductId.get(
product: productData item.stock_transfer_detail_id
? { value: productData.id, label: productData.name } );
: null, return {
product_id: productData?.id ?? 0, product: productData
product_qty: item.quantity, ? { 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: '',
},
],
},
],
}; };
}; };
@@ -36,6 +36,8 @@ import CheckboxInput from '@/components/input/CheckboxInput';
import Badge from '@/components/Badge'; import Badge from '@/components/Badge';
import Card from '@/components/Card'; import Card from '@/components/Card';
import { S3_PUBLIC_BASE_URL } from '@/config/constant'; import { S3_PUBLIC_BASE_URL } from '@/config/constant';
import { getUniqueFormikErrors } from '@/lib/formik-helper';
import AlertErrorList from '@/components/helper/form/FormErrors';
interface MovementFormProps { interface MovementFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -53,6 +55,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
] = useState(''); ] = useState('');
const [selectedProducts, setSelectedProducts] = useState<number[]>([]); const [selectedProducts, setSelectedProducts] = useState<number[]>([]);
const [selectedDeliveries, setSelectedDeliveries] = useState<number[]>([]); const [selectedDeliveries, setSelectedDeliveries] = useState<number[]>([]);
const [formErrorList, setFormErrorList] = useState<string[]>([]);
// ===== FORM HANDLERS ===== // ===== FORM HANDLERS =====
const createMovementHandler = useCallback( const createMovementHandler = useCallback(
@@ -761,8 +764,36 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
type !== 'edit' && type !== 'edit' &&
type !== 'detail' type !== 'detail'
) { ) {
formik.setFieldValue('products', []); if (formik.values.products.length === 0) {
formik.setFieldValue('deliveries', []); 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]); }, [formik.values.source_warehouse_id]);
@@ -791,6 +822,22 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
formik.errors.destination_warehouse_id, 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<HTMLFormElement>) => {
e.preventDefault();
handleValidateForm();
formik.handleSubmit(e);
};
return ( return (
<> <>
<section className='w-full'> <section className='w-full'>
@@ -810,10 +857,29 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
</h1> </h1>
</header> </header>
<form <form
onSubmit={formik.handleSubmit} onSubmit={handleFormSubmit}
onReset={formik.handleReset} onReset={formik.handleReset}
className='w-full mt-8 flex flex-col gap-6' className='w-full mt-8 flex flex-col gap-6'
> >
{movementFormErrorMessage && (
<div role='alert' className='alert alert-error'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{movementFormErrorMessage}</span>
</div>
)}
{/* Error List Alert */}
{formErrorList.length > 0 && (
<AlertErrorList
formErrorList={formErrorList}
onClose={() => setFormErrorList([])}
/>
)}
{/* Top card - Movement details */} {/* Top card - Movement details */}
<Card <Card
title='Detail Movement' title='Detail Movement'
@@ -1097,7 +1163,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<th> <th>
Produk Produk
<span <span
className='tooltip tooltip-error tooltip-bottom z-[9999]' className='tooltip tooltip-error tooltip-bottom z-9999'
data-tip='required' data-tip='required'
> >
<span className='text-error'>*</span> <span className='text-error'>*</span>
@@ -1106,7 +1172,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<th> <th>
Qty Qty
<span <span
className='tooltip tooltip-error tooltip-bottom z-[9999]' className='tooltip tooltip-error tooltip-bottom z-9999'
data-tip='required' data-tip='required'
> >
<span className='text-error'>*</span> <span className='text-error'>*</span>
@@ -1119,7 +1185,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
{formik.values.products?.map((product, idx) => ( {formik.values.products?.map((product, idx) => (
<tr key={`product-row-${idx}-${product.product_id}`}> <tr key={`product-row-${idx}-${product.product_id}`}>
{type !== 'detail' && ( {type !== 'detail' && (
<td className='!align-middle'> <td className='align-middle!'>
<CheckboxInput <CheckboxInput
name={`product-${idx}`} name={`product-${idx}`}
checked={selectedProducts.includes(idx)} checked={selectedProducts.includes(idx)}
@@ -1311,7 +1377,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<th> <th>
Produk Produk
<span <span
className='tooltip tooltip-error tooltip-bottom z-[9999]' className='tooltip tooltip-error tooltip-bottom z-9999'
data-tip='required' data-tip='required'
> >
<span className='text-error'>*</span> <span className='text-error'>*</span>
@@ -1320,7 +1386,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<th> <th>
Qty Qty
<span <span
className='tooltip tooltip-error tooltip-bottom z-[9999]' className='tooltip tooltip-error tooltip-bottom z-9999'
data-tip='required' data-tip='required'
> >
<span className='text-error'>*</span> <span className='text-error'>*</span>
@@ -1329,7 +1395,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<th> <th>
Supplier Supplier
<span <span
className='tooltip tooltip-error tooltip-bottom z-[9999]' className='tooltip tooltip-error tooltip-bottom z-9999'
data-tip='required' data-tip='required'
> >
<span className='text-error'>*</span> <span className='text-error'>*</span>
@@ -1338,7 +1404,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<th> <th>
Plat Nomor Plat Nomor
<span <span
className='tooltip tooltip-error tooltip-bottom z-[9999]' className='tooltip tooltip-error tooltip-bottom z-9999'
data-tip='required' data-tip='required'
> >
<span className='text-error'>*</span> <span className='text-error'>*</span>
@@ -1348,7 +1414,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<th> <th>
Biaya Pengiriman (Rp.) Biaya Pengiriman (Rp.)
<span <span
className='tooltip tooltip-error tooltip-bottom z-[9999]' className='tooltip tooltip-error tooltip-bottom z-9999'
data-tip='required' data-tip='required'
> >
<span className='text-error'>*</span> <span className='text-error'>*</span>
@@ -1357,7 +1423,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<th> <th>
Biaya Per Item (Rp.) Biaya Per Item (Rp.)
<span <span
className='tooltip tooltip-error tooltip-bottom z-[9999]' className='tooltip tooltip-error tooltip-bottom z-9999'
data-tip='required' data-tip='required'
> >
<span className='text-error'>*</span> <span className='text-error'>*</span>
@@ -1366,7 +1432,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<th> <th>
Nama Sopir Nama Sopir
<span <span
className='tooltip tooltip-error tooltip-bottom z-[9999]' className='tooltip tooltip-error tooltip-bottom z-9999'
data-tip='required' data-tip='required'
> >
<span className='text-error'>*</span> <span className='text-error'>*</span>
@@ -1379,7 +1445,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
{formik.values.deliveries?.map((delivery, idx) => ( {formik.values.deliveries?.map((delivery, idx) => (
<tr key={`delivery-row-${idx}`}> <tr key={`delivery-row-${idx}`}>
{type !== 'detail' && ( {type !== 'detail' && (
<td className='!align-middle'> <td className='align-middle!'>
<CheckboxInput <CheckboxInput
name={`delivery-${idx}`} name={`delivery-${idx}`}
checked={selectedDeliveries.includes(idx)} checked={selectedDeliveries.includes(idx)}
@@ -1589,8 +1655,8 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
onChange={(e) => { onChange={(e) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (file) { if (file) {
if (file.size > 2 * 1024 * 1024) { if (file.size > 5 * 1024 * 1024) {
toast.error('Ukuran dokumen maksimal 2 MB!'); toast.error('Ukuran dokumen maksimal 5 MB!');
e.target.value = ''; e.target.value = '';
return; return;
} }
@@ -1747,7 +1813,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
disabled={ disabled={
hasInvalidQty || hasInvalidQty ||
hasExceededStock || hasExceededStock ||
!formik.isValid ||
formik.isSubmitting || formik.isSubmitting ||
(formik.values.source_warehouse_id === (formik.values.source_warehouse_id ===
formik.values.destination_warehouse_id && formik.values.destination_warehouse_id &&
@@ -1760,17 +1825,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
</div> </div>
)} )}
</div> </div>
{movementFormErrorMessage && (
<div role='alert' className='alert alert-error'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{movementFormErrorMessage}</span>
</div>
)}
</form> </form>
</section> </section>
</> </>
@@ -11,6 +11,8 @@ import TextInput from '@/components/input/TextInput';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import { getUniqueFormikErrors } from '@/lib/formik-helper';
import AlertErrorList from '@/components/helper/form/FormErrors';
import { import {
ProductCategoryFormSchema, ProductCategoryFormSchema,
@@ -39,6 +41,7 @@ const ProductCategoryForm = ({
const deleteModal = useModal(); const deleteModal = useModal();
const [formErrorMessage, setFormErrorMessage] = useState(''); const [formErrorMessage, setFormErrorMessage] = useState('');
const [formErrorList, setFormErrorList] = useState<string[]>([]);
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const createProductCategoryHandler = useCallback( const createProductCategoryHandler = useCallback(
@@ -129,6 +132,22 @@ const ProductCategoryForm = ({
formikSetValues(formikInitialValues); formikSetValues(formikInitialValues);
}, [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<HTMLFormElement>) => {
e.preventDefault();
handleValidateForm();
formik.handleSubmit(e);
};
return ( return (
<> <>
<section className='w-full max-w-2xl'> <section className='w-full max-w-2xl'>
@@ -150,10 +169,29 @@ const ProductCategoryForm = ({
</header> </header>
<form <form
onSubmit={formik.handleSubmit} onSubmit={handleFormSubmit}
onReset={formik.handleReset} onReset={formik.handleReset}
className='w-full mt-8 flex flex-col gap-6' className='w-full mt-8 flex flex-col gap-6'
> >
{formErrorMessage && (
<div role='alert' className='alert alert-error'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{formErrorMessage}</span>
</div>
)}
{/* Error List Alert */}
{formErrorList.length > 0 && (
<AlertErrorList
formErrorList={formErrorList}
onClose={() => setFormErrorList([])}
/>
)}
<div className='flex flex-col gap-4'> <div className='flex flex-col gap-4'>
<TextInput <TextInput
required required
@@ -236,7 +274,7 @@ const ProductCategoryForm = ({
type='submit' type='submit'
color='primary' color='primary'
isLoading={formik.isSubmitting} isLoading={formik.isSubmitting}
disabled={!formik.isValid || formik.isSubmitting} disabled={formik.isSubmitting}
className='px-4' className='px-4'
> >
Submit Submit
@@ -244,17 +282,6 @@ const ProductCategoryForm = ({
</div> </div>
)} )}
</div> </div>
{formErrorMessage && (
<div role='alert' className='alert alert-error'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{formErrorMessage}</span>
</div>
)}
</form> </form>
</section> </section>
@@ -29,36 +29,38 @@ export const ProductFormSchema: Yup.ObjectSchema<ProductFormSchemaType> =
sku: Yup.string().required('SKU wajib diisi!'), sku: Yup.string().required('SKU wajib diisi!'),
uom: Yup.object({ uom: Yup.object({
value: Yup.number().min(1).required(), value: Yup.number()
label: Yup.string().required(), .min(1, 'Satuan wajib dipilih!')
}) .required('Satuan wajib dipilih!'),
.nullable() label: Yup.string().required('Satuan wajib dipilih!'),
.required('Satuan wajib diisi!'), }).nullable(),
uom_id: Yup.number() uom_id: Yup.number()
.required('Satuan wajib diisi!') .min(1, 'Satuan wajib dipilih!')
.typeError('Satuan wajib diisi!'), .required('Satuan wajib dipilih!')
.typeError('Satuan wajib dipilih!'),
product_category: Yup.object({ product_category: Yup.object({
value: Yup.number().min(1).required(), value: Yup.number()
label: Yup.string().required(), .min(1, 'Kategori produk wajib dipilih!')
}) .required('Kategori produk wajib dipilih!'),
.nullable() label: Yup.string().required('Kategori produk wajib dipilih!'),
.required('Kategori produk wajib diisi!'), }).nullable(),
product_category_id: Yup.number() product_category_id: Yup.number()
.required('Kategori produk wajib diisi!') .min(1, 'Kategori produk wajib dipilih!')
.typeError('Kategori produk wajib diisi!'), .required('Kategori produk wajib dipilih!')
.typeError('Kategori produk wajib dipilih!'),
product_price: Yup.number() product_price: Yup.number()
.required('Harga produk wajib diisi!') .required('Harga produk wajib diisi!')
.typeError('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() selling_price: Yup.number()
.required('Harga jual wajib diisi!') .required('Harga jual wajib diisi!')
.typeError('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() tax: Yup.number()
.required('Pajak wajib diisi!') .required('Pajak wajib diisi!')
@@ -69,7 +71,7 @@ export const ProductFormSchema: Yup.ObjectSchema<ProductFormSchemaType> =
expiry_period: Yup.number() expiry_period: Yup.number()
.required('Periode kadaluarsa wajib diisi!') .required('Periode kadaluarsa wajib diisi!')
.typeError('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() supplier_ids: Yup.array()
.of(Yup.number().required().typeError('Supplier tidak valid!')) .of(Yup.number().required().typeError('Supplier tidak valid!'))
@@ -17,6 +17,8 @@ import SelectInput, {
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import { getUniqueFormikErrors } from '@/lib/formik-helper';
import AlertErrorList from '@/components/helper/form/FormErrors';
import { import {
ProductFormSchema, ProductFormSchema,
@@ -48,6 +50,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
const deleteModal = useModal(); const deleteModal = useModal();
const [productFormErrorMessage, setProductFormErrorMessage] = useState(''); const [productFormErrorMessage, setProductFormErrorMessage] = useState('');
const [formErrorList, setFormErrorList] = useState<string[]>([]);
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const createProductHandler = useCallback( const createProductHandler = useCallback(
@@ -201,6 +204,22 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
formikSetValues(formikInitialValues); formikSetValues(formikInitialValues);
}, [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<HTMLFormElement>) => {
e.preventDefault();
handleValidateForm();
formik.handleSubmit(e);
};
return ( return (
<> <>
<section className='w-full max-w-2xl'> <section className='w-full max-w-2xl'>
@@ -220,11 +239,30 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
</h1> </h1>
</header> </header>
<form <form
onSubmit={formik.handleSubmit} onSubmit={handleFormSubmit}
onReset={formik.handleReset} onReset={formik.handleReset}
className='w-full mt-8 flex flex-col gap-6' className='w-full mt-8 flex flex-col gap-6'
> >
<div className='flex flex-col gap-4'> {productFormErrorMessage && (
<div role='alert' className='alert alert-error'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{productFormErrorMessage}</span>
</div>
)}
{/* Error List Alert */}
{formErrorList.length > 0 && (
<AlertErrorList
formErrorList={formErrorList}
onClose={() => setFormErrorList([])}
/>
)}
<div className='grid grid-cols-1 gap-4'>
<TextInput <TextInput
required required
label='Nama' label='Nama'
@@ -237,179 +275,193 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
errorMessage={formik.errors.name} errorMessage={formik.errors.name}
readOnly={type === 'detail'} readOnly={type === 'detail'}
/> />
<TextInput <div className='grid sm:grid-cols-2 gap-4'>
required <TextInput
label='Merek' required
name='brand' label='Merek'
placeholder='Masukkan merek...' name='brand'
value={formik.values.brand} placeholder='Masukkan merek...'
onChange={formik.handleChange} value={formik.values.brand}
onBlur={formik.handleBlur} onChange={formik.handleChange}
isError={formik.touched.brand && Boolean(formik.errors.brand)} onBlur={formik.handleBlur}
errorMessage={formik.errors.brand} isError={formik.touched.brand && Boolean(formik.errors.brand)}
readOnly={type === 'detail'} errorMessage={formik.errors.brand}
/> readOnly={type === 'detail'}
<TextInput />
required <TextInput
label='SKU' required
name='sku' label='SKU'
placeholder='Masukkan SKU...' name='sku'
value={formik.values.sku} placeholder='Masukkan SKU...'
onChange={formik.handleChange} value={formik.values.sku}
onBlur={formik.handleBlur} onChange={formik.handleChange}
isError={formik.touched.sku && Boolean(formik.errors.sku)} onBlur={formik.handleBlur}
errorMessage={formik.errors.sku} isError={formik.touched.sku && Boolean(formik.errors.sku)}
readOnly={type === 'detail'} errorMessage={formik.errors.sku}
/> readOnly={type === 'detail'}
<SelectInput />
required </div>
label='Satuan' <div className='grid sm:grid-cols-2 gap-4'>
placeholder='Pilih satuan...' <SelectInput
value={formik.values.uom ?? undefined} required
onChange={uomChangeHandler} label='Satuan'
options={uomOptions} placeholder='Pilih satuan...'
onInputChange={setUomSelectInputValue} value={formik.values.uom ?? undefined}
isLoading={isLoadingUoms} onChange={uomChangeHandler}
isError={formik.touched.uom_id && Boolean(formik.errors.uom_id)} options={uomOptions}
errorMessage={formik.errors.uom_id as string} onInputChange={setUomSelectInputValue}
isDisabled={type === 'detail'} isLoading={isLoadingUoms}
isClearable isError={
/> (formik.touched.uom || formik.touched.uom_id) &&
<SelectInput Boolean(formik.errors.uom_id)
required }
label='Kategori Produk' errorMessage={formik.errors.uom_id as string}
placeholder='Pilih kategori produk...' isDisabled={type === 'detail'}
value={formik.values.product_category ?? undefined} isClearable
onChange={categoryChangeHandler} />
options={categoryOptions} <SelectInput
onInputChange={setCategorySelectInputValue} required
isLoading={isLoadingCategories} label='Kategori Produk'
isError={ placeholder='Pilih kategori produk...'
formik.touched.product_category_id && value={formik.values.product_category ?? undefined}
Boolean(formik.errors.product_category_id) onChange={categoryChangeHandler}
} options={categoryOptions}
errorMessage={formik.errors.product_category_id as string} onInputChange={setCategorySelectInputValue}
isDisabled={type === 'detail'} isLoading={isLoadingCategories}
isClearable isError={
/> (formik.touched.product_category ||
<NumberInput formik.touched.product_category_id) &&
required Boolean(formik.errors.product_category_id)
label='Harga Produk' }
name='product_price' errorMessage={formik.errors.product_category_id as string}
placeholder='Masukkan harga produk...' isDisabled={type === 'detail'}
value={formik.values.product_price} isClearable
onChange={formik.handleChange} />
onBlur={formik.handleBlur} </div>
decimalScale={2} <div className='grid sm:grid-cols-2 gap-4'>
allowNegative={false} <NumberInput
thousandSeparator=',' required
decimalSeparator='.' label='Harga Produk'
inputPrefix='Rp ' name='product_price'
isError={ placeholder='Masukkan harga produk...'
formik.touched.product_price && value={formik.values.product_price}
Boolean(formik.errors.product_price) onChange={formik.handleChange}
} onBlur={formik.handleBlur}
errorMessage={formik.errors.product_price as string} decimalScale={2}
readOnly={type === 'detail'} allowNegative={false}
/> thousandSeparator=','
<NumberInput decimalSeparator='.'
required inputPrefix='Rp '
label='Harga Jual' isError={
name='selling_price' formik.touched.product_price &&
placeholder='Masukkan harga jual...' Boolean(formik.errors.product_price)
value={formik.values.selling_price} }
onChange={formik.handleChange} errorMessage={formik.errors.product_price as string}
onBlur={formik.handleBlur} readOnly={type === 'detail'}
decimalScale={2} />
allowNegative={false} <NumberInput
thousandSeparator=',' required
decimalSeparator='.' label='Harga Jual'
inputPrefix='Rp ' name='selling_price'
isError={ placeholder='Masukkan harga jual...'
formik.touched.selling_price && value={formik.values.selling_price}
Boolean(formik.errors.selling_price) onChange={formik.handleChange}
} onBlur={formik.handleBlur}
errorMessage={formik.errors.selling_price as string} decimalScale={2}
readOnly={type === 'detail'} allowNegative={false}
/> thousandSeparator=','
<NumberInput decimalSeparator='.'
required inputPrefix='Rp '
label='Pajak (%)' isError={
name='tax' formik.touched.selling_price &&
placeholder='Masukkan pajak...' Boolean(formik.errors.selling_price)
value={formik.values.tax} }
onChange={formik.handleChange} errorMessage={formik.errors.selling_price as string}
onBlur={formik.handleBlur} readOnly={type === 'detail'}
decimalScale={2} />
allowNegative={false} </div>
thousandSeparator=',' <div className='grid sm:grid-cols-2 gap-4'>
decimalSeparator='.' <NumberInput
inputSuffix='%' required
isError={formik.touched.tax && Boolean(formik.errors.tax)} label='Pajak (%)'
errorMessage={formik.errors.tax as string} name='tax'
readOnly={type === 'detail'} placeholder='Masukkan pajak...'
/> value={formik.values.tax}
<NumberInput onChange={formik.handleChange}
required onBlur={formik.handleBlur}
label='Periode Kadaluarsa (hari)' decimalScale={2}
name='expiry_period' allowNegative={false}
placeholder='Masukkan periode kadaluarsa...' thousandSeparator=','
value={formik.values.expiry_period} decimalSeparator='.'
onChange={formik.handleChange} inputSuffix='%'
onBlur={formik.handleBlur} isError={formik.touched.tax && Boolean(formik.errors.tax)}
decimalScale={0} errorMessage={formik.errors.tax as string}
allowNegative={false} readOnly={type === 'detail'}
thousandSeparator=',' />
decimalSeparator='.' <NumberInput
inputSuffix='hari' required
isError={ label='Periode Kadaluarsa (hari)'
formik.touched.expiry_period && name='expiry_period'
Boolean(formik.errors.expiry_period) placeholder='Masukkan periode kadaluarsa...'
} value={formik.values.expiry_period}
errorMessage={formik.errors.expiry_period as string} onChange={formik.handleChange}
readOnly={type === 'detail'} onBlur={formik.handleBlur}
/> decimalScale={0}
<SelectInput allowNegative={false}
required thousandSeparator=','
label='Supplier' decimalSeparator='.'
placeholder='Pilih supplier...' inputSuffix='hari'
isMulti isError={
value={supplierOptions.filter((opt) => formik.touched.expiry_period &&
(formik.values.supplier_ids || []).includes(opt.value) Boolean(formik.errors.expiry_period)
)} }
onChange={supplierChangeHandler} errorMessage={formik.errors.expiry_period as string}
options={supplierOptions} readOnly={type === 'detail'}
onInputChange={setSupplierSelectInputValue} />
isLoading={isLoadingSuppliers} </div>
isError={ <div className='grid sm:grid-cols-2 gap-4'>
formik.touched.supplier_ids && <SelectInput
Boolean(formik.errors.supplier_ids) required
} label='Supplier'
errorMessage={formik.errors.supplier_ids as string} placeholder='Pilih supplier...'
isDisabled={type === 'detail'} isMulti
isClearable value={supplierOptions.filter((opt) =>
/> (formik.values.supplier_ids || []).includes(opt.value)
<SelectInput )}
required onChange={supplierChangeHandler}
label='Flags' options={supplierOptions}
placeholder='Pilih flags...' onInputChange={setSupplierSelectInputValue}
isMulti isLoading={isLoadingSuppliers}
value={PRODUCT_FLAG_OPTIONS.filter((opt) => isError={
(formik.values.flags || []).includes(opt.value) formik.touched.supplier_ids &&
)} Boolean(formik.errors.supplier_ids)
onChange={(val) => { }
const arr = Array.isArray(val) ? val : val ? [val] : []; errorMessage={formik.errors.supplier_ids as string}
formik.setFieldValue( isDisabled={type === 'detail'}
'flags', isClearable
arr.map((v) => (v as OptionType).value) />
); <SelectInput
}} required
options={PRODUCT_FLAG_OPTIONS} label='Flags'
isError={formik.touched.flags && Boolean(formik.errors.flags)} placeholder='Pilih flags...'
errorMessage={formik.errors.flags as string} isMulti
isDisabled={type === 'detail'} value={PRODUCT_FLAG_OPTIONS.filter((opt) =>
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
/>
</div>
</div> </div>
<div className='flex flex-row justify-between gap-2 flex-wrap'> <div className='flex flex-row justify-between gap-2 flex-wrap'>
{type !== 'add' && ( {type !== 'add' && (
@@ -463,7 +515,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
type='submit' type='submit'
color='primary' color='primary'
isLoading={formik.isSubmitting} isLoading={formik.isSubmitting}
disabled={!formik.isValid || formik.isSubmitting} disabled={formik.isSubmitting}
className='px-4' className='px-4'
> >
Submit Submit
@@ -471,16 +523,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
</div> </div>
)} )}
</div> </div>
{productFormErrorMessage && (
<div role='alert' className='alert alert-error'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{productFormErrorMessage}</span>
</div>
)}
</form> </form>
</section> </section>
{type !== 'add' && ( {type !== 'add' && (
@@ -17,6 +17,7 @@ import CheckboxInput from '@/components/input/CheckboxInput';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import AlertErrorList from '@/components/helper/form/FormErrors';
import { import {
ProjectFlockKandangApi, ProjectFlockKandangApi,
@@ -52,6 +53,7 @@ import {
import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
import { formatDate, formatNumber } from '@/lib/helper'; import { formatDate, formatNumber } from '@/lib/helper';
import { getUniqueFormikErrors } from '@/lib/formik-helper';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import ApprovalSteps, { import ApprovalSteps, {
useApprovalSteps, useApprovalSteps,
@@ -60,7 +62,6 @@ import {
GROWING_RECORDING_APPROVAL_LINE, GROWING_RECORDING_APPROVAL_LINE,
LAYING_RECORDING_APPROVAL_LINE, LAYING_RECORDING_APPROVAL_LINE,
} from '@/config/approval-line'; } from '@/config/approval-line';
import Table from '@/components/Table';
interface RecordingFormProps { interface RecordingFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -92,6 +93,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const [, setApprovalNotes] = useState(''); const [, setApprovalNotes] = useState('');
const [recordingFormErrorMessage, setRecordingFormErrorMessage] = const [recordingFormErrorMessage, setRecordingFormErrorMessage] =
useState(''); useState('');
const [formErrorList, setFormErrorList] = useState<string[]>([]);
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [, setNewRecordingData] = useState<Recording | null>(null); const [, setNewRecordingData] = useState<Recording | null>(null);
const [nextDayRecording, setNextDayRecording] = 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<HTMLFormElement>) => {
e.preventDefault();
handleValidateForm();
formik.handleSubmit(e);
};
// ===== HELPER FUNCTIONS ===== // ===== HELPER FUNCTIONS =====
useCallback((): OptionType | null => { useCallback((): OptionType | null => {
if ( if (
@@ -1323,9 +1341,28 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
)} )}
<form <form
onSubmit={formik.handleSubmit} onSubmit={handleFormSubmit}
className='w-full mt-8 flex flex-col gap-6' className='w-full mt-8 flex flex-col gap-6'
> >
{recordingFormErrorMessage && (
<div role='alert' className='alert alert-error'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{recordingFormErrorMessage}</span>
</div>
)}
{/* Error List Alert */}
{formErrorList.length > 0 && (
<AlertErrorList
formErrorList={formErrorList}
onClose={() => setFormErrorList([])}
/>
)}
{/* Basic Info Card */} {/* Basic Info Card */}
{(type === 'add' || type === 'edit') && ( {(type === 'add' || type === 'edit') && (
<Card <Card
@@ -2507,9 +2544,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
color='primary' color='primary'
className='px-4' className='px-4'
isLoading={formik.isSubmitting} isLoading={formik.isSubmitting}
disabled={ disabled={hasExceededStock || formik.isSubmitting}
hasExceededStock || !formik.isValid || formik.isSubmitting
}
> >
Submit Submit
</Button> </Button>
@@ -2534,9 +2569,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
color='primary' color='primary'
className='px-4' className='px-4'
isLoading={formik.isSubmitting} isLoading={formik.isSubmitting}
disabled={ disabled={hasExceededStock || formik.isSubmitting}
hasExceededStock || !formik.isValid || formik.isSubmitting
}
> >
Submit Submit
</Button> </Button>
@@ -2544,16 +2577,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
)} )}
</div> </div>
</div> </div>
{recordingFormErrorMessage && (
<div role='alert' className='alert alert-error'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{recordingFormErrorMessage}</span>
</div>
)}
</form> </form>
</section> </section>
@@ -1,93 +1,86 @@
import React, { useMemo } from 'react'; import React, { useMemo, useState } from 'react';
import Card from '@/components/Card'; import Card from '@/components/Card';
import UniformityBarChart from '@/components/pages/production/uniformity/chart/UniformityBarChart'; import UniformityBarChart from '@/components/pages/production/uniformity/chart/UniformityBarChart';
import UniformityGaugeChart from '@/components/pages/production/uniformity/chart/UniformityGaugeChart'; import UniformityGaugeChart from '@/components/pages/production/uniformity/chart/UniformityGaugeChart';
import UniformityBarChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton'; import UniformityBarChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton';
import UniformityGaugeChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton'; import UniformityGaugeChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton';
import { import { Uniformity, type ChartData } from '@/types/api/production/uniformity';
UniformityDetailItem,
Uniformity,
} from '@/types/api/production/uniformity';
interface UniformityChartProps { interface UniformityChartProps {
uniformityData?: Uniformity | null; uniformityData?: Uniformity | null;
uniformityDetails?: UniformityDetailItem[]; isFiltered?: boolean;
} }
const UniformityChart = ({ const UniformityChart = ({
uniformityData, uniformityData,
uniformityDetails, isFiltered = false,
}: UniformityChartProps) => { }: UniformityChartProps) => {
const defaultUniformityDetails: UniformityDetailItem[] = [ const [currentWeekIndex, setCurrentWeekIndex] = useState(0);
{ 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 detailsToUse = uniformityDetails || defaultUniformityDetails; const chartData = useMemo((): ChartData | undefined => {
if (!uniformityData?.chart_data) return undefined;
return uniformityData.chart_data;
}, [uniformityData]);
const barChartData = useMemo(() => { const barChartData = useMemo(() => {
if (!uniformityData) { if (!chartData?.bar_chart) {
return []; 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 []; return [];
} }
const weights = detailsToUse.map((d) => d.weight); return weekData.weight_distribution.map((range) => ({
const minWeight = Math.floor(Math.min(...weights) / 5) * 5; name: range.range,
const maxWeight = Math.ceil(Math.max(...weights) / 5) * 5; uv: range.bird_count,
isIdeal: range.is_ideal_range,
const rangeSize = maxWeight - minWeight < 11 ? 4 : 5; idealCount: range.is_ideal_range
const ranges: string[] = []; ? weekData.ideal_range.total_ideal_birds
: undefined,
for (let start = minWeight; start <= maxWeight; start += rangeSize) { }));
const end = start + rangeSize; }, [chartData]);
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]);
const gaugeChartData = useMemo(() => { 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 { return {
value: uniformityData.uniformity, value: currentWeekData.uniformity_percentage,
label: 'Uniformity', label: 'Uniformity',
week: `Week ${uniformityData.week}`, week: `Week ${currentWeekData.week}`,
currentValue: uniformityData.uniform_qty, currentValue: currentWeekData.ideal_count,
totalValue: uniformityData.chick_qty_of_weight, 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 ( return (
<section className='w-full grid grid-cols-1 xl:grid-cols-2 2xl:grid-cols-4 gap-4'> <section className='w-full grid grid-cols-1 xl:grid-cols-2 2xl:grid-cols-4 gap-4'>
@@ -100,14 +93,16 @@ const UniformityChart = ({
}} }}
> >
<div className='w-full h-full flex items-center justify-center'> <div className='w-full h-full flex items-center justify-center'>
{!uniformityData || barChartData.length === 0 ? ( {shouldShowEmptyState ||
!uniformityData ||
barChartData.length === 0 ? (
<UniformityBarChartSkeleton /> <UniformityBarChartSkeleton />
) : ( ) : (
<UniformityBarChart data={barChartData} /> <UniformityBarChart data={barChartData} />
)} )}
</div> </div>
</Card> </Card>
{!uniformityData || !gaugeChartData ? ( {shouldShowEmptyState || !uniformityData || !gaugeChartData ? (
<Card <Card
variant='bordered' variant='bordered'
title='Weekly Performance ⓘ' title='Weekly Performance ⓘ'
@@ -133,6 +128,9 @@ const UniformityChart = ({
week={gaugeChartData.week} week={gaugeChartData.week}
currentValue={gaugeChartData.currentValue} currentValue={gaugeChartData.currentValue}
totalValue={gaugeChartData.totalValue} totalValue={gaugeChartData.totalValue}
onWeekChange={handleWeekChange}
hasPrevWeek={gaugeChartData.hasPrevWeek}
hasNextWeek={gaugeChartData.hasNextWeek}
/> />
</Card> </Card>
)} )}
@@ -151,8 +151,10 @@ const UniformityConfirmationPreview = ({
const UniformityChartWrapper = ({ const UniformityChartWrapper = ({
uniformitySwrKey, uniformitySwrKey,
isFiltered,
}: { }: {
uniformitySwrKey: string; uniformitySwrKey: string;
isFiltered: boolean;
}) => { }) => {
const { data: uniformities } = useSWR( const { data: uniformities } = useSWR(
uniformitySwrKey, uniformitySwrKey,
@@ -166,31 +168,8 @@ const UniformityChartWrapper = ({
return null; return null;
}, [uniformities]); }, [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 ( return (
<UniformityChart <UniformityChart uniformityData={uniformityData} isFiltered={isFiltered} />
uniformityData={uniformityData}
uniformityDetails={uniformityDetails}
/>
); );
}; };
@@ -374,6 +353,7 @@ const UniformityTable = () => {
if (filterEndDate) { if (filterEndDate) {
queryParams.append('end_date', filterEndDate); queryParams.append('end_date', filterEndDate);
} }
queryParams.append('with_chart', 'true');
} }
const tableQueryString = getTableFilterQueryString(); const tableQueryString = getTableFilterQueryString();
@@ -433,6 +413,7 @@ const UniformityTable = () => {
); );
const handleResetFilters = useCallback(() => { const handleResetFilters = useCallback(() => {
setIsSubmitted(false);
setFilterLocation(null); setFilterLocation(null);
setFilterProjectFlock(null); setFilterProjectFlock(null);
setFilterKandang(null); setFilterKandang(null);
@@ -896,7 +877,10 @@ const UniformityTable = () => {
<div className='my-4 divider'></div> <div className='my-4 divider'></div>
<section> <section>
<UniformityChartWrapper uniformitySwrKey={uniformitySwrKey} /> <UniformityChartWrapper
uniformitySwrKey={uniformitySwrKey}
isFiltered={isSubmitted}
/>
</section> </section>
<Card <Card
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useMemo, useEffect } from 'react'; import { useMemo, useEffect, useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { ColumnDef } from '@tanstack/react-table'; import { ColumnDef } from '@tanstack/react-table';
@@ -11,9 +11,12 @@ import Badge from '@/components/Badge';
import Tooltip from '@/components/Tooltip'; import Tooltip from '@/components/Tooltip';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import { UniformityDetail as UniformityDetailType } from '@/types/api/production/uniformity'; 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 { useUiStore } from '@/stores/ui/ui.store';
import UniformityDetailsPreview from '@/components/pages/production/uniformity/detail/UniformityDetailsPreview'; import UniformityDetailsPreview from '@/components/pages/production/uniformity/detail/UniformityDetailsPreview';
import { UniformityApi } from '@/services/api/uniformity';
import useSWR from 'swr';
import { isResponseSuccess } from '@/lib/api-helper';
import { import {
getStatusColor, getStatusColor,
getStatusIndicatorColor, getStatusIndicatorColor,
@@ -33,6 +36,22 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
const setExpandedDrawerContent = useUiStore( const setExpandedDrawerContent = useUiStore(
(s) => s.setExpandedDrawerContent (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 = () => { const handleApprove = () => {
router.push(`/production/uniformity?action=approve&id=${initialValues.id}`); router.push(`/production/uniformity?action=approve&id=${initialValues.id}`);
@@ -43,12 +62,15 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
}; };
const handleViewUniformityDetails = () => { const handleViewUniformityDetails = () => {
if (!uniformity_details || uniformity_details.length === 0) {
setShouldFetchDetails(true);
return;
}
setExpandedDrawerContent( setExpandedDrawerContent(
<UniformityDetailsPreview <UniformityDetailsPreview
info_umum={initialValues.info_umum} info_umum={initialValues.info_umum}
uniformity_details={initialValues.uniformity_details} uniformity_details={uniformity_details}
sampling={initialValues.sampling}
result={initialValues.result}
uniformityId={initialValues.id} uniformityId={initialValues.id}
/> />
); );
@@ -58,6 +80,28 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
}, 0); }, 0);
}; };
useEffect(() => {
if (
shouldFetchDetails &&
uniformity_details &&
uniformity_details.length > 0 &&
!hasFetchedDetails
) {
setExpandedDrawerContent(
<UniformityDetailsPreview
info_umum={initialValues.info_umum}
uniformity_details={uniformity_details}
uniformityId={initialValues.id}
/>
);
setHasFetchedDetails(true);
setTimeout(() => {
setExpandedDrawerOpen(true);
}, 0);
}
}, [shouldFetchDetails, uniformity_details, hasFetchedDetails]);
useEffect(() => { useEffect(() => {
return () => { return () => {
setExpandedDrawerOpen(false); setExpandedDrawerOpen(false);
@@ -154,12 +198,22 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
return ( return (
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<span>{valueMap[id]}</span> <span>{valueMap[id]}</span>
<Tooltip content='Lihat Detail'> <Tooltip content='Lihat Detail' position='left'>
<button <button
className='p-1 hover:bg-gray-100 rounded cursor-pointer' className='p-1 hover:bg-gray-100 rounded cursor-pointer disabled:opacity-50'
onClick={handleViewUniformityDetails} onClick={handleViewUniformityDetails}
disabled={isLoading}
> >
<Icon icon='mdi:eye-outline' width={18} height={18} /> {isLoading ? (
<Icon
icon='mdi:loading'
width={18}
height={18}
className='animate-spin'
/>
) : (
<Icon icon='mdi:eye-outline' width={18} height={18} />
)}
</button> </button>
</Tooltip> </Tooltip>
</div> </div>
@@ -173,6 +227,92 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
[initialValues] [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<DetailOptionType>[] = useMemo(
() => [
{
accessorKey: 'label',
header: 'Label',
cell: (props) => props.row.original.label,
},
{
accessorKey: 'value',
header: 'Value',
cell: (props) => <span>{props.row.original.value}</span>,
},
],
[]
);
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<DetailOptionType>[] = useMemo(
() => [
{
accessorKey: 'label',
header: 'Label',
cell: (props) => props.row.original.label,
},
{
accessorKey: 'value',
header: 'Value',
cell: (props) => <span>{props.row.original.value}</span>,
},
],
[]
);
return ( return (
<section className='w-full h-full bg-white border-l border-gray-200'> <section className='w-full h-full bg-white border-l border-gray-200'>
{/* Header */} {/* Header */}
@@ -185,7 +325,7 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
{/* Form Section */} {/* Form Section */}
<div className='divider mt-3.5'></div> <div className='divider mt-3.5'></div>
<section className='w-full px-6'> <section className='w-full px-6 mb-6'>
{initialValues ? ( {initialValues ? (
<div className='flex flex-col gap-4'> <div className='flex flex-col gap-4'>
{/* Info Umum */} {/* Info Umum */}
@@ -200,23 +340,55 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
paginationClassName: 'hidden', paginationClassName: 'hidden',
}} }}
/> />
{/* Approve/Reject Buttons */}
{initialValues.result &&
initialValues.latest_approval?.step_name === 'CREATED' ? (
<>
<div className='divider my-3.5' />
<RequirePermission permissions='lti.production.uniformity.approve'>
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4 [&_button]:rounded-lg'>
<Button variant='outline' onClick={handleReject}>
Reject
</Button>
<Button onClick={handleApprove}>Approve</Button>
</div>
</RequirePermission>
</>
) : null}
</div> </div>
{/* Sampling and Range */}
{initialValues.sampling && (
<div className=''>
<p className='text-sm font-medium mb-5'>Sampling and Range</p>
<Table<DetailOptionType>
data={samplingTableData}
columns={columnsSampling}
pageSize={4}
className={{
containerClassName: 'mb-0',
paginationClassName: 'hidden',
}}
/>
</div>
)}
{/* Result */}
{initialValues.result && (
<div className=''>
<p className='text-sm font-medium mb-5'>Result</p>
<Table<DetailOptionType>
data={resultTableData}
columns={resultColumns}
pageSize={4}
className={{
containerClassName: 'mb-0',
paginationClassName: 'hidden',
}}
/>
</div>
)}
{/* Approve/Reject Buttons */}
{initialValues.result &&
initialValues.latest_approval?.step_name === 'CREATED' ? (
<>
<div className='divider my-3.5' />
<RequirePermission permissions='lti.production.uniformity.approve'>
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4 [&_button]:rounded-lg'>
<Button variant='outline' onClick={handleReject}>
Reject
</Button>
<Button onClick={handleApprove}>Approve</Button>
</div>
</RequirePermission>
</>
) : null}
</div> </div>
) : ( ) : (
<div className='flex flex-col items-center justify-center py-10 text-gray-400'> <div className='flex flex-col items-center justify-center py-10 text-gray-400'>
@@ -1,152 +1,39 @@
'use client'; 'use client';
import React, { useMemo, useState } from 'react'; import React, { useMemo } from 'react';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { ColumnDef } from '@tanstack/react-table'; import { ColumnDef } from '@tanstack/react-table';
import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
import { useUiStore } from '@/stores/ui/ui.store'; import { useUiStore } from '@/stores/ui/ui.store';
import { import {
UniformityDetailItem, UniformityDetailItem,
UniformitySampling,
UniformityResult,
UniformityInfoUmum, UniformityInfoUmum,
} from '@/types/api/production/uniformity'; } from '@/types/api/production/uniformity';
import Table from '@/components/Table'; import Table from '@/components/Table';
import Badge from '@/components/Badge'; import Badge from '@/components/Badge';
import { formatNumber } from '@/lib/helper';
import { DetailOptionType } from '@/types/api/production/uniformity';
import { import {
getWeightStatusColor, getWeightStatusColor,
getWeightStatusIndicatorColor, getWeightStatusIndicatorColor,
getWeightStatusText, getWeightStatusText,
} from '@/components/pages/production/uniformity/uniformity-utils'; } from '@/components/pages/production/uniformity/uniformity-utils';
import { BodyWeightData } from '@/types/api/production/uniformity'; 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 { interface UniformityDetailsPreviewProps {
info_umum: UniformityInfoUmum; info_umum: UniformityInfoUmum;
sampling: UniformitySampling;
result: UniformityResult;
uniformity_details?: UniformityDetailItem[]; uniformity_details?: UniformityDetailItem[];
uniformityId: number; uniformityId: number;
} }
const UniformityDetailsPreview = ({ const UniformityDetailsPreview = ({
info_umum, info_umum,
uniformity_details: initialUniformityDetails, uniformity_details,
sampling,
result,
uniformityId,
}: UniformityDetailsPreviewProps) => { }: UniformityDetailsPreviewProps) => {
const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen); 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 = () => { const handleClose = () => {
setExpandedDrawerOpen(false); 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<DetailOptionType>[] = useMemo(
() => [
{
accessorKey: 'label',
header: 'Label',
cell: (props) => props.row.original.label,
},
{
accessorKey: 'value',
header: 'Value',
cell: (props) => <span>{props.row.original.value}</span>,
},
],
[]
);
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<DetailOptionType>[] = useMemo(
() => [
{
accessorKey: 'label',
header: 'Label',
cell: (props) => props.row.original.label,
},
{
accessorKey: 'value',
header: 'Value',
cell: (props) => <span>{props.row.original.value}</span>,
},
],
[]
);
const tableData = useMemo(() => { const tableData = useMemo(() => {
if (!uniformity_details) return []; if (!uniformity_details) return [];
@@ -229,55 +116,10 @@ const UniformityDetailsPreview = ({
{/* Form Section */} {/* Form Section */}
<div className='divider mt-3.5'></div> <div className='divider mt-3.5'></div>
<section className='w-full px-6'> <section className='w-full px-6'>
{info_umum || sampling || result ? ( {info_umum ? (
<div className='flex flex-col gap-4'> <div className='flex flex-col gap-4'>
{/* Sampling and Range */}
{sampling && (
<div className=''>
<p className='text-sm font-medium mb-5'>Sampling and Range</p>
<Table<DetailOptionType>
data={samplingTableData}
columns={columnsSampling}
pageSize={4}
className={{
containerClassName: 'mb-0',
paginationClassName: 'hidden',
}}
/>
</div>
)}
{/* Result */}
{result && (
<div className=''>
<p className='text-sm font-medium mb-5'>Result</p>
<Table<DetailOptionType>
data={resultTableData}
columns={resultColumns}
pageSize={4}
className={{
containerClassName: 'mb-0',
paginationClassName: 'hidden',
}}
/>
</div>
)}
{!uniformity_details || uniformity_details.length === 0 ? (
<div className='mt-4'>
<Button
type='button'
onClick={fetchWeightData}
disabled={isLoading}
className='w-full'
>
{isLoading ? 'Loading...' : 'Show Body Weight Details'}
</Button>
</div>
) : null}
{/* Body Weight Details */} {/* Body Weight Details */}
{uniformity_details && uniformity_details.length > 0 && ( {uniformity_details && uniformity_details.length > 0 ? (
<div className='mt-4'> <div className='mt-4'>
<Table<BodyWeightData> <Table<BodyWeightData>
data={tableData} data={tableData}
@@ -286,6 +128,17 @@ const UniformityDetailsPreview = ({
className={{ containerClassName: 'mb-5' }} className={{ containerClassName: 'mb-5' }}
/> />
</div> </div>
) : (
<div className='flex flex-col items-center justify-center py-10 text-gray-400'>
<Icon
icon='mdi:file-document-outline'
width={64}
height={64}
className='mb-4'
/>
<p className='text-lg'>No data available</p>
<p className='text-sm'>Body weight details not found</p>
</div>
)} )}
</div> </div>
) : ( ) : (
@@ -24,9 +24,9 @@ type UniformityFormSchemaType = {
}; };
const FileSchema = Yup.mixed<File>() const FileSchema = Yup.mixed<File>()
.test('documentSize', 'Ukuran file maksimal 2 MB', (value): boolean => { .test('documentSize', 'Ukuran file maksimal 5 MB', (value): boolean => {
if (!value) return true; 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; return false;
}) })
.test('documentType', 'Format file harus Excel', (value): boolean => { .test('documentType', 'Format file harus Excel', (value): boolean => {
@@ -43,7 +43,9 @@ import UniformityResultForm from '@/components/pages/production/uniformity/form/
import { generateUniformityTemplate } from '@/components/pages/production/uniformity/export/UniformityTemplate'; import { generateUniformityTemplate } from '@/components/pages/production/uniformity/export/UniformityTemplate';
import useSWR from 'swr'; import useSWR from 'swr';
import { cn, formatNumber } from '@/lib/helper'; import { cn, formatNumber } from '@/lib/helper';
import { getUniqueFormikErrors } from '@/lib/formik-helper';
import Tooltip from '@/components/Tooltip'; import Tooltip from '@/components/Tooltip';
import AlertErrorList from '@/components/helper/form/FormErrors';
interface UniformityFormProps { interface UniformityFormProps {
formType?: 'add' | 'edit'; formType?: 'add' | 'edit';
@@ -77,6 +79,7 @@ const UniformityForm = ({
const [uniformityFormErrorMessage, setUniformityFormErrorMessage] = const [uniformityFormErrorMessage, setUniformityFormErrorMessage] =
useState(''); useState('');
const [formErrorList, setFormErrorList] = useState<string[]>([]);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(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<HTMLFormElement>) => {
e.preventDefault();
handleValidateForm();
formik.handleSubmit(e);
};
// ===== FORM HANDLERS ===== // ===== FORM HANDLERS =====
const handleLocationChange = useCallback( const handleLocationChange = useCallback(
(val: OptionType | OptionType[] | null) => { (val: OptionType | OptionType[] | null) => {
@@ -339,8 +358,8 @@ const UniformityForm = ({
return; return;
} }
if (document.size > 2 * 1024 * 1024) { if (document.size > 5 * 1024 * 1024) {
toast.error(`Ukuran file ${document.name} maksimal 2 MB!`); toast.error(`Ukuran file ${document.name} maksimal 5 MB!`);
return; return;
} }
@@ -454,7 +473,7 @@ const UniformityForm = ({
<section className='w-full px-6 mb-6'> <section className='w-full px-6 mb-6'>
<h2 className='text-2xl font-semibold mb-6'>Informasi Umum</h2> <h2 className='text-2xl font-semibold mb-6'>Informasi Umum</h2>
<form onSubmit={formik.handleSubmit} className='flex flex-col gap-6'> <form onSubmit={handleFormSubmit} className='flex flex-col gap-6'>
{uniformityFormErrorMessage && ( {uniformityFormErrorMessage && (
<div className='alert alert-error' role='alert'> <div className='alert alert-error' role='alert'>
<Icon <Icon
@@ -466,6 +485,14 @@ const UniformityForm = ({
</div> </div>
)} )}
{/* Error List Alert */}
{formErrorList.length > 0 && (
<AlertErrorList
formErrorList={formErrorList}
onClose={() => setFormErrorList([])}
/>
)}
<DateInput <DateInput
required required
label='Tanggal' label='Tanggal'
@@ -693,7 +720,7 @@ const UniformityForm = ({
type='submit' type='submit'
color='primary' color='primary'
className='w-full' className='w-full'
disabled={!formik.isValid || formik.isSubmitting} disabled={formik.isSubmitting}
> >
{formik.isSubmitting ? ( {formik.isSubmitting ? (
<span className='loading loading-spinner'></span> <span className='loading loading-spinner'></span>
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
@@ -14,6 +14,7 @@ import SelectInput, {
useSelect, useSelect,
} from '@/components/input/SelectInput'; } from '@/components/input/SelectInput';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import AlertErrorList from '@/components/helper/form/FormErrors';
import { import {
PurchaseRequestAcceptApprovalFormDefaultValues, PurchaseRequestAcceptApprovalFormDefaultValues,
@@ -28,6 +29,7 @@ import {
} from '@/types/api/purchase/purchase'; } from '@/types/api/purchase/purchase';
import DateInput from '@/components/input/DateInput'; import DateInput from '@/components/input/DateInput';
import { formatNumber } from '@/lib/helper'; import { formatNumber } from '@/lib/helper';
import { getUniqueFormikErrors } from '@/lib/formik-helper';
import { Supplier } from '@/types/api/master-data/supplier'; import { Supplier } from '@/types/api/master-data/supplier';
import { SupplierApi } from '@/services/api/master-data'; import { SupplierApi } from '@/services/api/master-data';
@@ -52,7 +54,9 @@ const PurchaseOrderAcceptApprovalForm = ({
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const [purchaseOrderFormErrorMessage, setPurchaseOrderFormErrorMessage] = const [purchaseOrderFormErrorMessage, setPurchaseOrderFormErrorMessage] =
useState(''); useState('');
const [formErrorList, setFormErrorList] = useState<string[]>([]);
const [key, setKey] = useState(0); const [key, setKey] = useState(0);
const fileInputRef = useRef<HTMLInputElement>(null);
const isRejected = initialValues?.latest_approval?.action === 'REJECTED'; const isRejected = initialValues?.latest_approval?.action === 'REJECTED';
@@ -67,7 +71,6 @@ const PurchaseOrderAcceptApprovalForm = ({
| 'purchase_item_id' | 'purchase_item_id'
| 'received_date' | 'received_date'
| 'travel_number' | 'travel_number'
| 'travel_document_path'
| 'vehicle_number' | 'vehicle_number'
| 'expedition_vendor_id' | 'expedition_vendor_id'
| 'received_qty' | 'received_qty'
@@ -180,7 +183,6 @@ const PurchaseOrderAcceptApprovalForm = ({
purchase_item_id: formItem.purchase_item_id || 0, purchase_item_id: formItem.purchase_item_id || 0,
received_date: formItem.received_date || '', received_date: formItem.received_date || '',
travel_number: formItem.travel_number || '', travel_number: formItem.travel_number || '',
travel_document_path: formItem.travel_document_path || '',
vehicle_number: formItem.vehicle_number || '', vehicle_number: formItem.vehicle_number || '',
expedition_vendor_id: formItem.expedition_vendor_id || 0, expedition_vendor_id: formItem.expedition_vendor_id || 0,
received_qty: 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<HTMLFormElement>) => {
e.preventDefault();
handleValidateForm();
formik.handleSubmit(e);
};
// ===== API DATA FETCHING ===== // ===== API DATA FETCHING =====
const purchaseItems = useMemo(() => { const purchaseItems = useMemo(() => {
if (initialValues?.items) { if (initialValues?.items) {
@@ -235,6 +253,9 @@ const PurchaseOrderAcceptApprovalForm = ({
useEffect(() => { useEffect(() => {
if (purchaseItems.length > 0 && initialValues?.items) { if (purchaseItems.length > 0 && initialValues?.items) {
const updatedItems = initialValues.items.map((item) => { const updatedItems = initialValues.items.map((item) => {
const expeditionVendorId =
item.expedition_vendor_id || item.expedition_vendor?.id || 0;
return { return {
purchase_item: null, purchase_item: null,
purchase_item_id: item.id, purchase_item_id: item.id,
@@ -242,7 +263,6 @@ const PurchaseOrderAcceptApprovalForm = ({
? new Date(item.received_date).toISOString().split('T')[0] ? new Date(item.received_date).toISOString().split('T')[0]
: '', : '',
travel_number: item.travel_number || '', travel_number: item.travel_number || '',
travel_document_path: item.travel_document_path || '',
vehicle_number: item.vehicle_number || '', vehicle_number: item.vehicle_number || '',
expedition_vendor: item.expedition_vendor expedition_vendor: item.expedition_vendor
? { ? {
@@ -250,7 +270,7 @@ const PurchaseOrderAcceptApprovalForm = ({
label: item.expedition_vendor.name, label: item.expedition_vendor.name,
} }
: null, : null,
expedition_vendor_id: item.expedition_vendor_id || 0, expedition_vendor_id: expeditionVendorId,
received_qty: item.total_qty || '', received_qty: item.total_qty || '',
transport_per_item: item.transport_per_item || '', transport_per_item: item.transport_per_item || '',
}; };
@@ -259,20 +279,6 @@ const PurchaseOrderAcceptApprovalForm = ({
} }
}, [purchaseItems, initialValues, key]); }, [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 ===== // ===== HELPER FUNCTIONS =====
const getQuantityExceededError = useCallback( const getQuantityExceededError = useCallback(
(idx: number, receivedQty: number) => { (idx: number, receivedQty: number) => {
@@ -349,7 +355,7 @@ const PurchaseOrderAcceptApprovalForm = ({
return ( return (
<form <form
key={key} key={key}
onSubmit={formik.handleSubmit} onSubmit={handleFormSubmit}
className='w-full flex flex-col gap-6' className='w-full flex flex-col gap-6'
> >
<div className='w-full'> <div className='w-full'>
@@ -358,6 +364,24 @@ const PurchaseOrderAcceptApprovalForm = ({
? 'Konfirmasi Penerimaan Produk' ? 'Konfirmasi Penerimaan Produk'
: 'Edit Penerimaan Produk'} : 'Edit Penerimaan Produk'}
</h2> </h2>
{purchaseOrderFormErrorMessage && (
<div role='alert' className='alert alert-error'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{purchaseOrderFormErrorMessage}</span>
</div>
)}
{/* Error List Alert */}
{formErrorList.length > 0 && (
<AlertErrorList
formErrorList={formErrorList}
onClose={() => setFormErrorList([])}
/>
)}
<div className='overflow-x-auto'> <div className='overflow-x-auto'>
<table className='table'> <table className='table'>
@@ -510,33 +534,6 @@ const PurchaseOrderAcceptApprovalForm = ({
}} }}
/> />
</td> </td>
<td className='hidden'>
<TextInput
required
name={`items.${idx}.travel_document_path`}
type='text'
value={formItem?.travel_document_path || ''}
onChange={(e) =>
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',
}}
/>
</td>
<td> <td>
<TextInput <TextInput
required required
@@ -687,14 +684,15 @@ const PurchaseOrderAcceptApprovalForm = ({
name='travel_documents' name='travel_documents'
label='Dokumen Surat Jalan' label='Dokumen Surat Jalan'
accept='.pdf,.jpg,.jpeg,.png' accept='.pdf,.jpg,.jpeg,.png'
ref={fileInputRef}
onChange={(e) => { onChange={(e) => {
const files = Array.from(e.target.files || []); const files = Array.from(e.target.files || []);
const invalidFiles = files.filter( const invalidFiles = files.filter(
(file) => file.size > 2 * 1024 * 1024 (file) => file.size > 5 * 1024 * 1024
); );
if (invalidFiles.length > 0) { if (invalidFiles.length > 0) {
toast.error('Ukuran dokumen maksimal 2 MB!'); toast.error('Ukuran dokumen maksimal 5 MB!');
e.target.value = ''; e.target.value = '';
return; return;
} }
@@ -726,6 +724,10 @@ const PurchaseOrderAcceptApprovalForm = ({
onClick={() => { onClick={() => {
if (type === 'add') { if (type === 'add') {
formik.resetForm(); formik.resetForm();
formik.setFieldValue('travel_documents', []);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
} }
setPurchaseOrderFormErrorMessage(''); setPurchaseOrderFormErrorMessage('');
onCancel?.(); onCancel?.();
@@ -741,27 +743,13 @@ const PurchaseOrderAcceptApprovalForm = ({
className='px-4' className='px-4'
isLoading={formik.isSubmitting} isLoading={formik.isSubmitting}
disabled={ disabled={
!formik.isValid || formik.isSubmitting || hasQuantityExceededErrors || isRejected
formik.isSubmitting ||
hasQuantityExceededErrors ||
isRejected
} }
> >
Submit Submit
</Button> </Button>
</div> </div>
</div> </div>
{purchaseOrderFormErrorMessage && (
<div role='alert' className='alert alert-error'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{purchaseOrderFormErrorMessage}</span>
</div>
)}
</div> </div>
</form> </form>
); );
@@ -38,7 +38,6 @@ type PurchaseRequestAcceptApprovalFormSchemaType = {
purchase_item_id: number; purchase_item_id: number;
received_date: string; received_date: string;
travel_number: string; travel_number: string;
travel_document_path: string;
vehicle_number: string; vehicle_number: string;
expedition_vendor?: { expedition_vendor?: {
value: number; value: number;
@@ -76,7 +75,6 @@ export type PurchaseAcceptApprovalItemSchema = {
purchase_item_id: number; purchase_item_id: number;
received_date: string; received_date: string;
travel_number: string; travel_number: string;
travel_document_path: string;
vehicle_number: string; vehicle_number: string;
expedition_vendor?: { expedition_vendor?: {
value: number; value: number;
@@ -185,9 +183,6 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApp
travel_number: Yup.string() travel_number: Yup.string()
.required('No. Surat jalan wajib diisi!') .required('No. Surat jalan wajib diisi!')
.typeError('No. Surat jalan wajib diisi!'), .typeError('No. Surat jalan wajib diisi!'),
travel_document_path: Yup.string()
.required('Dokumen Surat jalan wajib diisi!')
.typeError('Dokumen Surat jalan wajib diisi!'),
vehicle_number: Yup.string() vehicle_number: Yup.string()
.required('Nomor kendaraan wajib diisi!') .required('Nomor kendaraan wajib diisi!')
.typeError('Nomor kendaraan wajib diisi!'), .typeError('Nomor kendaraan wajib diisi!'),
@@ -395,9 +390,9 @@ export const PurchaseRequestAcceptApprovalFormSchema: Yup.ObjectSchema<PurchaseR
.of( .of(
Yup.mixed<File>() Yup.mixed<File>()
.required('Dokumen surat jalan wajib diupload!') .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) return true;
if (value instanceof File) return value.size <= 2 * 1024 * 1024; if (value instanceof File) return value.size <= 5 * 1024 * 1024;
return true; return true;
}) })
) )
@@ -415,7 +410,6 @@ export const PurchaseRequestAcceptApprovalFormInitialValues: PurchaseRequestAcce
purchase_item_id: 0, purchase_item_id: 0,
received_date: '', received_date: '',
travel_number: '', travel_number: '',
travel_document_path: '',
vehicle_number: '', vehicle_number: '',
expedition_vendor_id: 0, expedition_vendor_id: 0,
received_qty: '', received_qty: '',
@@ -436,7 +430,6 @@ export const PurchaseRequestAcceptApprovalFormDefaultValues = (
purchase_item_id: item.id, purchase_item_id: item.id,
received_date: '', received_date: '',
travel_number: '', travel_number: '',
travel_document_path: '',
vehicle_number: '', vehicle_number: '',
expedition_vendor_id: 0, expedition_vendor_id: 0,
received_qty: '', received_qty: '',
@@ -447,7 +440,6 @@ export const PurchaseRequestAcceptApprovalFormDefaultValues = (
purchase_item_id: 0, purchase_item_id: 0,
received_date: '', received_date: '',
travel_number: '', travel_number: '',
travel_document_path: '',
vehicle_number: '', vehicle_number: '',
expedition_vendor_id: 0, expedition_vendor_id: 0,
received_qty: '', received_qty: '',
@@ -12,10 +12,12 @@ import NumberInput from '@/components/input/NumberInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput'; import SelectInput, { OptionType } from '@/components/input/SelectInput';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import AlertErrorList from '@/components/helper/form/FormErrors';
import { SupplierApi } from '@/services/api/master-data'; import { SupplierApi } from '@/services/api/master-data';
import { SupplierProducts } from '@/types/api/master-data/supplier'; import { SupplierProducts } from '@/types/api/master-data/supplier';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { getUniqueFormikErrors } from '@/lib/formik-helper';
import { import {
PurchaseRequestStaffApprovalFormDefaultValues, PurchaseRequestStaffApprovalFormDefaultValues,
PurchaseRequestStaffApprovalFormInitialValues, PurchaseRequestStaffApprovalFormInitialValues,
@@ -87,6 +89,7 @@ const PurchaseOrderStaffApprovalForm = ({
const deleteModal = useModal(); const deleteModal = useModal();
const [purchaseOrderFormErrorMessage, setPurchaseOrderFormErrorMessage] = const [purchaseOrderFormErrorMessage, setPurchaseOrderFormErrorMessage] =
useState(''); useState('');
const [formErrorList, setFormErrorList] = useState<string[]>([]);
const [selectedItemForDelete, setSelectedItemForDelete] = useState< const [selectedItemForDelete, setSelectedItemForDelete] = useState<
number | null number | null
>(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<HTMLFormElement>) => {
e.preventDefault();
handleValidateForm();
formik.handleSubmit(e);
};
const supplierProductOptions = baseSupplierProductOptions; const supplierProductOptions = baseSupplierProductOptions;
// ===== API DATA FETCHING ===== // ===== API DATA FETCHING =====
@@ -652,16 +671,32 @@ const PurchaseOrderStaffApprovalForm = ({
return ( return (
<> <>
<form <form onSubmit={handleFormSubmit} className='w-full flex flex-col gap-6'>
onSubmit={formik.handleSubmit}
className='w-full flex flex-col gap-6'
>
<div className='w-full'> <div className='w-full'>
<h2 className='text-lg font-semibold mb-4'> <h2 className='text-lg font-semibold mb-4'>
{type === 'add' {type === 'add'
? 'Konfirmasi Item Pembelian' ? 'Konfirmasi Item Pembelian'
: 'Edit Item Pembelian'} : 'Edit Item Pembelian'}
</h2> </h2>
{purchaseOrderFormErrorMessage && (
<div role='alert' className='alert alert-error'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{purchaseOrderFormErrorMessage}</span>
</div>
)}
{/* Error List Alert */}
{formErrorList.length > 0 && (
<AlertErrorList
formErrorList={formErrorList}
onClose={() => setFormErrorList([])}
/>
)}
<div className='overflow-x-auto'> <div className='overflow-x-auto'>
{groupedPurchaseItems.length > 0 ? ( {groupedPurchaseItems.length > 0 ? (
<div> <div>
@@ -1164,23 +1199,12 @@ const PurchaseOrderStaffApprovalForm = ({
color='primary' color='primary'
className='px-4' className='px-4'
isLoading={formik.isSubmitting} isLoading={formik.isSubmitting}
disabled={!formik.isValid || formik.isSubmitting || isRejected} disabled={formik.isSubmitting || isRejected}
> >
Submit Submit
</Button> </Button>
</div> </div>
</div> </div>
{purchaseOrderFormErrorMessage && (
<div role='alert' className='alert alert-error'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{purchaseOrderFormErrorMessage}</span>
</div>
)}
</div> </div>
</form> </form>
@@ -16,6 +16,7 @@ import SelectInput, {
} from '@/components/input/SelectInput'; } from '@/components/input/SelectInput';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import AlertErrorList from '@/components/helper/form/FormErrors';
import { import {
PurchaseRequestFormSchema, PurchaseRequestFormSchema,
@@ -32,6 +33,7 @@ import {
import { Supplier, SupplierProducts } from '@/types/api/master-data/supplier'; import { Supplier, SupplierProducts } from '@/types/api/master-data/supplier';
import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
import { formatNumber } from '@/lib/helper'; import { formatNumber } from '@/lib/helper';
import { getUniqueFormikErrors } from '@/lib/formik-helper';
import { PurchaseApi } from '@/services/api/purchase'; import { PurchaseApi } from '@/services/api/purchase';
import Card from '@/components/Card'; import Card from '@/components/Card';
@@ -59,6 +61,7 @@ const PurchaseRequestForm = ({
); );
const [purchaseRequestFormErrorMessage, setPurchaseRequestFormErrorMessage] = const [purchaseRequestFormErrorMessage, setPurchaseRequestFormErrorMessage] =
useState(''); useState('');
const [formErrorList, setFormErrorList] = useState<string[]>([]);
// ===== TYPE DEFINITIONS ===== // ===== TYPE DEFINITIONS =====
interface ProductOptionType { 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<HTMLFormElement>) => {
e.preventDefault();
handleValidateForm();
formik.handleSubmit(e);
};
// ===== API DATA FETCHING ===== // ===== API DATA FETCHING =====
const { data: supplierData, isLoading: isLoadingProducts } = useSWR( const { data: supplierData, isLoading: isLoadingProducts } = useSWR(
formik.values.supplier_id && Number(formik.values.supplier_id) > 0 formik.values.supplier_id && Number(formik.values.supplier_id) > 0
@@ -487,10 +506,29 @@ const PurchaseRequestForm = ({
</h1> </h1>
</header> </header>
<form <form
onSubmit={formik.handleSubmit} onSubmit={handleFormSubmit}
onReset={formik.handleReset} onReset={formik.handleReset}
className='w-full mt-8 flex flex-col gap-6' className='w-full mt-8 flex flex-col gap-6'
> >
{purchaseRequestFormErrorMessage && (
<div role='alert' className='alert alert-error'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{purchaseRequestFormErrorMessage}</span>
</div>
)}
{/* Error List Alert */}
{formErrorList.length > 0 && (
<AlertErrorList
formErrorList={formErrorList}
onClose={() => setFormErrorList([])}
/>
)}
{/* Basic Info Card */} {/* Basic Info Card */}
<Card <Card
title='Informasi Purchase Request' title='Informasi Purchase Request'
@@ -896,7 +934,7 @@ const PurchaseRequestForm = ({
color='primary' color='primary'
className='px-4' className='px-4'
isLoading={formik.isSubmitting} isLoading={formik.isSubmitting}
disabled={!formik.isValid || formik.isSubmitting} disabled={formik.isSubmitting}
> >
Submit Submit
</Button> </Button>
@@ -935,17 +973,6 @@ const PurchaseRequestForm = ({
</div> </div>
)} )}
</div> </div>
{purchaseRequestFormErrorMessage && (
<div role='alert' className='alert alert-error'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{purchaseRequestFormErrorMessage}</span>
</div>
)}
</form> </form>
</section> </section>
+63
View File
@@ -1,6 +1,68 @@
import { BaseMetadata } from '@/types/api/api-general'; import { BaseMetadata } from '@/types/api/api-general';
import { BaseApproval } from '@/types/api/approval/approval'; 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<string, WeekBarChartData>;
};
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 ==================== // ==================== GET ALL RESPONSE ====================
export type Uniformity = BaseMetadata & { export type Uniformity = BaseMetadata & {
id: number; id: number;
@@ -21,6 +83,7 @@ export type Uniformity = BaseMetadata & {
standard_mean_weight: number | null; standard_mean_weight: number | null;
standard_uniformity: number | null; standard_uniformity: number | null;
created_at: string; created_at: string;
chart_data?: ChartData;
created_by: number; created_by: number;
latest_approval?: BaseApproval; latest_approval?: BaseApproval;
}; };
-1
View File
@@ -120,7 +120,6 @@ export type CreateAcceptApprovalRequestPayload = {
purchase_item_id: number; purchase_item_id: number;
received_date: string; received_date: string;
travel_number: string; travel_number: string;
travel_document_path: string;
vehicle_number: string; vehicle_number: string;
expedition_vendor_id: number; expedition_vendor_id: number;
received_qty: number; received_qty: number;