refactor(FE-212): group purchase items by warehouse in PurchaseOrderStaffApprovalForm

This commit is contained in:
rstubryan
2025-11-21 09:17:00 +07:00
parent e00b7bc3f2
commit 835074f538
@@ -51,11 +51,19 @@ const PurchaseOrderStaffApprovalForm = ({
}, [initialValues?.approval]); }, [initialValues?.approval]);
const isRepeaterInputError = ( const isRepeaterInputError = (
idx: number, purchaseItemId: number,
field: 'price' | 'total_price' field: 'price' | 'total_price'
): { isError: boolean; errorMessage: string } => { ): { isError: boolean; errorMessage: string } => {
const touchedItem = formik.touched.items?.[idx]; const formItemIndex = formik.values.items?.findIndex(
const errorItem = formik.errors.items?.[idx] as (item) => item.purchase_item_id === purchaseItemId
);
if (formItemIndex === -1) {
return { isError: false, errorMessage: '' };
}
const touchedItem = formik.touched.items?.[formItemIndex];
const errorItem = formik.errors.items?.[formItemIndex] as
| Record<string, string> | Record<string, string>
| undefined; | undefined;
@@ -200,10 +208,41 @@ const PurchaseOrderStaffApprovalForm = ({
return []; return [];
}, [initialValues?.items]); }, [initialValues?.items]);
const groupedPurchaseItems = useMemo(() => {
if (!purchaseItems.length) return [];
const warehouseGroups = purchaseItems.reduce(
(acc, item) => {
const warehouseId = item.warehouse_id;
if (!acc[warehouseId]) {
acc[warehouseId] = {
warehouseId,
warehouseName: item.warehouse.name,
items: [],
};
}
acc[warehouseId].items.push(item);
return acc;
},
{} as Record<
number,
{
warehouseId: number;
warehouseName: string;
items: typeof purchaseItems;
}
>
);
return Object.values(warehouseGroups);
}, [purchaseItems]);
useEffect(() => { useEffect(() => {
if (purchaseItems.length > 0 && initialValues?.items) { if (purchaseItems.length > 0 && initialValues?.items) {
const updatedItems = purchaseItems.map((purchaseItem, idx) => { const updatedItems = purchaseItems.map((purchaseItem, idx) => {
const originalItem = initialValues.items?.[idx]; const originalItem = initialValues.items?.find(
(item) => item.id === purchaseItem.id
);
return { return {
purchase_item_id: type === 'edit' ? purchaseItem.value : undefined, purchase_item_id: type === 'edit' ? purchaseItem.value : undefined,
product_id: purchaseItem.product_id || 0, product_id: purchaseItem.product_id || 0,
@@ -219,17 +258,28 @@ const PurchaseOrderStaffApprovalForm = ({
}, [purchaseItems, type, initialValues]); }, [purchaseItems, type, initialValues]);
// ===== PURCHASE ITEM OPERATIONS ===== // ===== PURCHASE ITEM OPERATIONS =====
const findItemIndex = (purchaseItemId: number) => {
return purchaseItems.findIndex((item) => item.id === purchaseItemId);
};
const handlePurchaseItemChange = ( const handlePurchaseItemChange = (
idx: number, purchaseItemId: number,
field: 'price' | 'total_price', field: 'price' | 'total_price',
value: string | number value: string | number
) => { ) => {
const itemIndex = findItemIndex(purchaseItemId);
const formItemIndex = formik.values.items?.findIndex(
(item) => item.purchase_item_id === purchaseItemId
);
if (itemIndex === -1 || formItemIndex === -1) return;
if (field === 'price' || field === 'total_price') { if (field === 'price' || field === 'total_price') {
const numValue = const numValue =
typeof value === 'string' ? parseFloat(value) || 0 : value; typeof value === 'string' ? parseFloat(value) || 0 : value;
formik.setFieldValue(`items.${idx}.${field}`, numValue); formik.setFieldValue(`items.${formItemIndex}.${field}`, numValue);
const selectedItem = purchaseItems[idx]; const selectedItem = purchaseItems[itemIndex];
if ( if (
field === 'price' && field === 'price' &&
@@ -238,7 +288,10 @@ const PurchaseOrderStaffApprovalForm = ({
numValue >= 0 numValue >= 0
) { ) {
const calculatedTotal = numValue * selectedItem.quantity; const calculatedTotal = numValue * selectedItem.quantity;
formik.setFieldValue(`items.${idx}.total_price`, calculatedTotal); formik.setFieldValue(
`items.${formItemIndex}.total_price`,
calculatedTotal
);
} }
if ( if (
@@ -248,7 +301,7 @@ const PurchaseOrderStaffApprovalForm = ({
numValue >= 0 numValue >= 0
) { ) {
const calculatedPrice = numValue / selectedItem.quantity; const calculatedPrice = numValue / selectedItem.quantity;
formik.setFieldValue(`items.${idx}.price`, calculatedPrice); formik.setFieldValue(`items.${formItemIndex}.price`, calculatedPrice);
} }
} }
}; };
@@ -266,163 +319,202 @@ const PurchaseOrderStaffApprovalForm = ({
: 'Edit Item Pembelian'} : 'Edit Item Pembelian'}
</h2> </h2>
<div className='overflow-x-auto'> <div className='overflow-x-auto'>
<table className='table'> {groupedPurchaseItems.length > 0 ? (
<thead> <div>
<tr> {groupedPurchaseItems.map((warehouseData, index) => (
<th>Gudang</th> <div key={warehouseData.warehouseId}>
<th>Produk</th> <div className='border border-gray-200 rounded-lg overflow-hidden mb-6'>
<th>Jenis Produk</th> {/* Warehouse Header */}
<th>Jumlah</th> <div className='font-semibold text-gray-900 bg-gray-100 px-6 py-4 text-lg'>
<th>Satuan</th> {index + 1}. {warehouseData.warehouseName.toUpperCase()}
<th> </div>
Harga Satuan
<span className='text-error'>*</span> {/* Items Table */}
</th> <div className='overflow-x-auto'>
<th> <table className='table'>
Total (Rp.) <thead>
<span className='text-error'>*</span> <tr>
</th> <th>Produk</th>
</tr> <th>Jenis Produk</th>
</thead> <th>Jumlah</th>
<tbody> <th>Satuan</th>
{purchaseItems?.map((purchaseItem, idx) => { <th>
const formItem = formik.values.items?.[idx]; Harga Satuan
return ( <span className='text-error'>*</span>
<tr key={`purchase-item-${idx}`}> </th>
<td> <th>
<TextInput Total (Rp.)
name={`items.${idx}.warehouse`} <span className='text-error'>*</span>
type='text' </th>
value={purchaseItem?.warehouse?.name || ''} </tr>
readOnly={true} </thead>
className={{ <tbody>
wrapper: 'min-w-40 md:min-w-52 lg:min-w-64', {warehouseData.items.map((purchaseItem) => {
}} const formItem = formik.values.items?.find(
disabled={true} (item) =>
/> item.purchase_item_id === purchaseItem.id
</td> );
<td> return (
<TextInput <tr key={`purchase-item-${purchaseItem.id}`}>
name={`items.${idx}.product_name`} <td>
type='text' <TextInput
value={purchaseItem?.product?.name || ''} name={`items.${purchaseItem.id}.product_name`}
readOnly={true} type='text'
className={{ value={purchaseItem?.product?.name || ''}
wrapper: 'min-w-52 md:min-w-72 lg:min-w-80', readOnly={true}
}} className={{
disabled={true} wrapper:
/> 'min-w-52 md:min-w-72 lg:min-w-80',
</td> }}
<td> disabled={true}
<TextInput />
name={`items.${idx}.product_category`} </td>
type='text' <td>
value={ <TextInput
typeof purchaseItem?.product?.product_category === name={`items.${purchaseItem.id}.product_category`}
'string' type='text'
? purchaseItem.product.product_category value={
: purchaseItem?.product?.product_category?.name || typeof purchaseItem?.product
'' ?.product_category === 'string'
} ? purchaseItem.product
readOnly={true} .product_category
className={{ : purchaseItem?.product
wrapper: 'min-w-40 md:min-w-52 lg:min-w-64', ?.product_category?.name || ''
}} }
disabled={true} readOnly={true}
/> className={{
</td> wrapper:
<td> 'min-w-40 md:min-w-52 lg:min-w-64',
<TextInput }}
name={`items.${idx}.quantity`} disabled={true}
type='text' />
value={ </td>
purchaseItem?.quantity <td>
? purchaseItem.quantity.toLocaleString('id-ID') <TextInput
: '' name={`items.${purchaseItem.id}.quantity`}
} type='text'
readOnly={true} value={
className={{ purchaseItem?.quantity
wrapper: 'min-w-24', ? purchaseItem.quantity.toLocaleString(
}} 'id-ID'
disabled={true} )
/> : ''
</td> }
<td> readOnly={true}
<TextInput className={{
name={`items.${idx}.uom`} wrapper: 'min-w-24',
type='text' }}
value={purchaseItem?.product?.uom?.name || ''} disabled={true}
readOnly={true} />
className={{ </td>
wrapper: 'min-w-24', <td>
}} <TextInput
disabled={true} name={`items.${purchaseItem.id}.uom`}
/> type='text'
</td> value={
<td> purchaseItem?.product?.uom?.name || ''
<NumberInput }
required readOnly={true}
name={`items.${idx}.price`} className={{
value={formItem?.price || ''} wrapper: 'min-w-24',
onChange={(e) => }}
handlePurchaseItemChange( disabled={true}
idx, />
'price', </td>
e.target.value <td>
) <NumberInput
} required
onBlur={formik.handleBlur} name={`items.${purchaseItem.id}.price`}
placeholder='Masukkan harga satuan' value={formItem?.price || ''}
allowNegative={false} onChange={(e) =>
decimalScale={2} handlePurchaseItemChange(
thousandSeparator=',' purchaseItem.id,
decimalSeparator='.' 'price',
inputPrefix={'Rp'} e.target.value
isError={isRepeaterInputError(idx, 'price').isError} )
errorMessage={ }
isRepeaterInputError(idx, 'price').errorMessage onBlur={formik.handleBlur}
} placeholder='Masukkan harga satuan'
className={{ allowNegative={false}
wrapper: 'min-w-48 md:min-w-64 lg:min-w-72', decimalScale={2}
}} thousandSeparator=','
/> decimalSeparator='.'
</td> inputPrefix={'Rp'}
<td> isError={
<NumberInput isRepeaterInputError(
required purchaseItem.id,
name={`items.${idx}.total_price`} 'price'
value={formItem?.total_price || ''} ).isError
onChange={(e) => }
handlePurchaseItemChange( errorMessage={
idx, isRepeaterInputError(
'total_price', purchaseItem.id,
e.target.value 'price'
) ).errorMessage
} }
onBlur={formik.handleBlur} className={{
placeholder='Masukkan total harga' wrapper:
allowNegative={false} 'min-w-48 md:min-w-64 lg:min-w-72',
decimalScale={2} }}
thousandSeparator=',' />
decimalSeparator='.' </td>
inputPrefix={'Rp'} <td>
isError={ <NumberInput
isRepeaterInputError(idx, 'total_price').isError required
} name={`items.${purchaseItem.id}.total_price`}
errorMessage={ value={formItem?.total_price || ''}
isRepeaterInputError(idx, 'total_price') onChange={(e) =>
.errorMessage handlePurchaseItemChange(
} purchaseItem.id,
className={{ 'total_price',
wrapper: 'min-w-48 md:min-w-64 lg:min-w-72', e.target.value
}} )
/> }
</td> onBlur={formik.handleBlur}
</tr> placeholder='Masukkan total harga'
); allowNegative={false}
})} decimalScale={2}
</tbody> thousandSeparator=','
</table> decimalSeparator='.'
inputPrefix={'Rp'}
isError={
isRepeaterInputError(
purchaseItem.id,
'total_price'
).isError
}
errorMessage={
isRepeaterInputError(
purchaseItem.id,
'total_price'
).errorMessage
}
className={{
wrapper:
'min-w-48 md:min-w-64 lg:min-w-72',
}}
/>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
{/* Add divider after table except for last item */}
{index < groupedPurchaseItems.length - 1 && (
<div className='border-t border-gray-200 my-6'></div>
)}
</div>
))}
</div>
) : (
<div className='text-center py-8 text-gray-500'>
Tidak ada data item pembelian
</div>
)}
</div> </div>
<div className={'col-span-2'}> <div className={'col-span-2'}>
<TextInput <TextInput