mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat(FE-114,136): integrate location selection and update flock handling in RecordingForm
This commit is contained in:
@@ -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
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user