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);
} }
} }
}; };
@@ -265,11 +318,22 @@ const PurchaseOrderStaffApprovalForm = ({
? 'Konfirmasi Item Pembelian' ? 'Konfirmasi Item Pembelian'
: 'Edit Item Pembelian'} : 'Edit Item Pembelian'}
</h2> </h2>
<div className='overflow-x-auto'>
{groupedPurchaseItems.length > 0 ? (
<div>
{groupedPurchaseItems.map((warehouseData, index) => (
<div key={warehouseData.warehouseId}>
<div className='border border-gray-200 rounded-lg overflow-hidden mb-6'>
{/* Warehouse Header */}
<div className='font-semibold text-gray-900 bg-gray-100 px-6 py-4 text-lg'>
{index + 1}. {warehouseData.warehouseName.toUpperCase()}
</div>
{/* Items Table */}
<div className='overflow-x-auto'> <div className='overflow-x-auto'>
<table className='table'> <table className='table'>
<thead> <thead>
<tr> <tr>
<th>Gudang</th>
<th>Produk</th> <th>Produk</th>
<th>Jenis Produk</th> <th>Jenis Produk</th>
<th>Jumlah</th> <th>Jumlah</th>
@@ -285,59 +349,55 @@ const PurchaseOrderStaffApprovalForm = ({
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{purchaseItems?.map((purchaseItem, idx) => { {warehouseData.items.map((purchaseItem) => {
const formItem = formik.values.items?.[idx]; const formItem = formik.values.items?.find(
(item) =>
item.purchase_item_id === purchaseItem.id
);
return ( return (
<tr key={`purchase-item-${idx}`}> <tr key={`purchase-item-${purchaseItem.id}`}>
<td> <td>
<TextInput <TextInput
name={`items.${idx}.warehouse`} name={`items.${purchaseItem.id}.product_name`}
type='text'
value={purchaseItem?.warehouse?.name || ''}
readOnly={true}
className={{
wrapper: 'min-w-40 md:min-w-52 lg:min-w-64',
}}
disabled={true}
/>
</td>
<td>
<TextInput
name={`items.${idx}.product_name`}
type='text' type='text'
value={purchaseItem?.product?.name || ''} value={purchaseItem?.product?.name || ''}
readOnly={true} readOnly={true}
className={{ className={{
wrapper: 'min-w-52 md:min-w-72 lg:min-w-80', wrapper:
'min-w-52 md:min-w-72 lg:min-w-80',
}} }}
disabled={true} disabled={true}
/> />
</td> </td>
<td> <td>
<TextInput <TextInput
name={`items.${idx}.product_category`} name={`items.${purchaseItem.id}.product_category`}
type='text' type='text'
value={ value={
typeof purchaseItem?.product?.product_category === typeof purchaseItem?.product
'string' ?.product_category === 'string'
? purchaseItem.product.product_category ? purchaseItem.product
: purchaseItem?.product?.product_category?.name || .product_category
'' : purchaseItem?.product
?.product_category?.name || ''
} }
readOnly={true} readOnly={true}
className={{ className={{
wrapper: 'min-w-40 md:min-w-52 lg:min-w-64', wrapper:
'min-w-40 md:min-w-52 lg:min-w-64',
}} }}
disabled={true} disabled={true}
/> />
</td> </td>
<td> <td>
<TextInput <TextInput
name={`items.${idx}.quantity`} name={`items.${purchaseItem.id}.quantity`}
type='text' type='text'
value={ value={
purchaseItem?.quantity purchaseItem?.quantity
? purchaseItem.quantity.toLocaleString('id-ID') ? purchaseItem.quantity.toLocaleString(
'id-ID'
)
: '' : ''
} }
readOnly={true} readOnly={true}
@@ -349,9 +409,11 @@ const PurchaseOrderStaffApprovalForm = ({
</td> </td>
<td> <td>
<TextInput <TextInput
name={`items.${idx}.uom`} name={`items.${purchaseItem.id}.uom`}
type='text' type='text'
value={purchaseItem?.product?.uom?.name || ''} value={
purchaseItem?.product?.uom?.name || ''
}
readOnly={true} readOnly={true}
className={{ className={{
wrapper: 'min-w-24', wrapper: 'min-w-24',
@@ -362,11 +424,11 @@ const PurchaseOrderStaffApprovalForm = ({
<td> <td>
<NumberInput <NumberInput
required required
name={`items.${idx}.price`} name={`items.${purchaseItem.id}.price`}
value={formItem?.price || ''} value={formItem?.price || ''}
onChange={(e) => onChange={(e) =>
handlePurchaseItemChange( handlePurchaseItemChange(
idx, purchaseItem.id,
'price', 'price',
e.target.value e.target.value
) )
@@ -378,23 +440,32 @@ const PurchaseOrderStaffApprovalForm = ({
thousandSeparator=',' thousandSeparator=','
decimalSeparator='.' decimalSeparator='.'
inputPrefix={'Rp'} inputPrefix={'Rp'}
isError={isRepeaterInputError(idx, 'price').isError} isError={
isRepeaterInputError(
purchaseItem.id,
'price'
).isError
}
errorMessage={ errorMessage={
isRepeaterInputError(idx, 'price').errorMessage isRepeaterInputError(
purchaseItem.id,
'price'
).errorMessage
} }
className={{ className={{
wrapper: 'min-w-48 md:min-w-64 lg:min-w-72', wrapper:
'min-w-48 md:min-w-64 lg:min-w-72',
}} }}
/> />
</td> </td>
<td> <td>
<NumberInput <NumberInput
required required
name={`items.${idx}.total_price`} name={`items.${purchaseItem.id}.total_price`}
value={formItem?.total_price || ''} value={formItem?.total_price || ''}
onChange={(e) => onChange={(e) =>
handlePurchaseItemChange( handlePurchaseItemChange(
idx, purchaseItem.id,
'total_price', 'total_price',
e.target.value e.target.value
) )
@@ -407,14 +478,20 @@ const PurchaseOrderStaffApprovalForm = ({
decimalSeparator='.' decimalSeparator='.'
inputPrefix={'Rp'} inputPrefix={'Rp'}
isError={ isError={
isRepeaterInputError(idx, 'total_price').isError isRepeaterInputError(
purchaseItem.id,
'total_price'
).isError
} }
errorMessage={ errorMessage={
isRepeaterInputError(idx, 'total_price') isRepeaterInputError(
.errorMessage purchaseItem.id,
'total_price'
).errorMessage
} }
className={{ className={{
wrapper: 'min-w-48 md:min-w-64 lg:min-w-72', wrapper:
'min-w-48 md:min-w-64 lg:min-w-72',
}} }}
/> />
</td> </td>
@@ -424,6 +501,21 @@ const PurchaseOrderStaffApprovalForm = ({
</tbody> </tbody>
</table> </table>
</div> </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 className={'col-span-2'}> <div className={'col-span-2'}>
<TextInput <TextInput
label='Notes' label='Notes'