mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 21:41:57 +00:00
Merge branch 'feat/FE/US-281-439/TASK-440-441-recording-page-and-form-adjustment' into 'development'
[FEAT/FE][US#281-439/TASK-440-441] Recording Page and Form Adjustment See merge request mbugroup/lti-web-client!125
This commit is contained in:
@@ -12,11 +12,6 @@ type RecordingGrowingFormSchemaType = {
|
||||
label: string;
|
||||
} | null;
|
||||
project_flock_kandang_id: number;
|
||||
body_weights: {
|
||||
weight: number | string;
|
||||
avg_weight: number | string;
|
||||
qty: number | string;
|
||||
}[];
|
||||
stocks: {
|
||||
product_warehouse_id: number;
|
||||
qty: number | string;
|
||||
@@ -35,12 +30,6 @@ type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & {
|
||||
}[];
|
||||
};
|
||||
|
||||
export type BodyWeightSchema = {
|
||||
weight: number | string;
|
||||
avg_weight: number | string;
|
||||
qty: number | string;
|
||||
};
|
||||
|
||||
export type StockSchema = {
|
||||
product_warehouse_id: number;
|
||||
qty: number | string;
|
||||
@@ -57,20 +46,6 @@ export type EggSchema = {
|
||||
weight: number | string;
|
||||
};
|
||||
|
||||
const BodyWeightObjectSchema: Yup.ObjectSchema<BodyWeightSchema> = Yup.object({
|
||||
weight: Yup.number()
|
||||
.required('Berat ayam total wajib diisi!')
|
||||
.min(1, 'Berat ayam total minimal 1 gram!')
|
||||
.typeError('Berat ayam total harus berupa angka!'),
|
||||
avg_weight: Yup.number()
|
||||
.required('Berat ayam rata-rata wajib diisi!')
|
||||
.typeError('Berat ayam rata-rata harus berupa angka!'),
|
||||
qty: Yup.number()
|
||||
.required('Jumlah ayam wajib diisi!')
|
||||
.min(1, 'Jumlah ayam minimal 1 ekor!')
|
||||
.typeError('Jumlah ayam harus berupa angka!'),
|
||||
});
|
||||
|
||||
const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
|
||||
product_warehouse_id: Yup.number()
|
||||
.required('Produk wajib diisi!')
|
||||
@@ -140,10 +115,6 @@ export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSc
|
||||
return true;
|
||||
}
|
||||
),
|
||||
body_weights: Yup.array()
|
||||
.of(BodyWeightObjectSchema)
|
||||
.min(1, 'Minimal harus ada 1 data bobot badan!')
|
||||
.required('Data bobot badan wajib diisi!'),
|
||||
stocks: Yup.array()
|
||||
.of(StockObjectSchema)
|
||||
.min(1, 'Minimal harus ada 1 data stok!')
|
||||
@@ -196,7 +167,6 @@ export type RecordingLayingFormValues = Yup.InferType<
|
||||
>;
|
||||
|
||||
type RecordingFormData = Partial<Recording> & {
|
||||
body_weights?: CreateGrowingRecordingPayload['body_weights'];
|
||||
stocks?: CreateGrowingRecordingPayload['stocks'] | Recording['stocks'];
|
||||
depletions?:
|
||||
| CreateGrowingRecordingPayload['depletions']
|
||||
@@ -216,19 +186,6 @@ export const getRecordingGrowingFormInitialValues = (
|
||||
}
|
||||
: null,
|
||||
project_flock_kandang_id: initialValues?.project_flock_kandang_id ?? 0,
|
||||
body_weights: initialValues?.body_weights?.map(
|
||||
(bw: NonNullable<CreateGrowingRecordingPayload['body_weights']>[0]) => ({
|
||||
weight: bw.avg_weight * bw.qty,
|
||||
avg_weight: bw.avg_weight,
|
||||
qty: bw.qty,
|
||||
})
|
||||
) ?? [
|
||||
{
|
||||
weight: '',
|
||||
avg_weight: '',
|
||||
qty: '',
|
||||
},
|
||||
],
|
||||
stocks: initialValues?.stocks?.map((stock) => ({
|
||||
product_warehouse_id: stock.product_warehouse_id,
|
||||
qty:
|
||||
|
||||
@@ -60,6 +60,7 @@ import {
|
||||
GROWING_RECORDING_APPROVAL_LINE,
|
||||
LAYING_RECORDING_APPROVAL_LINE,
|
||||
} from '@/config/approval-line';
|
||||
import Table from '@/components/Table';
|
||||
|
||||
interface RecordingFormProps {
|
||||
type?: 'add' | 'edit' | 'detail';
|
||||
@@ -71,16 +72,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
// ===== STATE MANAGEMENT =====
|
||||
const [selectedBodyWeights, setSelectedBodyWeights] = useState<number[]>([]);
|
||||
const [selectedStocks, setSelectedStocks] = useState<number[]>([]);
|
||||
const [selectedDepletions, setSelectedDepletions] = useState<number[]>([]);
|
||||
const [selectedEggs, setSelectedEggs] = useState<number[]>([]);
|
||||
|
||||
const [editingAverageIndex] = useState<number | null>(null);
|
||||
const [manuallyEditedRows, setManuallyEditedRows] = useState<Set<number>>(
|
||||
new Set()
|
||||
);
|
||||
|
||||
const [locationSearchValue, setLocationSearchValue] = useState('');
|
||||
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
||||
null
|
||||
@@ -122,19 +117,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
(values: RecordingGrowingFormValues) => {
|
||||
return {
|
||||
project_flock_kandang_id: values.project_flock_kandang_id,
|
||||
body_weights: (values.body_weights ?? []).map((bw) => {
|
||||
const qty = Number(bw.qty) || 0;
|
||||
const weight = Number(bw.weight) || 0;
|
||||
const totalWeight = qty * weight;
|
||||
return {
|
||||
avg_weight:
|
||||
typeof bw.avg_weight === 'number'
|
||||
? bw.avg_weight
|
||||
: parseFloat(String(bw.avg_weight)) || 0,
|
||||
qty: qty,
|
||||
total_weight: parseFloat(String(totalWeight)) || 0,
|
||||
};
|
||||
}),
|
||||
stocks: (values.stocks ?? []).map((stock) => ({
|
||||
product_warehouse_id: stock.product_warehouse_id,
|
||||
qty: Number(stock.qty) || 0,
|
||||
@@ -152,15 +134,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
(values: RecordingLayingFormValues) => {
|
||||
return {
|
||||
project_flock_kandang_id: values.project_flock_kandang_id,
|
||||
body_weights: (values.body_weights ?? []).map((bw) => {
|
||||
return {
|
||||
avg_weight:
|
||||
typeof bw.avg_weight === 'number'
|
||||
? bw.avg_weight
|
||||
: parseFloat(String(bw.avg_weight)) || 0,
|
||||
qty: Number(bw.qty) || 0,
|
||||
};
|
||||
}),
|
||||
stocks: (values.stocks ?? []).map((stock) => ({
|
||||
product_warehouse_id: stock.product_warehouse_id,
|
||||
qty: Number(stock.qty) || 0,
|
||||
@@ -587,28 +560,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
return recordedIds;
|
||||
}, [existingRecordings, today]);
|
||||
|
||||
const getLatestTotalChickQty = useCallback(
|
||||
(projectFlockKandangId: number) => {
|
||||
if (!isResponseSuccess(existingRecordings)) return null;
|
||||
|
||||
const projectFlockRecordings = existingRecordings.data.filter(
|
||||
(recording) =>
|
||||
recording.project_flock_kandang_id === projectFlockKandangId
|
||||
);
|
||||
|
||||
if (projectFlockRecordings.length === 0) return null;
|
||||
|
||||
projectFlockRecordings.sort(
|
||||
(a, b) =>
|
||||
new Date(b.record_datetime).getTime() -
|
||||
new Date(a.record_datetime).getTime()
|
||||
);
|
||||
|
||||
return projectFlockRecordings[0].total_chick_qty;
|
||||
},
|
||||
[existingRecordings]
|
||||
);
|
||||
|
||||
const unifiedStockProducts = useMemo(() => {
|
||||
const options: OptionType[] = [];
|
||||
if (isResponseSuccess(stockProducts) && selectedKandang) {
|
||||
@@ -808,25 +759,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
});
|
||||
|
||||
// ===== HELPER FUNCTIONS =====
|
||||
const getTotalChickQtyError = useCallback(
|
||||
(qty: number) => {
|
||||
if (type === 'detail') return null;
|
||||
if (!formik.values.project_flock_kandang_id) return null;
|
||||
|
||||
const totalChickQty = getLatestTotalChickQty(
|
||||
formik.values.project_flock_kandang_id
|
||||
);
|
||||
if (!totalChickQty) return null;
|
||||
|
||||
if (qty > totalChickQty) {
|
||||
return `Jumlah ayam tidak boleh melebihi total ayam tersedia! Maksimal: ${formatNumber(totalChickQty)}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[formik.values.project_flock_kandang_id, getLatestTotalChickQty, type]
|
||||
);
|
||||
|
||||
useCallback((): OptionType | null => {
|
||||
if (
|
||||
!formik.values.project_flock_kandang ||
|
||||
@@ -1193,124 +1125,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
setIsRejectLoading(false);
|
||||
};
|
||||
|
||||
const addBodyWeight = () => {
|
||||
const newBodyWeights = [
|
||||
...(formik.values.body_weights || []),
|
||||
{
|
||||
weight: '',
|
||||
avg_weight: '',
|
||||
qty: '',
|
||||
},
|
||||
];
|
||||
formik.setFieldValue('body_weights', newBodyWeights);
|
||||
};
|
||||
|
||||
const handleWeightChange = (idx: number, value: number) => {
|
||||
formik.setFieldValue(`body_weights.${idx}.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 = Number(currentWeight.qty) || 0;
|
||||
const totalWeight = qty * value;
|
||||
|
||||
if (qty > 0 && value > 0) {
|
||||
const avgWeight = parseFloat((value / qty).toFixed(2));
|
||||
formik.setFieldValue(`body_weights.${idx}.avg_weight`, avgWeight);
|
||||
} else {
|
||||
formik.setFieldValue(`body_weights.${idx}.avg_weight`, '');
|
||||
}
|
||||
|
||||
formik.setFieldValue(`body_weights.${idx}.total_weight`, totalWeight);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAvgWeightChange = (idx: number, value: number) => {
|
||||
formik.setFieldValue(`body_weights.${idx}.avg_weight`, value);
|
||||
|
||||
setManuallyEditedRows((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.add(idx);
|
||||
return newSet;
|
||||
});
|
||||
|
||||
const currentWeight = formik.values.body_weights?.[idx];
|
||||
if (currentWeight) {
|
||||
const qty = Number(currentWeight.qty) || 0;
|
||||
if (qty > 0 && value > 0) {
|
||||
const totalWeight = value * qty;
|
||||
formik.setFieldValue(`body_weights.${idx}.weight`, totalWeight);
|
||||
formik.setFieldValue(`body_weights.${idx}.total_weight`, totalWeight);
|
||||
} else {
|
||||
formik.setFieldValue(`body_weights.${idx}.weight`, 0);
|
||||
formik.setFieldValue(`body_weights.${idx}.total_weight`, 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleQtyChange = (idx: number, value: number) => {
|
||||
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 = Number(currentWeight.weight) || 0;
|
||||
const totalWeight = value * weight;
|
||||
|
||||
if (value > 0 && weight > 0) {
|
||||
const avgWeight = parseFloat((weight / value).toFixed(2));
|
||||
formik.setFieldValue(`body_weights.${idx}.avg_weight`, avgWeight);
|
||||
} else {
|
||||
formik.setFieldValue(`body_weights.${idx}.avg_weight`, '');
|
||||
}
|
||||
|
||||
formik.setFieldValue(`body_weights.${idx}.total_weight`, totalWeight);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWeightChangeWrapper =
|
||||
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseFloat(e.target.value) || 0;
|
||||
handleWeightChange(idx, value);
|
||||
};
|
||||
|
||||
const handleAvgWeightChangeWrapper =
|
||||
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseFloat(e.target.value) || 0;
|
||||
handleAvgWeightChange(idx, value);
|
||||
};
|
||||
|
||||
const handleQtyChangeWrapper =
|
||||
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseFloat(e.target.value) || 0;
|
||||
handleQtyChange(idx, value);
|
||||
};
|
||||
|
||||
const removeBodyWeight = (idx: number) => {
|
||||
const updatedBodyWeights = formik.values.body_weights?.filter(
|
||||
(_, i) => i !== idx
|
||||
);
|
||||
formik.setFieldValue('body_weights', updatedBodyWeights);
|
||||
};
|
||||
|
||||
const removeSelectedBodyWeights = () => {
|
||||
const updatedBodyWeights = formik.values.body_weights?.filter(
|
||||
(_, idx) => !selectedBodyWeights.includes(idx)
|
||||
);
|
||||
formik.setFieldValue('body_weights', updatedBodyWeights);
|
||||
setSelectedBodyWeights([]);
|
||||
};
|
||||
|
||||
const addStock = () => {
|
||||
const newStocks = [
|
||||
...(formik.values.stocks || []),
|
||||
@@ -1435,45 +1249,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
}
|
||||
}, [isLayingCategory, type]);
|
||||
|
||||
const bodyWeightValues = useMemo(() => {
|
||||
if (!formik.values.body_weights) return [];
|
||||
return formik.values.body_weights.map((w) => ({
|
||||
weight: w.weight,
|
||||
qty: w.qty,
|
||||
}));
|
||||
}, [formik.values.body_weights]);
|
||||
|
||||
useEffect(() => {
|
||||
if (formik.values.body_weights && editingAverageIndex === null) {
|
||||
const updatedBodyWeights = formik.values.body_weights.map(
|
||||
(weight, idx) => {
|
||||
if (idx === editingAverageIndex || manuallyEditedRows.has(idx)) {
|
||||
return weight;
|
||||
}
|
||||
const qty = Number(weight.qty) || 0;
|
||||
const weightValue = Number(weight.weight) || 0;
|
||||
return {
|
||||
...weight,
|
||||
avg_weight:
|
||||
qty > 0 && weightValue > 0
|
||||
? parseFloat((weightValue / qty).toFixed(2))
|
||||
: 0,
|
||||
};
|
||||
}
|
||||
);
|
||||
const hasChanges = updatedBodyWeights.some(
|
||||
(updated, idx) =>
|
||||
idx !== editingAverageIndex &&
|
||||
!manuallyEditedRows.has(idx) &&
|
||||
updated.avg_weight !==
|
||||
(formik.values.body_weights[idx]?.avg_weight || 0)
|
||||
);
|
||||
if (hasChanges) {
|
||||
formik.setFieldValue('body_weights', updatedBodyWeights, false);
|
||||
}
|
||||
}
|
||||
}, [bodyWeightValues, editingAverageIndex, manuallyEditedRows]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className='w-full'>
|
||||
@@ -1763,266 +1538,241 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Body Weights Table */}
|
||||
<Card
|
||||
title='Bobot Badan'
|
||||
className={{
|
||||
wrapper: 'w-full mb-4 shadow',
|
||||
title: 'mb-4',
|
||||
}}
|
||||
>
|
||||
<div className='overflow-x-auto'>
|
||||
<table className='table'>
|
||||
<thead>
|
||||
<tr>
|
||||
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
||||
<th>
|
||||
<CheckboxInput
|
||||
name='select-all-body-weights'
|
||||
checked={
|
||||
formik.values.body_weights?.length ===
|
||||
selectedBodyWeights.length &&
|
||||
formik.values.body_weights?.length > 0
|
||||
}
|
||||
onChange={(
|
||||
e: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedBodyWeights(
|
||||
formik.values.body_weights?.map(
|
||||
(_, idx) => idx
|
||||
) ?? []
|
||||
);
|
||||
} else {
|
||||
setSelectedBodyWeights([]);
|
||||
}
|
||||
}}
|
||||
classNames={{
|
||||
wrapper: 'flex justify-center',
|
||||
checkbox: 'checkbox checkbox-sm',
|
||||
}}
|
||||
/>
|
||||
</th>
|
||||
)}
|
||||
<th>
|
||||
Berat Ayam (gram)
|
||||
<span
|
||||
className='tooltip tooltip-error tooltip-bottom '
|
||||
data-tip='required'
|
||||
>
|
||||
<span className='text-error'>*</span>
|
||||
</span>
|
||||
</th>
|
||||
<th>
|
||||
Jumlah Ayam
|
||||
<span
|
||||
className='tooltip tooltip-error tooltip-bottom'
|
||||
data-tip='required'
|
||||
>
|
||||
<span className='text-error'>*</span>
|
||||
</span>
|
||||
</th>
|
||||
<th>
|
||||
Rata-rata Berat Ayam (gram)
|
||||
<span
|
||||
className='tooltip tooltip-error tooltip-bottom '
|
||||
data-tip='required'
|
||||
>
|
||||
<span className='text-error'>*</span>
|
||||
</span>
|
||||
</th>
|
||||
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
||||
<th>Action</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{formik.values.body_weights?.map((bw, idx) => (
|
||||
<tr key={`body-weight-${idx}`}>
|
||||
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
||||
<td className='align-middle!'>
|
||||
<CheckboxInput
|
||||
name={`body-weight-${idx}`}
|
||||
checked={selectedBodyWeights.includes(idx)}
|
||||
onChange={(
|
||||
e: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedBodyWeights([
|
||||
...selectedBodyWeights,
|
||||
idx,
|
||||
]);
|
||||
} else {
|
||||
setSelectedBodyWeights(
|
||||
selectedBodyWeights.filter((i) => i !== idx)
|
||||
);
|
||||
}
|
||||
}}
|
||||
classNames={{
|
||||
wrapper: 'flex justify-center',
|
||||
checkbox: 'checkbox checkbox-sm',
|
||||
}}
|
||||
/>
|
||||
{/* FCR & Mortality Metrics - Detail View Only */}
|
||||
{type === 'detail' && initialValues && (
|
||||
<div
|
||||
className={`grid gap-6 mb-6 grid-cols-1 ${
|
||||
initialValues.project_flock_category === 'LAYING'
|
||||
? 'xl:grid-cols-3'
|
||||
: 'xl:grid-cols-2'
|
||||
}`}
|
||||
>
|
||||
{/* FCR Section */}
|
||||
<div className='border border-gray-200 rounded-lg bg-white'>
|
||||
<div className='px-4 py-3 border-b border-gray-200'>
|
||||
<span className='card-title font-bold text-xl'>FCR</span>
|
||||
</div>
|
||||
<div className='p-4'>
|
||||
<table className='w-full text-sm'>
|
||||
<thead>
|
||||
<tr className='border-b border-gray-200'>
|
||||
<th className='text-left py-2 font-semibold text-gray-600'></th>
|
||||
<th className='text-center py-2 font-semibold text-gray-600'>
|
||||
AKTUAL
|
||||
</th>
|
||||
<th className='text-center py-2 font-semibold text-gray-600'>
|
||||
STANDAR
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className='py-3 font-medium'>FCR</td>
|
||||
<td className='text-center py-3'>
|
||||
<span className='font-semibold'>
|
||||
{initialValues.fcr_value &&
|
||||
initialValues.fcr_value > 0
|
||||
? formatNumber(initialValues.fcr_value)
|
||||
: '-'}
|
||||
</span>
|
||||
</td>
|
||||
)}
|
||||
<td>
|
||||
<NumberInput
|
||||
required
|
||||
name={`body_weights.${idx}.weight`}
|
||||
value={bw.weight ?? ''}
|
||||
onChange={handleWeightChangeWrapper(idx)}
|
||||
onBlur={formik.handleBlur}
|
||||
decimalScale={2}
|
||||
allowNegative={false}
|
||||
thousandSeparator=','
|
||||
decimalSeparator='.'
|
||||
inputSuffix='gram'
|
||||
placeholder='Masukkan berat total...'
|
||||
isError={
|
||||
isRepeaterInputError('body_weights', 'weight', idx)
|
||||
.isError
|
||||
}
|
||||
errorMessage={
|
||||
isRepeaterInputError('body_weights', 'weight', idx)
|
||||
.errorMessage
|
||||
}
|
||||
readOnly={type === 'detail'}
|
||||
className={{
|
||||
wrapper: 'w-full min-w-32',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<NumberInput
|
||||
required
|
||||
name={`body_weights.${idx}.qty`}
|
||||
value={bw.qty ?? ''}
|
||||
onChange={handleQtyChangeWrapper(idx)}
|
||||
onBlur={formik.handleBlur}
|
||||
decimalScale={0}
|
||||
allowNegative={false}
|
||||
thousandSeparator=','
|
||||
decimalSeparator='.'
|
||||
placeholder='Masukkan jumlah ayam...'
|
||||
bottomLabel={
|
||||
formik.values.project_flock_kandang_id &&
|
||||
type === 'add'
|
||||
? `Total Ayam: ${
|
||||
getLatestTotalChickQty(
|
||||
formik.values.project_flock_kandang_id
|
||||
) !== null
|
||||
? formatNumber(
|
||||
getLatestTotalChickQty(
|
||||
formik.values.project_flock_kandang_id
|
||||
)!
|
||||
)
|
||||
: 'N/A'
|
||||
}`
|
||||
: undefined
|
||||
}
|
||||
isError={
|
||||
isRepeaterInputError('body_weights', 'qty', idx)
|
||||
.isError ||
|
||||
(bw.qty
|
||||
? getTotalChickQtyError(Number(bw.qty)) !== null
|
||||
: false)
|
||||
}
|
||||
errorMessage={
|
||||
isRepeaterInputError('body_weights', 'qty', idx)
|
||||
.errorMessage ||
|
||||
(bw.qty
|
||||
? getTotalChickQtyError(Number(bw.qty)) ||
|
||||
undefined
|
||||
: undefined)
|
||||
}
|
||||
readOnly={type === 'detail'}
|
||||
className={{
|
||||
wrapper: 'w-full min-w-24',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<NumberInput
|
||||
required
|
||||
name={`body_weights.${idx}.avg_weight`}
|
||||
value={bw.avg_weight ?? ''}
|
||||
onChange={handleAvgWeightChangeWrapper(idx)}
|
||||
onBlur={formik.handleBlur}
|
||||
decimalScale={2}
|
||||
allowNegative={false}
|
||||
thousandSeparator=','
|
||||
decimalSeparator='.'
|
||||
inputSuffix='gram'
|
||||
placeholder='Masukkan berat rata-rata...'
|
||||
isError={
|
||||
isRepeaterInputError(
|
||||
'body_weights',
|
||||
'avg_weight',
|
||||
idx
|
||||
).isError
|
||||
}
|
||||
errorMessage={
|
||||
isRepeaterInputError(
|
||||
'body_weights',
|
||||
'avg_weight',
|
||||
idx
|
||||
).errorMessage
|
||||
}
|
||||
readOnly={type === 'detail'}
|
||||
className={{
|
||||
wrapper: 'w-full min-w-32',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
|
||||
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
||||
<td>
|
||||
<div className='flex items-center'>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={() => removeBodyWeight(idx)}
|
||||
>
|
||||
<Icon
|
||||
icon='mdi:trash-can'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<td className='text-center py-3 text-gray-600'>
|
||||
{initialValues.fcr_std && initialValues.fcr_std > 0
|
||||
? formatNumber(initialValues.fcr_std)
|
||||
: '-'}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
||||
<div className='flex justify-center items-center mt-4 gap-4'>
|
||||
{selectedBodyWeights.length > 0 && (
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={removeSelectedBodyWeights}
|
||||
disabled={selectedBodyWeights.length === 0}
|
||||
className='w-fit'
|
||||
>
|
||||
<Icon icon='mdi:trash-can' width={24} height={24} />
|
||||
Hapus Terpilih ({selectedBodyWeights.length})
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type='button'
|
||||
color='success'
|
||||
onClick={addBodyWeight}
|
||||
className='w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah Bobot Badan
|
||||
</Button>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='py-3 font-medium'>Feed Intake (KG)</td>
|
||||
<td className='text-center py-3'>
|
||||
<span className='font-semibold'>
|
||||
{initialValues.feed_intake &&
|
||||
initialValues.feed_intake > 0
|
||||
? formatNumber(initialValues.feed_intake)
|
||||
: '-'}
|
||||
</span>
|
||||
</td>
|
||||
<td className='text-center py-3 text-gray-600'>
|
||||
{initialValues.feed_intake_std &&
|
||||
initialValues.feed_intake_std > 0
|
||||
? formatNumber(initialValues.feed_intake_std)
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Mortality Section */}
|
||||
<div className='border border-gray-200 rounded-lg bg-white'>
|
||||
<div className='px-4 py-3 border-b border-gray-200'>
|
||||
<span className='card-title font-bold text-xl'>
|
||||
Mortalitas
|
||||
</span>
|
||||
</div>
|
||||
<div className='p-4'>
|
||||
<table className='w-full text-sm'>
|
||||
<thead>
|
||||
<tr className='border-b border-gray-200'>
|
||||
<th
|
||||
colSpan={2}
|
||||
className='text-center py-2 font-semibold text-gray-600'
|
||||
>
|
||||
DEPLESI KUMULATIF
|
||||
</th>
|
||||
</tr>
|
||||
<tr className='border-b border-gray-200'>
|
||||
<th className='text-center py-2 font-semibold text-xs text-gray-500'>
|
||||
Total
|
||||
</th>
|
||||
<th className='text-center py-2 font-semibold text-xs text-gray-500'>
|
||||
(%)
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className='text-center py-3 border-r border-gray-100'>
|
||||
<span className='font-semibold'>
|
||||
{initialValues.total_depletion_qty &&
|
||||
initialValues.total_depletion_qty > 0
|
||||
? formatNumber(initialValues.total_depletion_qty)
|
||||
: '-'}
|
||||
</span>
|
||||
</td>
|
||||
<td className='text-center py-3 text-gray-600'>
|
||||
{initialValues.cum_depletion_rate &&
|
||||
initialValues.cum_depletion_rate > 0
|
||||
? initialValues.cum_depletion_rate.toFixed(2)
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
colSpan={2}
|
||||
className='text-center py-3 border-r border-gray-200 text-gray-600'
|
||||
>
|
||||
Total Ayam
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
colSpan={2}
|
||||
className='text-center py-3 font-semibold'
|
||||
>
|
||||
{initialValues.total_chick_qty &&
|
||||
initialValues.total_chick_qty > 0
|
||||
? formatNumber(initialValues.total_chick_qty)
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Egg Production Section - Only for LAYING category */}
|
||||
{type === 'detail' &&
|
||||
initialValues &&
|
||||
initialValues.project_flock_category === 'LAYING' && (
|
||||
<div className='border border-gray-200 rounded-lg bg-white'>
|
||||
<div className='px-4 py-3 border-b border-gray-200'>
|
||||
<span className='card-title font-bold text-xl'>
|
||||
Produksi
|
||||
</span>
|
||||
</div>
|
||||
<div className='p-4'>
|
||||
<table className='w-full text-sm'>
|
||||
<thead>
|
||||
<tr className='border-b border-gray-200'>
|
||||
<th className='text-left py-2 font-semibold text-gray-600'></th>
|
||||
<th className='text-center py-2 font-semibold text-gray-600'>
|
||||
AKTUAL
|
||||
</th>
|
||||
<th className='text-center py-2 font-semibold text-gray-600'>
|
||||
STANDAR
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className='py-3 font-medium'>Egg Mass</td>
|
||||
<td className='text-center py-3'>
|
||||
<span className='font-semibold'>
|
||||
{initialValues.egg_mesh &&
|
||||
initialValues.egg_mesh > 0
|
||||
? formatNumber(initialValues.egg_mesh)
|
||||
: '-'}
|
||||
</span>
|
||||
</td>
|
||||
<td className='text-center py-3 text-gray-600'>
|
||||
{initialValues.egg_mesh_std &&
|
||||
initialValues.egg_mesh_std > 0
|
||||
? formatNumber(initialValues.egg_mesh_std)
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='py-3 font-medium'>
|
||||
Egg Weight (KG)
|
||||
</td>
|
||||
<td className='text-center py-3'>
|
||||
<span className='font-semibold'>
|
||||
{initialValues.egg_weight &&
|
||||
initialValues.egg_weight > 0
|
||||
? formatNumber(initialValues.egg_weight)
|
||||
: '-'}
|
||||
</span>
|
||||
</td>
|
||||
<td className='text-center py-3 text-gray-600'>
|
||||
{initialValues.egg_weight_std &&
|
||||
initialValues.egg_weight_std > 0
|
||||
? formatNumber(initialValues.egg_weight_std)
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='py-3 font-medium'>Hen Day</td>
|
||||
<td className='text-center py-3'>
|
||||
<span className='font-semibold'>
|
||||
{initialValues.hand_day &&
|
||||
initialValues.hand_day > 0
|
||||
? formatNumber(initialValues.hand_day)
|
||||
: '-'}
|
||||
</span>
|
||||
</td>
|
||||
<td className='text-center py-3 text-gray-600'>
|
||||
{initialValues.hand_day_std !== undefined &&
|
||||
initialValues.hand_day_std > 0
|
||||
? `${initialValues.hand_day_std}%`
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='py-3 font-medium'>Hen House</td>
|
||||
<td className='text-center py-3'>
|
||||
<span className='font-semibold'>
|
||||
{initialValues.hand_house &&
|
||||
initialValues.hand_house > 0
|
||||
? formatNumber(initialValues.hand_house)
|
||||
: '-'}
|
||||
</span>
|
||||
</td>
|
||||
<td className='text-center py-3 text-gray-600'>
|
||||
{initialValues.hand_house_std !== undefined &&
|
||||
initialValues.hand_house_std > 0
|
||||
? `${initialValues.hand_house_std}%`
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stocks Table */}
|
||||
<Card
|
||||
|
||||
+14
-14
@@ -4,12 +4,23 @@ import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
||||
export type ProductionMetrics = {
|
||||
total_depletion_qty: number;
|
||||
cum_depletion_rate: number;
|
||||
daily_gain: number;
|
||||
avg_daily_gain: number;
|
||||
cum_intake: number;
|
||||
fcr_value: number;
|
||||
fcr_std?: number;
|
||||
total_chick_qty: number;
|
||||
cum_depletion: number;
|
||||
hand_day?: number;
|
||||
hand_house?: number;
|
||||
feed_intake?: number;
|
||||
egg_mesh?: number;
|
||||
egg_weight?: number;
|
||||
hand_day_std?: number;
|
||||
hand_house_std?: number;
|
||||
feed_intake_std?: number;
|
||||
egg_mesh_std?: number;
|
||||
egg_weight_std?: number;
|
||||
daily_gain?: number;
|
||||
avg_daily_gain?: number;
|
||||
cum_depletion?: number;
|
||||
};
|
||||
|
||||
export type BaseRecording = {
|
||||
@@ -20,12 +31,6 @@ export type BaseRecording = {
|
||||
project_flock_category?: 'GROWING' | 'LAYING';
|
||||
} & ProductionMetrics;
|
||||
|
||||
export type RecordingBW = {
|
||||
avg_weight: number;
|
||||
qty: number;
|
||||
total_weight: number;
|
||||
};
|
||||
|
||||
export type RecordingDepletion = {
|
||||
product_warehouse_id: number;
|
||||
qty: number;
|
||||
@@ -63,7 +68,6 @@ export type Recording = BaseMetadata &
|
||||
BaseRecording & {
|
||||
approval?: BaseApproval;
|
||||
created_user: User;
|
||||
body_weights?: RecordingBW[];
|
||||
depletions?: RecordingDepletion[];
|
||||
stocks?: RecordingStock[];
|
||||
eggs?: RecordingEgg[];
|
||||
@@ -77,10 +81,6 @@ export type NextDayRecording = {
|
||||
|
||||
export type CreateGrowingRecordingPayload = {
|
||||
project_flock_kandang_id: number;
|
||||
body_weights: {
|
||||
avg_weight: number;
|
||||
qty: number;
|
||||
}[];
|
||||
stocks?: {
|
||||
product_warehouse_id: number;
|
||||
qty: number;
|
||||
|
||||
Reference in New Issue
Block a user