feat(FE-114): integrate product stock fetching and selection in RecordingForm

This commit is contained in:
rstubryan
2025-10-23 22:59:41 +07:00
parent 6060ec0f7e
commit 7f5ae94706
@@ -1,6 +1,6 @@
'use client';
import { useMemo, useState, useEffect } from 'react';
import { useMemo, useState, useEffect, useCallback } from 'react';
import { useFormik } from 'formik';
import useSWR from 'swr';
import { Icon } from '@iconify/react';
@@ -26,6 +26,7 @@ import {
import { useRecordingFormHandlers } from './useRecordingFormHandlers';
import { ProjectFlockApi } from '@/services/api/production';
import { LocationApi } from '@/services/api/master-data';
import { ProductWarehouseApi } from '@/services/api/inventory';
import { isResponseSuccess } from '@/lib/api-helper';
import Card from '@/components/Card';
@@ -71,6 +72,22 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
ProjectFlockApi.getAllFetcher
);
// Fetch Products with location filter (both PAKAN and OVK) - using selectedLocation for now
const stockProductsUrl = useMemo(() => {
if (!selectedLocation) return null;
const params = new URLSearchParams({
flags: 'PAKAN,OVK', // Fetch both flags in one request
search: '',
location_id: selectedLocation.value.toString(),
});
return `${ProductWarehouseApi.basePath}?${params.toString()}`;
}, [selectedLocation]);
const { data: stockProducts, isLoading: isLoadingStockProducts } = useSWR(
stockProductsUrl,
ProductWarehouseApi.getAllFetcher
);
// Extract location options from locations data
const locationOptions = useMemo(() => {
if (!isResponseSuccess(locations)) return [];
@@ -99,6 +116,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
return options;
}, [projectFlocks]);
const {
deleteModal,
recordingFormErrorMessage,
@@ -179,6 +197,26 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
},
});
// Get location from selected project flock for stock filtering
const getProjectFlockLocation = useCallback((): OptionType | null => {
if (!formik.values.project_flock_kandang || !isResponseSuccess(projectFlocks)) {
return selectedLocation; // Fallback to manual location selection
}
const kandangId = formik.values.project_flock_kandang.value;
for (const projectFlock of projectFlocks.data) {
const kandang = projectFlock.kandangs.find(k => k.id === kandangId);
if (kandang && projectFlock.location) {
return {
value: projectFlock.location.id,
label: projectFlock.location.name
};
}
}
return selectedLocation; // Fallback to manual location selection
}, [formik.values.project_flock_kandang, projectFlocks, selectedLocation]);
// EVENT HANDLERS - Select Inputs
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedLocation(val as OptionType);
@@ -386,6 +424,40 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
formik.setFieldValue('stocks', newStocks);
};
// Memoized unified products for stock selection
const unifiedStockProducts = useMemo(() => {
if (!isResponseSuccess(stockProducts)) return [];
const options: OptionType[] = [];
stockProducts.data.forEach((product) => {
const warehouse = product.warehouse;
const stockText = product.quantity.toLocaleString('id-ID');
// Check if product has any of the flags
const hasPakanFlag = product.product.flags?.includes('PAKAN');
const hasOvkFlag = product.product.flags?.includes('OVK');
// Add products with warehouse and location grouping in label (similar to projectFlockKandangOptions pattern)
if (hasPakanFlag) {
options.push({
value: product.id,
label: `[PAKAN] ${product.product.name} - ${warehouse?.name || ''} (${stockText})`
});
}
if (hasOvkFlag) {
options.push({
value: product.id,
label: `[OVK] ${product.product.name} - ${warehouse?.name || ''} (${stockText})`
});
}
});
return options;
}, [stockProducts, getProjectFlockLocation()]);
// Unified Stock remove handlers
const removeStock = (idx: number) => {
const updatedStocks = formik.values.stocks?.filter((_, i) => i !== idx);
formik.setFieldValue('stocks', updatedStocks);
@@ -888,13 +960,24 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</td>
)}
<td>
<TextInput
<SelectInput
required
name={`stocks.${idx}.product_warehouse_id`}
type='number'
value={stock.product_warehouse_id?.toString() || ''}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={
unifiedStockProducts.find(
(product) =>
product.value === stock.product_warehouse_id
) || null
}
onChange={(selectedOption) => {
const option = selectedOption as OptionType | null;
formik.setFieldValue(
`stocks.${idx}.product_warehouse_id`,
option?.value || 0
);
}}
options={unifiedStockProducts}
placeholder='Pilih Produk'
isLoading={isLoadingStockProducts}
isError={
isRepeaterInputError(
'stocks',
@@ -909,24 +992,23 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
idx
).errorMessage
}
readOnly={type === 'detail'}
className={{
wrapper: 'w-full min-w-32',
wrapper: 'w-full min-w-48',
}}
placeholder='Product Warehouse ID'
isSearchable
/>
</td>
<td>
<TextInput
<NumberInput
name={`stocks.${idx}.increase`}
type='number'
value={
typeof stock.increase === 'number'
? stock.increase.toString()
: stock.increase
}
value={stock.increase || 0}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
maskType='number'
decimals={0}
min={0}
thousandSeparator=','
decimalSeparator='.'
isError={
isRepeaterInputError('stocks', 'increase', idx)
.isError
@@ -943,16 +1025,16 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
/>
</td>
<td>
<TextInput
<NumberInput
name={`stocks.${idx}.decrease`}
type='number'
value={
typeof stock.decrease === 'number'
? stock.decrease.toString()
: stock.decrease
}
value={stock.decrease || 0}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
maskType='number'
decimals={0}
min={0}
thousandSeparator=','
decimalSeparator='.'
isError={
isRepeaterInputError('stocks', 'decrease', idx)
.isError
@@ -969,16 +1051,16 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
/>
</td>
<td>
<TextInput
<NumberInput
name={`stocks.${idx}.usage_amount`}
type='number'
value={
typeof stock.usage_amount === 'number'
? stock.usage_amount.toString()
: stock.usage_amount
}
value={stock.usage_amount || 0}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
maskType='number'
decimals={0}
min={0}
thousandSeparator=','
decimalSeparator='.'
isError={
isRepeaterInputError('stocks', 'usage_amount', idx)
.isError