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]);
const isRepeaterInputError = (
idx: number,
purchaseItemId: number,
field: 'price' | 'total_price'
): { isError: boolean; errorMessage: string } => {
const touchedItem = formik.touched.items?.[idx];
const errorItem = formik.errors.items?.[idx] as
const formItemIndex = formik.values.items?.findIndex(
(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>
| undefined;
@@ -200,10 +208,41 @@ const PurchaseOrderStaffApprovalForm = ({
return [];
}, [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(() => {
if (purchaseItems.length > 0 && initialValues?.items) {
const updatedItems = purchaseItems.map((purchaseItem, idx) => {
const originalItem = initialValues.items?.[idx];
const originalItem = initialValues.items?.find(
(item) => item.id === purchaseItem.id
);
return {
purchase_item_id: type === 'edit' ? purchaseItem.value : undefined,
product_id: purchaseItem.product_id || 0,
@@ -219,17 +258,28 @@ const PurchaseOrderStaffApprovalForm = ({
}, [purchaseItems, type, initialValues]);
// ===== PURCHASE ITEM OPERATIONS =====
const findItemIndex = (purchaseItemId: number) => {
return purchaseItems.findIndex((item) => item.id === purchaseItemId);
};
const handlePurchaseItemChange = (
idx: number,
purchaseItemId: number,
field: 'price' | 'total_price',
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') {
const numValue =
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 (
field === 'price' &&
@@ -238,7 +288,10 @@ const PurchaseOrderStaffApprovalForm = ({
numValue >= 0
) {
const calculatedTotal = numValue * selectedItem.quantity;
formik.setFieldValue(`items.${idx}.total_price`, calculatedTotal);
formik.setFieldValue(
`items.${formItemIndex}.total_price`,
calculatedTotal
);
}
if (
@@ -248,7 +301,7 @@ const PurchaseOrderStaffApprovalForm = ({
numValue >= 0
) {
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'}
</h2>
<div className='overflow-x-auto'>
<table className='table'>
<thead>
<tr>
<th>Gudang</th>
<th>Produk</th>
<th>Jenis Produk</th>
<th>Jumlah</th>
<th>Satuan</th>
<th>
Harga Satuan
<span className='text-error'>*</span>
</th>
<th>
Total (Rp.)
<span className='text-error'>*</span>
</th>
</tr>
</thead>
<tbody>
{purchaseItems?.map((purchaseItem, idx) => {
const formItem = formik.values.items?.[idx];
return (
<tr key={`purchase-item-${idx}`}>
<td>
<TextInput
name={`items.${idx}.warehouse`}
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'
value={purchaseItem?.product?.name || ''}
readOnly={true}
className={{
wrapper: 'min-w-52 md:min-w-72 lg:min-w-80',
}}
disabled={true}
/>
</td>
<td>
<TextInput
name={`items.${idx}.product_category`}
type='text'
value={
typeof purchaseItem?.product?.product_category ===
'string'
? purchaseItem.product.product_category
: purchaseItem?.product?.product_category?.name ||
''
}
readOnly={true}
className={{
wrapper: 'min-w-40 md:min-w-52 lg:min-w-64',
}}
disabled={true}
/>
</td>
<td>
<TextInput
name={`items.${idx}.quantity`}
type='text'
value={
purchaseItem?.quantity
? purchaseItem.quantity.toLocaleString('id-ID')
: ''
}
readOnly={true}
className={{
wrapper: 'min-w-24',
}}
disabled={true}
/>
</td>
<td>
<TextInput
name={`items.${idx}.uom`}
type='text'
value={purchaseItem?.product?.uom?.name || ''}
readOnly={true}
className={{
wrapper: 'min-w-24',
}}
disabled={true}
/>
</td>
<td>
<NumberInput
required
name={`items.${idx}.price`}
value={formItem?.price || ''}
onChange={(e) =>
handlePurchaseItemChange(
idx,
'price',
e.target.value
)
}
onBlur={formik.handleBlur}
placeholder='Masukkan harga satuan'
allowNegative={false}
decimalScale={2}
thousandSeparator=','
decimalSeparator='.'
inputPrefix={'Rp'}
isError={isRepeaterInputError(idx, 'price').isError}
errorMessage={
isRepeaterInputError(idx, 'price').errorMessage
}
className={{
wrapper: 'min-w-48 md:min-w-64 lg:min-w-72',
}}
/>
</td>
<td>
<NumberInput
required
name={`items.${idx}.total_price`}
value={formItem?.total_price || ''}
onChange={(e) =>
handlePurchaseItemChange(
idx,
'total_price',
e.target.value
)
}
onBlur={formik.handleBlur}
placeholder='Masukkan total harga'
allowNegative={false}
decimalScale={2}
thousandSeparator=','
decimalSeparator='.'
inputPrefix={'Rp'}
isError={
isRepeaterInputError(idx, 'total_price').isError
}
errorMessage={
isRepeaterInputError(idx, 'total_price')
.errorMessage
}
className={{
wrapper: 'min-w-48 md:min-w-64 lg:min-w-72',
}}
/>
</td>
</tr>
);
})}
</tbody>
</table>
{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'>
<table className='table'>
<thead>
<tr>
<th>Produk</th>
<th>Jenis Produk</th>
<th>Jumlah</th>
<th>Satuan</th>
<th>
Harga Satuan
<span className='text-error'>*</span>
</th>
<th>
Total (Rp.)
<span className='text-error'>*</span>
</th>
</tr>
</thead>
<tbody>
{warehouseData.items.map((purchaseItem) => {
const formItem = formik.values.items?.find(
(item) =>
item.purchase_item_id === purchaseItem.id
);
return (
<tr key={`purchase-item-${purchaseItem.id}`}>
<td>
<TextInput
name={`items.${purchaseItem.id}.product_name`}
type='text'
value={purchaseItem?.product?.name || ''}
readOnly={true}
className={{
wrapper:
'min-w-52 md:min-w-72 lg:min-w-80',
}}
disabled={true}
/>
</td>
<td>
<TextInput
name={`items.${purchaseItem.id}.product_category`}
type='text'
value={
typeof purchaseItem?.product
?.product_category === 'string'
? purchaseItem.product
.product_category
: purchaseItem?.product
?.product_category?.name || ''
}
readOnly={true}
className={{
wrapper:
'min-w-40 md:min-w-52 lg:min-w-64',
}}
disabled={true}
/>
</td>
<td>
<TextInput
name={`items.${purchaseItem.id}.quantity`}
type='text'
value={
purchaseItem?.quantity
? purchaseItem.quantity.toLocaleString(
'id-ID'
)
: ''
}
readOnly={true}
className={{
wrapper: 'min-w-24',
}}
disabled={true}
/>
</td>
<td>
<TextInput
name={`items.${purchaseItem.id}.uom`}
type='text'
value={
purchaseItem?.product?.uom?.name || ''
}
readOnly={true}
className={{
wrapper: 'min-w-24',
}}
disabled={true}
/>
</td>
<td>
<NumberInput
required
name={`items.${purchaseItem.id}.price`}
value={formItem?.price || ''}
onChange={(e) =>
handlePurchaseItemChange(
purchaseItem.id,
'price',
e.target.value
)
}
onBlur={formik.handleBlur}
placeholder='Masukkan harga satuan'
allowNegative={false}
decimalScale={2}
thousandSeparator=','
decimalSeparator='.'
inputPrefix={'Rp'}
isError={
isRepeaterInputError(
purchaseItem.id,
'price'
).isError
}
errorMessage={
isRepeaterInputError(
purchaseItem.id,
'price'
).errorMessage
}
className={{
wrapper:
'min-w-48 md:min-w-64 lg:min-w-72',
}}
/>
</td>
<td>
<NumberInput
required
name={`items.${purchaseItem.id}.total_price`}
value={formItem?.total_price || ''}
onChange={(e) =>
handlePurchaseItemChange(
purchaseItem.id,
'total_price',
e.target.value
)
}
onBlur={formik.handleBlur}
placeholder='Masukkan total harga'
allowNegative={false}
decimalScale={2}
thousandSeparator=','
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 className={'col-span-2'}>
<TextInput