diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index 1af77cf7..898a1d56 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -25,11 +25,8 @@ import { DeliverySchema, } from '@/components/pages/inventory/movement/form/MovementForm.schema'; import { useMovementFormHandlers } from './useMovementFormHandlers'; -import { - ProductApi, - SupplierApi, - WarehouseApi, -} from '@/services/api/master-data'; +import { SupplierApi, WarehouseApi } from '@/services/api/master-data'; +import { ProductWarehouseApi } from '@/services/api/inventory'; import { toast } from 'react-hot-toast'; import FileInput from '@/components/input/FileInput'; @@ -40,6 +37,10 @@ interface MovementFormProps { const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { const [, setMovementFormErrorMessage] = useState(''); + const [ + productWarehouseSelectInputValue, + setProductWarehouseSelectInputValue, + ] = useState(''); const [selectedProducts, setSelectedProducts] = useState([]); const [selectedDeliveries, setSelectedDeliveries] = useState([]); @@ -67,7 +68,43 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { validateOnMount: false, enableReinitialize: true, onSubmit: async (values) => { + console.log('=== FORM SUBMIT DEBUG ==='); + console.log('1. Form values received:', values); + setMovementFormErrorMessage(''); + const documents: File[] = []; + const deliveriesPayload = values.deliveries.map((d, idx) => { + let documentIndex = 0; + + console.log(`2. Processing delivery ${idx}:`, { + driver_name: d.driver_name, + document: d.document, + documentType: d.document instanceof File ? 'File' : typeof d.document, + documentSize: d.document instanceof File ? d.document.size : 'N/A', + }); + + if (d.document && d.document instanceof File) { + documents.push(d.document); + documentIndex = documents.length - 1; + console.log(` → Document added at index ${documentIndex}`); + } else { + console.log(` → No document for delivery ${idx}, using index 0`); + } + + return { + delivery_cost: d.delivery_cost, + delivery_cost_per_item: d.delivery_cost_per_item ?? 0, + document_index: documentIndex, + driver_name: d.driver_name, + vehicle_plate: d.vehicle_plate, + supplier_id: d.supplier_id, + products: d.products.map((p) => ({ + product_id: p.product_id, + product_qty: p.product_qty, + })), + }; + }); + const payload: CreateMovementPayload = { transfer_reason: values.transfer_reason, transfer_date: values.transfer_date, @@ -77,26 +114,34 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { product_id: p.product_id, product_qty: p.product_qty, })), - deliveries: values.deliveries.map((d) => ({ - delivery_cost: d.delivery_cost, - delivery_cost_per_item: d.delivery_cost_per_item ?? 0, - document: d.document instanceof File ? d.document : d.document, - driver_name: d.driver_name, - vehicle_plate: d.vehicle_plate, - supplier_id: d.supplier_id, - products: d.products.map((p) => ({ - product_id: p.product_id, - product_qty: p.product_qty, - })), - })), + deliveries: deliveriesPayload, }; + console.log('3. Final payload structure:', { + ...payload, + }); + + console.log( + '4. Document indices in deliveries:', + deliveriesPayload.map((d, i) => ({ + delivery: i, + document_index: d.document_index, + })) + ); + + console.log('5. Total documents:', documents.length); + console.log('=== END SUBMIT DEBUG ==='); + switch (type) { case 'add': - await createMovementHandler(payload); + await createMovementHandler(payload, documents); break; case 'edit': - await updateMovementHandler(initialValues?.id as number, payload); + await updateMovementHandler( + initialValues?.id as number, + payload, + documents + ); break; } }, @@ -144,7 +189,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { { delivery_cost: 0, delivery_cost_per_item: 0, - document: '', + document: null, driver_name: '', vehicle_plate: '', supplier: null, @@ -265,15 +310,31 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { })) : []; - // Product selection - const [productSelectInputValue, setProductSelectInputValue] = useState(''); - const productsUrl = `${ProductApi.basePath}?${new URLSearchParams({ search: productSelectInputValue }).toString()}`; - const { data: products, isLoading: isLoadingProducts } = useSWR( - productsUrl, - ProductApi.getAllFetcher - ); - const productOptions = isResponseSuccess(products) - ? products?.data.map((p) => ({ value: p.id, label: p.name })) + // Product Warehouse selection - Filter by source warehouse + const productWarehouseParams = new URLSearchParams({ + search: productWarehouseSelectInputValue, + }); + if (formik.values.source_warehouse_id) { + productWarehouseParams.append( + 'warehouse_id', + formik.values.source_warehouse_id.toString() + ); + } + const productWarehousesUrl = `${ProductWarehouseApi.basePath}?${productWarehouseParams.toString()}`; + const { data: productWarehouses, isLoading: isLoadingProductWarehouses } = + useSWR( + formik.values.source_warehouse_id ? productWarehousesUrl : null, + ProductWarehouseApi.getAllFetcher + ); + const productWarehouseOptions = isResponseSuccess(productWarehouses) + ? productWarehouses?.data.map((pw) => ({ + value: pw.id, + label: pw.product.name, + product_id: pw.product.id, + warehouse_id: pw.warehouse.id, + warehouse_name: pw.warehouse.name, + quantity: pw.quantity, + })) : []; // Supplier selection @@ -303,7 +364,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { }); }, [formik.values.deliveries]); - const getFilteredProductOptions = useCallback(() => { + useEffect(() => { + if (formik.values.source_warehouse_id && type !== 'edit') { + formik.setFieldValue('products', []); + formik.setFieldValue('deliveries', []); + } + }, [formik.values.source_warehouse_id]); + + const getFilteredProductWarehouseOptions = useCallback(() => { return ( formik.values.products ?.filter((p) => p.product) @@ -314,6 +382,33 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { ); }, [formik.values.products]); + const getAvailableStock = useCallback( + (productId: number) => { + const productWarehouse = productWarehouseOptions.find( + (pw) => pw.product_id === productId + ); + return productWarehouse?.quantity ?? 0; + }, + [productWarehouseOptions] + ); + + const getProductQtyError = useCallback( + (productIdx: number) => { + const product = formik.values.products?.[productIdx]; + if (!product || !product.product_id) return null; + + const availableStock = getAvailableStock(product.product_id); + const requestedQty = Number(product.product_qty) || 0; + + if (requestedQty > availableStock) { + return `Qty melebihi stok tersedia! Maksimal: ${availableStock.toLocaleString('id-ID')}`; + } + + return null; + }, + [formik.values.products, getAvailableStock] + ); + const validateDeliveryQty = useCallback( (deliveryIdx: number, deliveryProductIdx: number, qty: number) => { const delivery = formik.values.deliveries?.[deliveryIdx]; @@ -406,6 +501,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { [invalidQtyRows] ); + const hasExceededStock = useMemo(() => { + return ( + formik.values.products?.some((product, idx) => { + return getProductQtyError(idx) !== null; + }) ?? false + ); + }, [formik.values.products, getProductQtyError]); + return ( <>
@@ -656,10 +759,18 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { (val as OptionType)?.value ); }} - options={productOptions} - onInputChange={setProductSelectInputValue} - isLoading={isLoadingProducts} - isDisabled={type === 'detail'} + options={productWarehouseOptions} + onInputChange={setProductWarehouseSelectInputValue} + isLoading={isLoadingProductWarehouses} + isDisabled={ + type === 'detail' || + !formik.values.source_warehouse_id + } + placeholder={ + !formik.values.source_warehouse_id + ? 'Pilih gudang asal terlebih dahulu' + : 'Pilih produk' + } isClearable {...isRepeaterInputError( 'products', @@ -669,23 +780,46 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { /> - + + {product.product_id && ( +
+ + Stok tersedia: + {' '} + {getAvailableStock( + product.product_id + ).toLocaleString('id-ID')} +
)} - readOnly={type === 'detail'} - className={{ - wrapper: 'w-full min-w-24', - }} - /> + {type !== 'detail' && ( @@ -819,7 +953,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { (val as OptionType)?.value ); }} - options={getFilteredProductOptions()} + options={getFilteredProductWarehouseOptions()} isDisabled={type === 'detail'} isClearable /> @@ -886,7 +1020,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { { const file = e.target.files?.[0]; @@ -1016,7 +1149,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { : undefined } onDelete={deleteMovementClickHandler} - disableSubmit={hasInvalidQty} + disableSubmit={hasInvalidQty || hasExceededStock} /> {movementFormErrorMessage && ( diff --git a/src/components/pages/inventory/movement/form/useMovementFormHandlers.ts b/src/components/pages/inventory/movement/form/useMovementFormHandlers.ts index 1894b1a7..3f46b71a 100644 --- a/src/components/pages/inventory/movement/form/useMovementFormHandlers.ts +++ b/src/components/pages/inventory/movement/form/useMovementFormHandlers.ts @@ -24,7 +24,6 @@ export const useMovementFormHandlers = (initialValuesId?: number) => { let finalPayload: CreateMovementPayload | FormData; if (documents.length > 0) { - // Ada dokumen: kirim sebagai FormData dengan "data" field console.log('3. Creating FormData (has documents)'); const formData = new FormData(); formData.append('data', JSON.stringify(payload)); @@ -35,7 +34,9 @@ export const useMovementFormHandlers = (initialValuesId?: number) => { console.log('4. FormData entries:'); for (const [key, value] of formData.entries()) { if (value instanceof File) { - console.log(` ${key}: [File] ${value.name} (${value.size} bytes)`); + console.log( + ` ${key}: [File] ${value.name} (${value.size} bytes)` + ); } else { console.log(` ${key}: ${value}`); } @@ -43,7 +44,6 @@ export const useMovementFormHandlers = (initialValuesId?: number) => { finalPayload = formData as unknown as CreateMovementPayload; } else { - // Tidak ada dokumen: kirim sebagai JSON biasa console.log('3. Sending as JSON (no documents)'); console.log('4. Payload:', JSON.stringify(payload, null, 2)); finalPayload = payload; @@ -64,7 +64,11 @@ export const useMovementFormHandlers = (initialValuesId?: number) => { ); const updateMovementHandler = useCallback( - async (movementId: number, payload: UpdateMovementPayload, documents: File[] = []) => { + async ( + movementId: number, + payload: UpdateMovementPayload, + documents: File[] = [] + ) => { console.log('=== UPDATE HANDLER DEBUG ==='); console.log('1. Received payload:', payload); console.log('2. Movement ID:', movementId); @@ -73,7 +77,6 @@ export const useMovementFormHandlers = (initialValuesId?: number) => { let finalPayload: UpdateMovementPayload | FormData; if (documents.length > 0) { - // Ada dokumen: kirim sebagai FormData dengan "data" field console.log('4. Creating FormData (has documents)'); const formData = new FormData(); formData.append('data', JSON.stringify(payload)); @@ -84,7 +87,9 @@ export const useMovementFormHandlers = (initialValuesId?: number) => { console.log('5. FormData entries:'); for (const [key, value] of formData.entries()) { if (value instanceof File) { - console.log(` ${key}: [File] ${value.name} (${value.size} bytes)`); + console.log( + ` ${key}: [File] ${value.name} (${value.size} bytes)` + ); } else { console.log(` ${key}: ${value}`); } @@ -92,7 +97,6 @@ export const useMovementFormHandlers = (initialValuesId?: number) => { finalPayload = formData as unknown as UpdateMovementPayload; } else { - // Tidak ada dokumen: kirim sebagai JSON biasa console.log('4. Sending as JSON (no documents)'); console.log('5. Payload:', JSON.stringify(payload, null, 2)); finalPayload = payload;