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> & { type RecordingFormData = Partial<Recording> & {
body_weights?: CreateGrowingRecordingPayload['body_weights']; body_weights?: CreateGrowingRecordingPayload['body_weights'];
stocks?: CreateGrowingRecordingPayload['stocks']; stocks?: CreateGrowingRecordingPayload['stocks'] | Recording['stocks'];
depletions?: CreateGrowingRecordingPayload['depletions']; depletions?: CreateGrowingRecordingPayload['depletions'] | Recording['depletions'];
eggs?: CreateLayingRecordingPayload['eggs']; eggs?: CreateLayingRecordingPayload['eggs'] | Recording['eggs'];
project_flock_kandang_id?: number;
project_flock_category?: string;
}; };
export const getRecordingGrowingFormInitialValues = ( export const getRecordingGrowingFormInitialValues = (
@@ -265,17 +267,15 @@ export const getRecordingGrowingFormInitialValues = (
total_weight: 0, total_weight: 0,
}, },
], ],
stocks: initialValues?.stocks?.map( stocks: initialValues?.stocks?.map((stock) => ({
(stock: NonNullable<CreateGrowingRecordingPayload['stocks']>[0]) => ({
product_warehouse_id: stock.product_warehouse_id, product_warehouse_id: stock.product_warehouse_id,
qty: stock.qty, qty: (stock as { qty?: number; usage_amount?: number }).qty || (stock as { qty?: number; usage_amount?: number }).usage_amount || '',
}) })) ?? [
) ?? [ {
{ product_warehouse_id: 0,
product_warehouse_id: 0, qty: '',
qty: '', },
}, ],
],
depletions: initialValues?.depletions?.map( depletions: initialValues?.depletions?.map(
( (
depletion: NonNullable<CreateGrowingRecordingPayload['depletions']>[0] depletion: NonNullable<CreateGrowingRecordingPayload['depletions']>[0]
@@ -34,7 +34,7 @@ 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 { ProductWarehouseApi } from '@/services/api/inventory';
import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; 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 { ProjectFlockKandangLookup } from '@/types/api/production/project-flock';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import toast from 'react-hot-toast'; 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 today = new Date().toISOString().split('T')[0];
const existingRecordingsUrl = useMemo(() => { const existingRecordingsUrl = useMemo(() => {
return `${RecordingApi.basePath}?record_date=${today}`; return `${RecordingApi.basePath}?record_date=${today}`;
}, []); }, [today]);
const { data: existingRecordings } = useSWR( const { data: existingRecordings } = useSWR(
existingRecordingsUrl, existingRecordingsUrl,
@@ -310,30 +310,15 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
initialValues.stocks && initialValues.stocks &&
type !== 'add' type !== 'add'
) { ) {
const initialValuesWithStocks = initialValues as Recording & { initialValues.stocks?.forEach((stock) => {
stocks?: Array<{ if (stock.product_warehouse && stock.product_warehouse.product) {
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) {
const existingOption = options.find( const existingOption = options.find(
(opt) => opt.value === stock.product_warehouse_id (opt) => opt.value === stock.product_warehouse_id
); );
if (!existingOption) { if (!existingOption) {
options.push({ options.push({
value: stock.product_warehouse_id, 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; return options;
}, [depletionProductsData]); }, [depletionProductsData, initialValues, type]);
const eggProducts = useMemo(() => { const eggProducts = useMemo(() => {
const options: OptionType[] = []; 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; return options;
}, [eggProductsData]); }, [eggProductsData, initialValues, type]);
const isLayingCategory = const isLayingCategory =
initialValues?.project_flock_category === 'LAYING' ||
projectFlockKandangLookup?.project_flock?.category === 'LAYING'; projectFlockKandangLookup?.project_flock?.category === 'LAYING';
const formikInitialValues = useMemo(() => { const formikInitialValues = useMemo(() => {
@@ -824,7 +845,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
formik.setFieldValue(`body_weights.${idx}.avg_weight`, 0); formik.setFieldValue(`body_weights.${idx}.avg_weight`, 0);
} }
// Update total_weight
formik.setFieldValue(`body_weights.${idx}.total_weight`, totalWeight); formik.setFieldValue(`body_weights.${idx}.total_weight`, totalWeight);
} }
}; };
@@ -844,7 +864,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
if (qty > 0 && value > 0) { if (qty > 0 && value > 0) {
const totalWeight = value * qty; const totalWeight = value * qty;
formik.setFieldValue(`body_weights.${idx}.weight`, totalWeight); formik.setFieldValue(`body_weights.${idx}.weight`, totalWeight);
// Update total_weight
formik.setFieldValue(`body_weights.${idx}.total_weight`, totalWeight); formik.setFieldValue(`body_weights.${idx}.total_weight`, totalWeight);
} else { } else {
formik.setFieldValue(`body_weights.${idx}.weight`, 0); 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); formik.setFieldValue(`body_weights.${idx}.avg_weight`, 0);
} }
// Update total_weight
formik.setFieldValue(`body_weights.${idx}.total_weight`, totalWeight); formik.setFieldValue(`body_weights.${idx}.total_weight`, totalWeight);
} }
}; };
@@ -1045,6 +1063,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
} }
}, [isLayingCategory, type]); }, [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(() => { useEffect(() => {
if (formik.values.body_weights && editingAverageIndex === null) { if (formik.values.body_weights && editingAverageIndex === null) {
const updatedBodyWeights = formik.values.body_weights.map( const updatedBodyWeights = formik.values.body_weights.map(
@@ -1074,12 +1100,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
formik.setFieldValue('body_weights', updatedBodyWeights, false); formik.setFieldValue('body_weights', updatedBodyWeights, false);
} }
} }
}, [ }, [bodyWeightValues, editingAverageIndex, manuallyEditedRows]);
formik.values.body_weights?.map((w) => w.weight),
formik.values.body_weights?.map((w) => w.qty),
editingAverageIndex,
manuallyEditedRows,
]);
return ( return (
<> <>
@@ -1172,73 +1193,124 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
className='w-full mt-8 flex flex-col gap-6' className='w-full mt-8 flex flex-col gap-6'
> >
{/* Basic Info Card */} {/* Basic Info Card */}
<Card {(type === 'add' || type === 'edit') && (
title='Informasi Recording' <Card
className={{ title='Informasi Recording'
wrapper: 'w-full mb-4 shadow', className={{
body: 'flex flex-col gap-6', wrapper: 'w-full mb-4 shadow',
}} body: 'flex flex-col gap-6',
> }}
<div className={'grid grid-cols-3 gap-4'}> >
<> <div className={'grid grid-cols-3 gap-4'}>
<SelectInput <>
key={`location-select-${selectedLocation?.value || 'default'}`} <SelectInput
required key={`location-select-${selectedLocation?.value || 'default'}`}
label='Lokasi' required
value={selectedLocation} label='Lokasi'
onChange={locationChangeHandler} value={selectedLocation}
options={locationOptions} onChange={locationChangeHandler}
onInputChange={setLocationSearchValue} options={locationOptions}
isLoading={isLoadingLocations} onInputChange={setLocationSearchValue}
placeholder='Pilih Lokasi' isLoading={isLoadingLocations}
isClearable placeholder='Pilih Lokasi'
isSearchable isClearable
/> isSearchable
/>
<SelectInput <SelectInput
key={`project-flock-select-${selectedProjectFlock?.value || 'default'}`} key={`project-flock-select-${selectedProjectFlock?.value || 'default'}`}
required required
label='Project Flock' label='Project Flock'
value={selectedProjectFlock} value={selectedProjectFlock}
onChange={projectFlockChangeHandler} onChange={projectFlockChangeHandler}
options={projectFlockOptions} options={projectFlockOptions}
onInputChange={setProjectFlockSearchValue} onInputChange={setProjectFlockSearchValue}
isLoading={isLoadingProjectFlocks} isLoading={isLoadingProjectFlocks}
isDisabled={!selectedLocation} isDisabled={!selectedLocation}
placeholder={ placeholder={
selectedLocation selectedLocation
? 'Pilih Project Flock' ? 'Pilih Project Flock'
: 'Pilih Lokasi terlebih dahulu' : 'Pilih Lokasi terlebih dahulu'
} }
isClearable isClearable
isSearchable isSearchable
/> />
<SelectInput <SelectInput
key={`kandang-select-${projectFlockKandangLookup?.project_flock_kandang_id || 'default'}`} key={`kandang-select-${projectFlockKandangLookup?.project_flock_kandang_id || 'default'}`}
required required
label='Kandang' label='Kandang'
value={selectedKandang} value={selectedKandang}
onChange={kandangChangeHandler} onChange={kandangChangeHandler}
options={kandangOptions} options={kandangOptions}
isLoading={false} isLoading={false}
isDisabled={!selectedProjectFlock} isDisabled={!selectedProjectFlock}
placeholder={ placeholder={
selectedProjectFlock selectedProjectFlock
? 'Pilih Kandang' ? 'Pilih Kandang'
: 'Pilih Project Flock terlebih dahulu' : 'Pilih Project Flock terlebih dahulu'
} }
isClearable isClearable
isSearchable={false} isSearchable={false}
startAdornment={ startAdornment={
projectFlockKandangLookup projectFlockKandangLookup
? getProjectFlockBadgeAdornment() ? getProjectFlockBadgeAdornment()
: undefined : undefined
} }
/> />
</> </>
</div> </div>
</Card> </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 */} {/* Body Weights Table */}
<Card <Card
@@ -2014,7 +2086,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
option?.value || 0 option?.value || 0
); );
}} }}
options={depletionProducts} options={eggProducts}
placeholder='Pilih Kondisi Telur' placeholder='Pilih Kondisi Telur'
isLoading={isLoadingEggProducts} isLoading={isLoadingEggProducts}
isError={ isError={
@@ -2228,7 +2300,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
hasExceededStock || !formik.isValid || formik.isSubmitting hasExceededStock || !formik.isValid || formik.isSubmitting
} }
onClick={async () => { onClick={async () => {
const result = await formik.submitForm(); await formik.submitForm();
if ( if (
formik.isValid && formik.isValid &&
!formik.isSubmitting && !formik.isSubmitting &&
+10
View File
@@ -1,4 +1,5 @@
import { BaseApproval, BaseMetadata, User } from '@/types/api/api-general'; import { BaseApproval, BaseMetadata, User } from '@/types/api/api-general';
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
export type ProductionMetrics = { export type ProductionMetrics = {
total_depletion_qty: number; total_depletion_qty: number;
@@ -33,14 +34,18 @@ export type RecordingDepletion = {
recording_id: number; recording_id: number;
product_warehouse_id: number; product_warehouse_id: number;
qty: number; qty: number;
product_warehouse: ProductWarehouse;
}; };
export type RecordingStock = { export type RecordingStock = {
id: number; id: number;
recording_id: number; recording_id: number;
product_warehouse_id: number; product_warehouse_id: number;
usage_amount?: number;
usage_qty: number; usage_qty: number;
qty: number;
pending_qty: number; pending_qty: number;
product_warehouse: ProductWarehouse;
}; };
export type RecordingEgg = { export type RecordingEgg = {
@@ -49,6 +54,7 @@ export type RecordingEgg = {
product_warehouse_id: number; product_warehouse_id: number;
qty: number; qty: number;
created_by: User; created_by: User;
product_warehouse: ProductWarehouse;
}; };
export type GradingEgg = { export type GradingEgg = {
@@ -66,6 +72,10 @@ export type Recording = BaseMetadata &
egg_grading_status?: string | null; egg_grading_status?: string | null;
egg_grading_pending_qty?: number | null; egg_grading_pending_qty?: number | null;
egg_grading_completed_qty?: number | null; egg_grading_completed_qty?: number | null;
body_weights?: RecordingBW[];
depletions?: RecordingDepletion[];
stocks?: RecordingStock[];
eggs?: RecordingEgg[];
recording_bws?: RecordingBW[]; recording_bws?: RecordingBW[];
recording_depletions?: RecordingDepletion[]; recording_depletions?: RecordingDepletion[];
recording_stocks?: RecordingStock[]; recording_stocks?: RecordingStock[];