feat(FE): Add travel document upload to purchase form

This commit is contained in:
rstubryan
2025-12-31 11:02:06 +07:00
parent e32b9ddcb2
commit 5af00faa32
4 changed files with 75 additions and 6 deletions
@@ -8,6 +8,7 @@ import { useSearchParams } from 'next/navigation';
import Button from '@/components/Button'; import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput'; import TextInput from '@/components/input/TextInput';
import NumberInput from '@/components/input/NumberInput'; import NumberInput from '@/components/input/NumberInput';
import FileInput from '@/components/input/FileInput';
import SelectInput, { import SelectInput, {
OptionType, OptionType,
useSelect, useSelect,
@@ -66,6 +67,7 @@ const PurchaseOrderAcceptApprovalForm = ({
| 'expedition_vendor_id' | 'expedition_vendor_id'
| 'received_qty' | 'received_qty'
| 'transport_per_item' | 'transport_per_item'
| 'travel_documents'
): { isError: boolean; errorMessage: string } => { ): { isError: boolean; errorMessage: string } => {
const touchedItem = formik.touched.items?.[idx]; const touchedItem = formik.touched.items?.[idx];
const errorItem = formik.errors.items?.[idx] as const errorItem = formik.errors.items?.[idx] as
@@ -185,6 +187,7 @@ const PurchaseOrderAcceptApprovalForm = ({
: formItem.transport_per_item || 0, : formItem.transport_per_item || 0,
}; };
}) || [], }) || [],
travel_documents: values.travel_documents || [],
}; };
switch (type) { switch (type) {
@@ -236,15 +239,29 @@ const PurchaseOrderAcceptApprovalForm = ({
travel_document_path: item.travel_document_path || '', travel_document_path: item.travel_document_path || '',
vehicle_number: item.vehicle_number || '', vehicle_number: item.vehicle_number || '',
expedition_vendor: null, expedition_vendor: null,
expedition_vendor_id: 0, expedition_vendor_id: item.expedition_vendor_id || 0,
received_qty: item.total_qty || '', received_qty: item.total_qty || '',
transport_per_item: '', transport_per_item: item.transport_per_item || '',
}; };
}); });
formik.setFieldValue('items', updatedItems); formik.setFieldValue('items', updatedItems);
} }
}, [purchaseItems, initialValues]); }, [purchaseItems, initialValues]);
useEffect(() => {
if (
formik.values.travel_documents &&
formik.values.travel_documents.length > 0
) {
const fileNames = formik.values.travel_documents
.map((file) => file.name)
.join(', ');
formik.values.items?.forEach((item, idx) => {
formik.setFieldValue(`items.${idx}.travel_document_path`, fileNames);
});
}
}, [formik.values.travel_documents]);
// ===== HELPER FUNCTIONS ===== // ===== HELPER FUNCTIONS =====
const getQuantityExceededError = useCallback( const getQuantityExceededError = useCallback(
(idx: number, receivedQty: number) => { (idx: number, receivedQty: number) => {
@@ -343,7 +360,7 @@ const PurchaseOrderAcceptApprovalForm = ({
No. Surat Jalan No. Surat Jalan
<span className='text-error'>*</span> <span className='text-error'>*</span>
</th> </th>
<th> <th className='hidden'>
Dokumen Surat Jalan Dokumen Surat Jalan
<span className='text-error'>*</span> <span className='text-error'>*</span>
</th> </th>
@@ -478,7 +495,7 @@ const PurchaseOrderAcceptApprovalForm = ({
}} }}
/> />
</td> </td>
<td> <td className='hidden'>
<TextInput <TextInput
required required
name={`items.${idx}.travel_document_path`} name={`items.${idx}.travel_document_path`}
@@ -636,7 +653,7 @@ const PurchaseOrderAcceptApprovalForm = ({
</tbody> </tbody>
</table> </table>
</div> </div>
<div className={'col-span-2'}> <div className={'col-span-2 my-2'}>
<TextInput <TextInput
label='Notes' label='Notes'
name='notes' name='notes'
@@ -649,6 +666,31 @@ const PurchaseOrderAcceptApprovalForm = ({
/> />
</div> </div>
<div className={'col-span-2 my-2'}>
<FileInput
required
name='travel_documents'
label='Dokumen Surat Jalan'
accept='.pdf,.jpg,.jpeg,.png'
onChange={(e) => {
const files = Array.from(e.target.files || []);
formik.setFieldValue('travel_documents', files);
}}
onBlur={formik.handleBlur}
bottomLabel={
formik.values.travel_documents &&
formik.values.travel_documents.length > 0
? `${formik.values.travel_documents.length} file(s) dipilih`
: undefined
}
isError={
formik.touched.travel_documents &&
Boolean(formik.errors.travel_documents)
}
errorMessage={formik.errors.travel_documents as string}
/>
</div>
{/* Action buttons */} {/* Action buttons */}
<div className='flex flex-row justify-between gap-2 flex-wrap mt-5'> <div className='flex flex-row justify-between gap-2 flex-wrap mt-5'>
<div className='flex flex-row justify-end gap-2 w-full'> <div className='flex flex-row justify-end gap-2 w-full'>
@@ -48,6 +48,7 @@ type PurchaseRequestAcceptApprovalFormSchemaType = {
received_qty: number | string; received_qty: number | string;
transport_per_item: number | string; transport_per_item: number | string;
}[]; }[];
travel_documents: File[];
}; };
export type PurchaseStaffApprovalItemSchema = { export type PurchaseStaffApprovalItemSchema = {
@@ -379,6 +380,11 @@ export const PurchaseRequestAcceptApprovalFormSchema: Yup.ObjectSchema<PurchaseR
.min(1, 'Minimal harus ada 1 item pembelian!') .min(1, 'Minimal harus ada 1 item pembelian!')
.required('Item pembelian wajib diisi!') .required('Item pembelian wajib diisi!')
.typeError('Item pembelian wajib diisi!'), .typeError('Item pembelian wajib diisi!'),
travel_documents: Yup.array()
.of(Yup.mixed<File>().required())
.required('Dokumen surat jalan wajib diupload!')
.min(1, 'Minimal upload 1 dokumen surat jalan!')
.typeError('Dokumen surat jalan wajib diupload!'),
}); });
export const PurchaseRequestAcceptApprovalFormInitialValues: PurchaseRequestAcceptApprovalFormSchemaType = export const PurchaseRequestAcceptApprovalFormInitialValues: PurchaseRequestAcceptApprovalFormSchemaType =
@@ -397,6 +403,7 @@ export const PurchaseRequestAcceptApprovalFormInitialValues: PurchaseRequestAcce
transport_per_item: '', transport_per_item: '',
}, },
], ],
travel_documents: [],
}; };
export const PurchaseRequestAcceptApprovalFormDefaultValues = ( export const PurchaseRequestAcceptApprovalFormDefaultValues = (
@@ -428,6 +435,7 @@ export const PurchaseRequestAcceptApprovalFormDefaultValues = (
transport_per_item: '', transport_per_item: '',
}, },
], ],
travel_documents: [],
}; };
}; };
+19 -1
View File
@@ -72,11 +72,29 @@ export const PurchaseApi = {
purchaseRequestId: number, purchaseRequestId: number,
payload: CreateAcceptApprovalRequestPayload payload: CreateAcceptApprovalRequestPayload
): Promise<BaseApiResponse<{ message: string }> | undefined> => { ): Promise<BaseApiResponse<{ message: string }> | undefined> => {
const formData = new FormData();
formData.append('action', payload.action);
if (payload.notes) {
formData.append('notes', payload.notes);
}
if (payload.items) {
formData.append('items', JSON.stringify(payload.items));
}
if (payload.travel_documents) {
payload.travel_documents.forEach((file) => {
formData.append('travel_documents', file);
});
}
return await basePurchaseApi.customRequest< return await basePurchaseApi.customRequest<
BaseApiResponse<{ message: string }> BaseApiResponse<{ message: string }>
>(`${purchaseRequestId}/receipts`, { >(`${purchaseRequestId}/receipts`, {
method: 'POST', method: 'POST',
payload, payload: formData as unknown as Record<string, unknown>,
}); });
}, },
}, },
+1
View File
@@ -118,6 +118,7 @@ export type CreateAcceptApprovalRequestPayload = {
received_qty: number; received_qty: number;
transport_per_item: number; transport_per_item: number;
}[]; }[];
travel_documents?: File[];
}; };
export type DeletePurchaseRequestItemPayload = { export type DeletePurchaseRequestItemPayload = {