refactor(FE-208,212,213): add transport-related fields and update form handling in PurchaseOrder forms

This commit is contained in:
rstubryan
2025-11-19 10:19:05 +07:00
parent 89d9d40713
commit b520b4ee54
5 changed files with 150 additions and 99 deletions
@@ -1,6 +1,6 @@
'use client';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useFormik } from 'formik';
import { Icon } from '@iconify/react';
import { toast } from 'react-hot-toast';
@@ -39,18 +39,6 @@ const PurchaseOrderAcceptApprovalForm = ({
useState('');
// ===== TYPE DEFINITIONS =====
interface PurchaseItemOptionType extends OptionType {
id: number;
quantity: number;
product: {
name: string;
product_category: string | { name: string };
uom: {
name: string;
};
};
}
interface ExpeditionVendorOptionType {
value: number;
label: string;
@@ -145,32 +133,29 @@ const PurchaseOrderAcceptApprovalForm = ({
onSubmit: async (values) => {
const payload: CreateAcceptApprovalRequestPayload = {
notes: values.notes || '',
items: (values.items || []).map((item) => ({
purchase_item_id:
typeof item.purchase_item_id === 'string'
? parseInt(item.purchase_item_id) || 0
: item.purchase_item_id || 0,
received_date: item.received_date || '',
travel_number: item.travel_number || '',
travel_document_path: item.travel_document_path || '',
vehicle_number: item.vehicle_number || '',
expedition_vendor_id:
typeof item.expedition_vendor_id === 'string'
? parseInt(item.expedition_vendor_id) || 0
: item.expedition_vendor_id || 0,
received_qty:
typeof item.received_qty === 'string'
? parseFloat(item.received_qty) || 0
: item.received_qty || 0,
transport_per_item:
typeof item.transport_per_item === 'string'
? parseFloat(item.transport_per_item) || 0
: item.transport_per_item || 0,
transport_total:
typeof item.transport_total === 'string'
? parseFloat(item.transport_total) || 0
: item.transport_total || 0,
})),
items:
values.items?.map((formItem) => {
return {
purchase_item_id: formItem.purchase_item_id || 0,
received_date: formItem.received_date || '',
travel_number: formItem.travel_number || '',
travel_document_path: formItem.travel_document_path || '',
vehicle_number: formItem.vehicle_number || '',
expedition_vendor_id: formItem.expedition_vendor_id || 0,
received_qty:
typeof formItem.received_qty === 'string'
? parseFloat(formItem.received_qty) || 0
: formItem.received_qty || 0,
transport_per_item:
typeof formItem.transport_per_item === 'string'
? parseFloat(formItem.transport_per_item) || 0
: formItem.transport_per_item || 0,
transport_total:
typeof formItem.transport_total === 'string'
? parseFloat(formItem.transport_total) || 0
: formItem.transport_total || 0,
};
}) || [],
};
switch (type) {
@@ -192,13 +177,13 @@ const PurchaseOrderAcceptApprovalForm = ({
if (initialValues?.items) {
return initialValues.items.map((item) => ({
value: item.id,
label: `${item.product.name} (${item.quantity} ${item.product.uom?.name || 'unit'})`,
label: item.product.name,
id: item.id,
quantity: item.quantity,
quantity: item.sub_qty,
product: {
name: item.product.name,
product_category: item.product.product_category,
uom: item.product.uom,
product_category: item.product.product_category || '',
uom: item.product.uom || { name: 'unit' },
},
}));
}
@@ -206,33 +191,64 @@ const PurchaseOrderAcceptApprovalForm = ({
return [];
}, [initialValues?.items]);
const expeditionVendors: ExpeditionVendorOptionType[] = useMemo(() => {
return [];
}, []);
useEffect(() => {
if (purchaseItems.length > 0 && initialValues?.items) {
const updatedItems = initialValues.items.map((item) => {
return {
purchase_item: null,
purchase_item_id: item.id,
received_date: item.received_date
? new Date(item.received_date).toISOString().split('T')[0]
: '',
travel_number: item.travel_number || '',
travel_document_path: item.travel_document_path || '',
vehicle_number: item.vehicle_number || '',
expedition_vendor: null,
expedition_vendor_id: 0,
received_qty: item.sub_qty ? item.sub_qty.toString() : '',
transport_per_item: '',
transport_total: '',
};
});
formik.setFieldValue('items', updatedItems);
}
}, [purchaseItems, initialValues]);
const getPurchaseItemOptions = useCallback(() => {
return purchaseItems;
}, [purchaseItems]);
const expeditionVendors: ExpeditionVendorOptionType[] = useMemo(() => {
return [
{
value: 1,
label: 'PT. Ekspedisi Sejahtera',
id: 1,
},
{
value: 2,
label: 'PT. Jaya Trans Logistics',
id: 2,
},
{
value: 3,
label: 'PT. Cargo Express Indonesia',
id: 3,
},
{
value: 4,
label: 'PT. Fast Delivery Service',
id: 4,
},
{
value: 5,
label: 'PT. Mitra Angkutan Sejahtera',
id: 5,
},
];
}, []);
const getExpeditionVendorOptions = useCallback(() => {
return expeditionVendors;
}, [expeditionVendors]);
// ===== FIELD CHANGE HANDLERS =====
const purchaseItemChangeHandler = (
idx: number,
val: OptionType | OptionType[] | null
) => {
const purchaseItem = val as PurchaseItemOptionType | null;
formik.setFieldTouched(`items.${idx}.purchase_item`, true);
formik.setFieldValue(`items.${idx}.purchase_item`, purchaseItem);
formik.setFieldTouched(`items.${idx}.purchase_item_id`, true);
formik.setFieldValue(
`items.${idx}.purchase_item_id`,
(purchaseItem as OptionType)?.value || 0
);
};
const expeditionVendorChangeHandler = (
idx: number,
val: OptionType | OptionType[] | null
@@ -298,14 +314,21 @@ const PurchaseOrderAcceptApprovalForm = ({
? 'Konfirmasi Penerimaan Produk'
: 'Edit Penerimaan Produk'}
</h2>
{/* Debug Info */}
<div className='mb-4 p-4 bg-gray-100 rounded-lg text-xs'>
<div>Form Valid: {formik.isValid ? '✅' : '❌'}</div>
<div>Form Submitting: {formik.isSubmitting ? '✅' : '❌'}</div>
<div>Form Errors: {JSON.stringify(formik.errors, null, 2)}</div>
<div>Form Touched: {JSON.stringify(formik.touched, null, 2)}</div>
</div>
<div className='overflow-x-auto'>
<table className='table'>
<thead>
<tr>
<th>
Item
<span className='text-error'>*</span>
</th>
<th>Produk</th>
<th>Jumlah</th>
<th>Satuan</th>
<th>
Tanggal Diterima
<span className='text-error'>*</span>
@@ -341,29 +364,53 @@ const PurchaseOrderAcceptApprovalForm = ({
</tr>
</thead>
<tbody>
{formik.values.items?.map((item, idx) => {
purchaseItems.find((p) => p.value === item.purchase_item_id);
{purchaseItems?.map((purchaseItem, idx) => {
const formItem = formik.values.items?.[idx];
return (
<tr key={`purchase-item-${idx}`}>
<td>
<SelectInput
required
isClearable={true}
value={item.purchase_item}
key={`purchase-item-${idx}`}
onChange={(val) => purchaseItemChangeHandler(idx, val)}
options={getPurchaseItemOptions()}
isError={
isRepeaterInputError(idx, 'purchase_item_id').isError
}
errorMessage={
isRepeaterInputError(idx, 'purchase_item_id')
.errorMessage
}
placeholder='Pilih Item...'
<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}
/>
<input
type='hidden'
name={`items.${idx}.purchase_item_id`}
value={purchaseItem?.value || 0}
/>
</td>
<td>
<TextInput
name={`items.${idx}.quantity`}
type='text'
value={
purchaseItem?.quantity
? purchaseItem.quantity.toLocaleString('id-ID')
: ''
}
readOnly={true}
className={{
wrapper: 'min-w-32',
}}
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>
@@ -371,7 +418,7 @@ const PurchaseOrderAcceptApprovalForm = ({
required
isNestedModal={true}
name={`items.${idx}.received_date`}
value={item.received_date || ''}
value={formItem?.received_date || ''}
onChange={(e) =>
formik.setFieldValue(
`items.${idx}.received_date`,
@@ -396,7 +443,7 @@ const PurchaseOrderAcceptApprovalForm = ({
required
name={`items.${idx}.travel_number`}
type='text'
value={item.travel_number || ''}
value={formItem?.travel_number || ''}
onChange={(e) =>
formik.setFieldValue(
`items.${idx}.travel_number`,
@@ -422,7 +469,7 @@ const PurchaseOrderAcceptApprovalForm = ({
required
name={`items.${idx}.travel_document_path`}
type='text'
value={item.travel_document_path || ''}
value={formItem?.travel_document_path || ''}
onChange={(e) =>
formik.setFieldValue(
`items.${idx}.travel_document_path`,
@@ -449,7 +496,7 @@ const PurchaseOrderAcceptApprovalForm = ({
required
name={`items.${idx}.vehicle_number`}
type='text'
value={item.vehicle_number || ''}
value={formItem?.vehicle_number || ''}
onChange={(e) =>
formik.setFieldValue(
`items.${idx}.vehicle_number`,
@@ -474,7 +521,7 @@ const PurchaseOrderAcceptApprovalForm = ({
<SelectInput
required
isClearable={true}
value={item.expedition_vendor}
value={formItem?.expedition_vendor}
key={`expedition-vendor-${idx}`}
onChange={(val) =>
expeditionVendorChangeHandler(idx, val)
@@ -498,7 +545,7 @@ const PurchaseOrderAcceptApprovalForm = ({
<NumberInput
required
name={`items.${idx}.received_qty`}
value={item.received_qty || ''}
value={formItem?.received_qty || ''}
onChange={(e) =>
handlePurchaseItemChange(
idx,
@@ -527,7 +574,7 @@ const PurchaseOrderAcceptApprovalForm = ({
<NumberInput
required
name={`items.${idx}.transport_per_item`}
value={item.transport_per_item || ''}
value={formItem?.transport_per_item || ''}
onChange={(e) =>
handlePurchaseItemChange(
idx,
@@ -559,7 +606,7 @@ const PurchaseOrderAcceptApprovalForm = ({
<NumberInput
required
name={`items.${idx}.transport_total`}
value={item.transport_total || ''}
value={formItem?.transport_total || ''}
onChange={(e) =>
handlePurchaseItemChange(
idx,
@@ -131,19 +131,18 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApp
purchase_item: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
}).nullable().optional(),
purchase_item_id: Yup.number()
.min(1, 'Purchase item is required!')
.required('Purchase item is required!')
.typeError('Purchase item is required!')
.test(
'is-valid-purchase-item',
'Purchase item must be selected!',
function (value) {
if (!this.parent.purchase_item) return true;
return Boolean(value && value > 0);
}
)
.typeError('Purchase item must be selected!'),
),
received_date: Yup.string()
.required('Tanggal penerimaan wajib diisi!')
.typeError('Tanggal penerimaan wajib diisi!'),
@@ -405,12 +405,12 @@ const PurchaseOrderDetail = ({
cell: (props) => formatNumber(props.getValue() as number),
},
{
accessorKey: 'price',
accessorKey: 'transport_per_item',
header: 'Transport /Item',
cell: (props) => formatCurrency(props.getValue() as number),
},
{
accessorKey: 'total_price',
accessorKey: 'transport_total',
header: 'Transport Total',
cell: (props) => formatCurrency(props.getValue() as number),
},
+1 -1
View File
@@ -90,7 +90,7 @@ export class AcceptApprovalService extends BaseApiService<
payload: CreateAcceptApprovalRequestPayload
): Promise<BaseApiResponse<{ message: string }> | undefined> {
return await this.customRequest<BaseApiResponse<{ message: string }>>(
`${purchaseRequestId}/approvals/receipts`,
`${purchaseRequestId}/receipts`,
{
method: 'POST',
payload,
+5
View File
@@ -38,6 +38,11 @@ export type PurchaseItem = {
travel_number_docs?: string | null;
travel_document_path?: string | null;
vehicle_number?: string | null;
expedition_vendor_id?: number | null;
expedition_vendor_name?: string | null;
received_qty?: number | null;
transport_per_item?: number | null;
transport_total?: number | null;
};
export type BasePurchase = {