feat(FE-65): enhance MovementForm to support file uploads with FormData conversion

This commit is contained in:
rstubryan
2025-10-14 14:00:58 +07:00
parent b2f0bd6698
commit 6facfd3d3c
4 changed files with 88 additions and 13 deletions
@@ -24,13 +24,17 @@ import {
ProductSchema, ProductSchema,
EkspedisiSchema, EkspedisiSchema,
} from '@/components/pages/inventory/movement/form/MovementForm.schema'; } from '@/components/pages/inventory/movement/form/MovementForm.schema';
import { useMovementFormHandlers } from './useMovementFormHandlers'; import {
useMovementFormHandlers,
containsFile,
} from './useMovementFormHandlers';
import { import {
ProductApi, ProductApi,
SupplierApi, SupplierApi,
WarehouseApi, WarehouseApi,
} from '@/services/api/master-data'; } from '@/services/api/master-data';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import FileInput from '@/components/input/FileInput';
interface MovementFormProps { interface MovementFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -62,6 +66,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
validationSchema: validationSchema:
type === 'edit' ? UpdateMovementFormSchema : MovementFormSchema, type === 'edit' ? UpdateMovementFormSchema : MovementFormSchema,
onSubmit: async (values) => { onSubmit: async (values) => {
console.log(
'Dokumen:',
values.ekspedisi?.map((e) => e.dokumen)
);
setMovementFormErrorMessage(''); setMovementFormErrorMessage('');
const payload: CreateMovementPayload = { const payload: CreateMovementPayload = {
alasan_transfer: values.alasan_transfer, alasan_transfer: values.alasan_transfer,
@@ -88,6 +97,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
})), })),
}; };
console.log('containsFile:', containsFile(payload));
console.log('payload:', payload);
switch (type) { switch (type) {
case 'add': case 'add':
await createMovementHandler(payload); await createMovementHandler(payload);
@@ -810,9 +822,8 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
/> />
</td> </td>
<td> <td>
<TextInput <FileInput
required required
type='file'
name={`ekspedisi.${idx}.dokumen`} name={`ekspedisi.${idx}.dokumen`}
onChange={(e) => { onChange={(e) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
@@ -8,6 +8,7 @@ import {
UpdateMovementPayload, UpdateMovementPayload,
} from '@/types/api/inventory/movement'; } from '@/types/api/inventory/movement';
import { isResponseError } from '@/lib/api-helper'; import { isResponseError } from '@/lib/api-helper';
import { containsFile, toFormData } from '@/lib/form-data';
export const useMovementFormHandlers = (initialValuesId?: number) => { export const useMovementFormHandlers = (initialValuesId?: number) => {
const router = useRouter(); const router = useRouter();
@@ -17,7 +18,11 @@ export const useMovementFormHandlers = (initialValuesId?: number) => {
const createMovementHandler = useCallback( const createMovementHandler = useCallback(
async (payload: CreateMovementPayload) => { async (payload: CreateMovementPayload) => {
const res = await MovementApi.create(payload); const finalPayload = containsFile(payload)
? (toFormData(payload) as unknown as CreateMovementPayload)
: payload;
const res = await MovementApi.create(finalPayload);
if (isResponseError(res)) { if (isResponseError(res)) {
setMovementFormErrorMessage(res.message); setMovementFormErrorMessage(res.message);
return; return;
@@ -30,7 +35,11 @@ export const useMovementFormHandlers = (initialValuesId?: number) => {
const updateMovementHandler = useCallback( const updateMovementHandler = useCallback(
async (movementId: number, payload: UpdateMovementPayload) => { async (movementId: number, payload: UpdateMovementPayload) => {
const res = await MovementApi.update(movementId, payload); const finalPayload = containsFile(payload)
? (toFormData(payload) as unknown as UpdateMovementPayload)
: payload;
const res = await MovementApi.update(movementId, finalPayload);
if (res?.status === 'error') { if (res?.status === 'error') {
setMovementFormErrorMessage(res.message); setMovementFormErrorMessage(res.message);
return; return;
+45
View File
@@ -0,0 +1,45 @@
export function toFormData(
value: unknown,
form = new FormData(),
parentKey?: string
) {
if (value === undefined || value === null) {
if (parentKey) form.append(parentKey, '');
return form;
}
if (value instanceof File) {
if (!parentKey) throw new Error('File must have a key');
form.append(parentKey, value);
return form;
}
if (Array.isArray(value)) {
value.forEach((v, i) => {
const key = parentKey ? `${parentKey}[${i}]` : `${i}`;
toFormData(v, form, key);
});
return form;
}
if (typeof value === 'object') {
Object.entries(value as Record<string, unknown>).forEach(([k, v]) => {
const key = parentKey ? `${parentKey}[${k}]` : k;
toFormData(v, form, key);
});
return form;
}
if (parentKey) form.append(parentKey, String(value));
return form;
}
export function containsFile(obj: unknown): boolean {
if (!obj) return false;
if (obj instanceof File) return true;
if (Array.isArray(obj)) return obj.some(containsFile);
if (typeof obj === 'object') {
return Object.values(obj as Record<string, unknown>).some(containsFile);
}
return false;
}
+18 -8
View File
@@ -4,9 +4,11 @@ import { BaseApiResponse } from '@/types/api/api-general';
export class BaseApiService<T, CreatePayloadGeneric, UpdatePayloadGeneric> { export class BaseApiService<T, CreatePayloadGeneric, UpdatePayloadGeneric> {
basePath: string; basePath: string;
header?: Record<string, string>;
constructor(basePath: string) { constructor(basePath: string, header?: Record<string, string>) {
this.basePath = basePath; this.basePath = basePath;
this.header = header;
} }
async getAllFetcher(endpoint: string): Promise<BaseApiResponse<T[]>> { async getAllFetcher(endpoint: string): Promise<BaseApiResponse<T[]>> {
@@ -23,42 +25,52 @@ export class BaseApiService<T, CreatePayloadGeneric, UpdatePayloadGeneric> {
if (axios.isAxiosError<BaseApiResponse<T>>(error)) { if (axios.isAxiosError<BaseApiResponse<T>>(error)) {
return error.response?.data; return error.response?.data;
} }
return undefined; return undefined;
} }
} }
async create(payload: CreatePayloadGeneric) { async create(payload: CreatePayloadGeneric) {
const isFormData =
typeof FormData !== 'undefined' && payload instanceof FormData;
try { try {
const headers = isFormData
? { ...(this.header ?? {}) }
: { 'Content-Type': 'application/json', ...(this.header ?? {}) };
const createRes = await httpClient<BaseApiResponse<T>>(this.basePath, { const createRes = await httpClient<BaseApiResponse<T>>(this.basePath, {
method: 'POST', method: 'POST',
body: payload, body: payload,
headers,
}); });
return createRes; return createRes;
} catch (error: unknown) { } catch (error: unknown) {
if (axios.isAxiosError<BaseApiResponse<T>>(error)) { if (axios.isAxiosError<BaseApiResponse<T>>(error)) {
return error.response?.data; return error.response?.data;
} }
return undefined; return undefined;
} }
} }
async update(id: number, payload: UpdatePayloadGeneric) { async update(id: number, payload: UpdatePayloadGeneric) {
const isFormData =
typeof FormData !== 'undefined' && payload instanceof FormData;
try { try {
const updatePath = `${this.basePath}/${id}`; const updatePath = `${this.basePath}/${id}`;
const headers = isFormData
? { ...(this.header ?? {}) }
: { 'Content-Type': 'application/json', ...(this.header ?? {}) };
const updateRes = await httpClient<BaseApiResponse<T>>(updatePath, { const updateRes = await httpClient<BaseApiResponse<T>>(updatePath, {
method: 'PATCH', method: 'PATCH',
body: payload, body: payload,
headers,
}); });
return updateRes; return updateRes;
} catch (error: unknown) { } catch (error: unknown) {
if (axios.isAxiosError<BaseApiResponse<T>>(error)) { if (axios.isAxiosError<BaseApiResponse<T>>(error)) {
return error.response?.data; return error.response?.data;
} }
return undefined; return undefined;
} }
} }
@@ -69,13 +81,11 @@ export class BaseApiService<T, CreatePayloadGeneric, UpdatePayloadGeneric> {
const deleteRes = await httpClient<BaseApiResponse>(deletePath, { const deleteRes = await httpClient<BaseApiResponse>(deletePath, {
method: 'DELETE', method: 'DELETE',
}); });
return deleteRes; return deleteRes;
} catch (error) { } catch (error) {
if (axios.isAxiosError<BaseApiResponse>(error)) { if (axios.isAxiosError<BaseApiResponse>(error)) {
return error.response?.data; return error.response?.data;
} }
return undefined; return undefined;
} }
} }