mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 21:41:57 +00:00
refactor(FE-212): group purchase items by warehouse in PurchaseOrderStaffApprovalForm
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user