refactor(FE-114): simplify CreateRecordingPayload structure and update validation in RecordingForm

This commit is contained in:
rstubryan
2025-10-24 08:53:41 +07:00
parent 7f5ae94706
commit c30fcd81b2
3 changed files with 31 additions and 289 deletions
@@ -19,14 +19,6 @@ export const RecordingFormSchema = 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!'),
record_datetime: Yup.date()
.required('Tanggal dan waktu recording wajib diisi')
.typeError('Format tanggal dan waktu tidak valid'),
status: Yup.number()
.optional()
.oneOf([0, 1, 2, 3], 'Status tidak valid')
.typeError('Status harus berupa angka!'),
ontime: Yup.boolean().optional(),
body_weights: Yup.array() body_weights: Yup.array()
.of( .of(
Yup.object({ Yup.object({
@@ -44,7 +36,6 @@ export const RecordingFormSchema = Yup.object({
.min(0, 'Rata-rata berat tidak boleh negatif!') .min(0, 'Rata-rata berat tidak boleh negatif!')
.typeError('Rata-rata berat harus berupa angka!') .typeError('Rata-rata berat harus berupa angka!')
.default(0), .default(0),
notes: Yup.string().optional(),
}) })
) )
.min(1, 'Minimal harus ada 1 data bobot badan!') .min(1, 'Minimal harus ada 1 data bobot badan!')
@@ -56,14 +47,6 @@ 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!'),
increase: Yup.number()
.optional()
.min(0, 'Penambahan tidak boleh negatif!')
.typeError('Penambahan harus berupa angka!'),
decrease: Yup.number()
.optional()
.min(0, 'Pengurangan tidak boleh negatif!')
.typeError('Pengurangan harus berupa angka!'),
usage_amount: Yup.number() usage_amount: Yup.number()
.optional() .optional()
.min(0, 'Jumlah penggunaan tidak boleh negatif!') .min(0, 'Jumlah penggunaan tidak boleh negatif!')
@@ -77,10 +60,14 @@ export const RecordingFormSchema = Yup.object({
.of( .of(
Yup.object({ Yup.object({
product_warehouse_id: Yup.number() product_warehouse_id: Yup.number()
.required('Produk wajib diisi!') .optional()
.min(1, 'Produk wajib diisi!') .min(1, 'Produk wajib diisi!')
.typeError('Produk harus berupa angka!'), .typeError('Produk harus berupa angka!'),
condition: Yup.string() 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!') .required('Kondisi depletions wajib diisi!')
.oneOf( .oneOf(
RECORDING_FLAG_OPTIONS.map((option) => option.value), RECORDING_FLAG_OPTIONS.map((option) => option.value),
@@ -88,11 +75,6 @@ export const RecordingFormSchema = Yup.object({
) )
.typeError('Kondisi depletions harus berupa teks!') .typeError('Kondisi depletions harus berupa teks!')
.min(1, 'Kondisi depletions wajib diisi!'), .min(1, 'Kondisi depletions wajib diisi!'),
total: Yup.number()
.required('Jumlah depletions wajib diisi!')
.min(1, 'Jumlah depletions minimal 1!')
.typeError('Jumlah depletions harus berupa angka!'),
notes: Yup.string().optional(),
}) })
) )
.min(1, 'Minimal harus ada 1 data depletions!') .min(1, 'Minimal harus ada 1 data depletions!')
@@ -119,39 +101,28 @@ export const getRecordingFormInitialValues = (
} }
: null, : null,
project_flock_kandang_id: initialValues?.project_flock_kandang_id ?? 0, project_flock_kandang_id: initialValues?.project_flock_kandang_id ?? 0,
record_datetime: initialValues?.record_datetime
? new Date(initialValues.record_datetime)
: new Date(),
status: initialValues?.status ?? 1,
ontime: initialValues?.ontime ?? true,
body_weights: initialValues?.body_weights?.map( body_weights: initialValues?.body_weights?.map(
(bw: NonNullable<CreateRecordingPayload['body_weights']>[0]) => ({ (bw: NonNullable<CreateRecordingPayload['body_weights']>[0]) => ({
weight: bw.weight, weight: bw.weight,
qty: bw.qty, qty: bw.qty,
average_weight: bw.qty > 0 ? Math.round(bw.weight / bw.qty) : 0, average_weight: bw.qty > 0 ? Math.round(bw.weight / bw.qty) : 0,
notes: bw.notes || '',
}) })
) ?? [ ) ?? [
{ {
weight: 0, weight: 0,
qty: 1, qty: 1,
average_weight: 0, average_weight: 0,
notes: '',
}, },
], ],
stocks: initialValues?.stocks?.map( stocks: initialValues?.stocks?.map(
(stock: NonNullable<CreateRecordingPayload['stocks']>[0]) => ({ (stock: NonNullable<CreateRecordingPayload['stocks']>[0]) => ({
product_warehouse_id: stock.product_warehouse_id, product_warehouse_id: stock.product_warehouse_id,
increase: stock.increase,
decrease: stock.decrease,
usage_amount: stock.usage_amount, usage_amount: stock.usage_amount,
notes: stock.notes, notes: stock.notes,
}) })
) ?? [ ) ?? [
{ {
product_warehouse_id: 0, product_warehouse_id: 0,
increase: 0,
decrease: 0,
usage_amount: 0, usage_amount: 0,
notes: '', notes: '',
}, },
@@ -159,14 +130,12 @@ export const getRecordingFormInitialValues = (
depletions: initialValues?.depletions?.map( depletions: initialValues?.depletions?.map(
(depletion: NonNullable<CreateRecordingPayload['depletions']>[0]) => ({ (depletion: NonNullable<CreateRecordingPayload['depletions']>[0]) => ({
product_warehouse_id: depletion.product_warehouse_id, product_warehouse_id: depletion.product_warehouse_id,
condition: depletion.condition,
total: depletion.total, total: depletion.total,
notes: depletion.notes, notes: depletion.notes,
}) })
) ?? [ ) ?? [
{ {
product_warehouse_id: 0, product_warehouse_id: 0,
condition: '',
total: 0, total: 0,
notes: '', notes: '',
}, },
@@ -28,6 +28,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 } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { RECORDING_FLAG_OPTIONS } from '@/config/constant';
import Card from '@/components/Card'; import Card from '@/components/Card';
@@ -141,12 +142,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
onSubmit: async (values) => { onSubmit: async (values) => {
const payload: CreateRecordingPayload = { const payload: CreateRecordingPayload = {
project_flock_kandang_id: values.project_flock_kandang_id, project_flock_kandang_id: values.project_flock_kandang_id,
record_datetime:
values.record_datetime instanceof Date
? values.record_datetime.toISOString()
: '',
status: values.status,
ontime: values.ontime,
body_weights: (values.body_weights ?? []).map((bw) => ({ body_weights: (values.body_weights ?? []).map((bw) => ({
weight: weight:
typeof bw.weight === 'number' typeof bw.weight === 'number'
@@ -156,28 +151,16 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
typeof bw.qty === 'number' typeof bw.qty === 'number'
? bw.qty ? bw.qty
: parseFloat(String(bw.qty)) || 0, : parseFloat(String(bw.qty)) || 0,
notes: bw.notes,
// average_weight is not included in payload as it's calculated field only
})), })),
stocks: (values.stocks ?? []).map((stock) => ({ stocks: (values.stocks ?? []).map((stock) => ({
product_warehouse_id: stock.product_warehouse_id, product_warehouse_id: stock.product_warehouse_id,
increase:
typeof stock.increase === 'number'
? stock.increase
: parseFloat(String(stock.increase)) || 0,
decrease:
typeof stock.decrease === 'number'
? stock.decrease
: parseFloat(String(stock.decrease)) || 0,
usage_amount: usage_amount:
typeof stock.usage_amount === 'number' typeof stock.usage_amount === 'number'
? stock.usage_amount ? stock.usage_amount
: parseFloat(String(stock.usage_amount)) || 0, : parseFloat(String(stock.usage_amount)) || 0,
notes: stock.notes, notes: stock.notes || '',
})), })),
depletions: (values.depletions ?? []).map((depletion) => ({ depletions: (values.depletions ?? []).map((depletion) => ({
product_warehouse_id: depletion.product_warehouse_id,
condition: depletion.condition,
total: total:
typeof depletion.total === 'number' typeof depletion.total === 'number'
? depletion.total ? depletion.total
@@ -236,42 +219,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
// EVENT HANDLERS - Date Time // EVENT HANDLERS - Date Time
const recordDateTimeChangeHandler = (datetime: Date | null) => { const recordDateTimeChangeHandler = (datetime: Date | null) => {
formik.setFieldValue('record_datetime', datetime, false); formik.setFieldValue('record_datetime', datetime, false);
// Auto-set ontime based on date difference
if (datetime) {
const today = new Date();
const recordDate = new Date(datetime);
// Reset time to compare only dates
const todayDateOnly = new Date(today.getFullYear(), today.getMonth(), today.getDate());
const recordDateOnly = new Date(recordDate.getFullYear(), recordDate.getMonth(), recordDate.getDate());
// Set ontime to true if recording date is today, false otherwise
const isOnTime = todayDateOnly.getTime() === recordDateOnly.getTime();
formik.setFieldValue('ontime', isOnTime, false);
}
}; };
// Set initial ontime value when form loads or record_datetime changes
useEffect(() => {
if (formik.values.record_datetime) {
const today = new Date();
const recordDate = new Date(formik.values.record_datetime);
// Reset time to compare only dates
const todayDateOnly = new Date(today.getFullYear(), today.getMonth(), today.getDate());
const recordDateOnly = new Date(recordDate.getFullYear(), recordDate.getMonth(), recordDate.getDate());
// Set ontime to true if recording date is today, false otherwise
const isOnTime = todayDateOnly.getTime() === recordDateOnly.getTime();
// Only update if ontime is not set or different from calculated value
if (formik.values.ontime !== isOnTime) {
formik.setFieldValue('ontime', isOnTime, false);
}
}
}, [formik.values.record_datetime]);
// Auto-calculate average weight when weight or qty changes (but not when editing average weight manually) // Auto-calculate average weight when weight or qty changes (but not when editing average weight manually)
useEffect(() => { useEffect(() => {
if (formik.values.body_weights && editingAverageIndex === null) { if (formik.values.body_weights && editingAverageIndex === null) {
@@ -315,7 +264,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{ {
weight: 0, weight: 0,
qty: 1, qty: 1,
notes: '',
average_weight: 0, average_weight: 0,
}, },
]; ];
@@ -415,8 +363,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
...(formik.values.stocks || []), ...(formik.values.stocks || []),
{ {
product_warehouse_id: 0, product_warehouse_id: 0,
increase: 0,
decrease: 0,
usage_amount: 0, usage_amount: 0,
notes: '', notes: '',
}, },
@@ -476,8 +422,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const newDepletions = [ const newDepletions = [
...(formik.values.depletions || []), ...(formik.values.depletions || []),
{ {
product_warehouse_id: 0,
condition: '',
total: 0, total: 0,
notes: '', notes: '',
}, },
@@ -597,49 +541,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
isClearable isClearable
isSearchable isSearchable
/> />
<TextInput
required
label='Tanggal & Waktu Recording'
type='datetime-local'
name='record_datetime'
value={
formik.values.record_datetime instanceof Date
? formik.values.record_datetime
.toISOString()
.substring(0, 16)
: ''
}
onChange={(e) => {
const datetime = e.target.value
? new Date(e.target.value)
: null;
recordDateTimeChangeHandler(datetime);
}}
onBlur={formik.handleBlur}
isError={
formik.touched.record_datetime &&
Boolean(formik.errors.record_datetime)
}
errorMessage={formik.errors.record_datetime as string}
readOnly={type === 'detail'}
/>
<TextInput
label='Status'
type='number'
name='status'
value={formik.values.status?.toString() || ''}
onChange={(e) => {
const value = e.target.value;
formik.setFieldValue('status', parseInt(value) || 0);
}}
onBlur={formik.handleBlur}
isError={formik.touched.status && Boolean(formik.errors.status)}
errorMessage={formik.errors.status as string}
readOnly={type === 'detail'}
placeholder='Masukkan status (0-3)'
/>
</div> </div>
</Card> </Card>
@@ -710,7 +611,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<Icon icon="material-symbols:info-outline" width={16} height={16} /> <Icon icon="material-symbols:info-outline" width={16} height={16} />
</span> </span>
</th> </th>
<th>Catatan</th>
{type !== 'detail' && <th>Action</th>} {type !== 'detail' && <th>Action</th>}
</tr> </tr>
</thead> </thead>
@@ -823,19 +723,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
}} }}
/> />
</td> </td>
<td>
<TextInput
name={`body_weights.${idx}.notes`}
value={bw.notes || ''}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
placeholder='Catatan...'
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 justify-center'>
@@ -928,8 +815,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<span className='text-error'>*</span> <span className='text-error'>*</span>
</span> </span>
</th> </th>
<th>Penambahan</th>
<th>Pengurangan</th>
<th>Jumlah Pakai</th> <th>Jumlah Pakai</th>
<th>Catatan</th> <th>Catatan</th>
{type !== 'detail' && <th>Action</th>} {type !== 'detail' && <th>Action</th>}
@@ -998,59 +883,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
isSearchable isSearchable
/> />
</td> </td>
<td> <td>
<NumberInput
name={`stocks.${idx}.increase`}
value={stock.increase || 0}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
maskType='number'
decimals={0}
min={0}
thousandSeparator=','
decimalSeparator='.'
isError={
isRepeaterInputError('stocks', 'increase', idx)
.isError
}
errorMessage={
isRepeaterInputError('stocks', 'increase', idx)
.errorMessage
}
readOnly={type === 'detail'}
className={{
wrapper: 'w-full min-w-24',
}}
placeholder='Penambahan'
/>
</td>
<td>
<NumberInput
name={`stocks.${idx}.decrease`}
value={stock.decrease || 0}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
maskType='number'
decimals={0}
min={0}
thousandSeparator=','
decimalSeparator='.'
isError={
isRepeaterInputError('stocks', 'decrease', idx)
.isError
}
errorMessage={
isRepeaterInputError('stocks', 'decrease', idx)
.errorMessage
}
readOnly={type === 'detail'}
className={{
wrapper: 'w-full min-w-24',
}}
placeholder='Pengurangan'
/>
</td>
<td>
<NumberInput <NumberInput
name={`stocks.${idx}.usage_amount`} name={`stocks.${idx}.usage_amount`}
value={stock.usage_amount || 0} value={stock.usage_amount || 0}
@@ -1173,23 +1006,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
/> />
</th> </th>
)} )}
<th>
Product Warehouse ID
<span
className='tooltip tooltip-error tooltip-bottom z-[9999]'
data-tip='required'
>
<span className='text-error'>*</span>
</span>
</th>
<th> <th>
Kondisi Kondisi
<span
className='tooltip tooltip-error tooltip-bottom z-[9999]'
data-tip='required'
>
<span className='text-error'>*</span>
</span>
</th> </th>
<th> <th>
Total Total
@@ -1198,9 +1016,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
data-tip='required' data-tip='required'
> >
<span className='text-error'>*</span> <span className='text-error'>*</span>
</span> </span>
</th> </th>
<th>Catatan</th>
{type !== 'detail' && <th>Action</th>} {type !== 'detail' && <th>Action</th>}
</tr> </tr>
</thead> </thead>
@@ -1232,56 +1049,32 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</td> </td>
)} )}
<td> <td>
<TextInput <SelectInput
required value={RECORDING_FLAG_OPTIONS.find(
name={`depletions.${idx}.product_warehouse_id`} (option) => option.value === depletion.notes
type='number' ) || null}
value={ onChange={(selectedOption) => {
depletion.product_warehouse_id?.toString() || '' const option = selectedOption as OptionType | null;
} formik.setFieldValue(
onChange={formik.handleChange} `depletions.${idx}.notes`,
onBlur={formik.handleBlur} option?.value || ''
isError={ );
isRepeaterInputError(
'depletions',
'product_warehouse_id',
idx
).isError
}
errorMessage={
isRepeaterInputError(
'depletions',
'product_warehouse_id',
idx
).errorMessage
}
readOnly={type === 'detail'}
className={{
wrapper: 'w-full min-w-32',
}} }}
placeholder='Product Warehouse ID' options={RECORDING_FLAG_OPTIONS}
/> placeholder='Pilih Kondisi'
</td>
<td>
<TextInput
required
name={`depletions.${idx}.condition`}
value={depletion.condition || ''}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={ isError={
isRepeaterInputError('depletions', 'condition', idx) isRepeaterInputError('depletions', 'notes', idx)
.isError .isError
} }
errorMessage={ errorMessage={
isRepeaterInputError('depletions', 'condition', idx) isRepeaterInputError('depletions', 'notes', idx)
.errorMessage .errorMessage
} }
readOnly={type === 'detail'} isDisabled={type === 'detail'}
className={{ className={{
wrapper: 'w-full min-w-32', wrapper: 'w-full min-w-32',
}} }}
placeholder='Kondisi (MATI/SAKIT/HILANG)' isSearchable={false}
/> />
</td> </td>
<td> <td>
@@ -1311,19 +1104,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
placeholder='Total' placeholder='Total'
/> />
</td> </td>
<td>
<TextInput
name={`depletions.${idx}.notes`}
value={depletion.notes || ''}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
placeholder='Catatan...'
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 justify-center'>
+5 -12
View File
@@ -27,26 +27,19 @@ export type Recording = BaseMetadata & BaseRecording;
export type CreateRecordingPayload = { export type CreateRecordingPayload = {
project_flock_kandang_id: number; project_flock_kandang_id: number;
record_datetime: string; body_weights: {
status?: number;
ontime?: boolean;
body_weights?: {
weight: number; weight: number;
qty: number; qty: number;
notes?: string;
}[]; }[];
stocks?: { stocks?: {
product_warehouse_id: number; product_warehouse_id: number;
increase?: number; usage_amount: number;
decrease?: number; notes: string;
usage_amount?: number;
notes?: string;
}[]; }[];
depletions?: { depletions?: {
product_warehouse_id: number; product_warehouse_id?: number;
condition: string;
total: number; total: number;
notes?: string; notes: string;
}[]; }[];
}; };