feat(FE-170,174,175): enhance RecordingForm with product warehouse integration and improve data handling

This commit is contained in:
rstubryan
2025-11-05 15:39:19 +07:00
parent a33a4167c1
commit 333212a1de
3 changed files with 193 additions and 111 deletions
@@ -235,9 +235,11 @@ export type RecordingGradingFormValues = Yup.InferType<
type RecordingFormData = Partial<Recording> & {
body_weights?: CreateGrowingRecordingPayload['body_weights'];
stocks?: CreateGrowingRecordingPayload['stocks'];
depletions?: CreateGrowingRecordingPayload['depletions'];
eggs?: CreateLayingRecordingPayload['eggs'];
stocks?: CreateGrowingRecordingPayload['stocks'] | Recording['stocks'];
depletions?: CreateGrowingRecordingPayload['depletions'] | Recording['depletions'];
eggs?: CreateLayingRecordingPayload['eggs'] | Recording['eggs'];
project_flock_kandang_id?: number;
project_flock_category?: string;
};
export const getRecordingGrowingFormInitialValues = (
@@ -265,17 +267,15 @@ export const getRecordingGrowingFormInitialValues = (
total_weight: 0,
},
],
stocks: initialValues?.stocks?.map(
(stock: NonNullable<CreateGrowingRecordingPayload['stocks']>[0]) => ({
stocks: initialValues?.stocks?.map((stock) => ({
product_warehouse_id: stock.product_warehouse_id,
qty: stock.qty,
})
) ?? [
{
product_warehouse_id: 0,
qty: '',
},
],
qty: (stock as { qty?: number; usage_amount?: number }).qty || (stock as { qty?: number; usage_amount?: number }).usage_amount || '',
})) ?? [
{
product_warehouse_id: 0,
qty: '',
},
],
depletions: initialValues?.depletions?.map(
(
depletion: NonNullable<CreateGrowingRecordingPayload['depletions']>[0]
@@ -34,7 +34,7 @@ import { ProjectFlockApi } from '@/services/api/production';
import { LocationApi } from '@/services/api/master-data';
import { ProductWarehouseApi } from '@/services/api/inventory';
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
import { cn } from '@/lib/helper';
import { cn, formatDate } from '@/lib/helper';
import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock';
import { useModal } from '@/components/Modal';
import toast from 'react-hot-toast';
@@ -197,7 +197,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const today = new Date().toISOString().split('T')[0];
const existingRecordingsUrl = useMemo(() => {
return `${RecordingApi.basePath}?record_date=${today}`;
}, []);
}, [today]);
const { data: existingRecordings } = useSWR(
existingRecordingsUrl,
@@ -310,30 +310,15 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
initialValues.stocks &&
type !== 'add'
) {
const initialValuesWithStocks = initialValues as Recording & {
stocks?: Array<{
product_warehouse_id: number;
usage_amount: number;
notes: string;
product_warehouse?: {
id: number;
product_id: number;
product_name: string;
warehouse_id: number;
warehouse_name: string;
};
}>;
};
initialValuesWithStocks.stocks?.forEach((stock) => {
if (stock.product_warehouse && stock.product_warehouse.product_name) {
initialValues.stocks?.forEach((stock) => {
if (stock.product_warehouse && stock.product_warehouse.product) {
const existingOption = options.find(
(opt) => opt.value === stock.product_warehouse_id
);
if (!existingOption) {
options.push({
value: stock.product_warehouse_id,
label: `${stock.product_warehouse.product_name} - ${stock.product_warehouse.warehouse_name}`,
label: `${stock.product_warehouse.product.name} - ${stock.product_warehouse.product?.sku || ''}`,
});
}
}
@@ -362,8 +347,27 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
});
}
if (initialValues && initialValues.depletions && type !== 'add') {
initialValues.depletions.forEach((depletion) => {
if (
depletion.product_warehouse &&
depletion.product_warehouse.product
) {
const existingOption = options.find(
(opt) => opt.value === depletion.product_warehouse_id
);
if (!existingOption) {
options.push({
value: depletion.product_warehouse_id,
label: depletion.product_warehouse.product.name,
});
}
}
});
}
return options;
}, [depletionProductsData]);
}, [depletionProductsData, initialValues, type]);
const eggProducts = useMemo(() => {
const options: OptionType[] = [];
@@ -383,10 +387,27 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
});
}
if (initialValues && initialValues.eggs && type !== 'add') {
initialValues.eggs.forEach((egg) => {
if (egg.product_warehouse && egg.product_warehouse.product) {
const existingOption = options.find(
(opt) => opt.value === egg.product_warehouse_id
);
if (!existingOption) {
options.push({
value: egg.product_warehouse_id,
label: egg.product_warehouse.product.name,
});
}
}
});
}
return options;
}, [eggProductsData]);
}, [eggProductsData, initialValues, type]);
const isLayingCategory =
initialValues?.project_flock_category === 'LAYING' ||
projectFlockKandangLookup?.project_flock?.category === 'LAYING';
const formikInitialValues = useMemo(() => {
@@ -824,7 +845,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
formik.setFieldValue(`body_weights.${idx}.avg_weight`, 0);
}
// Update total_weight
formik.setFieldValue(`body_weights.${idx}.total_weight`, totalWeight);
}
};
@@ -844,7 +864,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
if (qty > 0 && value > 0) {
const totalWeight = value * qty;
formik.setFieldValue(`body_weights.${idx}.weight`, totalWeight);
// Update total_weight
formik.setFieldValue(`body_weights.${idx}.total_weight`, totalWeight);
} else {
formik.setFieldValue(`body_weights.${idx}.weight`, 0);
@@ -874,7 +893,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
formik.setFieldValue(`body_weights.${idx}.avg_weight`, 0);
}
// Update total_weight
formik.setFieldValue(`body_weights.${idx}.total_weight`, totalWeight);
}
};
@@ -1045,6 +1063,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
}
}, [isLayingCategory, type]);
const bodyWeightValues = useMemo(() => {
if (!formik.values.body_weights) return [];
return formik.values.body_weights.map((w) => ({
weight: w.weight,
qty: w.qty,
}));
}, [formik.values.body_weights]);
useEffect(() => {
if (formik.values.body_weights && editingAverageIndex === null) {
const updatedBodyWeights = formik.values.body_weights.map(
@@ -1074,12 +1100,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
formik.setFieldValue('body_weights', updatedBodyWeights, false);
}
}
}, [
formik.values.body_weights?.map((w) => w.weight),
formik.values.body_weights?.map((w) => w.qty),
editingAverageIndex,
manuallyEditedRows,
]);
}, [bodyWeightValues, editingAverageIndex, manuallyEditedRows]);
return (
<>
@@ -1172,73 +1193,124 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
className='w-full mt-8 flex flex-col gap-6'
>
{/* Basic Info Card */}
<Card
title='Informasi Recording'
className={{
wrapper: 'w-full mb-4 shadow',
body: 'flex flex-col gap-6',
}}
>
<div className={'grid grid-cols-3 gap-4'}>
<>
<SelectInput
key={`location-select-${selectedLocation?.value || 'default'}`}
required
label='Lokasi'
value={selectedLocation}
onChange={locationChangeHandler}
options={locationOptions}
onInputChange={setLocationSearchValue}
isLoading={isLoadingLocations}
placeholder='Pilih Lokasi'
isClearable
isSearchable
/>
{(type === 'add' || type === 'edit') && (
<Card
title='Informasi Recording'
className={{
wrapper: 'w-full mb-4 shadow',
body: 'flex flex-col gap-6',
}}
>
<div className={'grid grid-cols-3 gap-4'}>
<>
<SelectInput
key={`location-select-${selectedLocation?.value || 'default'}`}
required
label='Lokasi'
value={selectedLocation}
onChange={locationChangeHandler}
options={locationOptions}
onInputChange={setLocationSearchValue}
isLoading={isLoadingLocations}
placeholder='Pilih Lokasi'
isClearable
isSearchable
/>
<SelectInput
key={`project-flock-select-${selectedProjectFlock?.value || 'default'}`}
required
label='Project Flock'
value={selectedProjectFlock}
onChange={projectFlockChangeHandler}
options={projectFlockOptions}
onInputChange={setProjectFlockSearchValue}
isLoading={isLoadingProjectFlocks}
isDisabled={!selectedLocation}
placeholder={
selectedLocation
? 'Pilih Project Flock'
: 'Pilih Lokasi terlebih dahulu'
}
isClearable
isSearchable
/>
<SelectInput
key={`project-flock-select-${selectedProjectFlock?.value || 'default'}`}
required
label='Project Flock'
value={selectedProjectFlock}
onChange={projectFlockChangeHandler}
options={projectFlockOptions}
onInputChange={setProjectFlockSearchValue}
isLoading={isLoadingProjectFlocks}
isDisabled={!selectedLocation}
placeholder={
selectedLocation
? 'Pilih Project Flock'
: 'Pilih Lokasi terlebih dahulu'
}
isClearable
isSearchable
/>
<SelectInput
key={`kandang-select-${projectFlockKandangLookup?.project_flock_kandang_id || 'default'}`}
required
label='Kandang'
value={selectedKandang}
onChange={kandangChangeHandler}
options={kandangOptions}
isLoading={false}
isDisabled={!selectedProjectFlock}
placeholder={
selectedProjectFlock
? 'Pilih Kandang'
: 'Pilih Project Flock terlebih dahulu'
}
isClearable
isSearchable={false}
startAdornment={
projectFlockKandangLookup
? getProjectFlockBadgeAdornment()
: undefined
}
/>
</>
</div>
</Card>
<SelectInput
key={`kandang-select-${projectFlockKandangLookup?.project_flock_kandang_id || 'default'}`}
required
label='Kandang'
value={selectedKandang}
onChange={kandangChangeHandler}
options={kandangOptions}
isLoading={false}
isDisabled={!selectedProjectFlock}
placeholder={
selectedProjectFlock
? 'Pilih Kandang'
: 'Pilih Project Flock terlebih dahulu'
}
isClearable
isSearchable={false}
startAdornment={
projectFlockKandangLookup
? getProjectFlockBadgeAdornment()
: undefined
}
/>
</>
</div>
</Card>
)}
{/* Recording Info for Detail View */}
{type === 'detail' && initialValues && (
<Card
title='Informasi Recording'
className={{
wrapper: 'w-full mb-4 shadow',
body: 'flex flex-col gap-4',
}}
>
<div className='grid grid-cols-2 md:grid-cols-4 gap-4'>
<div>
<span className='text-sm text-gray-600'>Recording ID</span>
<p className='font-semibold'>#{initialValues.id}</p>
</div>
<div>
<span className='text-sm text-gray-600'>
Tanggal Recording
</span>
<p className='font-semibold'>
{formatDate(
initialValues.record_datetime || '',
'DD MMMM YYYY'
)}
</p>
</div>
<div>
<span className='text-sm text-gray-600'>Hari</span>
<p className='font-semibold'>Hari ke-{initialValues.day}</p>
</div>
<div>
<span className='text-sm text-gray-600'>Kategori</span>
<p className='font-semibold'>
<Badge
variant='soft'
color={
initialValues.project_flock_category === 'LAYING'
? 'info'
: 'warning'
}
size='sm'
>
{initialValues.project_flock_category}
</Badge>
</p>
</div>
</div>
</Card>
)}
{/* Body Weights Table */}
<Card
@@ -2014,7 +2086,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
option?.value || 0
);
}}
options={depletionProducts}
options={eggProducts}
placeholder='Pilih Kondisi Telur'
isLoading={isLoadingEggProducts}
isError={
@@ -2228,7 +2300,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
hasExceededStock || !formik.isValid || formik.isSubmitting
}
onClick={async () => {
const result = await formik.submitForm();
await formik.submitForm();
if (
formik.isValid &&
!formik.isSubmitting &&
+10
View File
@@ -1,4 +1,5 @@
import { BaseApproval, BaseMetadata, User } from '@/types/api/api-general';
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
export type ProductionMetrics = {
total_depletion_qty: number;
@@ -33,14 +34,18 @@ export type RecordingDepletion = {
recording_id: number;
product_warehouse_id: number;
qty: number;
product_warehouse: ProductWarehouse;
};
export type RecordingStock = {
id: number;
recording_id: number;
product_warehouse_id: number;
usage_amount?: number;
usage_qty: number;
qty: number;
pending_qty: number;
product_warehouse: ProductWarehouse;
};
export type RecordingEgg = {
@@ -49,6 +54,7 @@ export type RecordingEgg = {
product_warehouse_id: number;
qty: number;
created_by: User;
product_warehouse: ProductWarehouse;
};
export type GradingEgg = {
@@ -66,6 +72,10 @@ export type Recording = BaseMetadata &
egg_grading_status?: string | null;
egg_grading_pending_qty?: number | null;
egg_grading_completed_qty?: number | null;
body_weights?: RecordingBW[];
depletions?: RecordingDepletion[];
stocks?: RecordingStock[];
eggs?: RecordingEgg[];
recording_bws?: RecordingBW[];
recording_depletions?: RecordingDepletion[];
recording_stocks?: RecordingStock[];