mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat(FE-170,174,175): enhance RecordingForm with product warehouse integration and improve data handling
This commit is contained in:
@@ -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
@@ -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[];
|
||||
|
||||
Reference in New Issue
Block a user