refactor(US-170,174): update recording types and validation schema for daily recording form

This commit is contained in:
rstubryan
2025-10-30 18:10:37 +07:00
parent ae0cca778e
commit 4cb045de6c
3 changed files with 251 additions and 419 deletions
@@ -1,16 +1,15 @@
import * as Yup from 'yup'; import * as Yup from 'yup';
import { RECORDING_FLAG_OPTIONS } from '@/config/constant';
import { import {
Recording, Recording,
CreateRecordingPayload, CreateGrowingRecordingPayload,
} from '@/types/api/production/recording'; } from '@/types/api/production/recording';
export const RecordingFormSchema = Yup.object({ export const RecordingGrowingFormSchema = 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(),
}).nullable(), }).nullable(),
project_flock_kandang_id: Yup.number() project_flock_kandangs_id: Yup.number()
.default(0) .default(0)
.typeError('Project Flock Kandang wajib diisi!') .typeError('Project Flock Kandang wajib diisi!')
.test( .test(
@@ -23,8 +22,12 @@ export const RecordingFormSchema = Yup.object({
'not-already-recorded', 'not-already-recorded',
'Project Flock ini sudah direcord hari ini!', 'Project Flock ini sudah direcord hari ini!',
function (value) { function (value) {
const recordedProjectFlockIds = this.options.context?.recordedProjectFlockIds as Set<number>; const recordedProjectFlockIds = this.options.context
const formType = this.options.context?.type as 'add' | 'edit' | 'detail'; ?.recordedProjectFlockIds as Set<number>;
const formType = this.options.context?.type as
| 'add'
| 'edit'
| 'detail';
if (formType !== 'add') return true; if (formType !== 'add') return true;
if (value && recordedProjectFlockIds?.has(value)) { if (value && recordedProjectFlockIds?.has(value)) {
return false; return false;
@@ -35,20 +38,15 @@ export const RecordingFormSchema = Yup.object({
body_weights: Yup.array() body_weights: Yup.array()
.of( .of(
Yup.object({ Yup.object({
weight: Yup.number() avg_weight: Yup.number()
.required('Berat ayam wajib diisi!') .required('Berat ayam rata-rata wajib diisi!')
.min(1, 'Berat ayam minimal 1 gram!') .min(1, 'Berat ayam rata-rata minimal 1 gram!')
.typeError('Berat ayam harus berupa angka!'), .typeError('Berat ayam rata-rata harus berupa angka!'),
qty: Yup.number() qty: Yup.number()
.required('Jumlah ayam wajib diisi!') .required('Jumlah ayam wajib diisi!')
.min(1, 'Jumlah ayam minimal 1 ekor!') .min(1, 'Jumlah ayam minimal 1 ekor!')
.typeError('Jumlah ayam harus berupa angka!') .typeError('Jumlah ayam harus berupa angka!')
.default(1), .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!') .min(1, 'Minimal harus ada 1 data bobot badan!')
@@ -60,11 +58,10 @@ export const RecordingFormSchema = Yup.object({
.required('Produk wajib diisi!') .required('Produk wajib diisi!')
.min(1, 'Produk wajib diisi!') .min(1, 'Produk wajib diisi!')
.typeError('Produk harus berupa angka!'), .typeError('Produk harus berupa angka!'),
usage_amount: Yup.number() usage_qty: Yup.number()
.required('Jumlah penggunaan wajib diisi!') .required('Jumlah penggunaan wajib diisi!')
.min(0, 'Jumlah penggunaan tidak boleh negatif!') .min(0, 'Jumlah penggunaan tidak boleh negatif!')
.typeError('Jumlah penggunaan harus berupa angka!'), .typeError('Jumlah penggunaan harus berupa angka!'),
notes: Yup.string().optional(),
}) })
) )
.min(1, 'Minimal harus ada 1 data stok!') .min(1, 'Minimal harus ada 1 data stok!')
@@ -72,30 +69,23 @@ export const RecordingFormSchema = Yup.object({
depletions: Yup.array() depletions: Yup.array()
.of( .of(
Yup.object({ Yup.object({
total: Yup.number() product_warehouse_id: Yup.number()
.required('Produk depletions wajib diisi!')
.min(1, 'Produk depletions wajib diisi!')
.typeError('Produk depletions harus berupa angka!'),
qty: Yup.number()
.required('Jumlah depletions wajib diisi!') .required('Jumlah depletions wajib diisi!')
.min(1, 'Jumlah depletions minimal 1!') .min(1, 'Jumlah depletions minimal 1!')
.typeError('Jumlah depletions harus berupa angka!'), .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!') .min(1, 'Minimal harus ada 1 data depletions!')
.required('Data depletions wajib diisi!'), .required('Data depletions wajib diisi!'),
}); });
export const UpdateRecordingFormSchema = Yup.object({ export const UpdateRecordingGrowingFormSchema =
project_flock_kandang: Yup.object({ RecordingGrowingFormSchema.shape({
value: Yup.number().min(1).required(), project_flock_kandangs_id: Yup.number()
label: Yup.string().required(),
}).nullable(),
project_flock_kandang_id: Yup.number()
.default(0) .default(0)
.typeError('Project Flock Kandang wajib diisi!') .typeError('Project Flock Kandang wajib diisi!')
.test( .test(
@@ -104,119 +94,61 @@ export const UpdateRecordingFormSchema = Yup.object({
(value) => value !== undefined && value !== null && value > 0 (value) => value !== undefined && value !== null && value > 0
) )
.required('Project Flock Kandang wajib diisi!'), .required('Project Flock Kandang wajib diisi!'),
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 type RecordingFormValues = Yup.InferType<typeof RecordingFormSchema>; export type RecordingGrowingFormValues = Yup.InferType<
typeof RecordingGrowingFormSchema
>;
type RecordingFormData = Partial<Recording> & { type RecordingFormData = Partial<Recording> & {
body_weights?: CreateRecordingPayload['body_weights']; body_weights?: CreateGrowingRecordingPayload['body_weights'];
stocks?: CreateRecordingPayload['stocks']; stocks?: CreateGrowingRecordingPayload['stocks'];
depletions?: CreateRecordingPayload['depletions']; depletions?: CreateGrowingRecordingPayload['depletions'];
}; };
export const getRecordingFormInitialValues = ( export const getRecordingGrowingFormInitialValues = (
initialValues?: RecordingFormData initialValues?: RecordingFormData
): RecordingFormValues => ({ ): RecordingGrowingFormValues => ({
project_flock_kandang: initialValues?.project_flock_kandang_id project_flock_kandang: initialValues?.project_flock_kandangs_id
? { ? {
value: initialValues.project_flock_kandang_id, value: initialValues.project_flock_kandangs_id,
label: `Project Flock #${initialValues.project_flock_kandang_id}`, label: `Project Flock #${initialValues.project_flock_kandangs_id}`,
} }
: null, : null,
project_flock_kandang_id: initialValues?.project_flock_kandang_id ?? 0, project_flock_kandangs_id: initialValues?.project_flock_kandangs_id ?? 0,
body_weights: initialValues?.body_weights?.map( body_weights: initialValues?.body_weights?.map(
(bw: NonNullable<CreateRecordingPayload['body_weights']>[0]) => ({ (bw: NonNullable<CreateGrowingRecordingPayload['body_weights']>[0]) => ({
weight: bw.weight, avg_weight: bw.avg_weight,
qty: bw.qty, qty: bw.qty,
average_weight: bw.qty > 0 ? Math.round(bw.weight / bw.qty) : 0,
}) })
) ?? [ ) ?? [
{ {
weight: 0, avg_weight: 0,
qty: 0, qty: 0,
average_weight: 0,
}, },
], ],
stocks: initialValues?.stocks?.map( stocks: initialValues?.stocks?.map(
(stock: NonNullable<CreateRecordingPayload['stocks']>[0]) => ({ (stock: NonNullable<CreateGrowingRecordingPayload['stocks']>[0]) => ({
product_warehouse_id: stock.product_warehouse_id, product_warehouse_id: stock.product_warehouse_id,
usage_amount: stock.usage_amount, usage_qty: stock.usage_qty,
notes: stock.notes,
}) })
) ?? [ ) ?? [
{ {
product_warehouse_id: 0, product_warehouse_id: 0,
usage_amount: 0, usage_qty: 0,
notes: '',
}, },
], ],
depletions: initialValues?.depletions?.map( depletions: initialValues?.depletions?.map(
(depletion: NonNullable<CreateRecordingPayload['depletions']>[0]) => ({ (
depletion: NonNullable<CreateGrowingRecordingPayload['depletions']>[0]
) => ({
product_warehouse_id: depletion.product_warehouse_id, product_warehouse_id: depletion.product_warehouse_id,
total: depletion.total, qty: depletion.qty,
notes: depletion.notes,
}) })
) ?? [ ) ?? [
{ {
product_warehouse_id: 0, product_warehouse_id: 0,
total: 0, qty: 0,
notes: '',
}, },
], ],
}); });
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useMemo, useState, useEffect, useCallback } from 'react'; import { useMemo, useState, useCallback } from 'react';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import useSWR from 'swr'; import useSWR from 'swr';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
@@ -14,22 +14,21 @@ import { FormHeader } from '@/components/helper/form/FormHeader';
import { FormActions } from '@/components/helper/form/FormActions'; import { FormActions } from '@/components/helper/form/FormActions';
import { RecordingApi } from '@/services/api/production'; import { RecordingApi } from '@/services/api/production';
import { import {
CreateRecordingPayload, CreateGrowingRecordingPayload,
Recording, Recording,
} from '@/types/api/production/recording'; } from '@/types/api/production/recording';
import { type BaseApiResponse } from '@/types/api/api-general'; import { type BaseApiResponse } from '@/types/api/api-general';
import { import {
RecordingFormSchema, RecordingGrowingFormSchema,
RecordingFormValues, RecordingGrowingFormValues,
getRecordingFormInitialValues, getRecordingGrowingFormInitialValues,
UpdateRecordingFormSchema, UpdateRecordingGrowingFormSchema,
} from './RecordingForm.schema'; } from './RecordingForm.schema';
import { useRecordingFormHandlers } from './useRecordingFormHandlers'; import { useRecordingFormHandlers } from './useRecordingFormHandlers';
import { ProjectFlockApi } from '@/services/api/production'; 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 } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { RECORDING_FLAG_OPTIONS } from '@/config/constant';
import { PeriodFlock } from '@/types/api/production/project-flock'; import { PeriodFlock } 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';
@@ -47,13 +46,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const [selectedStocks, setSelectedStocks] = useState<number[]>([]); const [selectedStocks, setSelectedStocks] = useState<number[]>([]);
const [selectedDepletions, setSelectedDepletions] = useState<number[]>([]); const [selectedDepletions, setSelectedDepletions] = useState<number[]>([]);
const [editingAverageIndex, setEditingAverageIndex] = useState<number | null>(
null
);
const [manuallyEditedRows, setManuallyEditedRows] = useState<Set<number>>(
new Set()
);
const [locationSearchValue, setLocationSearchValue] = useState(''); const [locationSearchValue, setLocationSearchValue] = useState('');
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>( const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
null null
@@ -132,7 +124,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const recordedIds = new Set<number>(); const recordedIds = new Set<number>();
todayRecordings.forEach((recording) => { todayRecordings.forEach((recording) => {
const recordingDate = recording.record_date?.split('T')[0]; const recordingDate = recording.record_datetime?.split('T')[0];
const isRecordedToday = recordingDate === today; const isRecordedToday = recordingDate === today;
@@ -143,7 +135,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
isResponseSuccess(projectFlocks) isResponseSuccess(projectFlocks)
) { ) {
const flockIndex = projectFlocks.data.findIndex( const flockIndex = projectFlocks.data.findIndex(
(pf) => pf.id === recording.project_flock_kandang_id (pf) => pf.id === recording.project_flock_kandangs_id
); );
if ( if (
flockIndex !== undefined && flockIndex !== undefined &&
@@ -165,7 +157,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
} }
if (isRecordedToday && (isCorrectPeriod || !flockPeriodsData)) { if (isRecordedToday && (isCorrectPeriod || !flockPeriodsData)) {
recordedIds.add(recording.project_flock_kandang_id); recordedIds.add(recording.project_flock_kandangs_id);
} }
}); });
@@ -283,45 +275,36 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
confirmationModalDeleteClickHandler, confirmationModalDeleteClickHandler,
} = useRecordingFormHandlers(initialValues?.id); } = useRecordingFormHandlers(initialValues?.id);
const formikInitialValues = useMemo<RecordingFormValues>( const formikInitialValues = useMemo<RecordingGrowingFormValues>(
() => getRecordingFormInitialValues(initialValues), () => getRecordingGrowingFormInitialValues(initialValues),
[initialValues] [initialValues]
); );
const formik = useFormik<RecordingFormValues>({ const formik = useFormik<RecordingGrowingFormValues>({
initialValues: formikInitialValues, initialValues: formikInitialValues,
validationSchema: validationSchema:
type === 'edit' ? UpdateRecordingFormSchema : RecordingFormSchema, type === 'edit'
? UpdateRecordingGrowingFormSchema
: RecordingGrowingFormSchema,
validateOnChange: true, validateOnChange: true,
validateOnBlur: true, validateOnBlur: true,
onSubmit: async (values) => { onSubmit: async (values) => {
const payload: CreateRecordingPayload = { const payload: CreateGrowingRecordingPayload = {
project_flock_kandang_id: values.project_flock_kandang_id, project_flock_kandangs_id: values.project_flock_kandangs_id,
body_weights: (values.body_weights ?? []).map((bw) => ({ body_weights: (values.body_weights ?? []).map((bw) => ({
weight: avg_weight:
typeof bw.weight === 'number' typeof bw.avg_weight === 'number'
? bw.weight ? bw.avg_weight
: parseFloat(String(bw.weight)) || 0, : parseFloat(String(bw.avg_weight)) || 0,
qty: qty: bw.qty || 0,
typeof bw.qty === 'number'
? bw.qty
: parseFloat(String(bw.qty)) || 0,
})), })),
stocks: (values.stocks ?? []).map((stock) => ({ stocks: (values.stocks ?? []).map((stock) => ({
product_warehouse_id: stock.product_warehouse_id, product_warehouse_id: stock.product_warehouse_id,
usage_amount: usage_qty: stock.usage_qty || 0,
typeof stock.usage_amount === 'number'
? stock.usage_amount
: parseFloat(String(stock.usage_amount)) || 0,
notes: stock.notes || '',
})), })),
depletions: (values.depletions ?? []).map((depletion) => ({ depletions: (values.depletions ?? []).map((depletion) => ({
product_warehouse_id: 1, product_warehouse_id: depletion.product_warehouse_id,
total: qty: depletion.qty || 0,
typeof depletion.total === 'number'
? depletion.total
: parseFloat(String(depletion.total)) || 0,
notes: depletion.notes,
})), })),
}; };
@@ -375,7 +358,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const stock = formik.values.stocks?.[stockIdx]; const stock = formik.values.stocks?.[stockIdx];
if (!stock || !stock.product_warehouse_id) return null; if (!stock || !stock.product_warehouse_id) return null;
const availableStock = getAvailableStock(stock.product_warehouse_id); const availableStock = getAvailableStock(stock.product_warehouse_id);
const requestedUsage = Number(stock.usage_amount) || 0; const requestedUsage = Number(stock.usage_qty) || 0;
if (requestedUsage > availableStock) { if (requestedUsage > availableStock) {
return `Jumlah pakai melebihi stok tersedia! Maksimal: ${availableStock.toLocaleString('en-US')}`; return `Jumlah pakai melebihi stok tersedia! Maksimal: ${availableStock.toLocaleString('en-US')}`;
} }
@@ -390,7 +373,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const stock = formik.values.stocks?.[stockIdx]; const stock = formik.values.stocks?.[stockIdx];
if (!stock || !stock.product_warehouse_id) return null; if (!stock || !stock.product_warehouse_id) return null;
const availableStock = getAvailableStock(stock.product_warehouse_id); const availableStock = getAvailableStock(stock.product_warehouse_id);
const requestedUsage = Number(stock.usage_amount) || 0; const requestedUsage = Number(stock.usage_qty) || 0;
const remainingStock = availableStock - requestedUsage; const remainingStock = availableStock - requestedUsage;
if (requestedUsage > 0) { if (requestedUsage > 0) {
return ( return (
@@ -432,7 +415,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
color={color} color={color}
size='sm' size='sm'
className={{ className={{
badge: 'whitespace-nowrap font-semibold text-xs px-2 py-0.5', badge: 'whitespace-nowrap font-semibold text-xs px-2',
}} }}
> >
Periode {projectFlock.period} Periode {projectFlock.period}
@@ -461,7 +444,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
color='info' color='info'
size='sm' size='sm'
className={{ className={{
badge: 'whitespace-nowrap font-semibold text-xs px-2 py-0.5', badge: 'whitespace-nowrap font-semibold text-xs px-2',
}} }}
> >
PAKAN PAKAN
@@ -476,7 +459,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
color='secondary' color='secondary'
size='sm' size='sm'
className={{ className={{
badge: 'whitespace-nowrap font-semibold text-xs px-2 py-0.5', badge: 'whitespace-nowrap font-semibold text-xs px-2',
}} }}
> >
OVK OVK
@@ -503,11 +486,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
>( >(
arrayName: T, arrayName: T,
column: T extends 'body_weights' column: T extends 'body_weights'
? keyof RecordingFormValues['body_weights'][0] ? keyof RecordingGrowingFormValues['body_weights'][0]
: T extends 'stocks' : T extends 'stocks'
? keyof RecordingFormValues['stocks'][0] ? keyof RecordingGrowingFormValues['stocks'][0]
: T extends 'depletions' : T extends 'depletions'
? keyof RecordingFormValues['depletions'][0] ? keyof RecordingGrowingFormValues['depletions'][0]
: never, : never,
idx: number idx: number
) => { ) => {
@@ -540,7 +523,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const locationChangeHandler = (val: OptionType | OptionType[] | null) => { const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedLocation(val as OptionType); setSelectedLocation(val as OptionType);
formik.setFieldValue('project_flock_kandang', null); formik.setFieldValue('project_flock_kandang', null);
formik.setFieldValue('project_flock_kandang_id', 0); formik.setFieldValue('project_flock_kandangs_id', 0);
}; };
const projectFlockKandangChangeHandler = ( const projectFlockKandangChangeHandler = (
@@ -557,9 +540,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
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_kandangs_id', true);
formik.setFieldValue( formik.setFieldValue(
'project_flock_kandang_id', 'project_flock_kandangs_id',
(val as OptionType)?.value || 0 (val as OptionType)?.value || 0
); );
}; };
@@ -629,81 +612,25 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const newBodyWeights = [ const newBodyWeights = [
...(formik.values.body_weights || []), ...(formik.values.body_weights || []),
{ {
weight: 0, avg_weight: 0,
qty: 1, qty: 1,
average_weight: 0,
}, },
]; ];
formik.setFieldValue('body_weights', newBodyWeights); formik.setFieldValue('body_weights', newBodyWeights);
}; };
const handleWeightChange = (idx: number, value: number) => { const handleAvgWeightChange = (idx: number, value: number) => {
formik.setFieldValue(`body_weights.${idx}.weight`, value); formik.setFieldValue(`body_weights.${idx}.avg_weight`, value);
setManuallyEditedRows((prev) => {
const newSet = new Set(prev);
newSet.delete(idx);
return newSet;
});
const currentWeight = formik.values.body_weights?.[idx];
if (currentWeight) {
const qty = currentWeight.qty;
if (qty > 0 && value > 0) {
const averageWeight = parseFloat((value / qty).toFixed(2));
formik.setFieldValue(
`body_weights.${idx}.average_weight`,
averageWeight
);
} else {
formik.setFieldValue(`body_weights.${idx}.average_weight`, 0);
}
}
}; };
const handleQtyChange = (idx: number, value: number) => { const handleQtyChange = (idx: number, value: number) => {
formik.setFieldValue(`body_weights.${idx}.qty`, value); formik.setFieldValue(`body_weights.${idx}.qty`, value);
setManuallyEditedRows((prev) => {
const newSet = new Set(prev);
newSet.delete(idx);
return newSet;
});
const currentWeight = formik.values.body_weights?.[idx];
if (currentWeight) {
const weight = currentWeight.weight;
if (value > 0 && weight > 0) {
const averageWeight = parseFloat((weight / value).toFixed(2));
formik.setFieldValue(
`body_weights.${idx}.average_weight`,
averageWeight
);
} else {
formik.setFieldValue(`body_weights.${idx}.average_weight`, 0);
}
}
}; };
const handleAverageWeightChange = (idx: number, value: number) => { const handleAvgWeightChangeWrapper =
formik.setFieldValue(`body_weights.${idx}.average_weight`, value);
const currentWeight = formik.values.body_weights?.[idx];
if (currentWeight) {
const qty = currentWeight.qty;
if (qty > 0 && value > 0) {
const totalWeight = value * qty;
formik.setFieldValue(`body_weights.${idx}.weight`, totalWeight);
} else {
formik.setFieldValue(`body_weights.${idx}.weight`, 0);
}
}
};
const handleWeightChangeWrapper =
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => { (idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseFloat(e.target.value) || 0; const value = parseFloat(e.target.value) || 0;
handleWeightChange(idx, value); handleAvgWeightChange(idx, value);
}; };
const handleQtyChangeWrapper = const handleQtyChangeWrapper =
@@ -712,19 +639,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
handleQtyChange(idx, value); handleQtyChange(idx, value);
}; };
const handleAverageWeightChangeWrapper =
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
setEditingAverageIndex(idx);
setManuallyEditedRows((prev) => new Set(prev).add(idx));
const value = parseFloat(e.target.value) || 0;
handleAverageWeightChange(idx, value);
};
const handleAverageWeightBlur = (idx: number) => {
setEditingAverageIndex(null);
};
const removeBodyWeight = (idx: number) => { const removeBodyWeight = (idx: number) => {
const updatedBodyWeights = formik.values.body_weights?.filter( const updatedBodyWeights = formik.values.body_weights?.filter(
(_, i) => i !== idx (_, i) => i !== idx
@@ -746,17 +660,16 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
...(formik.values.stocks || []), ...(formik.values.stocks || []),
{ {
product_warehouse_id: 0, product_warehouse_id: 0,
usage_amount: 0, usage_qty: 0,
notes: '',
}, },
]; ];
formik.setFieldValue('stocks', newStocks); formik.setFieldValue('stocks', newStocks);
}; };
const handleStockUsageAmountChangeWrapper = useCallback( const handleStockUsageQtyChangeWrapper = useCallback(
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => { (idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseFloat(e.target.value) || 0; const value = parseFloat(e.target.value) || 0;
formik.setFieldValue(`stocks.${idx}.usage_amount`, value); formik.setFieldValue(`stocks.${idx}.usage_qty`, value);
}, },
[formik] [formik]
); );
@@ -779,17 +692,17 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const newDepletions = [ const newDepletions = [
...(formik.values.depletions || []), ...(formik.values.depletions || []),
{ {
total: 0, product_warehouse_id: 0,
notes: '', qty: 0,
}, },
]; ];
formik.setFieldValue('depletions', newDepletions); formik.setFieldValue('depletions', newDepletions);
}; };
const handleDepletionTotalChangeWrapper = useCallback( const handleDepletionQtyChangeWrapper = useCallback(
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => { (idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseFloat(e.target.value) || 0; const value = parseFloat(e.target.value) || 0;
formik.setFieldValue(`depletions.${idx}.total`, value); formik.setFieldValue(`depletions.${idx}.qty`, value);
}, },
[formik] [formik]
); );
@@ -809,41 +722,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
setSelectedDepletions([]); setSelectedDepletions([]);
}; };
// ===== EFFECTS =====
useEffect(() => {
if (formik.values.body_weights && editingAverageIndex === null) {
const updatedBodyWeights = formik.values.body_weights.map(
(weight, idx) => {
if (idx === editingAverageIndex || manuallyEditedRows.has(idx)) {
return weight;
}
return {
...weight,
average_weight:
weight.qty > 0 && weight.weight > 0
? parseFloat((weight.weight / weight.qty).toFixed(2))
: 0,
};
}
);
const hasChanges = updatedBodyWeights.some(
(updated, idx) =>
idx !== editingAverageIndex &&
!manuallyEditedRows.has(idx) &&
updated.average_weight !==
(formik.values.body_weights[idx]?.average_weight || 0)
);
if (hasChanges) {
formik.setFieldValue('body_weights', updatedBodyWeights, false);
}
}
}, [
formik.values.body_weights?.map((w) => w.weight),
formik.values.body_weights?.map((w) => w.qty),
editingAverageIndex,
manuallyEditedRows,
]);
return ( return (
<> <>
<section className='w-full'> <section className='w-full'>
@@ -889,6 +767,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<div> <div>
<SelectInput <SelectInput
required required
key={`project-flock-${formik.values.project_flock_kandangs_id}`}
label='Project Flock' label='Project Flock'
value={formik.values.project_flock_kandang ?? undefined} value={formik.values.project_flock_kandang ?? undefined}
onChange={projectFlockKandangChangeHandler} onChange={projectFlockKandangChangeHandler}
@@ -896,11 +775,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
onInputChange={setProjectFlockSearchValue} onInputChange={setProjectFlockSearchValue}
isLoading={isLoadingProjectFlocks} isLoading={isLoadingProjectFlocks}
isError={ isError={
formik.touched.project_flock_kandang_id && formik.touched.project_flock_kandangs_id &&
Boolean(formik.errors.project_flock_kandang_id) Boolean(formik.errors.project_flock_kandangs_id)
} }
errorMessage={ errorMessage={
formik.errors.project_flock_kandang_id as string formik.errors.project_flock_kandangs_id as string
} }
isDisabled={type === 'detail' || !selectedLocation} isDisabled={type === 'detail' || !selectedLocation}
placeholder={ placeholder={
@@ -911,9 +790,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
isClearable isClearable
isSearchable isSearchable
startAdornment={ startAdornment={
formik.values.project_flock_kandang_id formik.values.project_flock_kandangs_id
? getProjectFlockBadgeAdornment( ? getProjectFlockBadgeAdornment(
formik.values.project_flock_kandang_id formik.values.project_flock_kandangs_id
) )
: undefined : undefined
} }
@@ -964,7 +843,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</th> </th>
)} )}
<th> <th>
Berat Ayam (gram) Rata-rata Berat Ayam (gram)
<span <span
className='tooltip tooltip-error tooltip-bottom z-[9999]' className='tooltip tooltip-error tooltip-bottom z-[9999]'
data-tip='required' data-tip='required'
@@ -981,19 +860,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<span className='text-error'>*</span> <span className='text-error'>*</span>
</span> </span>
</th> </th>
<th>
Rata-rata Berat Ayam (gram)
<span
className='tooltip tooltip-info tooltip-bottom z-[9999]'
data-tip='Otomatis dihitung: Total Berat ÷ Jumlah Ayam'
>
<Icon
icon='material-symbols:info-outline'
width={16}
height={16}
/>
</span>
</th>
{type !== 'detail' && <th>Action</th>} {type !== 'detail' && <th>Action</th>}
</tr> </tr>
</thead> </thead>
@@ -1029,9 +895,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<td> <td>
<NumberInput <NumberInput
required required
name={`body_weights.${idx}.weight`} name={`body_weights.${idx}.avg_weight`}
value={bw.weight} value={bw.avg_weight}
onChange={handleWeightChangeWrapper(idx)} onChange={handleAvgWeightChangeWrapper(idx)}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
decimalScale={2} decimalScale={2}
allowNegative={false} allowNegative={false}
@@ -1039,12 +905,18 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
decimalSeparator='.' decimalSeparator='.'
inputSuffix='gram' inputSuffix='gram'
isError={ isError={
isRepeaterInputError('body_weights', 'weight', idx) isRepeaterInputError(
.isError 'body_weights',
'avg_weight',
idx
).isError
} }
errorMessage={ errorMessage={
isRepeaterInputError('body_weights', 'weight', idx) isRepeaterInputError(
.errorMessage 'body_weights',
'avg_weight',
idx
).errorMessage
} }
readOnly={type === 'detail'} readOnly={type === 'detail'}
className={{ className={{
@@ -1077,43 +949,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
}} }}
/> />
</td> </td>
<td>
<NumberInput
name={`body_weights.${idx}.average_weight`}
value={bw.average_weight || 0}
onChange={handleAverageWeightChangeWrapper(idx)}
onBlur={(e) => {
handleAverageWeightBlur(idx);
formik.handleBlur(e);
}}
decimalScale={2}
allowNegative={false}
thousandSeparator=','
decimalSeparator='.'
inputSuffix='gram'
isError={
isRepeaterInputError(
'body_weights',
'average_weight',
idx
).isError
}
errorMessage={
isRepeaterInputError(
'body_weights',
'average_weight',
idx
).errorMessage
}
readOnly={type === 'detail'}
className={{
wrapper: 'w-full min-w-32',
}}
/>
</td>
{type !== 'detail' && ( {type !== 'detail' && (
<td> <td>
<div className='flex justify-center'> <div className='flex items-center'>
<Button <Button
type='button' type='button'
color='error' color='error'
@@ -1249,6 +1087,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<td> <td>
<SelectInput <SelectInput
required required
key={`stock-product-${idx}-${stock.product_warehouse_id}`}
value={ value={
unifiedStockProducts.find( unifiedStockProducts.find(
(product) => (product) =>
@@ -1261,21 +1100,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
`stocks.${idx}.product_warehouse_id`, `stocks.${idx}.product_warehouse_id`,
option?.value || 0 option?.value || 0
); );
if (
option?.value &&
isResponseSuccess(stockProducts)
) {
const selectedProduct = stockProducts.data.find(
(product) => product.id === option.value
);
if (selectedProduct) {
formik.setFieldValue(
`stocks.${idx}.notes`,
selectedProduct.product.name
);
}
}
}} }}
options={unifiedStockProducts} options={unifiedStockProducts}
placeholder='Pilih Produk' placeholder='Pilih Produk'
@@ -1313,27 +1137,21 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<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_qty`}
value={stock.usage_amount} value={stock.usage_qty}
onChange={handleStockUsageAmountChangeWrapper(idx)} onChange={handleStockUsageQtyChangeWrapper(idx)}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
decimalScale={0} decimalScale={0}
allowNegative={false} allowNegative={false}
thousandSeparator=',' thousandSeparator=','
decimalSeparator='.' decimalSeparator='.'
isError={ isError={
isRepeaterInputError( isRepeaterInputError('stocks', 'usage_qty', idx)
'stocks', .isError || Boolean(getStockUsageError(idx))
'usage_amount',
idx
).isError || Boolean(getStockUsageError(idx))
} }
errorMessage={ errorMessage={
isRepeaterInputError( isRepeaterInputError('stocks', 'usage_qty', idx)
'stocks', .errorMessage ||
'usage_amount',
idx
).errorMessage ||
getStockUsageError(idx) || getStockUsageError(idx) ||
undefined undefined
} }
@@ -1348,7 +1166,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</td> </td>
{type !== 'detail' && ( {type !== 'detail' && (
<td> <td>
<div className='flex justify-center'> <div className='flex items-center'>
<Button <Button
type='button' type='button'
color='error' color='error'
@@ -1446,7 +1264,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</span> </span>
</th> </th>
<th> <th>
Total Jumlah
<span <span
className='tooltip tooltip-error tooltip-bottom z-[9999]' className='tooltip tooltip-error tooltip-bottom z-[9999]'
data-tip='required' data-tip='required'
@@ -1489,64 +1307,72 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<td> <td>
<SelectInput <SelectInput
value={ value={
RECORDING_FLAG_OPTIONS.find( unifiedStockProducts.find(
(option) => option.value === depletion.notes (product) =>
product.value === depletion.product_warehouse_id
) || null ) || null
} }
onChange={(selectedOption) => { onChange={(selectedOption) => {
const option = selectedOption as OptionType | null; const option = selectedOption as OptionType | null;
formik.setFieldValue( formik.setFieldValue(
`depletions.${idx}.notes`, `depletions.${idx}.product_warehouse_id`,
option?.value || '' option?.value || 0
); );
}} }}
options={RECORDING_FLAG_OPTIONS} options={unifiedStockProducts}
placeholder='Pilih Kondisi' placeholder='Pilih Kondisi'
isLoading={isLoadingStockProducts}
isError={ isError={
isRepeaterInputError('depletions', 'notes', idx) isRepeaterInputError(
.isError 'depletions',
'product_warehouse_id',
idx
).isError
} }
errorMessage={ errorMessage={
isRepeaterInputError('depletions', 'notes', idx) isRepeaterInputError(
.errorMessage 'depletions',
'product_warehouse_id',
idx
).errorMessage
} }
isDisabled={type === 'detail'} isDisabled={type === 'detail'}
className={{ className={{
wrapper: 'w-full min-w-32', wrapper: 'w-full min-w-48',
}} }}
isSearchable={false} isSearchable
isClearable={type !== 'detail'} isClearable={type !== 'detail'}
/> />
</td> </td>
<td> <td>
<NumberInput <NumberInput
required required
name={`depletions.${idx}.total`} name={`depletions.${idx}.qty`}
value={depletion.total} value={depletion.qty}
onChange={handleDepletionTotalChangeWrapper(idx)} onChange={handleDepletionQtyChangeWrapper(idx)}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
decimalScale={0} decimalScale={0}
allowNegative={false} allowNegative={false}
thousandSeparator=',' thousandSeparator=','
decimalSeparator='.' decimalSeparator='.'
isError={ isError={
isRepeaterInputError('depletions', 'total', idx) isRepeaterInputError('depletions', 'qty', idx)
.isError .isError
} }
errorMessage={ errorMessage={
isRepeaterInputError('depletions', 'total', idx) isRepeaterInputError('depletions', 'qty', idx)
.errorMessage .errorMessage
} }
readOnly={type === 'detail'} readOnly={type === 'detail'}
className={{ className={{
wrapper: 'w-full min-w-24', wrapper: 'w-full min-w-24',
}} }}
placeholder='Total' placeholder='Jumlah'
/> />
</td> </td>
{type !== 'detail' && ( {type !== 'detail' && (
<td> <td>
<div className='flex justify-center'> <div className='flex items-center'>
<Button <Button
type='button' type='button'
color='error' color='error'
@@ -1594,7 +1420,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</Card> </Card>
{/* Action buttons */} {/* Action buttons */}
<FormActions<RecordingFormValues> <FormActions<RecordingGrowingFormValues>
type={type} type={type}
formik={formik} formik={formik}
editUrl={ editUrl={
+90 -16
View File
@@ -1,46 +1,120 @@
import { BaseMetadata, User } from '@/types/api/api-general'; import { BaseMetadata, User } from '@/types/api/api-general';
export type ProductionMetrics = { export type ProductionMetrics = {
total_depletion: number; total_depletion_qty: number;
cum_depletion_rate: number; cum_depletion_rate: number;
daily_gain: number; daily_gain: number;
avg_daily_gain: number; avg_daily_gain: number;
cum_intake: number; cum_intake: number;
fcr_value: number; fcr_value: number;
total_chick: number; total_chick_qty: number;
daily_depletion_rate: number; daily_depletion_rate: number;
cum_depletion: number; cum_depletion: number;
}; };
export type BaseRecording = { export type BaseRecording = {
id: number; id: number;
project_flock_kandang_id: number; project_flock_kandangs_id: number;
record_datetime: string; record_datetime: string;
record_date: string;
status: number;
ontime: boolean;
day: number; day: number;
created_user: User; created_by: User;
} & ProductionMetrics; } & ProductionMetrics;
export type Recording = BaseMetadata & BaseRecording; export type RecordingBW = {
id: number;
recording_id: number;
avg_weight: number;
qty: number;
total_weight: number;
};
export type CreateRecordingPayload = { export type RecordingDepletion = {
project_flock_kandang_id: number; id: number;
recording_id: number;
product_warehouse_id: number;
qty: number;
};
export type RecordingStock = {
id: number;
recording_id: number;
product_warehouse_id: number;
usage_qty: number;
pending_qty: number;
};
export type RecordingEgg = {
id: number;
recording_id: number;
product_warehouse_id: number;
qty: number;
created_by: User;
};
export type GradingEgg = {
id: number;
recording_egg_id: number;
qty: number;
grade: string;
created_by: User;
};
export type Recording = BaseMetadata &
BaseRecording & {
recording_bws?: RecordingBW[];
recording_depletions?: RecordingDepletion[];
recording_stocks?: RecordingStock[];
recording_eggs?: RecordingEgg[];
grading_eggs?: GradingEgg[];
};
export type CreateGrowingRecordingPayload = {
project_flock_kandangs_id: number;
body_weights: { body_weights: {
weight: number; avg_weight: number;
qty: number; qty: number;
}[]; }[];
stocks?: { stocks?: {
product_warehouse_id: number; product_warehouse_id: number;
usage_amount: number; usage_qty: number;
notes: string; pending_qty?: number;
}[]; }[];
depletions?: { depletions?: {
product_warehouse_id?: number; product_warehouse_id: number;
total: number; qty: number;
notes: string;
}[]; }[];
}; };
export type CreateLayingRecordingPayload = {
project_flock_kandangs_id: number;
body_weights: {
avg_weight: number;
qty: number;
}[];
stocks?: {
product_warehouse_id: number;
usage_qty: number;
pending_qty?: number;
}[];
depletions?: {
product_warehouse_id: number;
qty: number;
}[];
eggs: {
product_warehouse_id: number;
qty: number;
grading?: {
grade: string;
qty: number;
}[];
}[];
};
export type CreateRecordingPayload =
| CreateGrowingRecordingPayload
| CreateLayingRecordingPayload;
export type UpdateGrowingRecordingPayload = CreateGrowingRecordingPayload;
export type UpdateLayingRecordingPayload = CreateLayingRecordingPayload;
export type UpdateRecordingPayload = CreateRecordingPayload; export type UpdateRecordingPayload = CreateRecordingPayload;