mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 07:45:47 +00:00
refactor(FE-137): enhance RecordingForm validation to prevent duplicate project flock entries
This commit is contained in:
@@ -6,6 +6,91 @@ import {
|
|||||||
} from '@/types/api/production/recording';
|
} from '@/types/api/production/recording';
|
||||||
|
|
||||||
export const RecordingFormSchema = Yup.object({
|
export const RecordingFormSchema = Yup.object({
|
||||||
|
project_flock_kandang: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable(),
|
||||||
|
project_flock_kandang_id: Yup.number()
|
||||||
|
.default(0)
|
||||||
|
.typeError('Project Flock Kandang wajib diisi!')
|
||||||
|
.test(
|
||||||
|
'is-valid-project-flock-kandang',
|
||||||
|
'Project Flock Kandang wajib diisi!',
|
||||||
|
(value) => value !== undefined && value !== null && value > 0
|
||||||
|
)
|
||||||
|
.required('Project Flock Kandang wajib diisi!')
|
||||||
|
.test(
|
||||||
|
'not-already-recorded',
|
||||||
|
'Project Flock ini sudah direcord hari ini!',
|
||||||
|
function(value) {
|
||||||
|
const recordedProjectFlockIds = this.options.context?.recordedProjectFlockIds as Set<number>;
|
||||||
|
const formType = this.options.context?.type as 'add' | 'edit' | 'detail';
|
||||||
|
if (formType !== 'add') return true;
|
||||||
|
if (value && recordedProjectFlockIds?.has(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
body_weights: Yup.array()
|
||||||
|
.of(
|
||||||
|
Yup.object({
|
||||||
|
weight: Yup.number()
|
||||||
|
.required('Berat ayam wajib diisi!')
|
||||||
|
.min(1, 'Berat ayam minimal 1 gram!')
|
||||||
|
.typeError('Berat ayam harus berupa angka!'),
|
||||||
|
qty: Yup.number()
|
||||||
|
.required('Jumlah ayam wajib diisi!')
|
||||||
|
.min(1, 'Jumlah ayam minimal 1 ekor!')
|
||||||
|
.typeError('Jumlah ayam harus berupa angka!')
|
||||||
|
.default(1),
|
||||||
|
average_weight: Yup.number()
|
||||||
|
.optional()
|
||||||
|
.min(0, 'Rata-rata berat tidak boleh negatif!')
|
||||||
|
.typeError('Rata-rata berat harus berupa angka!')
|
||||||
|
.default(0),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.min(1, 'Minimal harus ada 1 data bobot badan!')
|
||||||
|
.required('Data bobot badan wajib diisi!'),
|
||||||
|
stocks: Yup.array()
|
||||||
|
.of(
|
||||||
|
Yup.object({
|
||||||
|
product_warehouse_id: Yup.number()
|
||||||
|
.required('Produk wajib diisi!')
|
||||||
|
.min(1, 'Produk wajib diisi!')
|
||||||
|
.typeError('Produk harus berupa angka!'),
|
||||||
|
usage_amount: Yup.number()
|
||||||
|
.required('Jumlah penggunaan wajib diisi!')
|
||||||
|
.min(0, 'Jumlah penggunaan tidak boleh negatif!')
|
||||||
|
.typeError('Jumlah penggunaan harus berupa angka!'),
|
||||||
|
notes: Yup.string().optional(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.min(1, 'Minimal harus ada 1 data stok!')
|
||||||
|
.required('Data stok wajib diisi!'),
|
||||||
|
depletions: Yup.array()
|
||||||
|
.of(
|
||||||
|
Yup.object({
|
||||||
|
total: Yup.number()
|
||||||
|
.required('Jumlah depletions wajib diisi!')
|
||||||
|
.min(1, 'Jumlah depletions minimal 1!')
|
||||||
|
.typeError('Jumlah depletions harus berupa angka!'),
|
||||||
|
notes: Yup.string()
|
||||||
|
.required('Kondisi depletions wajib diisi!')
|
||||||
|
.oneOf(
|
||||||
|
RECORDING_FLAG_OPTIONS.map((option) => option.value),
|
||||||
|
'Kondisi depletions tidak valid!'
|
||||||
|
)
|
||||||
|
.typeError('Kondisi depletions harus berupa teks!')
|
||||||
|
.min(1, 'Kondisi depletions wajib diisi!'),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.min(1, 'Minimal harus ada 1 data depletions!')
|
||||||
|
.required('Data depletions wajib diisi!'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateRecordingFormSchema = Yup.object({
|
||||||
project_flock_kandang: Yup.object({
|
project_flock_kandang: Yup.object({
|
||||||
value: Yup.number().min(1).required(),
|
value: Yup.number().min(1).required(),
|
||||||
label: Yup.string().required(),
|
label: Yup.string().required(),
|
||||||
@@ -77,8 +162,6 @@ export const RecordingFormSchema = Yup.object({
|
|||||||
.required('Data depletions wajib diisi!'),
|
.required('Data depletions wajib diisi!'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateRecordingFormSchema = RecordingFormSchema;
|
|
||||||
|
|
||||||
export type RecordingFormValues = Yup.InferType<typeof RecordingFormSchema>;
|
export type RecordingFormValues = Yup.InferType<typeof RecordingFormSchema>;
|
||||||
|
|
||||||
type RecordingFormData = Partial<Recording> & {
|
type RecordingFormData = Partial<Recording> & {
|
||||||
|
|||||||
@@ -86,6 +86,21 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
return `${ProductWarehouseApi.basePath}?${params.toString()}`;
|
return `${ProductWarehouseApi.basePath}?${params.toString()}`;
|
||||||
}, [selectedLocation]);
|
}, [selectedLocation]);
|
||||||
|
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
const existingRecordingsUrl = useMemo(() => {
|
||||||
|
return `${RecordingApi.basePath}?record_date=${today}`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { data: existingRecordings } = useSWR(
|
||||||
|
existingRecordingsUrl,
|
||||||
|
RecordingApi.getAllFetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
const recordedProjectFlockIds = useMemo(() => {
|
||||||
|
if (!isResponseSuccess(existingRecordings)) return new Set<number>();
|
||||||
|
return new Set(existingRecordings?.data.map(rec => rec.project_flock_kandang_id) || []);
|
||||||
|
}, [existingRecordings]);
|
||||||
|
|
||||||
const { data: stockProducts, isLoading: isLoadingStockProducts } = useSWR(
|
const { data: stockProducts, isLoading: isLoadingStockProducts } = useSWR(
|
||||||
stockProductsUrl,
|
stockProductsUrl,
|
||||||
ProductWarehouseApi.getAllFetcher
|
ProductWarehouseApi.getAllFetcher
|
||||||
@@ -106,14 +121,19 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
const options: OptionType[] = [];
|
const options: OptionType[] = [];
|
||||||
projectFlocks?.data.forEach((projectFlock) => {
|
projectFlocks?.data.forEach((projectFlock) => {
|
||||||
projectFlock.kandangs.forEach((kandang) => {
|
projectFlock.kandangs.forEach((kandang) => {
|
||||||
|
const isAlreadyRecorded = recordedProjectFlockIds.has(projectFlock.id);
|
||||||
|
const label = isAlreadyRecorded
|
||||||
|
? `${projectFlock.flock.name} - ${projectFlock.area.name} - ${kandang.name} (Sudah Direcord)`
|
||||||
|
: `${projectFlock.flock.name} - ${projectFlock.area.name} - ${kandang.name}`;
|
||||||
|
|
||||||
options.push({
|
options.push({
|
||||||
value: projectFlock.id,
|
value: projectFlock.id,
|
||||||
label: `${projectFlock.flock.name} - ${projectFlock.area.name} - ${kandang.name}`,
|
label: label,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return options;
|
return options;
|
||||||
}, [projectFlocks]);
|
}, [projectFlocks, recordedProjectFlockIds, type]);
|
||||||
|
|
||||||
const unifiedStockProducts = useMemo(() => {
|
const unifiedStockProducts = useMemo(() => {
|
||||||
const options: OptionType[] = [];
|
const options: OptionType[] = [];
|
||||||
@@ -359,6 +379,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const projectFlockKandangChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const projectFlockKandangChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
if (type === 'add' && val && recordedProjectFlockIds.has((val as OptionType).value as number)) {
|
||||||
|
toast.error('Project Flock ini sudah direcord hari ini!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
formik.setFieldTouched('project_flock_kandang', true);
|
formik.setFieldTouched('project_flock_kandang', true);
|
||||||
formik.setFieldValue('project_flock_kandang', val);
|
formik.setFieldValue('project_flock_kandang', val);
|
||||||
formik.setFieldTouched('project_flock_kandang_id', true);
|
formik.setFieldTouched('project_flock_kandang_id', true);
|
||||||
@@ -665,28 +690,30 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
isSearchable
|
isSearchable
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectInput
|
<div>
|
||||||
required
|
<SelectInput
|
||||||
label='Project Flock'
|
required
|
||||||
value={formik.values.project_flock_kandang ?? undefined}
|
label='Project Flock'
|
||||||
onChange={projectFlockKandangChangeHandler}
|
value={formik.values.project_flock_kandang ?? undefined}
|
||||||
options={projectFlockKandangOptions}
|
onChange={projectFlockKandangChangeHandler}
|
||||||
onInputChange={setProjectFlockSearchValue}
|
options={projectFlockKandangOptions}
|
||||||
isLoading={isLoadingProjectFlocks}
|
onInputChange={setProjectFlockSearchValue}
|
||||||
isError={
|
isLoading={isLoadingProjectFlocks}
|
||||||
formik.touched.project_flock_kandang_id &&
|
isError={
|
||||||
Boolean(formik.errors.project_flock_kandang_id)
|
formik.touched.project_flock_kandang_id &&
|
||||||
}
|
Boolean(formik.errors.project_flock_kandang_id)
|
||||||
errorMessage={formik.errors.project_flock_kandang_id as string}
|
}
|
||||||
isDisabled={type === 'detail' || !selectedLocation}
|
errorMessage={formik.errors.project_flock_kandang_id as string}
|
||||||
placeholder={
|
isDisabled={type === 'detail' || !selectedLocation}
|
||||||
selectedLocation
|
placeholder={
|
||||||
? 'Pilih Project Flock - Kandang'
|
selectedLocation
|
||||||
: 'Pilih Lokasi terlebih dahulu'
|
? 'Pilih Project Flock - Kandang'
|
||||||
}
|
: 'Pilih Lokasi terlebih dahulu'
|
||||||
isClearable
|
}
|
||||||
isSearchable
|
isClearable
|
||||||
/>
|
isSearchable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -1044,38 +1071,38 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
isDisabled={type === 'detail'}
|
isDisabled={type === 'detail'}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
name={`stocks.${idx}.usage_amount`}
|
name={`stocks.${idx}.usage_amount`}
|
||||||
value={stock.usage_amount}
|
value={stock.usage_amount}
|
||||||
onChange={handleStockUsageAmountChangeWrapper(idx)}
|
onChange={handleStockUsageAmountChangeWrapper(idx)}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
decimalScale={0}
|
decimalScale={0}
|
||||||
allowNegative={false}
|
allowNegative={false}
|
||||||
thousandSeparator=','
|
thousandSeparator=','
|
||||||
decimalSeparator='.'
|
decimalSeparator='.'
|
||||||
isError={
|
isError={
|
||||||
isRepeaterInputError('stocks', 'usage_amount', idx)
|
isRepeaterInputError('stocks', 'usage_amount', idx)
|
||||||
.isError || Boolean(getStockUsageError(idx))
|
.isError || Boolean(getStockUsageError(idx))
|
||||||
}
|
}
|
||||||
errorMessage={
|
errorMessage={
|
||||||
isRepeaterInputError('stocks', 'usage_amount', idx)
|
isRepeaterInputError('stocks', 'usage_amount', idx)
|
||||||
.errorMessage ||
|
.errorMessage ||
|
||||||
getStockUsageError(idx) ||
|
getStockUsageError(idx) ||
|
||||||
undefined
|
undefined
|
||||||
}
|
}
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full min-w-24',
|
wrapper: 'w-full min-w-24',
|
||||||
}}
|
}}
|
||||||
placeholder="Jumlah Pakai"
|
placeholder="Jumlah Pakai"
|
||||||
/>
|
/>
|
||||||
{type !== 'detail' && getStockUsageAdornment(idx)}
|
{type !== 'detail' && getStockUsageAdornment(idx)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
{type !== 'detail' && (
|
{type !== 'detail' && (
|
||||||
<td>
|
<td>
|
||||||
<div className='flex justify-center'>
|
<div className='flex justify-center'>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
Reference in New Issue
Block a user