feat(FE-114,136): integrate location selection and update flock handling in RecordingForm

This commit is contained in:
rstubryan
2025-10-20 09:51:32 +07:00
parent e4f554bcd4
commit 16d72ebf6f
2 changed files with 224 additions and 121 deletions
@@ -23,8 +23,10 @@ import { ProjectFlockApi } from '@/services/api/production';
import { isResponseSuccess } from '@/lib/api-helper';
import { RECORDING_FLAG_OPTIONS } from '@/config/constant';
import useSWR from 'swr';
import { KandangApi, LocationApi } from '@/services/api/master-data';
import { ProductWarehouseApi } from '@/services/api/inventory';
import { ProjectFlock } from '@/types/api/production/project-flock';
import { Warehouse } from '@/types/api/master-data/warehouse';
import { LocationApi } from '@/services/api/master-data';
interface RecordingFormProps {
type?: 'add' | 'edit' | 'detail';
@@ -32,9 +34,10 @@ interface RecordingFormProps {
}
const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const [flockSelectInputValue, setFlockSelectInputValue] = useState('');
const [locationSelectInputValue, setLocationSelectInputValue] = useState('');
const [coopSelectInputValue, setCoopSelectInputValue] = useState('');
const [flockSelectInputValue, setFlockSelectInputValue] = useState('');
const [selectedProjectFlock, setSelectedProjectFlock] =
useState<ProjectFlock | null>(null);
const [selectedFeed, setSelectedFeed] = useState<number[]>([]);
const [selectedWeight, setSelectedWeight] = useState<number[]>([]);
const [selectedVaccine, setSelectedVaccine] = useState<number[]>([]);
@@ -104,87 +107,159 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
},
});
// Flock selection
const projectFlocksUrl = `${ProjectFlockApi.basePath}?${new URLSearchParams({ search: flockSelectInputValue }).toString()}`;
const { data: projectFlocks, isLoading: isLoadingFlocks } = useSWR(
projectFlocksUrl,
ProjectFlockApi.getAllFetcher
);
const flockOptions = isResponseSuccess(projectFlocks)
? projectFlocks?.data.map((flock) => ({
value: flock.id,
label: flock.flock.name,
}))
: [];
// Pakan selection
const pakanUrl = `${ProductWarehouseApi.basePath}?${new URLSearchParams({ flag: 'PAKAN', search: '' }).toString()}`;
const { data: pakanProducts } = useSWR(
pakanUrl,
ProductWarehouseApi.getAllFetcher
);
const pakanOptions = isResponseSuccess(pakanProducts)
? pakanProducts?.data.map((product) => ({
value: product.id,
label: `${product.product.name} - ${product.warehouse.name} (Stock: ${product.quantity.toLocaleString('id-ID')})`,
}))
: [];
// Create stock mapping for pakan (Feed)
const pakanStockMap = useMemo(() => {
if (!isResponseSuccess(pakanProducts))
return new Map<number, number | ''>();
const map = new Map<number, number | ''>();
pakanProducts.data.forEach((product) => {
map.set(product.id, product.quantity);
});
return map;
}, [pakanProducts]);
// OVK selection
const ovkUrl = `${ProductWarehouseApi.basePath}?${new URLSearchParams({ flag: 'OVK', search: '' }).toString()}`;
const { data: ovkProducts } = useSWR(
ovkUrl,
ProductWarehouseApi.getAllFetcher
);
const ovkOptions = isResponseSuccess(ovkProducts)
? ovkProducts?.data.map((product) => ({
value: product.id,
label: `${product.product.name} - ${product.warehouse.name} (Stock: ${product.quantity.toLocaleString('id-ID')})`,
}))
: [];
// Create stock mapping for OVK (Vaccination)
const ovkStockMap = useMemo(() => {
if (!isResponseSuccess(ovkProducts)) return new Map<number, number | ''>();
const map = new Map<number, number | ''>();
ovkProducts.data.forEach((product) => {
map.set(product.id, product.quantity);
});
return map;
}, [ovkProducts]);
const locationsUrl = `${LocationApi.basePath}?${new URLSearchParams({ search: locationSelectInputValue ?? '' }).toString()}`;
const locationsUrl = `${LocationApi.basePath}?${new URLSearchParams({ search: locationSelectInputValue }).toString()}`;
const { data: locations, isLoading: isLoadingLocations } = useSWR(
locationsUrl,
LocationApi.getAllFetcher
);
const locationOptions = isResponseSuccess(locations)
? locations?.data.map((loc) => ({ value: loc.id, label: loc.name }))
? locations.data.map((loc) => ({ value: loc.id, label: loc.name }))
: [];
const coopsUrl = `${KandangApi.basePath}?${new URLSearchParams({ search: coopSelectInputValue ?? '' }).toString()}`;
const { data: coops, isLoading: isLoadingCoops } = useSWR(
coopsUrl,
KandangApi.getAllFetcher
const projectFlocksUrl = useMemo(() => {
if (!formik.values.location_id) return null;
const params = new URLSearchParams({
search: flockSelectInputValue,
location_id: formik.values.location_id.toString(),
});
return `${ProjectFlockApi.basePath}?${params.toString()}`;
}, [formik.values.location_id, flockSelectInputValue]);
const { data: projectFlocks, isLoading: isLoadingFlocks } = useSWR(
projectFlocksUrl,
ProjectFlockApi.getAllFetcher
);
const flockOptions = isResponseSuccess(projectFlocks)
? projectFlocks.data.map((flock) => ({
value: flock.id,
label: flock.flock.name,
}))
: [];
const buildWarehouseLabel = useCallback((warehouse: Warehouse) => {
const parts: string[] = [warehouse.name];
if ('kandang' in warehouse && warehouse.kandang) {
parts.push(warehouse.kandang.name);
}
if ('location' in warehouse && warehouse.location) {
parts.push(warehouse.location.name);
}
if (warehouse.area) {
parts.push(warehouse.area.name);
}
return parts.join(' - ');
}, []);
const pakanUrl = useMemo(() => {
if (!formik.values.location_id) return null;
const params = new URLSearchParams({
flag: 'PAKAN',
search: '',
location_id: formik.values.location_id.toString(),
});
return `${ProductWarehouseApi.basePath}?${params.toString()}`;
}, [formik.values.location_id]);
const { data: pakanProducts, isLoading: isLoadingPakan } = useSWR(
pakanUrl,
ProductWarehouseApi.getAllFetcher
);
const filteredPakanProducts = useMemo(() => {
if (!isResponseSuccess(pakanProducts) || !formik.values.location_id)
return [];
return pakanProducts.data.filter((product) => {
const warehouse = product.warehouse;
if ('location' in warehouse && warehouse.location) {
return warehouse.location.id === formik.values.location_id;
}
// If warehouse only has area, include it if area matches the location's area
// Note: This might need adjustment based on your business logic
return false;
});
}, [pakanProducts, formik.values.location_id]);
const pakanOptions = useMemo(
() =>
filteredPakanProducts.map((product) => ({
value: product.id,
label: `${product.product.name} - ${buildWarehouseLabel(product.warehouse)} (Stock: ${product.quantity.toLocaleString('id-ID')})`,
})),
[filteredPakanProducts, buildWarehouseLabel]
);
const pakanStockMap = useMemo(() => {
const map = new Map<number, number | ''>();
filteredPakanProducts.forEach((product) => {
map.set(product.id, product.quantity);
});
return map;
}, [filteredPakanProducts]);
const ovkUrl = useMemo(() => {
if (!formik.values.location_id) return null;
const params = new URLSearchParams({
flag: 'OVK',
search: '',
location_id: formik.values.location_id.toString(),
});
return `${ProductWarehouseApi.basePath}?${params.toString()}`;
}, [formik.values.location_id]);
const { data: ovkProducts, isLoading: isLoadingOvk } = useSWR(
ovkUrl,
ProductWarehouseApi.getAllFetcher
);
const filteredOvkProducts = useMemo(() => {
if (!isResponseSuccess(ovkProducts) || !formik.values.location_id)
return [];
return ovkProducts.data.filter((product) => {
const warehouse = product.warehouse;
if ('location' in warehouse && warehouse.location) {
return warehouse.location.id === formik.values.location_id;
}
// If warehouse only has area, include it if area matches the location's area
// Note: This might need adjustment based on your business logic
return false;
});
}, [ovkProducts, formik.values.location_id]);
const ovkOptions = useMemo(
() =>
filteredOvkProducts.map((product) => ({
value: product.id,
label: `${product.product.name} - ${buildWarehouseLabel(product.warehouse)} (Stock: ${product.quantity.toLocaleString('id-ID')})`,
})),
[filteredOvkProducts, buildWarehouseLabel]
);
const ovkStockMap = useMemo(() => {
const map = new Map<number, number | ''>();
filteredOvkProducts.forEach((product) => {
map.set(product.id, product.quantity);
});
return map;
}, [filteredOvkProducts]);
const coopOptions = useMemo(() => {
if (!isResponseSuccess(coops) || !formik.values.location_id) return [];
return coops.data
.filter((coop) => coop.location.id === formik.values.location_id)
.map((coop) => ({ value: coop.id, label: coop.name }));
}, [coops, formik.values.location_id]);
if (!selectedProjectFlock || !selectedProjectFlock.kandangs) return [];
return selectedProjectFlock.kandangs.map((kandang) => ({
value: kandang.id,
label: kandang.name,
}));
}, [selectedProjectFlock]);
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
const locationValue = (val as OptionType)?.value;
@@ -192,9 +267,28 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
formik.setFieldValue('location', val, false);
formik.setFieldValue('location_id', locationValue || 0, false);
formik.setFieldValue('flock', null, false);
formik.setFieldValue('flock_id', 0, false);
formik.setFieldValue('coop', null, false);
formik.setFieldValue('coop_id', 0, false);
setSelectedProjectFlock(null);
setFlockSelectInputValue('');
};
const flockChangeHandler = (val: OptionType | OptionType[] | null) => {
const flockValue = (val as OptionType)?.value;
const selected = isResponseSuccess(projectFlocks)
? projectFlocks.data.find((flock) => flock.id === flockValue)
: null;
setSelectedProjectFlock(selected || null);
formik.setFieldValue('flock', val, false);
formik.setFieldValue('flock_id', flockValue || 0, false);
formik.setFieldValue('coop', null, false);
formik.setFieldValue('coop_id', 0, false);
setCoopSelectInputValue('');
};
const coopChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -204,6 +298,17 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
formik.setFieldValue('coop_id', coopValue || 0, false);
};
useEffect(() => {
if (initialValues?.flock && isResponseSuccess(projectFlocks)) {
const flock = projectFlocks.data.find(
(f) => f.id === initialValues.flock.id
);
if (flock) {
setSelectedProjectFlock(flock);
}
}
}, [initialValues, projectFlocks]);
const isRepeaterInputError = <T extends keyof CreateRecordingPayload>(
arrayName: T,
field: T extends 'feed_data'
@@ -245,13 +350,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
};
};
const flockChangeHandler = (val: OptionType | OptionType[] | null) => {
const flockValue = (val as OptionType)?.value;
formik.setFieldValue('flock', val, false);
formik.setFieldValue('flock_id', flockValue || 0, false);
};
const addFeedData = () => {
const newFeedData = [
...(formik.values.feed_data || []),
@@ -372,25 +470,28 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{/* Basic Info Card */}
<div className='card bg-base-100 shadow mb-4'>
<div className='card-body flex flex-col gap-6'>
<h2 className='card-title'>Flock</h2>
<h2 className='card-title'>Recording Information</h2>
<div className='flex flex-col gap-6'>
<div className='flex gap-4'>
<SelectInput
required
label='Flock'
value={formik.values.flock ?? undefined}
onChange={flockChangeHandler}
options={flockOptions}
onInputChange={setFlockSelectInputValue}
isLoading={isLoadingFlocks}
label='Lokasi'
value={formik.values.location ?? undefined}
onChange={locationChangeHandler}
options={locationOptions}
onInputChange={setLocationSelectInputValue}
isLoading={isLoadingLocations}
isError={
formik.touched.flock_id && Boolean(formik.errors.flock_id)
formik.touched.location_id &&
Boolean(formik.errors.location_id)
}
errorMessage={formik.errors.flock_id as string}
errorMessage={formik.errors.location_id as string}
isDisabled={type === 'detail'}
isClearable
placeholder='Pilih lokasi terlebih dahulu'
/>
<TextInput
required
label='Tanggal Recording'
@@ -418,42 +519,45 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
readOnly={type === 'detail'}
/>
</div>
<div className='flex gap-4'>
<SelectInput
required
label='Lokasi'
value={formik.values.location ?? undefined}
onChange={locationChangeHandler}
options={locationOptions}
onInputChange={setLocationSelectInputValue}
isLoading={isLoadingLocations}
label='Flock'
value={formik.values.flock ?? undefined}
onChange={flockChangeHandler}
options={flockOptions}
onInputChange={setFlockSelectInputValue}
isLoading={isLoadingFlocks}
isError={
formik.touched.location_id &&
Boolean(formik.errors.location_id)
formik.touched.flock_id && Boolean(formik.errors.flock_id)
}
errorMessage={formik.errors.location_id as string}
isDisabled={type === 'detail'}
isClearable
/>
<SelectInput
key={`coop-select-${formik.values.location_id || 'no-location'}`}
required
label='Kandang'
value={formik.values.coop ?? undefined}
onChange={coopChangeHandler}
options={coopOptions}
onInputChange={setCoopSelectInputValue}
isLoading={isLoadingCoops}
isError={
formik.touched.coop_id && Boolean(formik.errors.coop_id)
}
errorMessage={formik.errors.coop_id as string}
errorMessage={formik.errors.flock_id as string}
isDisabled={type === 'detail' || !formik.values.location_id}
isClearable
placeholder={
!formik.values.location_id
? 'Pilih lokasi terlebih dahulu'
: 'Pilih Flock'
}
/>
<SelectInput
key={`coop-select-${formik.values.flock_id || 'no-flock'}`}
required
label='Kandang'
value={formik.values.coop ?? undefined}
onChange={coopChangeHandler}
options={coopOptions}
isError={
formik.touched.coop_id && Boolean(formik.errors.coop_id)
}
errorMessage={formik.errors.coop_id as string}
isDisabled={type === 'detail' || !selectedProjectFlock}
isClearable
placeholder={
!selectedProjectFlock
? 'Pilih flock terlebih dahulu'
: 'Pilih Kandang'
}
/>
@@ -551,14 +655,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
`feed_data.${idx}.feed_qty`,
stock
);
// Reset feed_stock when changing feed
formik.setFieldValue(
`feed_data.${idx}.feed_stock`,
0
);
}}
options={pakanOptions}
isLoading={false}
isLoading={isLoadingPakan}
isError={
isRepeaterInputError('feed_data', 'feed_id', idx)
.isError
@@ -968,14 +1071,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
`vaccination.${idx}.total_stock`,
stock
);
// Reset used_stock when changing vaccine
formik.setFieldValue(
`vaccination.${idx}.used_stock`,
0
);
}}
options={ovkOptions}
isLoading={false}
isLoading={isLoadingOvk}
isError={
isRepeaterInputError(
'vaccination',
+1
View File
@@ -3,6 +3,7 @@ import { Area } from '@/types/api/master-data/area';
import { ProductCategory } from '@/types/api/master-data/product-category';
import { Fcr } from '@/types/api/master-data/fcr';
import { Kandang } from '@/types/api/master-data/kandang';
import { Location } from '@/types/api/master-data/location';
import { BaseMetadata } from '@/types/api/api-general';
export type BaseProjectFlock = {