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'; 'use client';
import { useMemo, useState, useEffect } from 'react'; import { useMemo, useState, useEffect, useCallback } from 'react';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import useSWR from 'swr'; import useSWR from 'swr';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
@@ -26,6 +26,7 @@ import {
import { useRecordingFormHandlers } from './useRecordingFormHandlers'; import { useRecordingFormHandlers } from './useRecordingFormHandlers';
import { ProjectFlockApi } from '@/services/api/production'; import { ProjectFlockApi } from '@/services/api/production';
import { LocationApi } from '@/services/api/master-data'; import { LocationApi } from '@/services/api/master-data';
import { ProductWarehouseApi } from '@/services/api/inventory';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import Card from '@/components/Card'; import Card from '@/components/Card';
@@ -71,6 +72,22 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
ProjectFlockApi.getAllFetcher 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 // Extract location options from locations data
const locationOptions = useMemo(() => { const locationOptions = useMemo(() => {
if (!isResponseSuccess(locations)) return []; if (!isResponseSuccess(locations)) return [];
@@ -99,6 +116,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
return options; return options;
}, [projectFlocks]); }, [projectFlocks]);
const { const {
deleteModal, deleteModal,
recordingFormErrorMessage, 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 // EVENT HANDLERS - Select Inputs
const locationChangeHandler = (val: OptionType | OptionType[] | null) => { const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedLocation(val as OptionType); setSelectedLocation(val as OptionType);
@@ -386,6 +424,40 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
formik.setFieldValue('stocks', newStocks); 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 removeStock = (idx: number) => {
const updatedStocks = formik.values.stocks?.filter((_, i) => i !== idx); const updatedStocks = formik.values.stocks?.filter((_, i) => i !== idx);
formik.setFieldValue('stocks', updatedStocks); formik.setFieldValue('stocks', updatedStocks);
@@ -888,13 +960,24 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</td> </td>
)} )}
<td> <td>
<TextInput <SelectInput
required required
name={`stocks.${idx}.product_warehouse_id`} value={
type='number' unifiedStockProducts.find(
value={stock.product_warehouse_id?.toString() || ''} (product) =>
onChange={formik.handleChange} product.value === stock.product_warehouse_id
onBlur={formik.handleBlur} ) || 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={ isError={
isRepeaterInputError( isRepeaterInputError(
'stocks', 'stocks',
@@ -909,24 +992,23 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
idx idx
).errorMessage ).errorMessage
} }
readOnly={type === 'detail'}
className={{ className={{
wrapper: 'w-full min-w-32', wrapper: 'w-full min-w-48',
}} }}
placeholder='Product Warehouse ID' isSearchable
/> />
</td> </td>
<td> <td>
<TextInput <NumberInput
name={`stocks.${idx}.increase`} name={`stocks.${idx}.increase`}
type='number' value={stock.increase || 0}
value={
typeof stock.increase === 'number'
? stock.increase.toString()
: stock.increase
}
onChange={formik.handleChange} onChange={formik.handleChange}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
maskType='number'
decimals={0}
min={0}
thousandSeparator=','
decimalSeparator='.'
isError={ isError={
isRepeaterInputError('stocks', 'increase', idx) isRepeaterInputError('stocks', 'increase', idx)
.isError .isError
@@ -943,16 +1025,16 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
/> />
</td> </td>
<td> <td>
<TextInput <NumberInput
name={`stocks.${idx}.decrease`} name={`stocks.${idx}.decrease`}
type='number' value={stock.decrease || 0}
value={
typeof stock.decrease === 'number'
? stock.decrease.toString()
: stock.decrease
}
onChange={formik.handleChange} onChange={formik.handleChange}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
maskType='number'
decimals={0}
min={0}
thousandSeparator=','
decimalSeparator='.'
isError={ isError={
isRepeaterInputError('stocks', 'decrease', idx) isRepeaterInputError('stocks', 'decrease', idx)
.isError .isError
@@ -969,16 +1051,16 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
/> />
</td> </td>
<td> <td>
<TextInput <NumberInput
name={`stocks.${idx}.usage_amount`} name={`stocks.${idx}.usage_amount`}
type='number' value={stock.usage_amount || 0}
value={
typeof stock.usage_amount === 'number'
? stock.usage_amount.toString()
: stock.usage_amount
}
onChange={formik.handleChange} onChange={formik.handleChange}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
maskType='number'
decimals={0}
min={0}
thousandSeparator=','
decimalSeparator='.'
isError={ isError={
isRepeaterInputError('stocks', 'usage_amount', idx) isRepeaterInputError('stocks', 'usage_amount', idx)
.isError .isError