mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 15:55:48 +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]);
|
}, [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'
|
||||||
|
|||||||
Reference in New Issue
Block a user