From 19bca9ec730438e8831c100ac0d6d56daaa829d3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 14 Oct 2025 14:00:58 +0700 Subject: [PATCH] feat(FE-65): enhance MovementForm to support file uploads with FormData conversion --- .../inventory/movement/form/MovementForm.tsx | 13 +++++- .../movement/form/useMovementFormHandlers.ts | 13 +++++- src/lib/form-data.ts | 45 +++++++++++++++++++ src/services/api/base.ts | 26 +++++++---- 4 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 src/lib/form-data.ts diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index cd81b8a0..89546442 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -31,6 +31,8 @@ import { WarehouseApi, } from '@/services/api/master-data'; import { toast } from 'react-hot-toast'; +import FileInput from '@/components/input/FileInput'; +import { containsFile } from '@/lib/form-data'; interface MovementFormProps { type?: 'add' | 'edit' | 'detail'; @@ -62,6 +64,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { validationSchema: type === 'edit' ? UpdateMovementFormSchema : MovementFormSchema, onSubmit: async (values) => { + console.log( + 'Dokumen:', + values.ekspedisi?.map((e) => e.dokumen) + ); + setMovementFormErrorMessage(''); const payload: CreateMovementPayload = { alasan_transfer: values.alasan_transfer, @@ -88,6 +95,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { })), }; + console.log('containsFile:', containsFile(payload)); + console.log('payload:', payload); + switch (type) { case 'add': await createMovementHandler(payload); @@ -810,9 +820,8 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { /> - { const file = e.target.files?.[0]; diff --git a/src/components/pages/inventory/movement/form/useMovementFormHandlers.ts b/src/components/pages/inventory/movement/form/useMovementFormHandlers.ts index f6531bf4..0b6b0962 100644 --- a/src/components/pages/inventory/movement/form/useMovementFormHandlers.ts +++ b/src/components/pages/inventory/movement/form/useMovementFormHandlers.ts @@ -8,6 +8,7 @@ import { UpdateMovementPayload, } from '@/types/api/inventory/movement'; import { isResponseError } from '@/lib/api-helper'; +import { containsFile, toFormData } from '@/lib/form-data'; export const useMovementFormHandlers = (initialValuesId?: number) => { const router = useRouter(); @@ -17,7 +18,11 @@ export const useMovementFormHandlers = (initialValuesId?: number) => { const createMovementHandler = useCallback( 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)) { setMovementFormErrorMessage(res.message); return; @@ -30,7 +35,11 @@ export const useMovementFormHandlers = (initialValuesId?: number) => { const updateMovementHandler = useCallback( 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') { setMovementFormErrorMessage(res.message); return; diff --git a/src/lib/form-data.ts b/src/lib/form-data.ts new file mode 100644 index 00000000..d94e0724 --- /dev/null +++ b/src/lib/form-data.ts @@ -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).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).some(containsFile); + } + return false; +} diff --git a/src/services/api/base.ts b/src/services/api/base.ts index d1ac4729..c4dd826e 100644 --- a/src/services/api/base.ts +++ b/src/services/api/base.ts @@ -4,9 +4,11 @@ import { BaseApiResponse } from '@/types/api/api-general'; export class BaseApiService { basePath: string; + header?: Record; - constructor(basePath: string) { + constructor(basePath: string, header?: Record) { this.basePath = basePath; + this.header = header; } async getAllFetcher(endpoint: string): Promise> { @@ -23,42 +25,52 @@ export class BaseApiService { if (axios.isAxiosError>(error)) { return error.response?.data; } - return undefined; } } async create(payload: CreatePayloadGeneric) { + const isFormData = + typeof FormData !== 'undefined' && payload instanceof FormData; try { + const headers = isFormData + ? { ...(this.header ?? {}) } + : { 'Content-Type': 'application/json', ...(this.header ?? {}) }; + const createRes = await httpClient>(this.basePath, { method: 'POST', body: payload, + headers, }); - return createRes; } catch (error: unknown) { if (axios.isAxiosError>(error)) { return error.response?.data; } - return undefined; } } async update(id: number, payload: UpdatePayloadGeneric) { + const isFormData = + typeof FormData !== 'undefined' && payload instanceof FormData; try { const updatePath = `${this.basePath}/${id}`; + + const headers = isFormData + ? { ...(this.header ?? {}) } + : { 'Content-Type': 'application/json', ...(this.header ?? {}) }; + const updateRes = await httpClient>(updatePath, { method: 'PATCH', body: payload, + headers, }); - return updateRes; } catch (error: unknown) { if (axios.isAxiosError>(error)) { return error.response?.data; } - return undefined; } } @@ -69,13 +81,11 @@ export class BaseApiService { const deleteRes = await httpClient(deletePath, { method: 'DELETE', }); - return deleteRes; } catch (error) { if (axios.isAxiosError(error)) { return error.response?.data; } - return undefined; } }