From 22f1a32e1b0dc699655f64f63d1a392429235561 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 23 Oct 2025 11:59:22 +0700 Subject: [PATCH] feat(FE-137): integrate API for daily recording with enhanced data structure and validation --- .../recording/form/RecordingForm.schema.ts | 275 +-- .../recording/form/RecordingForm.tsx | 1903 ++++++----------- src/types/api/production/recording.d.ts | 86 +- 3 files changed, 764 insertions(+), 1500 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.schema.ts b/src/components/pages/production/recording/form/RecordingForm.schema.ts index 4b0b37dd..beffa26a 100644 --- a/src/components/pages/production/recording/form/RecordingForm.schema.ts +++ b/src/components/pages/production/recording/form/RecordingForm.schema.ts @@ -1,212 +1,167 @@ import * as Yup from 'yup'; import { RECORDING_FLAG_OPTIONS } from '@/config/constant'; -import { Recording } from '@/types/api/production/recording'; +import { + Recording, + CreateRecordingPayload, +} from '@/types/api/production/recording'; export const RecordingFormSchema = Yup.object({ - flock: Yup.object({ + project_flock_kandang: Yup.object({ value: Yup.number().min(1).required(), label: Yup.string().required(), }).nullable(), - flock_id: Yup.number() + project_flock_kandang_id: Yup.number() .default(0) - .typeError('Flock wajib diisi!') + .typeError('Project Flock Kandang wajib diisi!') .test( - 'is-valid-flock', - 'Flock wajib diisi!', + 'is-valid-project-flock-kandang', + 'Project Flock Kandang wajib diisi!', (value) => value !== undefined && value !== null && value > 0 ) - .required('Flock wajib diisi!'), - location: Yup.object({ - value: Yup.number().min(1).required(), - label: Yup.string().required(), - }).nullable(), - location_id: Yup.number() - .default(0) - .typeError('Lokasi wajib diisi!') - .test( - 'is-valid-location', - 'Lokasi wajib diisi!', - (value) => value !== undefined && value !== null && value > 0 - ) - .required('Lokasi wajib diisi!'), - coop: Yup.object({ - value: Yup.number().min(1).required(), - label: Yup.string().required(), - }).nullable(), - coop_id: Yup.number() - .default(0) - .typeError('Kandang wajib diisi!') - .test( - 'is-valid-coop', - 'Kandang wajib diisi!', - (value) => value !== undefined && value !== null && value > 0 - ) - .required('Kandang wajib diisi!'), - recording_date: Yup.date() - .required('Tanggal recording wajib diisi') - .typeError('Format tanggal tidak valid'), - feed_data: Yup.array() + .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() .of( Yup.object({ - feed_id: Yup.string().required('Nama pakan wajib diisi!'), - feed_qty: Yup.mixed().notRequired(), - feed_stock: Yup.number() - .required('Jumlah pakan yang digunakan wajib diisi!') - .min(1, 'Jumlah pakan minimal 1!') - .typeError('Jumlah pakan yang digunakan harus berupa angka!') - .test( - 'is-not-exceed-qty', - 'Jumlah pakan yang digunakan tidak boleh melebihi stok tersedia!', - function (value) { - const { feed_qty } = this.parent; - if (value === undefined) return true; - if ( - feed_qty === undefined || - feed_qty === '' || - typeof feed_qty !== 'number' - ) - return true; - return value <= feed_qty; - } - ), - }) - ) - .min(1, 'Minimal harus ada 1 data pakan!') - .required('Data pakan wajib diisi!'), - body_weight: Yup.array() - .of( - Yup.object({ - chicken_weight: Yup.number() + weight: Yup.number() .required('Berat ayam wajib diisi!') .min(1, 'Berat ayam minimal 1 gram!') .typeError('Berat ayam harus berupa angka!'), - chicken_count: Yup.number() + qty: Yup.number() .required('Jumlah ayam wajib diisi!') .min(1, 'Jumlah ayam minimal 1 ekor!') - .typeError('Jumlah ayam harus berupa angka!'), - average_chicken_weight: Yup.number() - .required('Rata-rata berat ayam wajib diisi!') - .min(1, 'Rata-rata berat ayam minimal 1 gram!') - .typeError('Rata-rata berat ayam harus berupa angka!'), + .typeError('Jumlah ayam harus berupa angka!') + .default(1), + notes: Yup.string().optional(), }) ) .min(1, 'Minimal harus ada 1 data bobot badan!') .required('Data bobot badan wajib diisi!'), - vaccination: Yup.array() + stocks: Yup.array() .of( Yup.object({ - vaccine_id: Yup.string().required('Nama vaksin wajib diisi!'), - total_stock: Yup.mixed().notRequired(), - used_stock: Yup.number() - .required('Jumlah vaksin yang digunakan wajib diisi!') - .min(1, 'Jumlah vaksin minimal 1!') - .typeError('Jumlah vaksin yang digunakan harus berupa angka!') - .test( - 'is-not-exceed-total', - 'Jumlah vaksin yang digunakan tidak boleh melebihi stok tersedia!', - function (value) { - const { total_stock } = this.parent; - if (value === undefined) return true; - if ( - total_stock === undefined || - total_stock === '' || - typeof total_stock !== 'number' - ) - return true; - return value <= total_stock; - } - ), + product_warehouse_id: Yup.number() + .required('Produk wajib diisi!') + .min(1, 'Produk wajib diisi!') + .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() + .optional() + .min(0, 'Jumlah penggunaan tidak boleh negatif!') + .typeError('Jumlah penggunaan harus berupa angka!'), + notes: Yup.string().optional(), }) ) - .min(1, 'Minimal harus ada 1 data vaksinasi!') - .required('Data vaksinasi wajib diisi!'), - mortality: Yup.array() + .min(1, 'Minimal harus ada 1 data stok!') + .required('Data stok wajib diisi!'), + depletions: Yup.array() .of( Yup.object({ - condition: Yup.mixed() + product_warehouse_id: Yup.number() + .required('Produk wajib diisi!') + .min(1, 'Produk wajib diisi!') + .typeError('Produk harus berupa angka!'), + condition: Yup.string() + .required('Kondisi depletions wajib diisi!') .oneOf( - RECORDING_FLAG_OPTIONS.map((opt) => opt.value), - 'Kondisi tidak valid!' + RECORDING_FLAG_OPTIONS.map((option) => option.value), + 'Kondisi depletions tidak valid!' ) - .required('Kondisi wajib diisi!'), - count: Yup.number() - .required('Jumlah mortalitas wajib diisi!') - .min(1, 'Jumlah mortalitas minimal 1 ekor!') - .typeError('Jumlah mortalitas harus berupa angka!'), + .typeError('Kondisi depletions harus berupa teks!') + .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 mortalitas!') - .required('Data mortalitas wajib diisi!'), + .min(1, 'Minimal harus ada 1 data depletions!') + .required('Data depletions wajib diisi!'), }); export const UpdateRecordingFormSchema = RecordingFormSchema; export type RecordingFormValues = Yup.InferType; +type RecordingFormData = Partial & { + body_weights?: CreateRecordingPayload['body_weights']; + stocks?: CreateRecordingPayload['stocks']; + depletions?: CreateRecordingPayload['depletions']; +}; + export const getRecordingFormInitialValues = ( - initialValues?: Recording + initialValues?: RecordingFormData ): RecordingFormValues => ({ - flock: initialValues?.flock + project_flock_kandang: initialValues?.project_flock_kandang_id ? { - value: initialValues.flock.id, - label: initialValues.flock.name, + value: initialValues.project_flock_kandang_id, + label: `Project Flock Kandang #${initialValues.project_flock_kandang_id}`, } : null, - flock_id: initialValues?.flock?.id ?? 0, - location: initialValues?.location - ? { - value: initialValues.location.id, - label: initialValues.location.name, - } - : null, - location_id: initialValues?.location?.id ?? 0, - coop: initialValues?.coop - ? { - value: initialValues.coop.id, - label: initialValues.coop.name, - } - : null, - coop_id: initialValues?.coop?.id ?? 0, - recording_date: initialValues?.recording_date - ? new Date(initialValues.recording_date) + project_flock_kandang_id: initialValues?.project_flock_kandang_id ?? 0, + record_datetime: initialValues?.record_datetime + ? new Date(initialValues.record_datetime) : new Date(), - feed_data: initialValues?.feed_data - ? initialValues.feed_data.map((feed) => ({ - feed_id: feed.feed_name, - feed_qty: feed.feed_qty, - feed_stock: feed.feed_stock, - })) - : [ - { - feed_id: '', - feed_qty: '', - feed_stock: 0, - }, - ], - body_weight: initialValues?.body_weight ?? [ + status: initialValues?.status ?? 1, + ontime: initialValues?.ontime ?? true, + body_weights: initialValues?.body_weights?.map( + (bw: NonNullable[0]) => ({ + weight: bw.weight, + qty: bw.qty, + notes: bw.notes || '', + }) + ) ?? [ { - chicken_weight: 0, - chicken_count: 0, - average_chicken_weight: 0, + weight: 0, + qty: 1, + notes: '', }, ], - vaccination: initialValues?.vaccination - ? initialValues.vaccination.map((vaccine) => ({ - vaccine_id: vaccine.vaccine_name, - total_stock: vaccine.total_stock, - used_stock: vaccine.used_stock, - })) - : [ - { - vaccine_id: '', - total_stock: '', - used_stock: 0, - }, - ], - mortality: initialValues?.mortality ?? [ + stocks: initialValues?.stocks?.map( + (stock: NonNullable[0]) => ({ + product_warehouse_id: stock.product_warehouse_id, + increase: stock.increase, + decrease: stock.decrease, + usage_amount: stock.usage_amount, + notes: stock.notes, + }) + ) ?? [ { + product_warehouse_id: 0, + increase: 0, + decrease: 0, + usage_amount: 0, + notes: '', + }, + ], + depletions: initialValues?.depletions?.map( + (depletion: NonNullable[0]) => ({ + product_warehouse_id: depletion.product_warehouse_id, + condition: depletion.condition, + total: depletion.total, + notes: depletion.notes, + }) + ) ?? [ + { + product_warehouse_id: 0, condition: '', - count: 0, + total: 0, + notes: '', }, ], }); diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 8c166700..41d9401e 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -1,13 +1,11 @@ 'use client'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { useFormik } from 'formik'; import { Icon } from '@iconify/react'; -import Button from '@/components/Button'; + import TextInput from '@/components/input/TextInput'; -import NumberInput from '@/components/input/NumberInput'; import CheckboxInput from '@/components/input/CheckboxInput'; -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'; @@ -22,15 +20,7 @@ import { UpdateRecordingFormSchema, } from './RecordingForm.schema'; import { useRecordingFormHandlers } from './useRecordingFormHandlers'; -import { ProjectFlockApi } from '@/services/api/production'; -import { isResponseSuccess } from '@/lib/api-helper'; -import { RECORDING_FLAG_OPTIONS } from '@/config/constant'; -import useSWR from 'swr'; -import { ProductWarehouseApi } from '@/services/api/inventory'; -import { ProjectFlock } from '@/types/api/production/project-flock'; -import { Warehouse } from '@/types/api/master-data/warehouse'; -import { LocationApi } from '@/services/api/master-data'; -import FieldMessage from '@/components/helper/FieldMessage'; + import Card from '@/components/Card'; interface RecordingFormProps { @@ -39,15 +29,9 @@ interface RecordingFormProps { } const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { - const [locationSelectInputValue, setLocationSelectInputValue] = useState(''); - const [flockSelectInputValue, setFlockSelectInputValue] = useState(''); - const [selectedProjectFlock, setSelectedProjectFlock] = - useState(null); - const [selectedFeed, setSelectedFeed] = useState([]); - const [selectedWeight, setSelectedWeight] = useState([]); - const [selectedVaccine, setSelectedVaccine] = useState([]); - const [selectedMortality, setSelectedMortality] = useState([]); - const [, setRecordingFormErrorMessage] = useState(''); + const [selectedBodyWeights, setSelectedBodyWeights] = useState([]); + const [selectedStocks, setSelectedStocks] = useState([]); + const [selectedDepletions, setSelectedDepletions] = useState([]); const { deleteModal, @@ -71,57 +55,49 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { validateOnChange: true, validateOnBlur: true, onSubmit: async (values) => { - setRecordingFormErrorMessage(''); const payload: CreateRecordingPayload = { - flock_id: values.flock_id, - location_id: values.location_id, - coop_id: values.coop_id, - recording_date: - values.recording_date instanceof Date - ? values.recording_date.toISOString() + project_flock_kandang_id: values.project_flock_kandang_id, + record_datetime: + values.record_datetime instanceof Date + ? values.record_datetime.toISOString() : '', - feed_data: (values.feed_data ?? []).map((p) => ({ - feed_id: p.feed_id, - feed_qty: - typeof p.feed_qty === 'number' - ? p.feed_qty - : parseFloat(String(p.feed_qty)) || 0, - feed_stock: - typeof p.feed_stock === 'number' - ? p.feed_stock - : parseFloat(String(p.feed_stock)) || 0, + status: values.status, + ontime: values.ontime, + body_weights: (values.body_weights ?? []).map((bw) => ({ + weight: + typeof bw.weight === 'number' + ? bw.weight + : parseFloat(String(bw.weight)) || 0, + qty: + typeof bw.qty === 'number' + ? bw.qty + : parseFloat(String(bw.qty)) || 0, + notes: bw.notes, })), - body_weight: (values.body_weight ?? []).map((b) => ({ - chicken_weight: - typeof b.chicken_weight === 'number' - ? b.chicken_weight - : parseFloat(String(b.chicken_weight)) || 0, - chicken_count: - typeof b.chicken_count === 'number' - ? b.chicken_count - : parseFloat(String(b.chicken_count)) || 0, - average_chicken_weight: - typeof b.average_chicken_weight === 'number' - ? b.average_chicken_weight - : parseFloat(String(b.average_chicken_weight)) || 0, + stocks: (values.stocks ?? []).map((stock) => ({ + 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: + typeof stock.usage_amount === 'number' + ? stock.usage_amount + : parseFloat(String(stock.usage_amount)) || 0, + notes: stock.notes, })), - vaccination: (values.vaccination ?? []).map((v) => ({ - vaccine_id: v.vaccine_id, - total_stock: - typeof v.total_stock === 'number' - ? v.total_stock - : parseFloat(String(v.total_stock)) || 0, - used_stock: - typeof v.used_stock === 'number' - ? v.used_stock - : parseFloat(String(v.used_stock)) || 0, - })), - mortality: (values.mortality ?? []).map((m) => ({ - condition: m.condition, - count: - typeof m.count === 'number' - ? m.count - : parseFloat(String(m.count)) || 0, + depletions: (values.depletions ?? []).map((depletion) => ({ + product_warehouse_id: depletion.product_warehouse_id, + condition: depletion.condition, + total: + typeof depletion.total === 'number' + ? depletion.total + : parseFloat(String(depletion.total)) || 0, + notes: depletion.notes, })), }; @@ -136,465 +112,109 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }, }); - // Locations - const locationsUrl = `${LocationApi.basePath}?${new URLSearchParams({ search: locationSelectInputValue }).toString()}`; - const { data: locations, isLoading: isLoadingLocations } = useSWR( - locationsUrl, - LocationApi.getAllFetcher - ); - - // Project Flocks - const projectFlocksUrl = useMemo(() => { - if (!formik.values.location_id) return null; - const params = new URLSearchParams({ - search: flockSelectInputValue, - location_id: formik.values.location_id.toString(), - }); - return `${ProjectFlockApi.basePath}?${params.toString()}`; - }, [formik.values.location_id, flockSelectInputValue]); - - const { data: projectFlocks, isLoading: isLoadingFlocks } = useSWR( - projectFlocksUrl, - ProjectFlockApi.getAllFetcher - ); - - // Pakan Products - const pakanUrl = useMemo(() => { - if (!formik.values.location_id) return null; - const params = new URLSearchParams({ - flag: 'PAKAN', - search: '', - location_id: formik.values.location_id.toString(), - }); - return `${ProductWarehouseApi.basePath}?${params.toString()}`; - }, [formik.values.location_id]); - - const { data: pakanProducts, isLoading: isLoadingPakan } = useSWR( - pakanUrl, - ProductWarehouseApi.getAllFetcher - ); - - // OVK Products - const ovkUrl = useMemo(() => { - if (!formik.values.location_id) return null; - const params = new URLSearchParams({ - flag: 'OVK', - search: '', - location_id: formik.values.location_id.toString(), - }); - return `${ProductWarehouseApi.basePath}?${params.toString()}`; - }, [formik.values.location_id]); - - const { data: ovkProducts, isLoading: isLoadingOvk } = useSWR( - ovkUrl, - ProductWarehouseApi.getAllFetcher - ); - - // COMPUTED VALUES - const buildWarehouseLabel = useCallback((warehouse: Warehouse) => { - const parts: string[] = [warehouse.name]; - - if ('kandang' in warehouse && warehouse.kandang) { - parts.push(warehouse.kandang.name); - } - - if ('location' in warehouse && warehouse.location) { - parts.push(warehouse.location.name); - } - - if (warehouse.area) { - parts.push(warehouse.area.name); - } - - return parts.join(' - '); - }, []); - - const locationOptions = isResponseSuccess(locations) - ? locations.data.map((loc) => ({ value: loc.id, label: loc.name })) - : []; - - const flockOptions = isResponseSuccess(projectFlocks) - ? projectFlocks.data.map((flock) => ({ - value: flock.id, - label: flock.flock.name, - })) - : []; - - const coopOptions = useMemo(() => { - if (!selectedProjectFlock || !selectedProjectFlock.kandangs) return []; - return selectedProjectFlock.kandangs.map((kandang) => ({ - value: kandang.id, - label: kandang.name, - })); - }, [selectedProjectFlock]); - - const filteredPakanProducts = useMemo(() => { - if (!isResponseSuccess(pakanProducts) || !formik.values.location_id) - return []; - - return pakanProducts.data.filter((product) => { - const warehouse = product.warehouse; - - const hasLocationMatch = - 'location' in warehouse && warehouse.location - ? warehouse.location.id === formik.values.location_id - : false; - - const hasPakanFlag = product.product.flags?.includes('PAKAN'); - - return hasLocationMatch && hasPakanFlag; - }); - }, [pakanProducts, formik.values.location_id]); - - const pakanOptions = useMemo( - () => - filteredPakanProducts.map((product) => ({ - value: product.id, - label: `${product.product.name} - ${buildWarehouseLabel(product.warehouse)} (Stock: ${product.quantity.toLocaleString('id-ID')})`, - })), - [filteredPakanProducts, buildWarehouseLabel] - ); - - const pakanStockMap = useMemo(() => { - const map = new Map(); - filteredPakanProducts.forEach((product) => { - map.set(product.id, product.quantity); - }); - return map; - }, [filteredPakanProducts]); - - const filteredOvkProducts = useMemo(() => { - if (!isResponseSuccess(ovkProducts) || !formik.values.location_id) - return []; - - return ovkProducts.data.filter((product) => { - const warehouse = product.warehouse; - - // Validate location match - const hasLocationMatch = - 'location' in warehouse && warehouse.location - ? warehouse.location.id === formik.values.location_id - : false; - - // Validate product has OVK flag - const hasOvkFlag = product.product.flags?.includes('OVK'); - - return hasLocationMatch && hasOvkFlag; - }); - }, [ovkProducts, formik.values.location_id]); - - const ovkOptions = useMemo( - () => - filteredOvkProducts.map((product) => ({ - value: product.id, - label: `${product.product.name} - ${buildWarehouseLabel(product.warehouse)} (Stock: ${product.quantity.toLocaleString('id-ID')})`, - })), - [filteredOvkProducts, buildWarehouseLabel] - ); - - const ovkStockMap = useMemo(() => { - const map = new Map(); - filteredOvkProducts.forEach((product) => { - map.set(product.id, product.quantity); - }); - return map; - }, [filteredOvkProducts]); - - // EFFECTS - useEffect(() => { - if (initialValues?.flock && isResponseSuccess(projectFlocks)) { - const flock = projectFlocks.data.find( - (f) => f.id === initialValues.flock.id - ); - if (flock) { - setSelectedProjectFlock(flock); - } - } - }, [initialValues, projectFlocks]); - - // Auto-calculate average weight when chicken weight or count changes - useEffect(() => { - if (formik.values.body_weight) { - const updatedBodyWeight = formik.values.body_weight.map((weight) => ({ - ...weight, - average_chicken_weight: - weight.chicken_count > 0 - ? Math.round(weight.chicken_weight / weight.chicken_count) - : 0, - })); - - // Only update if values are different to avoid infinite loops - const hasChanges = updatedBodyWeight.some( - (updated, idx) => - updated.average_chicken_weight !== - formik.values.body_weight[idx]?.average_chicken_weight - ); - - if (hasChanges) { - formik.setFieldValue('body_weight', updatedBodyWeight); - } - } - }, [ - formik.values.body_weight?.map((w) => w.chicken_weight), - formik.values.body_weight?.map((w) => w.chicken_count), - ]); - // EVENT HANDLERS - Select Inputs - const locationChangeHandler = (val: OptionType | OptionType[] | null) => { - const locationValue = (val as OptionType)?.value; - - formik.setFieldValue('location', val, false); - formik.setFieldValue('location_id', locationValue || 0, false); - - formik.setFieldValue('flock', null, false); - formik.setFieldValue('flock_id', 0, false); - formik.setFieldValue('coop', null, false); - formik.setFieldValue('coop_id', 0, false); - setSelectedProjectFlock(null); - setFlockSelectInputValue(''); + const projectFlockChangeHandler = (value: string) => { + const projectFlockId = parseInt(value) || 0; + formik.setFieldValue('project_flock_kandang_id', projectFlockId, false); }; - const flockChangeHandler = (val: OptionType | OptionType[] | null) => { - const flockValue = (val as OptionType)?.value; - - const selected = isResponseSuccess(projectFlocks) - ? projectFlocks.data.find((flock) => flock.id === flockValue) - : null; - - setSelectedProjectFlock(selected || null); - - formik.setFieldValue('flock', val, false); - formik.setFieldValue('flock_id', flockValue || 0, false); - - formik.setFieldValue('coop', null, false); - formik.setFieldValue('coop_id', 0, false); - }; - - const coopChangeHandler = (val: OptionType | OptionType[] | null) => { - const coopValue = (val as OptionType)?.value; - - formik.setFieldValue('coop', val, false); - formik.setFieldValue('coop_id', coopValue || 0, false); - }; - - // EVENT HANDLERS - Feed Data - const addFeedData = () => { - const newFeedData = [ - ...(formik.values.feed_data || []), - { - feed: null, - feed_id: '', - feed_qty: '', - feed_stock: 0, - }, - ]; - formik.setFieldValue('feed_data', newFeedData); - }; - - const removeFeedData = (idx: number) => { - const updatedFeedData = formik.values.feed_data?.filter( - (_, i) => i !== idx - ); - formik.setFieldValue('feed_data', updatedFeedData); - }; - - const removeSelectedFeedData = () => { - const updatedFeedData = formik.values.feed_data?.filter( - (_, idx) => !selectedFeed.includes(idx) - ); - formik.setFieldValue('feed_data', updatedFeedData); - setSelectedFeed([]); - }; - - // EVENT HANDLERS - Body Weight + // EVENT HANDLERS - Body Weights const addBodyWeight = () => { - const newBodyWeight = [ - ...(formik.values.body_weight || []), + const newBodyWeights = [ + ...(formik.values.body_weights || []), { - chicken_weight: 0, - chicken_count: 0, - average_chicken_weight: 0, + weight: 0, + qty: 1, + notes: '', }, ]; - formik.setFieldValue('body_weight', newBodyWeight); + formik.setFieldValue('body_weights', newBodyWeights); }; - // Handle calculation when chicken_weight changes - const handleChickenWeightChange = useCallback( - (idx: number, value: number) => { - formik.setFieldValue(`body_weight.${idx}.chicken_weight`, value); - - const currentWeight = formik.values.body_weight?.[idx]; - if (currentWeight) { - const chickenCount = currentWeight.chicken_count; - if (chickenCount > 0 && value > 0) { - const averageWeight = Math.round(value / chickenCount); - formik.setFieldValue( - `body_weight.${idx}.average_chicken_weight`, - averageWeight - ); - } else { - formik.setFieldValue(`body_weight.${idx}.average_chicken_weight`, ''); - } - } - }, - [formik] - ); - - // Handle calculation when chicken_count changes - const handleChickenCountChange = useCallback( - (idx: number, value: number) => { - formik.setFieldValue(`body_weight.${idx}.chicken_count`, value); - - const currentWeight = formik.values.body_weight?.[idx]; - if (currentWeight) { - const chickenWeight = currentWeight.chicken_weight; - if (chickenWeight > 0 && value > 0) { - const averageWeight = Math.round(chickenWeight / value); - formik.setFieldValue( - `body_weight.${idx}.average_chicken_weight`, - averageWeight - ); - } else { - formik.setFieldValue(`body_weight.${idx}.average_chicken_weight`, ''); - } - } - }, - [formik] - ); - - // Handle calculation when average_weight changes - const handleAverageWeightChange = useCallback( - (idx: number, value: number) => { - formik.setFieldValue(`body_weight.${idx}.average_chicken_weight`, value); - - const currentWeight = formik.values.body_weight?.[idx]; - if (currentWeight) { - const chickenCount = currentWeight.chicken_count; - if (chickenCount > 0 && value > 0) { - const totalWeight = value * chickenCount; - formik.setFieldValue( - `body_weight.${idx}.chicken_weight`, - totalWeight - ); - } else if (value === 0) { - formik.setFieldValue(`body_weight.${idx}.chicken_weight`, ''); - } - } - }, - [formik] - ); - - // Create wrapper handlers that match NumberInput's onChange signature - const handleChickenWeightChangeWrapper = useCallback( - (idx: number) => (e: React.ChangeEvent) => { - const value = parseFloat(e.target.value) || 0; - handleChickenWeightChange(idx, value); - }, - [handleChickenWeightChange] - ); - - const handleChickenCountChangeWrapper = useCallback( - (idx: number) => (e: React.ChangeEvent) => { - const value = parseFloat(e.target.value) || 0; - handleChickenCountChange(idx, value); - }, - [handleChickenCountChange] - ); - - const handleAverageWeightChangeWrapper = useCallback( - (idx: number) => (e: React.ChangeEvent) => { - const value = parseFloat(e.target.value) || 0; - handleAverageWeightChange(idx, value); - }, - [handleAverageWeightChange] - ); - const removeBodyWeight = (idx: number) => { - const updatedBodyWeight = formik.values.body_weight?.filter( + const updatedBodyWeights = formik.values.body_weights?.filter( (_, i) => i !== idx ); - formik.setFieldValue('body_weight', updatedBodyWeight); + formik.setFieldValue('body_weights', updatedBodyWeights); }; - const removeSelectedBodyWeight = () => { - const updatedBodyWeight = formik.values.body_weight?.filter( - (_, idx) => !selectedWeight.includes(idx) + const removeSelectedBodyWeights = () => { + const updatedBodyWeights = formik.values.body_weights?.filter( + (_, idx) => !selectedBodyWeights.includes(idx) ); - formik.setFieldValue('body_weight', updatedBodyWeight); - setSelectedWeight([]); + formik.setFieldValue('body_weights', updatedBodyWeights); + setSelectedBodyWeights([]); }; - // EVENT HANDLERS - Vaccination - const addVaccination = () => { - const newVaccination = [ - ...(formik.values.vaccination || []), + // EVENT HANDLERS - Stocks + const addStock = () => { + const newStocks = [ + ...(formik.values.stocks || []), { - vaccine: null, - vaccine_id: '', - total_stock: '', - used_stock: 0, + product_warehouse_id: 0, + increase: 0, + decrease: 0, + usage_amount: 0, + notes: '', }, ]; - formik.setFieldValue('vaccination', newVaccination); + formik.setFieldValue('stocks', newStocks); }; - const removeVaccination = (idx: number) => { - const updatedVaccination = formik.values.vaccination?.filter( - (_, i) => i !== idx + const removeStock = (idx: number) => { + const updatedStocks = formik.values.stocks?.filter((_, i) => i !== idx); + formik.setFieldValue('stocks', updatedStocks); + }; + + const removeSelectedStocks = () => { + const updatedStocks = formik.values.stocks?.filter( + (_, idx) => !selectedStocks.includes(idx) ); - formik.setFieldValue('vaccination', updatedVaccination); + formik.setFieldValue('stocks', updatedStocks); + setSelectedStocks([]); }; - const removeSelectedVaccination = () => { - const updatedVaccination = formik.values.vaccination?.filter( - (_, idx) => !selectedVaccine.includes(idx) - ); - formik.setFieldValue('vaccination', updatedVaccination); - setSelectedVaccine([]); - }; - - // EVENT HANDLERS - Mortality - const addMortality = () => { - const newMortality = [ - ...(formik.values.mortality || []), + // EVENT HANDLERS - Depletions + const addDepletion = () => { + const newDepletions = [ + ...(formik.values.depletions || []), { - condition: RECORDING_FLAG_OPTIONS[0].value, - count: 0, + product_warehouse_id: 0, + condition: '', + total: 0, + notes: '', }, ]; - formik.setFieldValue('mortality', newMortality); + formik.setFieldValue('depletions', newDepletions); }; - const removeMortality = (idx: number) => { - const updatedMortality = formik.values.mortality?.filter( + const removeDepletion = (idx: number) => { + const updatedDepletions = formik.values.depletions?.filter( (_, i) => i !== idx ); - formik.setFieldValue('mortality', updatedMortality); + formik.setFieldValue('depletions', updatedDepletions); }; - const removeSelectedMortality = () => { - const updatedMortality = formik.values.mortality?.filter( - (_, idx) => !selectedMortality.includes(idx) + const removeSelectedDepletions = () => { + const updatedDepletions = formik.values.depletions?.filter( + (_, idx) => !selectedDepletions.includes(idx) ); - formik.setFieldValue('mortality', updatedMortality); - setSelectedMortality([]); + formik.setFieldValue('depletions', updatedDepletions); + setSelectedDepletions([]); }; // HELPER FUNCTIONS const isRepeaterInputError = < - T extends 'feed_data' | 'body_weight' | 'vaccination' | 'mortality', + T extends 'body_weights' | 'stocks' | 'depletions', >( arrayName: T, - column: T extends 'feed_data' - ? keyof RecordingFormValues['feed_data'][0] - : T extends 'body_weight' - ? keyof RecordingFormValues['body_weight'][0] - : T extends 'vaccination' - ? keyof RecordingFormValues['vaccination'][0] - : T extends 'mortality' - ? keyof RecordingFormValues['mortality'][0] - : never, + column: T extends 'body_weights' + ? keyof RecordingFormValues['body_weights'][0] + : T extends 'stocks' + ? keyof RecordingFormValues['stocks'][0] + : T extends 'depletions' + ? keyof RecordingFormValues['depletions'][0] + : never, idx: number ) => { if ( @@ -642,114 +262,88 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { body: 'flex flex-col gap-6', }} > -
- + { - locationChangeHandler(val); - setTimeout(() => { - formik.setFieldTouched('location', true); - formik.setFieldTouched('location_id', true); - }, 0); + label='Project Flock Kandang ID' + type='number' + name='project_flock_kandang_id' + value={formik.values.project_flock_kandang_id?.toString() || ''} + onChange={(e) => { + const value = e.target.value; + formik.setFieldValue( + 'project_flock_kandang_id', + parseInt(value) || 0 + ); + projectFlockChangeHandler(value); }} - options={locationOptions} - onInputChange={setLocationSelectInputValue} - isLoading={isLoadingLocations} + onBlur={formik.handleBlur} isError={ - formik.touched.location_id && - Boolean(formik.errors.location_id) + formik.touched.project_flock_kandang_id && + Boolean(formik.errors.project_flock_kandang_id) } - errorMessage={formik.errors.location_id as string} - isDisabled={type === 'detail'} - isClearable - placeholder='Pilih lokasi terlebih dahulu' + errorMessage={formik.errors.project_flock_kandang_id as string} + readOnly={type === 'detail'} + placeholder='Masukkan project flock kandang ID' /> { - const date = e.target.value ? new Date(e.target.value) : null; - formik.setFieldValue('recording_date', date); + const datetime = e.target.value + ? new Date(e.target.value) + : null; + formik.setFieldValue('record_datetime', datetime); }} onBlur={formik.handleBlur} isError={ - formik.touched.recording_date && - Boolean(formik.errors.recording_date) + formik.touched.record_datetime && + Boolean(formik.errors.record_datetime) } - errorMessage={formik.errors.recording_date as string} + errorMessage={formik.errors.record_datetime as string} readOnly={type === 'detail'} /> - { - flockChangeHandler(val); - setTimeout(() => { - formik.setFieldTouched('flock', true); - formik.setFieldTouched('flock_id', true); - }, 0); + { + const value = e.target.value; + formik.setFieldValue('status', parseInt(value) || 0); }} - options={flockOptions} - onInputChange={setFlockSelectInputValue} - isLoading={isLoadingFlocks} - isError={ - formik.touched.flock_id && Boolean(formik.errors.flock_id) - } - errorMessage={formik.errors.flock_id as string} - isDisabled={type === 'detail' || !formik.values.location_id} - isClearable - placeholder={ - !formik.values.location_id - ? 'Pilih lokasi terlebih dahulu' - : 'Pilih Flock' - } + 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)' /> - { - coopChangeHandler(val); - setTimeout(() => { - formik.setFieldTouched('coop', true); - formik.setFieldTouched('coop_id', true); - }, 0); + { + formik.setFieldValue('ontime', e.target.checked); }} - options={coopOptions} - isError={ - formik.touched.coop_id && Boolean(formik.errors.coop_id) - } - errorMessage={formik.errors.coop_id as string} - isDisabled={type === 'detail' || !selectedProjectFlock} - isClearable - placeholder={ - !selectedProjectFlock - ? 'Pilih flock terlebih dahulu' - : 'Pilih Kandang' - } + disabled={type === 'detail'} />
- - {/* Feed Data Table */} + {/* Body Weights Table */} {
0 + formik.values.body_weights?.length === + selectedBodyWeights.length && + formik.values.body_weights?.length > 0 } onChange={(e) => { if (e.target.checked) { - setSelectedFeed( - formik.values.feed_data?.map( + setSelectedBodyWeights( + formik.values.body_weights?.map( (_, idx) => idx ) ?? [] ); } else { - setSelectedFeed([]); + setSelectedBodyWeights([]); } }} naked={true} @@ -787,248 +381,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { )} - Nama Pakan - - * - - - Total Stock pada saat ini - - Jumlah Stock yang digunakan - - * - - - {type !== 'detail' && Action} - - - - {formik.values.feed_data?.map((feed, idx) => ( - - {type !== 'detail' && ( - -
- { - if (e.target.checked) { - setSelectedFeed([...selectedFeed, idx]); - } else { - setSelectedFeed( - selectedFeed.filter((i) => i !== idx) - ); - } - }} - naked={true} - size='sm' - /> - -
- - )} - - - Number(opt.value) === Number(feed.feed_id) - ) ?? null - } - onChange={(val) => { - const productWarehouseId = - (val as OptionType)?.value ?? 0; - const stock = productWarehouseId - ? (pakanStockMap.get( - productWarehouseId as number - ) ?? '') - : ''; - - formik.setFieldValue(`feed_data.${idx}.feed`, val); - formik.setFieldValue( - `feed_data.${idx}.feed_id`, - productWarehouseId || '' - ); - formik.setFieldValue( - `feed_data.${idx}.feed_qty`, - stock - ); - formik.setFieldValue( - `feed_data.${idx}.feed_stock`, - 0 - ); - setTimeout(() => { - formik.setFieldTouched( - `feed_data.${idx}.feed`, - true - ); - formik.setFieldTouched( - `feed_data.${idx}.feed_id`, - true - ); - }, 0); - }} - options={pakanOptions} - isLoading={isLoadingPakan} - isError={ - isRepeaterInputError('feed_data', 'feed_id', idx) - .isError - } - errorMessage={ - isRepeaterInputError('feed_data', 'feed_id', idx) - .errorMessage - } - isDisabled={type === 'detail'} - isClearable - className={{ - wrapper: 'w-full min-w-52 md:min-w-72 lg:min-w-80', - }} - /> - - - - - - - - {type !== 'detail' && ( - -
- - -
- - )} - - ))} - - -
- {type !== 'detail' && ( -
- {selectedFeed.length > 0 && ( - - )} - -
- )} -
- - {/* Body Weight Table */} - -
- - - - {type !== 'detail' && ( - - )} - - + {type !== 'detail' && } - {formik.values.body_weight?.map((weight, idx) => ( - + {formik.values.body_weights?.map((bw, idx) => ( + {type !== 'detail' && ( )} - - - {type !== 'detail' && ( - - )} - - ))} - -
-
- 0 - } - onChange={(e) => { - if (e.target.checked) { - setSelectedWeight( - formik.values.body_weight?.map( - (_, idx) => idx - ) ?? [] - ); - } else { - setSelectedWeight([]); - } - }} - naked={true} - size='sm' - /> -
-
- Berat (Gram) + Berat Ayam (gram) { * - Rata-rata berat Ayam - - * - - CatatanAction
-
+
{ if (e.target.checked) { - setSelectedWeight([...selectedWeight, idx]); + setSelectedBodyWeights([ + ...selectedBodyWeights, + idx, + ]); } else { - setSelectedWeight( - selectedWeight.filter((i) => i !== idx) + setSelectedBodyWeights( + selectedBodyWeights.filter((i) => i !== idx) ); } }} naked={true} size='sm' /> -
- - - - -
- -
-
-
- - -
-
-
- {type !== 'detail' && ( -
- {selectedWeight.length > 0 && ( - - )} - -
- )} -
- - {/* Vaccination Table */} - -
- - - - {type !== 'detail' && ( - - )} - - - - {type !== 'detail' && } - - - - {formik.values.vaccination?.map((vaccine, idx) => ( - - {type !== 'detail' && ( - - )} - - + {type !== 'detail' && ( )} @@ -1458,39 +513,32 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
-
- 0 - } - onChange={(e) => { - if (e.target.checked) { - setSelectedVaccine( - formik.values.vaccination?.map( - (_, idx) => idx - ) ?? [] - ); - } else { - setSelectedVaccine([]); - } - }} - naked={true} - size='sm' - /> -
-
- Name Vaksin - - * - - Total Stock pada saat ini - Jumlah Stock yang digunakan - - * - - Action
-
- { - if (e.target.checked) { - setSelectedVaccine([...selectedVaccine, idx]); - } else { - setSelectedVaccine( - selectedVaccine.filter((i) => i !== idx) - ); - } - }} - naked={true} - size='sm' - /> - -
-
- - Number(opt.value) === Number(vaccine.vaccine_id) - ) ?? null + typeof bw.weight === 'number' + ? bw.weight.toString() + : bw.weight } - onChange={(val) => { - const productWarehouseId = - (val as OptionType)?.value ?? 0; - const stock = productWarehouseId - ? (ovkStockMap.get( - productWarehouseId as number - ) ?? '') - : ''; - formik.setFieldValue( - `vaccination.${idx}.vaccine`, - val - ); - formik.setFieldValue( - `vaccination.${idx}.vaccine_id`, - productWarehouseId || '' - ); - formik.setFieldValue( - `vaccination.${idx}.total_stock`, - stock - ); - formik.setFieldValue( - `vaccination.${idx}.used_stock`, - 0 - ); - // Set touched after setting values to trigger validation - setTimeout(() => { - formik.setFieldTouched( - `vaccination.${idx}.vaccine`, - true - ); - formik.setFieldTouched( - `vaccination.${idx}.vaccine_id`, - true - ); - }, 0); - }} - options={ovkOptions} - isLoading={isLoadingOvk} + onChange={formik.handleChange} + onBlur={formik.handleBlur} isError={ - isRepeaterInputError( - 'vaccination', - 'vaccine_id', - idx - ).isError + isRepeaterInputError('body_weights', 'weight', idx) + .isError } errorMessage={ - isRepeaterInputError( - 'vaccination', - 'vaccine_id', - idx - ).errorMessage + isRepeaterInputError('body_weights', 'weight', idx) + .errorMessage } - isDisabled={type === 'detail'} - isClearable + readOnly={type === 'detail'} className={{ - wrapper: 'w-full min-w-52 md:min-w-72 lg:min-w-80', + wrapper: 'w-full min-w-32', }} + placeholder='Berat ayam (gram)' /> - - + + -
- - -
+
{type !== 'detail' && ( -
- {selectedVaccine.length > 0 && ( - + + Hapus Terpilih ({selectedBodyWeights.length}) + )} - + + Tambah Bobot Badan +
)}
- {/* Mortality Table */} + {/* Stocks Table */} {
0 + formik.values.stocks?.length === + selectedStocks.length && + formik.values.stocks?.length > 0 } onChange={(e) => { if (e.target.checked) { - setSelectedMortality( - formik.values.mortality?.map( - (_, idx) => idx - ) ?? [] + setSelectedStocks( + formik.values.stocks?.map((_, idx) => idx) ?? + [] ); } else { - setSelectedMortality([]); + setSelectedStocks([]); } }} naked={true} @@ -1528,16 +575,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { )} - Kondisi/Alasan Mortalitas - - * - - - - Jumlah + Product Warehouse ID { * + Penambahan + Pengurangan + Jumlah Pakai + Catatan {type !== 'detail' && Action} - {formik.values.mortality?.map((mortality, idx) => ( - + {formik.values.stocks?.map((stock, idx) => ( + {type !== 'detail' && ( -
+
{ if (e.target.checked) { - setSelectedMortality([ - ...selectedMortality, - idx, - ]); + setSelectedStocks([...selectedStocks, idx]); } else { - setSelectedMortality( - selectedMortality.filter((i) => i !== idx) + setSelectedStocks( + selectedStocks.filter((i) => i !== idx) ); } }} naked={true} size='sm' /> -
)} - opt.value === mortality.condition - )} - onChange={(val) => { - formik.setFieldTouched( - `mortality.${idx}.condition`, - true - ); - formik.setFieldValue( - `mortality.${idx}.condition`, - (val as OptionType)?.value - ); - }} + name={`stocks.${idx}.product_warehouse_id`} + type='number' + value={stock.product_warehouse_id?.toString() || ''} + onChange={formik.handleChange} + onBlur={formik.handleBlur} isError={ - isRepeaterInputError('mortality', 'condition', idx) - .isError + isRepeaterInputError( + 'stocks', + 'product_warehouse_id', + idx + ).isError } errorMessage={ - isRepeaterInputError('mortality', 'condition', idx) - .errorMessage + isRepeaterInputError( + 'stocks', + 'product_warehouse_id', + idx + ).errorMessage } - options={RECORDING_FLAG_OPTIONS} - isDisabled={type === 'detail'} - isClearable + readOnly={type === 'detail'} className={{ - wrapper: 'w-full min-w-52 md:min-w-72 lg:min-w-80', + wrapper: 'w-full min-w-32', }} + placeholder='Product Warehouse ID' /> - + + + + + + + + + {type !== 'detail' && ( -
- - -
+ )} @@ -1656,32 +751,255 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{type !== 'detail' && ( -
- {selectedMortality.length > 0 && ( - + + Hapus Terpilih ({selectedStocks.length}) + )} - + + Tambah Stok + +
+ )} + + + {/* Depletions Table */} + +
+ + + + {type !== 'detail' && ( + + )} + + + + + {type !== 'detail' && } + + + + {formik.values.depletions?.map((depletion, idx) => ( + + {type !== 'detail' && ( + + )} + + + + + {type !== 'detail' && ( + + )} + + ))} + +
+
+ 0 + } + onChange={(e) => { + if (e.target.checked) { + setSelectedDepletions( + formik.values.depletions?.map( + (_, idx) => idx + ) ?? [] + ); + } else { + setSelectedDepletions([]); + } + }} + naked={true} + size='sm' + /> +
+
+ Product Warehouse ID + + * + + + Kondisi + + * + + + Total + + * + + CatatanAction
+
+ { + if (e.target.checked) { + setSelectedDepletions([ + ...selectedDepletions, + idx, + ]); + } else { + setSelectedDepletions( + selectedDepletions.filter((i) => i !== idx) + ); + } + }} + naked={true} + size='sm' + /> +
+
+ + + + + + + + + +
+
+ {type !== 'detail' && ( +
+ {selectedDepletions.length > 0 && ( + + )} +
)}
@@ -1697,7 +1015,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } onDelete={deleteRecordingClickHandler} /> - {recordingFormErrorMessage && (