mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-24 23:35:45 +00:00
feat(FE-114,136): implement RecordingForm component with data handling and validation
This commit is contained in:
@@ -0,0 +1,873 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { FormikProps, useFormik } from 'formik';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import TextInput from '@/components/input/TextInput';
|
||||||
|
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||||
|
import { FormActions } from '@/components/helper/form/FormActions';
|
||||||
|
import { CreateRecordingPayload, Recording } from '@/types/api/flock/recording';
|
||||||
|
import {
|
||||||
|
RecordingFormSchema,
|
||||||
|
RecordingFormValues,
|
||||||
|
getRecordingFormInitialValues,
|
||||||
|
UpdateRecordingFormSchema,
|
||||||
|
} from './RecordingForm.schema';
|
||||||
|
import { useRecordingFormHandlers } from './useRecordingFormHandlers';
|
||||||
|
import { FlockApi } from '@/services/api/flock';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { RECORDING_FLAG_OPTIONS } from '@/config/constant';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
interface RecordingFormProps {
|
||||||
|
type?: 'add' | 'edit' | 'detail';
|
||||||
|
initialValues?: Recording;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DUMMY_FLOCKS = [
|
||||||
|
{ value: 1, label: 'Flock A' },
|
||||||
|
{ value: 2, label: 'Flock B' },
|
||||||
|
{ value: 3, label: 'Flock C' },
|
||||||
|
{ value: 4, label: 'Flock D' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||||
|
const [flockSelectInputValue, setFlockSelectInputValue] = useState('');
|
||||||
|
const [selectedPakan, setSelectedPakan] = useState<number[]>([]);
|
||||||
|
const [selectedBobot, setSelectedBobot] = useState<number[]>([]);
|
||||||
|
const [selectedVaksin, setSelectedVaksin] = useState<number[]>([]);
|
||||||
|
const [selectedMortal, setSelectedMortal] = useState<number[]>([]);
|
||||||
|
const [, setRecordingFormErrorMessage] = useState('');
|
||||||
|
const {
|
||||||
|
deleteModal,
|
||||||
|
recordingFormErrorMessage,
|
||||||
|
isDeleteLoading,
|
||||||
|
createRecordingHandler,
|
||||||
|
updateRecordingHandler,
|
||||||
|
deleteRecordingClickHandler,
|
||||||
|
confirmationModalDeleteClickHandler,
|
||||||
|
} = useRecordingFormHandlers(initialValues?.id);
|
||||||
|
|
||||||
|
const formikInitialValues = useMemo<RecordingFormValues>(
|
||||||
|
() => getRecordingFormInitialValues(initialValues),
|
||||||
|
[initialValues]
|
||||||
|
);
|
||||||
|
|
||||||
|
const formik = useFormik<RecordingFormValues>({
|
||||||
|
initialValues: formikInitialValues,
|
||||||
|
validationSchema:
|
||||||
|
type === 'edit' ? UpdateRecordingFormSchema : RecordingFormSchema,
|
||||||
|
validateOnChange: true,
|
||||||
|
validateOnBlur: true,
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
setRecordingFormErrorMessage('');
|
||||||
|
const payload: CreateRecordingPayload = {
|
||||||
|
flock_id: values.flock_id,
|
||||||
|
data_pakan: (values.data_pakan ?? []).map((p) => ({
|
||||||
|
nama_pakan: p.nama_pakan,
|
||||||
|
qty_pakan: p.qty_pakan,
|
||||||
|
stock_pakan: p.stock_pakan,
|
||||||
|
})),
|
||||||
|
bobot_badan: (values.bobot_badan ?? []).map((b) => ({
|
||||||
|
berat_ayam: b.berat_ayam,
|
||||||
|
jumlah_ayam: b.jumlah_ayam,
|
||||||
|
rata_rata_berat_ayam: b.rata_rata_berat_ayam,
|
||||||
|
})),
|
||||||
|
vaksinasi: (values.vaksinasi ?? []).map((v) => ({
|
||||||
|
nama_vaksin: v.nama_vaksin,
|
||||||
|
total_stock: v.total_stock,
|
||||||
|
jumlah_stock: v.jumlah_stock,
|
||||||
|
})),
|
||||||
|
mortalitas: (values.mortalitas ?? []).map((m) => ({
|
||||||
|
kondisi: m.kondisi,
|
||||||
|
jumlah: m.jumlah,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'add':
|
||||||
|
await createRecordingHandler(payload);
|
||||||
|
break;
|
||||||
|
case 'edit':
|
||||||
|
await updateRecordingHandler(initialValues?.id as number, payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// // Flock selection
|
||||||
|
// const flocksUrl = `${FlockApi.basePath}?${new URLSearchParams({ search: flockSelectInputValue }).toString()}`;
|
||||||
|
// const { data: flocks, isLoading: isLoadingFlocks } = useSWR(
|
||||||
|
// flocksUrl,
|
||||||
|
// FlockApi.getAllFetcher
|
||||||
|
// );
|
||||||
|
// const flockOptions = isResponseSuccess(flocks)
|
||||||
|
// ? flocks?.data.map((f) => ({ value: f.id, label: f.name }))
|
||||||
|
// : [];
|
||||||
|
|
||||||
|
const flockOptions = DUMMY_FLOCKS;
|
||||||
|
|
||||||
|
const addDataPakan = () => {
|
||||||
|
const newDataPakan = [
|
||||||
|
...(formik.values.data_pakan || []),
|
||||||
|
{
|
||||||
|
nama_pakan: '',
|
||||||
|
qty_pakan: 0,
|
||||||
|
stock_pakan: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
formik.setFieldValue('data_pakan', newDataPakan);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeDataPakan = (idx: number) => {
|
||||||
|
const updatedDataPakan = formik.values.data_pakan?.filter(
|
||||||
|
(_, i) => i !== idx
|
||||||
|
);
|
||||||
|
formik.setFieldValue('data_pakan', updatedDataPakan);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSelectedDataPakan = () => {
|
||||||
|
const updatedDataPakan = formik.values.data_pakan?.filter(
|
||||||
|
(_, idx) => !selectedPakan.includes(idx)
|
||||||
|
);
|
||||||
|
formik.setFieldValue('data_pakan', updatedDataPakan);
|
||||||
|
setSelectedPakan([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addBobotBadan = () => {
|
||||||
|
const newBobotBadan = [
|
||||||
|
...(formik.values.bobot_badan || []),
|
||||||
|
{
|
||||||
|
berat_ayam: 0,
|
||||||
|
jumlah_ayam: 0,
|
||||||
|
rata_rata_berat_ayam: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
formik.setFieldValue('bobot_badan', newBobotBadan);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeBobotBadan = (idx: number) => {
|
||||||
|
const updatedBobotBadan = formik.values.bobot_badan?.filter(
|
||||||
|
(_, i) => i !== idx
|
||||||
|
);
|
||||||
|
formik.setFieldValue('bobot_badan', updatedBobotBadan);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSelectedBobotBadan = () => {
|
||||||
|
const updatedBobotBadan = formik.values.bobot_badan?.filter(
|
||||||
|
(_, idx) => !selectedBobot.includes(idx)
|
||||||
|
);
|
||||||
|
formik.setFieldValue('bobot_badan', updatedBobotBadan);
|
||||||
|
setSelectedBobot([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addVaksinasi = () => {
|
||||||
|
const newVaksinasi = [
|
||||||
|
...(formik.values.vaksinasi || []),
|
||||||
|
{
|
||||||
|
nama_vaksin: '',
|
||||||
|
total_stock: 0,
|
||||||
|
jumlah_stock: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
formik.setFieldValue('vaksinasi', newVaksinasi);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeVaksinasi = (idx: number) => {
|
||||||
|
const updatedVaksinasi = formik.values.vaksinasi?.filter(
|
||||||
|
(_, i) => i !== idx
|
||||||
|
);
|
||||||
|
formik.setFieldValue('vaksinasi', updatedVaksinasi);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSelectedVaksinasi = () => {
|
||||||
|
const updatedVaksinasi = formik.values.vaksinasi?.filter(
|
||||||
|
(_, idx) => !selectedVaksin.includes(idx)
|
||||||
|
);
|
||||||
|
formik.setFieldValue('vaksinasi', updatedVaksinasi);
|
||||||
|
setSelectedVaksin([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addMortalitas = () => {
|
||||||
|
const newMortalitas = [
|
||||||
|
...(formik.values.mortalitas || []),
|
||||||
|
{
|
||||||
|
kondisi: RECORDING_FLAG_OPTIONS[0].value,
|
||||||
|
jumlah: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
formik.setFieldValue('mortalitas', newMortalitas);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeMortalitas = (idx: number) => {
|
||||||
|
const updatedMortalitas = formik.values.mortalitas?.filter(
|
||||||
|
(_, i) => i !== idx
|
||||||
|
);
|
||||||
|
formik.setFieldValue('mortalitas', updatedMortalitas);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSelectedMortalitas = () => {
|
||||||
|
const updatedMortalitas = formik.values.mortalitas?.filter(
|
||||||
|
(_, idx) => !selectedMortal.includes(idx)
|
||||||
|
);
|
||||||
|
formik.setFieldValue('mortalitas', updatedMortalitas);
|
||||||
|
setSelectedMortal([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className='w-full max-w-5xl'>
|
||||||
|
<FormHeader type={type} title='Recording' backUrl='/flock/recording' />
|
||||||
|
<form
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
className='w-full mt-8 flex flex-col gap-6'
|
||||||
|
>
|
||||||
|
{/* Basic Info Card */}
|
||||||
|
<div className='card bg-base-100 shadow mb-4'>
|
||||||
|
<div className='card-body'>
|
||||||
|
<div className='flex gap-4'>
|
||||||
|
<SelectInput
|
||||||
|
required
|
||||||
|
label='Flock'
|
||||||
|
value={formik.values.flock ?? undefined}
|
||||||
|
onChange={(val) => {
|
||||||
|
formik.setFieldValue('flock', val);
|
||||||
|
formik.setFieldValue(
|
||||||
|
'flock_id',
|
||||||
|
(val as OptionType)?.value
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
options={flockOptions}
|
||||||
|
onInputChange={(val) => {
|
||||||
|
// Filter options locally instead of API call
|
||||||
|
return val;
|
||||||
|
}}
|
||||||
|
isLoading={false} // Remove isLoadingFlocks
|
||||||
|
isError={
|
||||||
|
formik.touched.flock_id && Boolean(formik.errors.flock_id)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.flock_id as string}
|
||||||
|
isDisabled={type === 'detail'}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Data Pakan Table */}
|
||||||
|
<div className='card bg-base-100 shadow mb-4'>
|
||||||
|
<div className='card-body'>
|
||||||
|
<h2 className='card-title mb-4'>Data Pakan</h2>
|
||||||
|
<div className='overflow-x-auto'>
|
||||||
|
<table className='table'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<th>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
className='checkbox'
|
||||||
|
checked={
|
||||||
|
formik.values.data_pakan?.length ===
|
||||||
|
selectedPakan.length &&
|
||||||
|
formik.values.data_pakan?.length > 0
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedPakan(
|
||||||
|
formik.values.data_pakan?.map(
|
||||||
|
(_, idx) => idx
|
||||||
|
) ?? []
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedPakan([]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
)}
|
||||||
|
<th>Nama Pakan</th>
|
||||||
|
<th>Qty Pakan</th>
|
||||||
|
<th>Stock Pakan</th>
|
||||||
|
{type !== 'detail' && <th>Aksi</th>}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{formik.values.data_pakan?.map((pakan, idx) => (
|
||||||
|
<tr key={`pakan-${idx}`}>
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
className='checkbox'
|
||||||
|
checked={selectedPakan.includes(idx)}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedPakan([...selectedPakan, idx]);
|
||||||
|
} else {
|
||||||
|
setSelectedPakan(
|
||||||
|
selectedPakan.filter((i) => i !== idx)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
name={`data_pakan.${idx}.nama_pakan`}
|
||||||
|
value={pakan.nama_pakan}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
readOnly={type === 'detail'}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
type='number'
|
||||||
|
name={`data_pakan.${idx}.qty_pakan`}
|
||||||
|
value={pakan.qty_pakan}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
readOnly={type === 'detail'}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
type='number'
|
||||||
|
name={`data_pakan.${idx}.stock_pakan`}
|
||||||
|
value={pakan.stock_pakan}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
readOnly={type === 'detail'}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<td>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='error'
|
||||||
|
onClick={() => removeDataPakan(idx)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:delete-outline-rounded'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<div className='flex justify-center items-center mt-4 gap-4'>
|
||||||
|
{selectedPakan.length > 0 && (
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='error'
|
||||||
|
onClick={removeSelectedDataPakan}
|
||||||
|
disabled={selectedPakan.length === 0}
|
||||||
|
className='w-fit'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:delete-outline-rounded'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
Hapus Data Pakan Terpilih ({selectedPakan.length})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='success'
|
||||||
|
onClick={addDataPakan}
|
||||||
|
className='w-fit'
|
||||||
|
>
|
||||||
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
|
Tambah Data Pakan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bobot Badan Table */}
|
||||||
|
<div className='card bg-base-100 shadow mb-4'>
|
||||||
|
<div className='card-body'>
|
||||||
|
<h2 className='card-title mb-4'>Bobot Badan</h2>
|
||||||
|
<div className='overflow-x-auto'>
|
||||||
|
<table className='table'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<th>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
className='checkbox'
|
||||||
|
checked={
|
||||||
|
formik.values.bobot_badan?.length ===
|
||||||
|
selectedBobot.length &&
|
||||||
|
formik.values.bobot_badan?.length > 0
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedBobot(
|
||||||
|
formik.values.bobot_badan?.map(
|
||||||
|
(_, idx) => idx
|
||||||
|
) ?? []
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedBobot([]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
)}
|
||||||
|
<th>Berat Ayam</th>
|
||||||
|
<th>Jumlah Ayam</th>
|
||||||
|
<th>Rata-rata Berat Ayam</th>
|
||||||
|
{type !== 'detail' && <th>Aksi</th>}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{formik.values.bobot_badan?.map((bobot, idx) => (
|
||||||
|
<tr key={`bobot-${idx}`}>
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
className='checkbox'
|
||||||
|
checked={selectedBobot.includes(idx)}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedBobot([...selectedBobot, idx]);
|
||||||
|
} else {
|
||||||
|
setSelectedBobot(
|
||||||
|
selectedBobot.filter((i) => i !== idx)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
type='number'
|
||||||
|
name={`bobot_badan.${idx}.berat_ayam`}
|
||||||
|
value={bobot.berat_ayam}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
readOnly={type === 'detail'}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
type='number'
|
||||||
|
name={`bobot_badan.${idx}.jumlah_ayam`}
|
||||||
|
value={bobot.jumlah_ayam}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
readOnly={type === 'detail'}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
type='number'
|
||||||
|
name={`bobot_badan.${idx}.rata_rata_berat_ayam`}
|
||||||
|
value={bobot.rata_rata_berat_ayam}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
readOnly={type === 'detail'}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<td>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='error'
|
||||||
|
onClick={() => removeBobotBadan(idx)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:delete-outline-rounded'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<div className='flex justify-center items-center mt-4 gap-4'>
|
||||||
|
{selectedBobot.length > 0 && (
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='error'
|
||||||
|
onClick={removeSelectedBobotBadan}
|
||||||
|
disabled={selectedBobot.length === 0}
|
||||||
|
className='w-fit'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:delete-outline-rounded'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
Hapus Bobot Badan Terpilih ({selectedBobot.length})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='success'
|
||||||
|
onClick={addBobotBadan}
|
||||||
|
className='w-fit'
|
||||||
|
>
|
||||||
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
|
Tambah Data Bobot Badan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Vaksinasi Table */}
|
||||||
|
<div className='card bg-base-100 shadow mb-4'>
|
||||||
|
<div className='card-body'>
|
||||||
|
<h2 className='card-title mb-4'>Vaksinasi</h2>
|
||||||
|
<div className='overflow-x-auto'>
|
||||||
|
<table className='table'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<th>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
className='checkbox'
|
||||||
|
checked={
|
||||||
|
formik.values.vaksinasi?.length ===
|
||||||
|
selectedVaksin.length &&
|
||||||
|
formik.values.vaksinasi?.length > 0
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedVaksin(
|
||||||
|
formik.values.vaksinasi?.map(
|
||||||
|
(_, idx) => idx
|
||||||
|
) ?? []
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedVaksin([]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
)}
|
||||||
|
<th>Nama Vaksin</th>
|
||||||
|
<th>Total Stock</th>
|
||||||
|
<th>Jumlah Stock</th>
|
||||||
|
{type !== 'detail' && <th>Aksi</th>}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{formik.values.vaksinasi?.map((vaksin, idx) => (
|
||||||
|
<tr key={`vaksin-${idx}`}>
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
className='checkbox'
|
||||||
|
checked={selectedVaksin.includes(idx)}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedVaksin([...selectedVaksin, idx]);
|
||||||
|
} else {
|
||||||
|
setSelectedVaksin(
|
||||||
|
selectedVaksin.filter((i) => i !== idx)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
name={`vaksinasi.${idx}.nama_vaksin`}
|
||||||
|
value={vaksin.nama_vaksin}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
readOnly={type === 'detail'}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
type='number'
|
||||||
|
name={`vaksinasi.${idx}.total_stock`}
|
||||||
|
value={vaksin.total_stock}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
readOnly={type === 'detail'}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
type='number'
|
||||||
|
name={`vaksinasi.${idx}.jumlah_stock`}
|
||||||
|
value={vaksin.jumlah_stock}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
readOnly={type === 'detail'}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<td>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='error'
|
||||||
|
onClick={() => removeVaksinasi(idx)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:delete-outline-rounded'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<div className='flex justify-center items-center mt-4 gap-4'>
|
||||||
|
{selectedVaksin.length > 0 && (
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='error'
|
||||||
|
onClick={removeSelectedVaksinasi}
|
||||||
|
disabled={selectedVaksin.length === 0}
|
||||||
|
className='w-fit'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:delete-outline-rounded'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
Hapus Vaksinasi Terpilih ({selectedVaksin.length})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='success'
|
||||||
|
onClick={addVaksinasi}
|
||||||
|
className='w-fit'
|
||||||
|
>
|
||||||
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
|
Tambah Data Vaksinasi
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mortalitas Table */}
|
||||||
|
<div className='card bg-base-100 shadow mb-4'>
|
||||||
|
<div className='card-body'>
|
||||||
|
<h2 className='card-title mb-4'>Mortalitas</h2>
|
||||||
|
<div className='overflow-x-auto'>
|
||||||
|
<table className='table'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<th>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
className='checkbox'
|
||||||
|
checked={
|
||||||
|
formik.values.mortalitas?.length ===
|
||||||
|
selectedMortal.length &&
|
||||||
|
formik.values.mortalitas?.length > 0
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedMortal(
|
||||||
|
formik.values.mortalitas?.map(
|
||||||
|
(_, idx) => idx
|
||||||
|
) ?? []
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedMortal([]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
)}
|
||||||
|
<th>Kondisi</th>
|
||||||
|
<th>Jumlah</th>
|
||||||
|
{type !== 'detail' && <th>Aksi</th>}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{formik.values.mortalitas?.map((mortal, idx) => (
|
||||||
|
<tr key={`mortal-${idx}`}>
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
className='checkbox'
|
||||||
|
checked={selectedMortal.includes(idx)}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedMortal([...selectedMortal, idx]);
|
||||||
|
} else {
|
||||||
|
setSelectedMortal(
|
||||||
|
selectedMortal.filter((i) => i !== idx)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td>
|
||||||
|
<SelectInput
|
||||||
|
required
|
||||||
|
value={RECORDING_FLAG_OPTIONS.find(
|
||||||
|
(opt) => opt.value === mortal.kondisi
|
||||||
|
)}
|
||||||
|
onChange={(val) => {
|
||||||
|
formik.setFieldValue(
|
||||||
|
`mortalitas.${idx}.kondisi`,
|
||||||
|
(val as OptionType)?.value
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
options={RECORDING_FLAG_OPTIONS}
|
||||||
|
isDisabled={type === 'detail'}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
type='number'
|
||||||
|
name={`mortalitas.${idx}.jumlah`}
|
||||||
|
value={mortal.jumlah}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
readOnly={type === 'detail'}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<td>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='error'
|
||||||
|
onClick={() => removeMortalitas(idx)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:delete-outline-rounded'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<div className='flex justify-center items-center mt-4 gap-4'>
|
||||||
|
{selectedMortal.length > 0 && (
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='error'
|
||||||
|
onClick={removeSelectedMortalitas}
|
||||||
|
disabled={selectedMortal.length === 0}
|
||||||
|
className='w-fit'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:delete-outline-rounded'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
Hapus Mortalitas Terpilih ({selectedMortal.length})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='success'
|
||||||
|
onClick={addMortalitas}
|
||||||
|
className='w-fit'
|
||||||
|
>
|
||||||
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
|
Tambah Data Mortalitas
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<FormActions<RecordingFormValues>
|
||||||
|
type={type}
|
||||||
|
formik={formik}
|
||||||
|
editUrl={
|
||||||
|
initialValues
|
||||||
|
? `/flock/recording/detail/edit/?recordingId=${initialValues.id}`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onDelete={deleteRecordingClickHandler}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{recordingFormErrorMessage && (
|
||||||
|
<div role='alert' className='alert alert-error'>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:error-outline'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
<span>{recordingFormErrorMessage}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{type !== 'add' && (
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={deleteModal.ref}
|
||||||
|
type='error'
|
||||||
|
text='Apakah anda yakin ingin menghapus data Recording ini?'
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'error',
|
||||||
|
isLoading: isDeleteLoading,
|
||||||
|
onClick: confirmationModalDeleteClickHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecordingForm;
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
import { useModal } from '@/components/Modal';
|
||||||
|
import { RecordingApi } from '@/services/api/flock';
|
||||||
|
import {
|
||||||
|
CreateRecordingPayload,
|
||||||
|
UpdateRecordingPayload,
|
||||||
|
} from '@/types/api/flock/recording';
|
||||||
|
import { isResponseError } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
export const useRecordingFormHandlers = (initialValuesId?: number) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const deleteModal = useModal();
|
||||||
|
const [recordingFormErrorMessage, setRecordingFormErrorMessage] =
|
||||||
|
useState('');
|
||||||
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
|
const createRecordingHandler = useCallback(
|
||||||
|
async (payload: CreateRecordingPayload) => {
|
||||||
|
const res = await RecordingApi.create(payload);
|
||||||
|
if (isResponseError(res)) {
|
||||||
|
setRecordingFormErrorMessage(res.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.success(res?.message as string);
|
||||||
|
router.push('/flock/recording');
|
||||||
|
},
|
||||||
|
[router]
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateRecordingHandler = useCallback(
|
||||||
|
async (recordingId: number, payload: UpdateRecordingPayload) => {
|
||||||
|
const res = await RecordingApi.update(recordingId, payload);
|
||||||
|
if (res?.status === 'error') {
|
||||||
|
setRecordingFormErrorMessage(res.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.success(res?.message as string);
|
||||||
|
router.refresh();
|
||||||
|
router.push('/flock/recording');
|
||||||
|
},
|
||||||
|
[router]
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteRecordingClickHandler = useCallback(() => {
|
||||||
|
deleteModal.openModal();
|
||||||
|
}, [deleteModal]);
|
||||||
|
|
||||||
|
const confirmationModalDeleteClickHandler = useCallback(async () => {
|
||||||
|
if (!initialValuesId) return;
|
||||||
|
|
||||||
|
setIsDeleteLoading(true);
|
||||||
|
await RecordingApi.delete(initialValuesId);
|
||||||
|
deleteModal.closeModal();
|
||||||
|
toast.success('Successfully delete Recording!');
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
router.push('/flock/recording');
|
||||||
|
}, [deleteModal, initialValuesId, router]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
deleteModal,
|
||||||
|
recordingFormErrorMessage,
|
||||||
|
isDeleteLoading,
|
||||||
|
createRecordingHandler,
|
||||||
|
updateRecordingHandler,
|
||||||
|
deleteRecordingClickHandler,
|
||||||
|
confirmationModalDeleteClickHandler,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user