diff --git a/src/app/production/recording/add/layout.tsx b/src/app/production/recording/add/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/production/recording/add/layout.tsx @@ -0,0 +1,11 @@ +import SuspenseHelper from '@/components/helper/SuspenseHelper'; + +const Layout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return {children}; +}; + +export default Layout; diff --git a/src/app/production/recording/grading/add/page.tsx b/src/app/production/recording/grading/add/page.tsx new file mode 100644 index 00000000..9b918d98 --- /dev/null +++ b/src/app/production/recording/grading/add/page.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; +import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm'; +import { RecordingApi } from '@/services/api/production'; +import { isResponseSuccess } from '@/lib/api-helper'; + +const AddGrading = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const recordingId = searchParams.get('recording_id'); + + const { data: recording, isLoading: isLoadingRecording } = useSWR( + recordingId && recordingId !== 'new' ? [recordingId] : null, + ([id]) => RecordingApi.getSingle(parseInt(id)) + ); + + if ( + recordingId && + recordingId !== 'new' && + !isLoadingRecording && + (!recording || !isResponseSuccess(recording)) + ) { + router.replace('/404'); + return; + } + + return ( +
+ {recordingId && recordingId !== 'new' && isLoadingRecording && ( + + )} + {(!recordingId || + recordingId === 'new' || + (!isLoadingRecording && recording && isResponseSuccess(recording))) && ( + + )} +
+ ); +}; + +export default AddGrading; diff --git a/src/app/production/recording/grading/detail/edit/page.tsx b/src/app/production/recording/grading/detail/edit/page.tsx new file mode 100644 index 00000000..0a65f528 --- /dev/null +++ b/src/app/production/recording/grading/detail/edit/page.tsx @@ -0,0 +1,53 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; +import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm'; +import { RecordingApi } from '@/services/api/production'; +import { isResponseSuccess } from '@/lib/api-helper'; + +const EditGrading = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const recordingId = searchParams.get('recordingId'); + const gradingId = searchParams.get('gradingId'); + + const { data: recording, isLoading: isLoadingRecording } = useSWR( + recordingId ? [recordingId] : null, + ([id]) => RecordingApi.getSingle(parseInt(id)) + ); + + if (!recordingId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingRecording && (!recording || !isResponseSuccess(recording))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingRecording && ( + + )} + {!isLoadingRecording && recording && isResponseSuccess(recording) && ( + egg.id === parseInt(gradingId || '0') + )} + /> + )} +
+ ); +}; + +export default EditGrading; diff --git a/src/app/production/recording/grading/detail/page.tsx b/src/app/production/recording/grading/detail/page.tsx new file mode 100644 index 00000000..6a5fbcba --- /dev/null +++ b/src/app/production/recording/grading/detail/page.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; +import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm'; +import { RecordingApi } from '@/services/api/production'; +import { isResponseSuccess } from '@/lib/api-helper'; + +const DetailGrading = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const gradingId = searchParams.get('gradingId'); + + const { data: grading, isLoading: isLoadingGrading } = useSWR( + gradingId ? [gradingId] : null, + ([id]) => RecordingApi.getSingle(parseInt(id)) + ); + + if (!gradingId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingGrading && (!grading || !isResponseSuccess(grading))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingGrading && ( + + )} + {!isLoadingGrading && grading && isResponseSuccess(grading) && ( + egg.id === parseInt(gradingId) + )} + /> + )} +
+ ); +}; + +export default DetailGrading; diff --git a/src/app/production/recording/grading/layout.tsx b/src/app/production/recording/grading/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/production/recording/grading/layout.tsx @@ -0,0 +1,11 @@ +import SuspenseHelper from '@/components/helper/SuspenseHelper'; + +const Layout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return {children}; +}; + +export default Layout; diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 0a916fdb..6cf254e7 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -1,132 +1,63 @@ 'use client'; -import { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useState, useMemo, useEffect } from 'react'; +import { RefObject } from 'react'; +import useSWR from 'swr'; import { Icon } from '@iconify/react'; -import { SortingState } from '@tanstack/react-table'; -import { cn } from '@/lib/helper'; +import { SortingState, CellContext } from '@tanstack/react-table'; +import { cn, formatDate } from '@/lib/helper'; import { useModal } from '@/components/Modal'; +import Modal from '@/components/Modal'; import Button from '@/components/Button'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; import { OptionType } from '@/components/input/SelectInput'; +import SelectInput from '@/components/input/SelectInput'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import { ROWS_OPTIONS } from '@/config/constant'; -import { TableToolbar } from '@/components/table/TableToolbar'; -import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector'; import Table from '@/components/Table'; import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; -import { type CellContext } from '@tanstack/react-table'; import { type Recording } from '@/types/api/production/recording'; - -const dummyRecordings: Recording[] = [ - { - id: 1, - flock: { - id: 1, - name: 'Flock Recording 1', - created_at: '2024-01-01', - updated_at: '2024-01-01', - created_user: { - id: 1, - id_user: 1, - email: 'admin@example.com', - name: 'Admin', - }, - }, - recording_date: '2024-01-01', - location: { - id: 1, - name: 'Location 1', - address: 'Jl. Contoh No. 1', - area: { - id: 1, - name: 'Area 1', - }, - created_at: '2024-01-01', - updated_at: '2024-01-01', - created_user: { - id: 1, - id_user: 1, - email: 'admin@example.com', - name: 'Admin', - }, - }, - coop: { - id: 1, - capacity: 1000, - name: 'Coop 1', - status: 'ACTIVE', - location: { - id: 1, - name: 'Location 1', - address: 'Jl. Contoh No. 1', - area: { - id: 1, - name: 'Area 1', - }, - }, - pic: { - id: 1, - id_user: 1, - email: 'pic@example.com', - name: 'PIC User', - }, - created_at: '2024-01-01', - updated_at: '2024-01-01', - created_user: { - id: 1, - id_user: 1, - email: 'admin@example.com', - name: 'Admin', - }, - }, - feed_data: [ - { - feed_name: 'Feed 1', - feed_qty: 100, - feed_stock: 500, - }, - ], - body_weight: [ - { - chicken_weight: 2.5, - chicken_count: 1000, - average_chicken_weight: 2.5, - }, - ], - vaccination: [ - { - vaccine_name: 'Vaccine 1', - total_stock: 200, - used_stock: 150, - }, - ], - mortality: [ - { - condition: 'NORMAL', - count: 5, - }, - ], - created_at: '2024-01-01', - updated_at: '2024-01-01', - created_user: { - id: 1, - id_user: 1, - email: 'admin@example.com', - name: 'Admin', - }, - }, -]; +import { RecordingApi } from '@/services/api/production'; +import { ApprovalApi } from '@/services/api/approval'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import toast from 'react-hot-toast'; +import Badge from '@/components/Badge'; +import CheckboxInput from '@/components/input/CheckboxInput'; +import { BaseApproval, BaseApiResponse } from '@/types/api/api-general'; const RowOptionsMenu = ({ type = 'dropdown', props, deleteClickHandler, + approveClickHandler, + rejectClickHandler, + isGradingCompleted, }: { type: 'dropdown' | 'collapse'; props: CellContext; deleteClickHandler: () => void; + approveClickHandler: () => void; + rejectClickHandler: () => void; + isGradingCompleted: (recording: Recording) => boolean; }) => { + const isLayingCategory = + props.row.original.project_flock_category === 'LAYING'; + + const isRecordingApproved = (recording: Recording) => { + return ( + recording.approval?.action === 'APPROVED' && + recording.approval?.step_name === 'Disetujui' && + recording.approval?.step_number === 3 + ); + }; + + const isApproved = isRecordingApproved(props.row.original); + const isGradingDone = isGradingCompleted(props.row.original); + return ( + {!isApproved && !(isLayingCategory && !isGradingDone) && ( + + )} + {!isApproved && !(isLayingCategory && !isGradingDone) && ( + + )} + + + {isLoading ? ( +
+ +
+ ) : ( + <> + {/* Current Status */} + {currentApproval && ( +
+

Status Saat Ini

+
+ + {currentApproval.step_name} + + + {currentApproval.action === 'APPROVED' && 'Disetujui'} + {currentApproval.action === 'REJECTED' && 'Ditolak'} + {currentApproval.action === 'CREATED' && 'Dibuat'} + {currentApproval.action === 'UPDATED' && 'Diperbarui'} + +
+ {currentApproval.notes && ( +

+ Catatan:{' '} + {currentApproval.notes} +

+ )} +

+ Oleh: {currentApproval.action_by.name} •{' '} + {formatDate(currentApproval.action_at, 'DD MMMM YYYY HH:mm')} +

+
+ )} + + {/* Full History */} + {approvalHistory.length > 0 && ( +
+

Riwayat Lengkap

+
+ + + + + + + + + + + + {approvalHistory + .sort( + (a: BaseApproval, b: BaseApproval) => + new Date(b.action_at).getTime() - + new Date(a.action_at).getTime() + ) + .map((approval: BaseApproval, index: number) => ( + + + + + + + + ))} + +
TahapAksiCatatanOlehWaktu
{approval.step_name} + + {approval.action === 'APPROVED' && 'Disetujui'} + {approval.action === 'REJECTED' && 'Ditolak'} + {approval.action === 'CREATED' && 'Dibuat'} + {approval.action === 'UPDATED' && 'Diperbarui'} + + +
+ {approval.notes || '-'} +
+
{approval.action_by.name} + {formatDate( + approval.action_at, + 'DD MMMM YYYY HH:mm' + )} +
+
+
+ )} + + )} + + + ); +}; + const RecordingTable = () => { - const [search, setSearch] = useState(''); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(10); + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { + search: '', + areaFilter: '', + locationFilter: '', + kandangFilter: '', + periodFilter: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + search: 'search', + areaFilter: 'area_id', + locationFilter: 'location_id', + kandangFilter: 'kandang_id', + periodFilter: 'period', + }, + }); + const [sorting, setSorting] = useState([]); - const [selectedRecordings, setSelectedRecordings] = useState([]); - const [, setSelectedRecording] = useState(undefined); + const [rowSelection, setRowSelection] = useState>({}); + const selectedRowIds = Object.keys(rowSelection).map((item) => + parseInt(item) + ); + const [selectedRecording, setSelectedRecording] = useState< + Recording | undefined + >(undefined); const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const [isBulkApproveLoading, setIsBulkApproveLoading] = useState(false); - const [isBulkRejectLoading, setIsBulkRejectLoading] = useState(false); + const [isApproveLoading, setIsApproveLoading] = useState(false); + const [isRejectLoading, setIsRejectLoading] = useState(false); + const [approvalNotes, setApprovalNotes] = useState(''); const singleDeleteModal = useModal(); - const bulkApproveModal = useModal(); - const bulkRejectModal = useModal(); + const approveModal = useModal(); + const rejectModal = useModal(); + const approvalHistoryModal = useModal(); + + const { + data: recordings, + isLoading, + mutate: refreshRecordings, + } = useSWR( + `${RecordingApi.basePath}${getTableFilterQueryString()}`, + RecordingApi.getAllFetcher + ); + + const isRecordingFullyApproved = useCallback( + (recording: Recording): boolean => { + return ( + recording.approval?.action === 'APPROVED' && + recording.approval?.step_name === 'Disetujui' && + Number(recording.approval?.step_number) === 3 + ); + }, + [] + ); + + const isRecordingApproved = useCallback( + (recording: Recording) => { + return isRecordingFullyApproved(recording); + }, + [isRecordingFullyApproved] + ); + + const isGradingCompleted = useCallback((recording: Recording): boolean => { + if (recording.project_flock_category !== 'LAYING') { + return true; + } + + return ( + recording.egg_grading_status === 'COMPLETED' || + (recording.approval?.action === 'UPDATED' && + recording.approval?.step_number === 2) + ); + }, []); const searchChangeHandler = useCallback( (e: React.ChangeEvent) => { - setSearch(e.target.value); + updateFilter('search', e.target.value); setPage(1); }, - [] + [updateFilter, setPage] ); const pageSizeChangeHandler = useCallback( @@ -194,213 +430,415 @@ const RecordingTable = () => { setPageSize(newVal.value as number); setPage(1); }, - [] + [setPageSize, setPage] ); - const paginatedData = useMemo(() => { - const filteredData = dummyRecordings.filter( - (recording) => - recording.flock.name.toLowerCase().includes(search.toLowerCase()) || - recording.location.name.toLowerCase().includes(search.toLowerCase()) || - recording.coop.name.toLowerCase().includes(search.toLowerCase()) - ); - const start = (page - 1) * pageSize; - return filteredData.slice(start, start + pageSize); - }, [page, pageSize, search]); - - const bulkApproveHandler = async () => { - setIsBulkApproveLoading(true); - console.log( - 'Approved recordings:', - paginatedData.filter((_, idx) => selectedRecordings.includes(idx)) - ); - setTimeout(() => { - setIsBulkApproveLoading(false); - setSelectedRecordings([]); - bulkApproveModal.closeModal(); - }, 1000); - }; - - const bulkRejectHandler = async () => { - setIsBulkRejectLoading(true); - console.log( - 'Rejected recordings:', - paginatedData.filter((_, idx) => selectedRecordings.includes(idx)) - ); - setTimeout(() => { - setIsBulkRejectLoading(false); - setSelectedRecordings([]); - bulkRejectModal.closeModal(); - }, 1000); - }; - const singleDeleteHandler = async () => { setIsDeleteLoading(true); - setTimeout(() => { - setIsDeleteLoading(false); - singleDeleteModal.closeModal(); - }, 1000); + + await RecordingApi.delete(selectedRecording?.id as number); + refreshRecordings(); + + singleDeleteModal.closeModal(); + toast.success('Successfully delete Recording!'); + setIsDeleteLoading(false); }; + const approveHandler = async (notes: string) => { + setIsApproveLoading(true); + + if (eligibleRowIds.length === 0) { + toast.error( + 'Tidak ada recording yang bisa disetujui (sudah disetujui final)' + ); + setIsApproveLoading(false); + return; + } + + const approveResponse = await RecordingApi.approve(eligibleRowIds, notes); + + if (isResponseSuccess(approveResponse)) { + toast.success( + `Berhasil approve ${eligibleRowIds.length} data recording!` + ); + approveModal.closeModal(); + refreshRecordings(); + setApprovalNotes(''); + setRowSelection({}); + } else { + toast.error( + (approveResponse?.message as string) || 'Gagal menyetujui recording' + ); + } + + setIsApproveLoading(false); + }; + + const rejectHandler = async (notes: string) => { + setIsRejectLoading(true); + + if (eligibleRowIds.length === 0) { + toast.error( + 'Tidak ada recording yang bisa ditolak (sudah disetujui final)' + ); + setIsRejectLoading(false); + return; + } + + const rejectResponse = await RecordingApi.reject(eligibleRowIds, notes); + + if (isResponseSuccess(rejectResponse)) { + toast.success(`Berhasil reject ${eligibleRowIds.length} data recording!`); + rejectModal.closeModal(); + refreshRecordings(); + setApprovalNotes(''); + setRowSelection({}); + } else { + toast.error( + (rejectResponse?.message as string) || 'Gagal menolak recording' + ); + } + + setIsRejectLoading(false); + }; + + const eligibleRowIds = useMemo(() => { + if (!isResponseSuccess(recordings) || !recordings.data) return []; + return selectedRowIds.filter((id) => { + const recording = recordings.data.find((r) => r.id === id); + if (!recording || isRecordingApproved(recording)) return false; + + if (recording.project_flock_category === 'GROWING') { + return true; + } + + if (recording.project_flock_category === 'LAYING') { + return isGradingCompleted(recording); + } + + return false; + }); + }, [selectedRowIds, recordings, isRecordingApproved, isGradingCompleted]); + + useEffect(() => { + if (isResponseSuccess(recordings) && recordings.data) { + const newSelection: Record = {}; + + Object.entries(rowSelection).forEach(([rowId, isSelected]) => { + if (isSelected) { + const recording = recordings.data.find( + (r) => r.id === parseInt(rowId) + ); + if (recording && !isRecordingApproved(recording)) { + if (recording.project_flock_category === 'GROWING') { + newSelection[rowId] = true; + } else if ( + recording.project_flock_category === 'LAYING' && + isGradingCompleted(recording) + ) { + newSelection[rowId] = true; + } + } + } + }); + + if ( + Object.keys(newSelection).length !== Object.keys(rowSelection).length + ) { + setRowSelection(newSelection); + } + } + }, [ + recordings, + rowSelection, + isRecordingApproved, + isGradingCompleted, + setRowSelection, + ]); + return ( -
+
- - -
+
+
+ - {/* Bulk action buttons */} -
- {selectedRecordings.length > 0 && ( -
- - + {selectedRowIds.length > 0 && ( + <> + + + + + )}
- )} - + +
- +
+ +
- + data={isResponseSuccess(recordings) ? recordings?.data : []} columns={[ { id: 'select', - accessorKey: 'id', - header: ({ table }) => ( - 0 && - table - .getRowModel() - .rows.every((row) => selectedRecordings.includes(row.index)) - } - onChange={(e) => { - if (e.target.checked) { - setSelectedRecordings( - table.getRowModel().rows.map((row) => row.index) - ); - } else { - setSelectedRecordings([]); + header: ({ table }) => { + const allRows = table.getRowModel().rows; + + const selectableGrowingRows = allRows.filter((row) => { + const recording = row.original; + return ( + recording.project_flock_category === 'GROWING' && + !isRecordingApproved(recording) + ); + }); + + const hasNoSelectableGrowing = selectableGrowingRows.length === 0; + + const handleSelectAllGrowing = () => { + const isAllSelected = selectableGrowingRows.every((row) => + row.getIsSelected() + ); + + allRows.forEach((row) => { + const recording = row.original; + if ( + recording.project_flock_category === 'GROWING' && + !isRecordingApproved(recording) + ) { + row.toggleSelected(!isAllSelected); + } else if (recording.project_flock_category === 'LAYING') { + row.toggleSelected(false); } - }} - /> - ), - cell: ({ row }) => ( - { - if (e.target.checked) { - setSelectedRecordings([...selectedRecordings, row.index]); - } else { - setSelectedRecordings( - selectedRecordings.filter((i) => i !== row.index) - ); - } - }} - /> - ), + }); + }; + + const isAllGrowingSelected = + selectableGrowingRows.length > 0 && + selectableGrowingRows.every((row) => row.getIsSelected()); + + const isSomeGrowingSelected = selectableGrowingRows.some((row) => + row.getIsSelected() + ); + + return ( +
+ +
+ ); + }, + cell: ({ row }) => { + const isApproved = isRecordingApproved(row.original); + const isLayingCategory = + row.original.project_flock_category === 'LAYING'; + + if (isLayingCategory) { + return null; + } + + const isDisabled = !row.getCanSelect() || isApproved; + + return ( +
+ +
+ ); + }, }, { header: '#', - cell: (props) => pageSize * (page - 1) + props.row.index + 1, - }, - { - accessorKey: 'flock.name', - header: 'Flock', - }, - { - accessorKey: 'recording_date', - header: 'Tanggal Recording', cell: (props) => - new Date(props.row.original.recording_date).toLocaleDateString(), + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, }, { - accessorKey: 'location.name', - header: 'Lokasi', - }, - { - accessorKey: 'coop.name', - header: 'Kandang', - }, - { - accessorKey: 'mortality', - header: 'Total Mortality', + header: 'Nama Project', cell: (props) => - props.row.original.mortality.reduce( - (acc, curr) => acc + curr.count, - 0 - ), + `Project ${props.row.original.project_flock_kandang_id}`, + }, + { + header: 'Kategori', + cell: (props) => { + const category = props.row.original.project_flock_category; + if (!category) return '-'; + const color = category === 'LAYING' ? 'info' : 'warning'; + return ( + + {category} + + ); + }, + }, + { + header: 'Umur (hari)', + cell: (props) => props.row.original.day, + }, + { + accessorKey: 'record_date', + header: 'Waktu Recording', + cell: (props) => + formatDate(props.row.original.record_datetime, 'DD MMMM YYYY'), + }, + { + header: 'Populasi Awal', + cell: (props) => + props.row.original.total_chick_qty?.toLocaleString() || '-', + }, + { + header: 'Status Approval', + cell: (props) => { + const approval = props.row.original.approval; + if (!approval) return '-'; + + const statusColor = + approval.action === 'APPROVED' + ? 'success' + : approval.action === 'REJECTED' + ? 'error' + : approval.action === 'UPDATED' + ? 'warning' + : 'info'; + + const openApprovalHistory = () => { + setSelectedRecording(props.row.original); + approvalHistoryModal.openModal(); + }; + + const getStatusText = (action: string) => { + switch (action) { + case 'APPROVED': + return 'Disetujui'; + case 'REJECTED': + return 'Ditolak'; + case 'CREATED': + return 'Dibuat'; + case 'UPDATED': + return 'Diperbarui'; + default: + return action; + } + }; + + return ( + + {getStatusText(approval.action)} + + ); + }, + }, + { + header: 'Catatan Approval', + cell: (props) => { + const approval = props.row.original.approval; + if (!approval?.notes) return '-'; + + return ( +
+

{approval.notes}

+
+ ); + }, + }, + // { + // header: 'Status Grading Telur', + // cell: (props) => { + // const status = props.row.original.egg_grading_status; + // if (!status) return '-'; + // const color = status === 'COMPLETED' ? 'success' : 'warning'; + // return ( + // + // {status} + // + // ); + // }, + // }, + { + header: 'Dibuat Oleh', + cell: (props) => props.row.original.created_user?.name || '-', + }, + { + header: 'Tanggal Submit', + cell: (props) => + formatDate(props.row.original.created_at, 'DD MMMM YYYY'), }, { header: 'Aksi', @@ -419,6 +857,22 @@ const RecordingTable = () => { singleDeleteModal.openModal(); }; + const approveClickHandler = () => { + setRowSelection({ + [String(props.row.original.id)]: true, + }); + setApprovalNotes(''); + approveModal.openModal(); + }; + + const rejectClickHandler = () => { + setRowSelection({ + [String(props.row.original.id)]: true, + }); + setApprovalNotes(''); + rejectModal.openModal(); + }; + return ( <> {currentPageSize > 2 && ( @@ -427,6 +881,9 @@ const RecordingTable = () => { type='dropdown' props={props} deleteClickHandler={deleteClickHandler} + approveClickHandler={approveClickHandler} + rejectClickHandler={rejectClickHandler} + isGradingCompleted={isGradingCompleted} /> )} @@ -437,6 +894,9 @@ const RecordingTable = () => { type='collapse' props={props} deleteClickHandler={deleteClickHandler} + approveClickHandler={approveClickHandler} + rejectClickHandler={rejectClickHandler} + isGradingCompleted={isGradingCompleted} /> )} @@ -445,18 +905,23 @@ const RecordingTable = () => { }, }, ]} - pageSize={pageSize} - page={page} - totalItems={dummyRecordings.length} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(recordings) ? recordings?.meta?.page : 0} + totalItems={ + isResponseSuccess(recordings) ? recordings?.meta?.total_results : 0 + } onPageChange={setPage} - isLoading={false} + isLoading={isLoading} sorting={sorting} setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} className={{ containerClassName: cn({ - 'mb-20': paginatedData.length === 0, + 'mb-20': + isResponseSuccess(recordings) && recordings?.data?.length === 0, }), - tableWrapperClassName: 'overflow-x-auto min-h-full!', + tableWrapperClassName: 'overflow-x-auto min-h-full overflow-visible!', tableClassName: 'font-inter w-full table-auto min-h-full!', headerRowClassName: 'border-b border-b-gray-200', headerColumnClassName: @@ -481,6 +946,49 @@ const RecordingTable = () => { onClick: singleDeleteHandler, }} /> + + setApprovalNotes(''), + }} + primaryButton={{ + text: 'Ya', + color: 'success', + isLoading: isApproveLoading, + onClick: approveHandler, + }} + placeholder='(Opsional) Tambahkan catatan untuk approval ini...' + rows={3} + /> + + setApprovalNotes(''), + }} + primaryButton={{ + text: 'Ya', + color: 'error', + isLoading: isRejectLoading, + onClick: rejectHandler, + }} + placeholder='(Opsional) Tambahkan catatan untuk reject ini...' + rows={3} + /> + + ); }; diff --git a/src/components/pages/production/recording/form/RecordingForm.schema.ts b/src/components/pages/production/recording/form/RecordingForm.schema.ts index 4b0b37dd..4d72e053 100644 --- a/src/components/pages/production/recording/form/RecordingForm.schema.ts +++ b/src/components/pages/production/recording/form/RecordingForm.schema.ts @@ -1,212 +1,320 @@ import * as Yup from 'yup'; -import { RECORDING_FLAG_OPTIONS } from '@/config/constant'; -import { Recording } from '@/types/api/production/recording'; +import { + Recording, + CreateGrowingRecordingPayload, + CreateLayingRecordingPayload, + CreateEggPayload, + CreateGradingPayload, +} from '@/types/api/production/recording'; -export const RecordingFormSchema = Yup.object({ - flock: Yup.object({ - value: Yup.number().min(1).required(), - label: Yup.string().required(), - }).nullable(), - flock_id: Yup.number() - .default(0) - .typeError('Flock wajib diisi!') - .test( - 'is-valid-flock', - 'Flock 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() - .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() - .required('Berat ayam wajib diisi!') - .min(1, 'Berat ayam minimal 1 gram!') - .typeError('Berat ayam harus berupa angka!'), - chicken_count: 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!'), - }) - ) - .min(1, 'Minimal harus ada 1 data bobot badan!') - .required('Data bobot badan wajib diisi!'), - vaccination: 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; - } - ), - }) - ) - .min(1, 'Minimal harus ada 1 data vaksinasi!') - .required('Data vaksinasi wajib diisi!'), - mortality: Yup.array() - .of( - Yup.object({ - condition: Yup.mixed() - .oneOf( - RECORDING_FLAG_OPTIONS.map((opt) => opt.value), - 'Kondisi 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!'), - }) - ) - .min(1, 'Minimal harus ada 1 data mortalitas!') - .required('Data mortalitas wajib diisi!'), +type RecordingGrowingFormSchemaType = { + project_flock_kandang: { + value: number; + 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; + }[]; + depletions: { + product_warehouse_id: number; + qty: number | string; + }[]; +}; + +type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & { + eggs: { + product_warehouse_id: number; + qty: number | string; + }[]; +}; + +type RecordingGradingFormSchemaType = { + eggs_grading: { + recording_egg_id: number; + grade: string; + qty: number | string; + }[]; +}; + +export type BodyWeightSchema = { + weight: number | string; + avg_weight: number | string; + qty: number | string; +}; + +export type StockSchema = { + product_warehouse_id: number; + qty: number | string; +}; + +export type DepletionSchema = { + product_warehouse_id: number; + qty: number | string; +}; + +export type EggSchema = { + product_warehouse_id: number; + qty: number | string; +}; + +const BodyWeightObjectSchema: Yup.ObjectSchema = 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!'), }); -export const UpdateRecordingFormSchema = RecordingFormSchema; +const StockObjectSchema: Yup.ObjectSchema = Yup.object({ + product_warehouse_id: Yup.number() + .required('Produk wajib diisi!') + .min(1, 'Produk wajib diisi!') + .typeError('Produk harus berupa angka!'), + qty: Yup.number() + .required('Jumlah penggunaan wajib diisi!') + .min(1, 'Jumlah penggunaan tidak boleh 0!') + .typeError('Jumlah penggunaan harus berupa angka!'), +}); -export type RecordingFormValues = Yup.InferType; +const DepletionObjectSchema: Yup.ObjectSchema = Yup.object({ + product_warehouse_id: Yup.number() + .required('Produk depletions wajib diisi!') + .min(1, 'Produk depletions wajib diisi!') + .typeError('Produk depletions harus berupa angka!'), + qty: Yup.number() + .required('Jumlah depletions wajib diisi!') + .min(1, 'Jumlah depletions minimal 1!') + .typeError('Jumlah depletions harus berupa angka!'), +}); -export const getRecordingFormInitialValues = ( - initialValues?: Recording -): RecordingFormValues => ({ - flock: initialValues?.flock +const EggObjectSchema: Yup.ObjectSchema = Yup.object({ + product_warehouse_id: Yup.number() + .required('Kondisi telur wajib diisi!') + .min(1, 'Kondisi telur wajib diisi!') + .typeError('Kondisi telur harus berupa angka!'), + qty: Yup.number() + .required('Jumlah telur wajib diisi!') + .min(1, 'Jumlah telur tidak boleh 0!') + .typeError('Jumlah telur harus berupa angka!'), +}); + +export const RecordingGrowingFormSchema: Yup.ObjectSchema = + Yup.object({ + project_flock_kandang: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + project_flock_kandang_id: Yup.number() + .default(0) + .typeError('Project Flock Kandang wajib diisi!') + .test( + 'is-valid-project-flock-kandang', + 'Project Flock Kandang wajib diisi!', + (value) => value !== undefined && value !== null && value > 0 + ) + .required('Project Flock Kandang wajib diisi!') + .test( + 'not-already-recorded', + 'Project Flock ini sudah direcord hari ini!', + function (value) { + const recordedProjectFlockIds = this.options.context + ?.recordedProjectFlockIds as Set; + const formType = this.options.context?.type as + | 'add' + | 'edit' + | 'detail'; + if (formType !== 'add') return true; + if (value && recordedProjectFlockIds?.has(value)) { + return false; + } + 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!') + .required('Data stok wajib diisi!'), + depletions: Yup.array() + .of(DepletionObjectSchema) + .min(1, 'Minimal harus ada 1 data depletions!') + .required('Data depletions wajib diisi!'), + }); + +export const RecordingLayingFormSchema: Yup.ObjectSchema = + RecordingGrowingFormSchema.shape({ + eggs: Yup.array() + .of(EggObjectSchema) + .min(1, 'Minimal harus ada 1 data telur!') + .required('Data telur wajib diisi!'), + }); + +export const UpdateRecordingGrowingFormSchema = + RecordingGrowingFormSchema.shape({ + project_flock_kandang_id: Yup.number() + .default(0) + .typeError('Project Flock Kandang wajib diisi!') + .test( + 'is-valid-project-flock-kandang', + 'Project Flock Kandang wajib diisi!', + (value) => value !== undefined && value !== null && value > 0 + ) + .required('Project Flock Kandang wajib diisi!'), + }); + +export const UpdateRecordingLayingFormSchema = RecordingLayingFormSchema.shape({ + project_flock_kandang_id: Yup.number() + .default(0) + .typeError('Project Flock Kandang wajib diisi!') + .test( + 'is-valid-project-flock-kandang', + 'Project Flock Kandang wajib diisi!', + (value) => value !== undefined && value !== null && value > 0 + ) + .required('Project Flock Kandang wajib diisi!'), +}); + +export const RecordingGradingFormSchema: Yup.ObjectSchema = + Yup.object({ + eggs_grading: Yup.array() + .of( + Yup.object({ + recording_egg_id: Yup.number() + .required('Recording Egg ID wajib diisi!') + .min(1, 'Recording Egg ID minimal 1!') + .typeError('Recording Egg ID harus berupa angka!'), + grade: Yup.string() + .required('Grade telur wajib diisi!') + .typeError('Grade telur harus berupa string!'), + qty: Yup.number() + .required('Jumlah telur wajib diisi!') + .min(1, 'Jumlah telur minimal 1!') + .typeError('Jumlah telur harus berupa angka!'), + }) + ) + .min(1, 'Minimal harus ada 1 data grading telur!') + .required('Data grading telur wajib diisi!'), + }); + +export const UpdateRecordingGradingFormSchema = RecordingGradingFormSchema; + +export type RecordingGrowingFormValues = Yup.InferType< + typeof RecordingGrowingFormSchema +>; + +export type RecordingLayingFormValues = Yup.InferType< + typeof RecordingLayingFormSchema +>; + +export type RecordingGradingFormValues = Yup.InferType< + typeof RecordingGradingFormSchema +>; + +type RecordingFormData = Partial & { + body_weights?: CreateGrowingRecordingPayload['body_weights']; + stocks?: CreateGrowingRecordingPayload['stocks'] | Recording['stocks']; + depletions?: + | CreateGrowingRecordingPayload['depletions'] + | Recording['depletions']; + eggs?: CreateLayingRecordingPayload['eggs'] | Recording['eggs']; + project_flock_kandang_id?: number; + project_flock_category?: string; +}; + +export const getRecordingGrowingFormInitialValues = ( + initialValues?: RecordingFormData +): RecordingGrowingFormValues => ({ + 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 #${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) - : 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 ?? [ + project_flock_kandang_id: initialValues?.project_flock_kandang_id ?? 0, + body_weights: initialValues?.body_weights?.map( + (bw: NonNullable[0]) => ({ + weight: bw.avg_weight * bw.qty, + avg_weight: bw.avg_weight, + qty: bw.qty, + }) + ) ?? [ { - chicken_weight: 0, - chicken_count: 0, - average_chicken_weight: 0, + weight: '', + avg_weight: '', + qty: '', }, ], - 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) => ({ + product_warehouse_id: stock.product_warehouse_id, + qty: + (stock as { qty?: number; usage_amount?: number }).qty || + (stock as { qty?: number; usage_amount?: number }).usage_amount || + '', + })) ?? [ { - condition: '', - count: 0, + product_warehouse_id: 0, + qty: '', + }, + ], + depletions: initialValues?.depletions?.map( + ( + depletion: NonNullable[0] + ) => ({ + product_warehouse_id: depletion.product_warehouse_id, + qty: depletion.qty, + }) + ) ?? [ + { + product_warehouse_id: 0, + qty: '', + }, + ], +}); + +export const getRecordingLayingFormInitialValues = ( + initialValues?: RecordingFormData +): RecordingLayingFormValues => ({ + ...getRecordingGrowingFormInitialValues(initialValues), + + eggs: initialValues?.eggs?.map((egg: CreateEggPayload) => ({ + product_warehouse_id: egg.product_warehouse_id, + qty: egg.qty, + })) ?? [ + { + product_warehouse_id: 0, + qty: '', + }, + ], +}); + +export const getRecordingGradingFormInitialValues = ( + initialValues?: Partial & { recording_egg_id?: number } +): RecordingGradingFormValues => ({ + eggs_grading: initialValues?.eggs_grading?.map((grading) => ({ + recording_egg_id: grading.recording_egg_id, + grade: grading.grade, + qty: grading.qty, + })) ?? [ + { + recording_egg_id: initialValues?.recording_egg_id ?? 0, + grade: '', + qty: '', }, ], }); diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index e3668237..5900c84a 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -1,36 +1,65 @@ 'use client'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useMemo, useState, useEffect, useCallback } from 'react'; +import { useRouter } from 'next/navigation'; + import { useFormik } from 'formik'; +import useSWR from 'swr'; + 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'; -import { - CreateRecordingPayload, - Recording, -} from '@/types/api/production/recording'; -import { - RecordingFormSchema, - RecordingFormValues, - getRecordingFormInitialValues, - UpdateRecordingFormSchema, -} from './RecordingForm.schema'; -import { useRecordingFormHandlers } from './useRecordingFormHandlers'; -import { ProjectFlockApi } from '@/services/api/production/project-flock'; -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 Card from '@/components/Card'; +import Badge from '@/components/Badge'; +import NumberInput from '@/components/input/NumberInput'; +import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import CheckboxInput from '@/components/input/CheckboxInput'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; +import { useModal } from '@/components/Modal'; +import Tooltip from '@/components/Tooltip'; + +import { + ProjectFlockKandangApi, + RecordingApi, + ProjectFlockApi, +} from '@/services/api/production'; +import { LocationApi } from '@/services/api/master-data'; +import { ProductWarehouseApi } from '@/services/api/inventory'; + +import { + CreateGrowingRecordingPayload, + CreateLayingRecordingPayload, + UpdateGrowingRecordingPayload, + UpdateLayingRecordingPayload, + Recording, + NextDayRecording, +} from '@/types/api/production/recording'; +import { type BaseApiResponse } from '@/types/api/api-general'; +import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock'; +import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; +import { Kandang } from '@/types/api/master-data/kandang'; + +import { + RecordingGrowingFormSchema, + RecordingLayingFormSchema, + RecordingGrowingFormValues, + RecordingLayingFormValues, + getRecordingGrowingFormInitialValues, + getRecordingLayingFormInitialValues, + UpdateRecordingGrowingFormSchema, + UpdateRecordingLayingFormSchema, +} from './RecordingForm.schema'; + +import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; +import { formatDate, formatNumber } from '@/lib/helper'; +import toast from 'react-hot-toast'; +import ApprovalSteps, { + useApprovalSteps, +} from '@/components/pages/ApprovalSteps'; +import { + GROWING_RECORDING_APPROVAL_LINE, + LAYING_RECORDING_APPROVAL_LINE, +} from '@/config/approval-line'; interface RecordingFormProps { type?: 'add' | 'edit' | 'detail'; @@ -38,747 +67,1781 @@ 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(''); + // ===== HOOKS & ROUTER ===== + const router = useRouter(); - const { - deleteModal, - recordingFormErrorMessage, - isDeleteLoading, - createRecordingHandler, - updateRecordingHandler, - deleteRecordingClickHandler, - confirmationModalDeleteClickHandler, - } = useRecordingFormHandlers(initialValues?.id); + // ===== STATE MANAGEMENT ===== + const [selectedBodyWeights, setSelectedBodyWeights] = useState([]); + const [selectedStocks, setSelectedStocks] = useState([]); + const [selectedDepletions, setSelectedDepletions] = useState([]); + const [selectedEggs, setSelectedEggs] = useState([]); - const formikInitialValues = useMemo( - () => getRecordingFormInitialValues(initialValues), - [initialValues] + const [editingAverageIndex] = useState(null); + const [manuallyEditedRows, setManuallyEditedRows] = useState>( + new Set() ); - const formik = useFormik({ - initialValues: formikInitialValues, - validationSchema: - type === 'edit' ? UpdateRecordingFormSchema : RecordingFormSchema, - 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() - : '', - 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, + const [locationSearchValue, setLocationSearchValue] = useState(''); + const [selectedLocation, setSelectedLocation] = useState( + null + ); + const [projectFlockSearchValue, setProjectFlockSearchValue] = useState(''); + const [selectedProjectFlock, setSelectedProjectFlock] = + useState(null); + const [selectedKandang, setSelectedKandang] = useState( + null + ); + + const [isApproveLoading, setIsApproveLoading] = useState(false); + const [isRejectLoading, setIsRejectLoading] = useState(false); + const [, setApprovalNotes] = useState(''); + const [recordingFormErrorMessage, setRecordingFormErrorMessage] = + useState(''); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const [newRecordingData, setNewRecordingData] = useState( + null + ); + const [nextDayRecording, setNextDayRecording] = + useState(null); + + const approveModal = useModal(); + const rejectModal = useModal(); + const deleteModal = useModal(); + + const isRecordingApproved = useCallback((recording?: Recording) => { + return ( + recording?.approval?.action === 'APPROVED' && + recording?.approval?.step_name === 'Disetujui' && + recording?.approval?.step_number === 3 + ); + }, []); + + const hasGradingData = useCallback((recording?: Recording) => { + if (!recording || !recording.eggs) return false; + return recording.eggs.some( + (egg) => + egg.gradings && + egg.gradings.length > 0 && + egg.gradings.some((grading) => grading.qty > 0) + ); + }, []); + + // ===== PAYLOAD CREATION HELPERS ===== + const createGrowingPayload = useCallback( + (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, })), - 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, - })), - 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, + qty: Number(depletion.qty) || 0, })), }; - - switch (type) { - case 'add': - await createRecordingHandler(payload); - break; - case 'edit': - await updateRecordingHandler(initialValues?.id as number, payload); - break; - } }, - }); + [] + ); - // Locations - const locationsUrl = `${LocationApi.basePath}?${new URLSearchParams({ search: locationSelectInputValue }).toString()}`; + const createLayingPayload = useCallback( + (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, + })), + depletions: (values.depletions ?? []).map((depletion) => ({ + product_warehouse_id: depletion.product_warehouse_id, + qty: Number(depletion.qty) || 0, + })), + eggs: (values.eggs ?? []).map((egg) => ({ + product_warehouse_id: egg.product_warehouse_id, + qty: Number(egg.qty) || 0, + })), + }; + }, + [] + ); + + // ===== FORM HANDLERS ===== + const createRecordingHandler = useCallback( + async ( + payload: CreateGrowingRecordingPayload | CreateLayingRecordingPayload + ) => { + const res = await RecordingApi.create(payload); + if (isResponseError(res)) { + setRecordingFormErrorMessage(res.message); + return; + } + toast.success(res?.message as string); + router.push('/production/recording'); + }, + [router] + ); + + const createRecordingHandlerWithRedirect = useCallback( + async ( + payload: CreateGrowingRecordingPayload | CreateLayingRecordingPayload, + redirectToGrading: boolean = false + ) => { + const res = await RecordingApi.create(payload); + if (isResponseError(res)) { + setRecordingFormErrorMessage(res.message); + return null; + } + + toast.success(res?.message as string); + + if (res?.status === 'success' && res.data) { + setNewRecordingData(res.data); + return res.data; + } + + if (redirectToGrading) { + toast.error( + 'Gagal mendapatkan ID recording. Silakan coba dari halaman list.' + ); + router.push('/production/recording'); + } + return null; + }, + [router] + ); + + const updateRecordingHandler = useCallback( + async ( + recordingId: number, + payload: UpdateGrowingRecordingPayload | UpdateLayingRecordingPayload + ) => { + 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('/production/recording'); + }, + [router] + ); + + const deleteRecordingClickHandler = useCallback(() => { + deleteModal.openModal(); + }, [deleteModal]); + + const confirmationModalDeleteClickHandler = useCallback(async () => { + if (!initialValues?.id) return; + + setIsDeleteLoading(true); + await RecordingApi.delete(initialValues.id); + deleteModal.closeModal(); + toast.success('Successfully delete Recording!'); + setIsDeleteLoading(false); + router.push('/production/recording'); + }, [deleteModal, initialValues?.id, router]); + + // ===== API DATA FETCHING ===== + const locationsUrl = `${LocationApi.basePath}?${new URLSearchParams({ + search: locationSearchValue || '', + limit: '100', + }).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( + const projectFlocksUrl = `${ProjectFlockApi.basePath}?${new URLSearchParams({ + search: projectFlockSearchValue || '', + limit: '100', + ...(selectedLocation + ? { location_id: selectedLocation.value.toString() } + : {}), + }).toString()}`; + const { data: projectFlocks, isLoading: isLoadingProjectFlocks } = useSWR( projectFlocksUrl, ProjectFlockApi.getAllFetcher ); - // Pakan Products - const pakanUrl = useMemo(() => { - if (!formik.values.location_id) return null; + const projectFlockKandangLookupUrl = useMemo(() => { + if (!selectedProjectFlock || !selectedKandang) return null; const params = new URLSearchParams({ - flag: 'PAKAN', - search: '', - location_id: formik.values.location_id.toString(), + project_flock_id: selectedProjectFlock.value.toString(), + kandang_id: selectedKandang.value.toString(), }); - return `${ProductWarehouseApi.basePath}?${params.toString()}`; - }, [formik.values.location_id]); + return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`; + }, [selectedProjectFlock, selectedKandang]); - const { data: pakanProducts, isLoading: isLoadingPakan } = useSWR( - pakanUrl, - ProductWarehouseApi.getAllFetcher + const { data: projectFlockKandangLookupData } = useSWR( + projectFlockKandangLookupUrl, + projectFlockKandangLookupUrl + ? () => + ProjectFlockApi.getAllFetcher( + projectFlockKandangLookupUrl + ) as Promise> + : null ); - // 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 projectFlockKandangLookup = + projectFlockKandangLookupData?.status === 'success' + ? projectFlockKandangLookupData.data + : undefined; - const { data: ovkProducts, isLoading: isLoadingOvk } = useSWR( - ovkUrl, - ProductWarehouseApi.getAllFetcher + const projectFlockKandangDetailUrl = useMemo(() => { + if (type === 'add' || !initialValues?.project_flock_kandang_id) return null; + return `${ProjectFlockKandangApi.basePath}/${initialValues.project_flock_kandang_id}`; + }, [type, initialValues?.project_flock_kandang_id]); + + const { data: projectFlockKandangDetailData } = useSWR( + projectFlockKandangDetailUrl, + projectFlockKandangDetailUrl + ? () => + ProjectFlockKandangApi.getAllFetcher( + projectFlockKandangDetailUrl + ) as Promise> + : null ); - // COMPUTED VALUES - const buildWarehouseLabel = useCallback((warehouse: Warehouse) => { - const parts: string[] = [warehouse.name]; + const projectFlockKandangDetail = + projectFlockKandangDetailData?.status === 'success' + ? projectFlockKandangDetailData.data + : undefined; - if ('kandang' in warehouse && warehouse.kandang) { - parts.push(warehouse.kandang.name); + const stockProductsUrl = useMemo(() => { + if (!selectedLocation || !selectedKandang) return null; + const params = new URLSearchParams({ + flags: 'PAKAN,OVK', + search: '', + limit: '100', + location_id: selectedLocation.value.toString(), + }); + + if (projectFlockKandangLookup?.kandang?.id) { + params.append( + 'kandang_id', + projectFlockKandangLookup.kandang.id.toString() + ); + } else if (selectedKandang) { + params.append('kandang_id', selectedKandang.value.toString()); } - if ('location' in warehouse && warehouse.location) { - parts.push(warehouse.location.name); + return `${ProductWarehouseApi.basePath}?${params.toString()}`; + }, [selectedLocation, selectedKandang, projectFlockKandangLookup]); + + const depletionProductsUrl = useMemo(() => { + if (!selectedLocation || !selectedKandang) return null; + const params = new URLSearchParams({ + search: '', + limit: '100', + location_id: selectedLocation.value.toString(), + }); + + if (projectFlockKandangLookup?.kandang?.id) { + params.append( + 'kandang_id', + projectFlockKandangLookup.kandang.id.toString() + ); + } else if (selectedKandang) { + params.append('kandang_id', selectedKandang.value.toString()); } - if (warehouse.area) { - parts.push(warehouse.area.name); - } + return `${ProductWarehouseApi.basePath}?${params.toString()}`; + }, [selectedLocation, selectedKandang, projectFlockKandangLookup]); - return parts.join(' - '); - }, []); + const today = new Date().toISOString().split('T')[0]; + const existingRecordingsUrl = `${RecordingApi.basePath}`; - const locationOptions = isResponseSuccess(locations) - ? locations.data.map((loc) => ({ value: loc.id, label: loc.name })) - : []; + const { data: existingRecordings } = useSWR( + existingRecordingsUrl, + RecordingApi.getAllFetcher + ); - 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, - })); + const nextDayRecordingUrl = useMemo(() => { + if (!selectedProjectFlock) return null; + const projectFlockId = + typeof selectedProjectFlock.value === 'string' + ? parseInt(selectedProjectFlock.value, 10) + : selectedProjectFlock.value; + return `${RecordingApi.basePath}/next-day?project_flock_id=${projectFlockId}`; }, [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 { data: nextDayRecordingData } = useSWR( + nextDayRecordingUrl, + nextDayRecordingUrl + ? () => { + const projectFlockId = + typeof selectedProjectFlock!.value === 'string' + ? parseInt(selectedProjectFlock!.value, 10) + : selectedProjectFlock!.value; + return RecordingApi.nextDayRecording(projectFlockId); + } + : null ); - const pakanStockMap = useMemo(() => { - const map = new Map(); - filteredPakanProducts.forEach((product) => { - map.set(product.id, product.quantity); - }); - return map; - }, [filteredPakanProducts]); + useEffect(() => { + if (nextDayRecordingData?.status === 'success') { + setNextDayRecording( + nextDayRecordingData.data as unknown as NextDayRecording + ); + } else { + setNextDayRecording(null); + } + }, [nextDayRecordingData]); - 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 { data: stockProducts, isLoading: isLoadingStockProducts } = useSWR( + stockProductsUrl, + ProductWarehouseApi.getAllFetcher ); - const ovkStockMap = useMemo(() => { - const map = new Map(); - filteredOvkProducts.forEach((product) => { - map.set(product.id, product.quantity); + const { data: depletionProductsData, isLoading: isLoadingDepletionProducts } = + useSWR(depletionProductsUrl, ProductWarehouseApi.getAllFetcher); + + const eggProductsUrl = useMemo(() => { + if (!selectedLocation || !selectedKandang) return null; + const params = new URLSearchParams({ + search: 'telur', + limit: '100', + location_id: selectedLocation.value.toString(), }); - return map; - }, [filteredOvkProducts]); - // EFFECTS - useEffect(() => { - if (initialValues?.flock && isResponseSuccess(projectFlocks)) { - const flock = projectFlocks.data.find( - (f) => f.id === initialValues.flock.id + if (projectFlockKandangLookup?.kandang?.id) { + params.append( + 'kandang_id', + projectFlockKandangLookup.kandang.id.toString() ); - if (flock) { - setSelectedProjectFlock(flock); + } else if (selectedKandang) { + params.append('kandang_id', selectedKandang.value.toString()); + } + + return `${ProductWarehouseApi.basePath}?${params.toString()}`; + }, [selectedLocation, selectedKandang, projectFlockKandangLookup]); + + const { data: eggProductsData, isLoading: isLoadingEggProducts } = useSWR( + eggProductsUrl, + ProductWarehouseApi.getAllFetcher + ); + + const approvedProjectFlockKandangsUrl = useMemo(() => { + const params = new URLSearchParams({ + step_name: 'Disetujui', + limit: '100', + }); + return `${ProjectFlockKandangApi.basePath}?${params.toString()}`; + }, []); + + const { data: approvedProjectFlockKandangsData } = useSWR( + approvedProjectFlockKandangsUrl, + ProjectFlockKandangApi.getAllFetcher + ); + + const approvedProjectFlockKandangs = useMemo(() => { + if (!isResponseSuccess(approvedProjectFlockKandangsData)) return []; + return approvedProjectFlockKandangsData.data; + }, [approvedProjectFlockKandangsData]); + + const isLayingCategory = + initialValues?.project_flock_category === 'LAYING' || + projectFlockKandangLookup?.project_flock?.category === 'LAYING' || + projectFlockKandangDetail?.project_flock?.category === 'LAYING'; + + const isGrowingCategory = + initialValues?.project_flock_category === 'GROWING' || + projectFlockKandangLookup?.project_flock?.category === 'GROWING' || + projectFlockKandangDetail?.project_flock?.category === 'GROWING'; + + const recordingApprovalLines = useMemo(() => { + if (isLayingCategory) { + return LAYING_RECORDING_APPROVAL_LINE; + } + if (isGrowingCategory) { + return GROWING_RECORDING_APPROVAL_LINE; + } + return GROWING_RECORDING_APPROVAL_LINE; + }, [isLayingCategory, isGrowingCategory]); + + // ===== APPROVAL DATA FETCHING USING HOOK ===== + const { + approvals, + isLoading: approvalsLoading, + refresh: refreshApprovals, + } = useApprovalSteps({ + latestApproval: initialValues?.approval, + approvalLines: recordingApprovalLines, + moduleName: 'RECORDINGS', + moduleId: initialValues?.id?.toString() ?? '', + params: { + limit: 100, + group_step_number: true, + }, + }); + + // ===== DATA PROCESSING ===== + const locationOptions = useMemo(() => { + let options: OptionType[] = []; + + if (isResponseSuccess(locations)) { + const locationOptionsList = + locations?.data.map((location) => ({ + value: location.id, + label: location.name || '', + })) || []; + options = options.concat(locationOptionsList); + } + + if (projectFlockKandangDetail && (type === 'edit' || type === 'detail')) { + const currentLocation = projectFlockKandangDetail.project_flock.location; + if ( + currentLocation && + !options.find((opt) => opt.value === currentLocation.id) + ) { + options.push({ + value: currentLocation.id, + label: currentLocation.name || '', + }); } } - }, [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, - })); + return options; + }, [locations, projectFlockKandangDetail, type]); - // 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 - ); + const projectFlockOptions = useMemo(() => { + let options: OptionType[] = []; - if (hasChanges) { - formik.setFieldValue('body_weight', updatedBodyWeight); + if (isResponseSuccess(projectFlocks)) { + const flockOptions = + projectFlocks?.data.map((projectFlock) => ({ + value: projectFlock.id, + label: projectFlock.flock_name || '', + })) || []; + options = options.concat(flockOptions); + } + + if (projectFlockKandangDetail && (type === 'edit' || type === 'detail')) { + const currentProjectFlock = projectFlockKandangDetail.project_flock; + if ( + currentProjectFlock && + !options.find((opt) => opt.value === currentProjectFlock.id) + ) { + options.push({ + value: currentProjectFlock.id, + label: currentProjectFlock.flock_name || '', + }); } } + + return options; + }, [projectFlocks, projectFlockKandangDetail, type]); + + const kandangOptions = useMemo(() => { + let options: OptionType[] = []; + + if (selectedProjectFlock && isResponseSuccess(projectFlocks)) { + const selectedProjectFlockData = projectFlocks.data.find( + (pf) => pf.id === selectedProjectFlock.value + ); + + if (selectedProjectFlockData?.kandangs) { + const approvedKandangIds = approvedProjectFlockKandangs + .filter((pfk) => pfk.project_flock_id === selectedProjectFlock.value) + .map((pfk) => pfk.kandang_id); + + const kandangOptions = selectedProjectFlockData.kandangs + .filter((kandang: Kandang) => { + if (type === 'add') { + return approvedKandangIds.includes(kandang.id); + } + return true; + }) + .map((kandang: Kandang) => ({ + value: kandang.id, + label: kandang.name || '', + })); + options = options.concat(kandangOptions); + } + } + + if (projectFlockKandangDetail && (type === 'edit' || type === 'detail')) { + const currentKandang = projectFlockKandangDetail.kandang; + if ( + currentKandang && + !options.find((opt) => opt.value === currentKandang.id) + ) { + options.push({ + value: currentKandang.id, + label: currentKandang.name || '', + }); + } + } + + return options; }, [ - formik.values.body_weight?.map((w) => w.chicken_weight), - formik.values.body_weight?.map((w) => w.chicken_count), + selectedProjectFlock, + projectFlocks, + projectFlockKandangDetail, + type, + approvedProjectFlockKandangs, ]); - // EVENT HANDLERS - Select Inputs - const locationChangeHandler = (val: OptionType | OptionType[] | null) => { - const locationValue = (val as OptionType)?.value; + const recordedProjectFlockKandangIds = useMemo(() => { + if (!isResponseSuccess(existingRecordings)) return new Set(); - formik.setFieldValue('location', val, false); - formik.setFieldValue('location_id', locationValue || 0, false); + const todayRecordings = existingRecordings.data; + const recordedIds = new Set(); - 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(''); - }; + todayRecordings.forEach((recording) => { + const recordingDate = recording.record_datetime?.split('T')[0]; + if (recordingDate === today) { + recordedIds.add(recording.project_flock_kandang_id); + } + }); - const flockChangeHandler = (val: OptionType | OptionType[] | null) => { - const flockValue = (val as OptionType)?.value; + return recordedIds; + }, [existingRecordings, today]); - const selected = isResponseSuccess(projectFlocks) - ? projectFlocks.data.find((flock) => flock.id === flockValue) - : null; + const getLatestTotalChickQty = useCallback( + (projectFlockKandangId: number) => { + if (!isResponseSuccess(existingRecordings)) return null; - setSelectedProjectFlock(selected || null); + const projectFlockRecordings = existingRecordings.data.filter( + (recording) => + recording.project_flock_kandang_id === projectFlockKandangId + ); - formik.setFieldValue('flock', val, false); - formik.setFieldValue('flock_id', flockValue || 0, false); + if (projectFlockRecordings.length === 0) return null; - formik.setFieldValue('coop', null, false); - formik.setFieldValue('coop_id', 0, false); - }; + projectFlockRecordings.sort( + (a, b) => + new Date(b.record_datetime).getTime() - + new Date(a.record_datetime).getTime() + ); - const coopChangeHandler = (val: OptionType | OptionType[] | null) => { - const coopValue = (val as OptionType)?.value; + return projectFlockRecordings[0].total_chick_qty; + }, + [existingRecordings] + ); - formik.setFieldValue('coop', val, false); - formik.setFieldValue('coop_id', coopValue || 0, false); - }; + const unifiedStockProducts = useMemo(() => { + const options: OptionType[] = []; + if (isResponseSuccess(stockProducts) && selectedKandang) { + stockProducts.data.forEach((product) => { + const hasPakanFlag = product.product.flags?.includes('PAKAN'); + const hasOvkFlag = product.product.flags?.includes('OVK'); - // 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); - }; + // Only include products that are in the same location as the selected kandang + if (hasPakanFlag || hasOvkFlag) { + options.push({ + value: product.id, + label: product.product.name, + }); + } + }); + } - 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 - const addBodyWeight = () => { - const newBodyWeight = [ - ...(formik.values.body_weight || []), - { - chicken_weight: 0, - chicken_count: 0, - average_chicken_weight: 0, - }, - ]; - formik.setFieldValue('body_weight', newBodyWeight); - }; - - // 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 + if ( + initialValues && + 'stocks' in initialValues && + initialValues.stocks && + type !== 'add' + ) { + initialValues.stocks?.forEach((stock) => { + if (stock.product_warehouse && stock.product_warehouse.product) { + const existingOption = options.find( + (opt) => opt.value === stock.product_warehouse_id ); - } else { - formik.setFieldValue(`body_weight.${idx}.average_chicken_weight`, ''); + if (!existingOption) { + options.push({ + value: stock.product_warehouse_id, + label: stock.product_warehouse.product.name, + }); + } + } + }); + } + + return options; + }, [stockProducts, initialValues, type, selectedKandang]); + + const depletionProducts = useMemo(() => { + const options: OptionType[] = []; + if (isResponseSuccess(depletionProductsData) && selectedKandang) { + depletionProductsData.data.forEach((product) => { + const productName = product.product.name; + + // Filter for depletion-related products (culling, mati, afkir) + if ( + productName.toLowerCase().includes('culling') || + productName.toLowerCase().includes('mati') || + productName.toLowerCase().includes('afkir') + ) { + options.push({ + value: product.id, + label: product.product.name, + }); + } + }); + } + + if (initialValues && initialValues.depletions && type !== 'add') { + initialValues.depletions.forEach((depletion) => { + if ( + depletion.product_warehouse && + depletion.product_warehouse.product + ) { + const existingOption = options.find( + (opt) => opt.value === depletion.product_warehouse_id + ); + if (!existingOption) { + options.push({ + value: depletion.product_warehouse_id, + label: depletion.product_warehouse.product.name, + }); + } + } + }); + } + + return options; + }, [depletionProductsData, initialValues, type, selectedKandang]); + + const eggProducts = useMemo(() => { + const options: OptionType[] = []; + if (isResponseSuccess(eggProductsData) && selectedKandang) { + eggProductsData.data.forEach((product) => { + const productName = product.product.name; + + // Filter for egg-related products + if ( + productName.toLowerCase().includes('telur') || + productName.toLowerCase().includes('egg') || + productName.toLowerCase().includes('pecah') || + productName.toLowerCase().includes('konsumsi') || + productName.toLowerCase().includes('baik') + ) { + options.push({ + value: product.id, + label: product.product.name, + }); + } + }); + } + + if (initialValues && initialValues.eggs && type !== 'add') { + initialValues.eggs.forEach((egg) => { + if (egg.product_warehouse && egg.product_warehouse.product) { + const existingOption = options.find( + (opt) => opt.value === egg.product_warehouse_id + ); + if (!existingOption) { + options.push({ + value: egg.product_warehouse_id, + label: egg.product_warehouse.product.name, + }); + } + } + }); + } + + return options; + }, [eggProductsData, initialValues, type, selectedKandang]); + + // ===== FORMIK SETUP ===== + const formikInitialValues = useMemo(() => { + let baseValues; + if (isLayingCategory) { + baseValues = getRecordingLayingFormInitialValues( + initialValues + ) as RecordingLayingFormValues; + } else { + baseValues = getRecordingGrowingFormInitialValues(initialValues); + } + + if (projectFlockKandangDetail && (type === 'edit' || type === 'detail')) { + baseValues.project_flock_kandang = { + value: projectFlockKandangDetail.project_flock.id, + label: projectFlockKandangDetail.project_flock.flock_name || '', + }; + } + + return baseValues; + }, [initialValues, isLayingCategory, projectFlockKandangDetail, type]); + + const formik = useFormik< + RecordingGrowingFormValues | RecordingLayingFormValues + >({ + initialValues: formikInitialValues, + enableReinitialize: true, + validationSchema: (() => { + if (isLayingCategory) { + return type === 'edit' + ? UpdateRecordingLayingFormSchema + : RecordingLayingFormSchema; + } + return type === 'edit' + ? UpdateRecordingGrowingFormSchema + : RecordingGrowingFormSchema; + })(), + validateOnChange: true, + validateOnBlur: true, + onSubmit: async (values) => { + if (isLayingCategory) { + const layingValues = values as RecordingLayingFormValues; + const layingPayload = createLayingPayload(layingValues); + + switch (type) { + case 'add': + await createRecordingHandler( + layingPayload as CreateLayingRecordingPayload + ); + break; + case 'edit': + await updateRecordingHandler( + initialValues?.id as number, + layingPayload as UpdateLayingRecordingPayload + ); + break; + } + } else { + const growingValues = values as RecordingGrowingFormValues; + const growingPayload = createGrowingPayload(growingValues); + + switch (type) { + case 'add': + await createRecordingHandler( + growingPayload as CreateGrowingRecordingPayload + ); + break; + case 'edit': + await updateRecordingHandler( + initialValues?.id as number, + growingPayload as UpdateGrowingRecordingPayload + ); + break; } } }, - [formik] - ); + }); - // Handle calculation when chicken_count changes - const handleChickenCountChange = useCallback( - (idx: number, value: number) => { - formik.setFieldValue(`body_weight.${idx}.chicken_count`, value); + // ===== HELPER FUNCTIONS ===== + const getTotalChickQtyError = useCallback( + (qty: number) => { + if (type === 'detail') return null; + if (!formik.values.project_flock_kandang_id) return null; - 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`, ''); - } + 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] + [formik.values.project_flock_kandang_id, getLatestTotalChickQty, type] ); - // Handle calculation when average_weight changes - const handleAverageWeightChange = useCallback( - (idx: number, value: number) => { - formik.setFieldValue(`body_weight.${idx}.average_chicken_weight`, value); + useCallback((): OptionType | null => { + if ( + !formik.values.project_flock_kandang || + !isResponseSuccess(projectFlocks) + ) { + return selectedLocation; + } + const projectFlockId = formik.values.project_flock_kandang.value; + const projectFlock = projectFlocks.data.find( + (pf) => pf.id === projectFlockId + ); + if (projectFlock && projectFlock.location) { + return { + value: projectFlock.location.id, + label: projectFlock.location.name, + }; + } + return selectedLocation; + }, [formik.values.project_flock_kandang, projectFlocks, selectedLocation]); - 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`, ''); - } + const getAvailableStock = useCallback( + (productWarehouseId: number) => { + if ((type as 'add' | 'edit' | 'detail') === 'detail') return 0; + if (!isResponseSuccess(stockProducts)) return 0; + const productWarehouse = stockProducts.data.find( + (pw) => pw.id === productWarehouseId + ); + return productWarehouse?.quantity ?? 0; + }, + [stockProducts, type] + ); + + const getStockUsageError = useCallback( + (stockIdx: number) => { + if ((type as 'add' | 'edit' | 'detail') === 'detail') return null; + const stock = formik.values.stocks?.[stockIdx]; + if (!stock || !stock.product_warehouse_id) return null; + const availableStock = getAvailableStock(stock.product_warehouse_id); + const requestedUsage = Number(stock.qty) || 0; + if (requestedUsage > availableStock) { + return `Jumlah pakai melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)}`; } + return null; }, - [formik] + [formik.values.stocks, getAvailableStock, type] ); - // Create wrapper handlers that match NumberInput's onChange signature - const handleChickenWeightChangeWrapper = useCallback( - (idx: number) => (e: React.ChangeEvent) => { - const value = - parseFloat(e.target.value.replace(/[^\d,.-]/g, '').replace(/,/g, '')) || - 0; - handleChickenWeightChange(idx, value); + const getStockUsageAdornment = useCallback( + (stockIdx: number) => { + if ((type as 'add' | 'edit' | 'detail') === 'detail') return null; + const stock = formik.values.stocks?.[stockIdx]; + if (!stock || !stock.product_warehouse_id) return null; + const availableStock = getAvailableStock(stock.product_warehouse_id); + const requestedUsage = Number(stock.qty) || 0; + const remainingStock = availableStock - requestedUsage; + if (requestedUsage > 0) { + return ( + + (sisa: {formatNumber(remainingStock)}) + + ); + } + return ( + + (tersedia: {formatNumber(availableStock)}) + + ); }, - [handleChickenWeightChange] + [formik.values.stocks, getAvailableStock, type] ); - const handleChickenCountChangeWrapper = useCallback( - (idx: number) => (e: React.ChangeEvent) => { - const value = - parseFloat(e.target.value.replace(/[^\d,.-]/g, '').replace(/,/g, '')) || - 0; - handleChickenCountChange(idx, value); - }, - [handleChickenCountChange] - ); + const getProjectFlockBadgeAdornment = useCallback(() => { + if (!projectFlockKandangLookup) return null; - const handleAverageWeightChangeWrapper = useCallback( - (idx: number) => (e: React.ChangeEvent) => { - const value = - parseFloat(e.target.value.replace(/[^\d,.-]/g, '').replace(/,/g, '')) || - 0; - handleAverageWeightChange(idx, value); - }, - [handleAverageWeightChange] - ); - - const handleVaccinationStockChangeWrapper = useCallback( - (idx: number) => (e: React.ChangeEvent) => { - const value = parseInt(e.target.value.replace(/[^\d.-]/g, '')) || 0; - formik.setFieldValue(`vaccination.${idx}.used_stock`, value); - }, - [formik] - ); - - const handleMortalityCountChangeWrapper = useCallback( - (idx: number) => (e: React.ChangeEvent) => { - const value = parseInt(e.target.value.replace(/[^\d.-]/g, '')) || 0; - formik.setFieldValue(`mortality.${idx}.count`, value); - }, - [formik] - ); - - const removeBodyWeight = (idx: number) => { - const updatedBodyWeight = formik.values.body_weight?.filter( - (_, i) => i !== idx + const isAlreadyRecorded = recordedProjectFlockKandangIds.has( + projectFlockKandangLookup.project_flock_kandang_id ); - formik.setFieldValue('body_weight', updatedBodyWeight); - }; + let color: 'neutral' | 'success' | 'warning' | 'error'; - const removeSelectedBodyWeight = () => { - const updatedBodyWeight = formik.values.body_weight?.filter( - (_, idx) => !selectedWeight.includes(idx) + if (isAlreadyRecorded) { + color = 'warning'; + } else { + color = 'success'; + } + + return ( + + Periode {projectFlockKandangLookup.project_flock?.period} + ); - formik.setFieldValue('body_weight', updatedBodyWeight); - setSelectedWeight([]); - }; + }, [recordedProjectFlockKandangIds, projectFlockKandangLookup]); - // EVENT HANDLERS - Vaccination - const addVaccination = () => { - const newVaccination = [ - ...(formik.values.vaccination || []), - { - vaccine: null, - vaccine_id: '', - total_stock: '', - used_stock: 0, - }, - ]; - formik.setFieldValue('vaccination', newVaccination); - }; + const getProductFlagBadgeAdornment = useCallback( + (productWarehouseId: number) => { + if (!isResponseSuccess(stockProducts)) return null; - const removeVaccination = (idx: number) => { - const updatedVaccination = formik.values.vaccination?.filter( - (_, i) => i !== idx - ); - formik.setFieldValue('vaccination', updatedVaccination); - }; + const productWarehouse = stockProducts.data.find( + (pw) => pw.id === productWarehouseId + ); + if (!productWarehouse) return null; - const removeSelectedVaccination = () => { - const updatedVaccination = formik.values.vaccination?.filter( - (_, idx) => !selectedVaccine.includes(idx) - ); - formik.setFieldValue('vaccination', updatedVaccination); - setSelectedVaccine([]); - }; + const hasPakanFlag = productWarehouse.product.flags?.includes('PAKAN'); + const hasOvkFlag = productWarehouse.product.flags?.includes('OVK'); - // EVENT HANDLERS - Mortality - const addMortality = () => { - const newMortality = [ - ...(formik.values.mortality || []), - { - condition: RECORDING_FLAG_OPTIONS[0].value, - count: 0, - }, - ]; - formik.setFieldValue('mortality', newMortality); - }; + if (hasPakanFlag) { + return ( + + PAKAN + + ); + } - const removeMortality = (idx: number) => { - const updatedMortality = formik.values.mortality?.filter( - (_, i) => i !== idx - ); - formik.setFieldValue('mortality', updatedMortality); - }; + if (hasOvkFlag) { + return ( + + OVK + + ); + } - const removeSelectedMortality = () => { - const updatedMortality = formik.values.mortality?.filter( - (_, idx) => !selectedMortality.includes(idx) - ); - formik.setFieldValue('mortality', updatedMortality); - setSelectedMortality([]); - }; - - const handleFeedStockChangeWrapper = useCallback( - (idx: number) => (e: React.ChangeEvent) => { - const value = parseInt(e.target.value.replace(/[^\d.-]/g, '')) || 0; - formik.setFieldValue(`feed_data.${idx}.feed_stock`, value); + return null; }, - [formik] + [stockProducts] ); - // HELPER FUNCTIONS - const isRepeaterInputError = < - T extends 'feed_data' | 'body_weight' | 'vaccination' | 'mortality', - >( - 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, + const hasExceededStock = useMemo(() => { + if ((type as 'add' | 'edit' | 'detail') === 'detail') return false; + return ( + formik.values.stocks?.some((stock, idx) => { + return getStockUsageError(idx) !== null; + }) ?? false + ); + }, [formik.values.stocks, getStockUsageError, type]); + + const hasConsumableEggs = useMemo(() => { + if (!isLayingCategory) return false; + const layingValues = formik.values as RecordingLayingFormValues; + if (!layingValues.eggs || layingValues.eggs.length === 0) return false; + + return layingValues.eggs.some((egg) => { + if (!egg.product_warehouse_id || Number(egg.qty) <= 0) return false; + + const product = eggProducts.find( + (opt) => opt.value === egg.product_warehouse_id + ); + + if (!product) return false; + + const productName = product.label.toLowerCase(); + return ( + productName.includes('konsumsi') && + productName.includes('baik') && + Number(egg.qty) > 0 + ); + }); + }, [isLayingCategory, formik.values, eggProducts]); + + const hasConsumableEggsInRecording = useCallback((recording?: Recording) => { + if (!recording || !recording.eggs || recording.eggs.length === 0) + return false; + + return recording.eggs.some((egg) => { + if (!egg.product_warehouse || !egg.product_warehouse.product) + return false; + if (Number(egg.qty) <= 0) return false; + + const productName = egg.product_warehouse.product.name.toLowerCase(); + return ( + productName.includes('konsumsi') && + productName.includes('baik') && + Number(egg.qty) > 0 + ); + }); + }, []); + + const hasConsumableEggsInCurrentRecording = useMemo(() => { + return ( + hasConsumableEggsInRecording(initialValues) || + hasConsumableEggsInRecording(newRecordingData || undefined) + ); + }, [initialValues, newRecordingData, hasConsumableEggsInRecording]); + + const isRepeaterInputError = ( + arrayName: 'body_weights' | 'stocks' | 'depletions' | 'eggs', + column: string, idx: number ) => { - if ( - !formik.touched[arrayName] || - !Array.isArray(formik.touched[arrayName]) - ) { + const touched = formik.touched as Record; + const errors = formik.errors as Record; + + if (!touched[arrayName] || !Array.isArray(touched[arrayName])) { return { isError: false, errorMessage: '', }; } - const touchedField = formik.touched[arrayName]?.[idx]?.[column as string]; - const errorField = formik.errors[arrayName]?.[idx] as Record< + const touchedField = (touched[arrayName] as unknown[])?.[idx] as Record< string, - string + unknown + >; + const errorField = (errors[arrayName] as unknown[])?.[idx] as Record< + string, + unknown >; return { - isError: touchedField && Boolean(errorField?.[column as string]), + isError: touchedField && Boolean(errorField?.[column]), errorMessage: - touchedField && errorField?.[column as string] - ? errorField[column as string] + touchedField && errorField?.[column] + ? (errorField[column] as string) : '', }; }; + // ===== EVENT HANDLERS ===== + const locationChangeHandler = (val: OptionType | OptionType[] | null) => { + setSelectedLocation(val as OptionType); + setSelectedProjectFlock(null); + setSelectedKandang(null); + formik.setFieldValue('project_flock_kandang', null); + formik.setFieldValue('project_flock_kandang_id', 0); + }; + + const projectFlockChangeHandler = (val: OptionType | OptionType[] | null) => { + setSelectedProjectFlock(val as OptionType); + setSelectedKandang(null); + formik.setFieldValue('project_flock_kandang', null); + formik.setFieldValue('project_flock_kandang_id', 0); + }; + + const kandangChangeHandler = (val: OptionType | OptionType[] | null) => { + setSelectedKandang(val as OptionType); + formik.setFieldTouched('project_flock_kandang', true); + formik.setFieldTouched('project_flock_kandang_id', true); + }; + + useEffect(() => { + if (projectFlockKandangLookup?.project_flock_kandang_id) { + const projectFlockKandangId = + projectFlockKandangLookup.project_flock_kandang_id; + + if (type === 'add') { + if (recordedProjectFlockKandangIds.has(projectFlockKandangId)) { + toast.error('Project Flock Kandang ini sudah direcord hari ini!'); + return; + } + + if ( + nextDayRecording && + nextDayRecording.project_flock_kandang_id === projectFlockKandangId + ) { + const hasSameDayRecording = isResponseSuccess(existingRecordings) + ? existingRecordings.data?.some( + (recording: Recording) => + recording.project_flock_kandang_id === + projectFlockKandangId && + recording.day === nextDayRecording.next_day + ) + : false; + + if (hasSameDayRecording) { + toast.error( + `Recording untuk hari ${nextDayRecording.next_day} sudah ada. + Tidak bisa membuat recording duplikat, mohon perbarui recording yang sudah ada terlebih dahulu.` + ); + return; + } + } + } + + if (formik.values.project_flock_kandang_id !== projectFlockKandangId) { + formik.setFieldValue('project_flock_kandang_id', projectFlockKandangId); + + formik.setFieldValue('project_flock_kandang', { + value: projectFlockKandangId, + label: projectFlockKandangLookup + ? `${projectFlockKandangLookup.project_flock.flock_name} - ${projectFlockKandangLookup.kandang.name}` + : `${selectedProjectFlock?.label || ''} - ${selectedKandang?.label || ''}`, + }); + } + } + }, [ + projectFlockKandangLookup, + selectedProjectFlock, + selectedKandang, + type, + recordedProjectFlockKandangIds, + formik.values.project_flock_kandang_id, + nextDayRecording, + existingRecordings, + today, + ]); + + useEffect(() => { + if (projectFlockKandangDetail && (type === 'edit' || type === 'detail')) { + const location = projectFlockKandangDetail.project_flock.location; + const projectFlock = projectFlockKandangDetail.project_flock; + const kandang = projectFlockKandangDetail.kandang; + + if (location) { + const locationOption = { + value: location.id, + label: location.name || '', + }; + setSelectedLocation(locationOption); + + if (projectFlock) { + const projectFlockOption = { + value: projectFlock.id, + label: projectFlock.flock_name || '', + }; + setSelectedProjectFlock(projectFlockOption); + + if (kandang) { + const kandangOption = { + value: kandang.id, + label: kandang.name || '', + }; + setSelectedKandang(kandangOption); + + if ( + formik.values.project_flock_kandang_id !== + projectFlockKandangDetail.id + ) { + formik.setFieldValue( + 'project_flock_kandang_id', + projectFlockKandangDetail.id + ); + formik.setFieldValue('project_flock_kandang', { + value: projectFlockKandangDetail.id, + label: `${projectFlock.flock_name} - ${kandang.name}`, + }); + } + } + } + } + } + }, [ + projectFlockKandangDetail, + type, + projectFlockOptions, + formik.values.project_flock_kandang_id, + ]); + + const approveHandler = async (notes: string) => { + setIsApproveLoading(true); + + const approveResponse = await RecordingApi.approve( + initialValues?.id as number, + notes + ); + + if (isResponseSuccess(approveResponse)) { + toast.success('Recording berhasil disetujui!'); + approveModal.closeModal(); + setApprovalNotes(''); + await refreshApprovals(); + router.push('/production/recording'); + } else { + toast.error( + (approveResponse?.message as string) || 'Gagal menyetujui recording' + ); + approveModal.closeModal(); + } + + setIsApproveLoading(false); + }; + + const rejectHandler = async (notes: string) => { + setIsRejectLoading(true); + + const rejectResponse = await RecordingApi.reject( + initialValues?.id as number, + notes + ); + + if (isResponseSuccess(rejectResponse)) { + toast.success('Recording berhasil ditolak!'); + rejectModal.closeModal(); + setApprovalNotes(''); + await refreshApprovals(); + router.push('/production/recording'); + } else { + toast.error( + (rejectResponse?.message as string) || 'Gagal menolak recording' + ); + rejectModal.closeModal(); + } + + setIsRejectLoading(false); + }; + + // Body Weights Handlers + 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) => { + const value = parseFloat(e.target.value) || 0; + handleWeightChange(idx, value); + }; + + const handleAvgWeightChangeWrapper = + (idx: number) => (e: React.ChangeEvent) => { + const value = parseFloat(e.target.value) || 0; + handleAvgWeightChange(idx, value); + }; + + const handleQtyChangeWrapper = + (idx: number) => (e: React.ChangeEvent) => { + 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([]); + }; + + // Stocks Handlers + const addStock = () => { + const newStocks = [ + ...(formik.values.stocks || []), + { + product_warehouse_id: 0, + qty: '', + }, + ]; + formik.setFieldValue('stocks', newStocks); + }; + + const handleStockUsageQtyChangeWrapper = useCallback( + (idx: number) => (e: React.ChangeEvent) => { + const value = parseFloat(e.target.value) || 0; + formik.setFieldValue(`stocks.${idx}.qty`, value); + }, + [formik] + ); + + 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('stocks', updatedStocks); + setSelectedStocks([]); + }; + + // Depletions Handlers + const addDepletion = () => { + const newDepletions = [ + ...(formik.values.depletions || []), + { + product_warehouse_id: 0, + qty: '', + }, + ]; + formik.setFieldValue('depletions', newDepletions); + }; + + const handleDepletionQtyChangeWrapper = useCallback( + (idx: number) => (e: React.ChangeEvent) => { + const value = parseFloat(e.target.value) || 0; + formik.setFieldValue(`depletions.${idx}.qty`, value); + }, + [formik] + ); + + const removeDepletion = (idx: number) => { + const updatedDepletions = formik.values.depletions?.filter( + (_, i) => i !== idx + ); + formik.setFieldValue('depletions', updatedDepletions); + }; + + const removeSelectedDepletions = () => { + const updatedDepletions = formik.values.depletions?.filter( + (_, idx) => !selectedDepletions.includes(idx) + ); + formik.setFieldValue('depletions', updatedDepletions); + setSelectedDepletions([]); + }; + + // Eggs Handlers + const addEgg = () => { + const newEggs = [ + ...((formik.values as RecordingLayingFormValues).eggs || []), + { + product_warehouse_id: 0, + qty: '', + }, + ]; + formik.setFieldValue('eggs', newEggs); + }; + + const handleEggQtyChangeWrapper = useCallback( + (idx: number) => (e: React.ChangeEvent) => { + const value = parseFloat(e.target.value) || 0; + formik.setFieldValue(`eggs.${idx}.qty`, value); + }, + [formik] + ); + + const removeEgg = (idx: number) => { + const updatedEggs = ( + formik.values as RecordingLayingFormValues + ).eggs?.filter((_, i) => i !== idx); + formik.setFieldValue('eggs', updatedEggs); + }; + + const removeSelectedEggs = () => { + const updatedEggs = ( + formik.values as RecordingLayingFormValues + ).eggs?.filter((_, idx) => !selectedEggs.includes(idx)); + formik.setFieldValue('eggs', updatedEggs); + setSelectedEggs([]); + }; + + // ===== EFFECTS ===== + useEffect(() => { + if (isLayingCategory && (type as 'add' | 'edit' | 'detail') !== 'detail') { + const layingValues = formik.values as RecordingLayingFormValues; + if (!layingValues.eggs || layingValues.eggs.length === 0) { + formik.setFieldValue('eggs', [{ product_warehouse_id: 0, qty: '' }]); + } + } + }, [isLayingCategory, type]); + + useEffect(() => { + if (type !== 'add') { + setNewRecordingData(null); + } + }, [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 ( <>
- +
+
+ + + {type === 'detail' && + !isRecordingApproved(initialValues) && + (!isLayingCategory || hasGradingData(initialValues)) && ( +
+ + + +
+ )} +
+ +

+ {type === 'add' && 'Tambah Recording'} + {type === 'edit' && 'Edit Recording'} + {type === 'detail' && 'Detail Recording'} +

+
+ + {type === 'detail' && approvals && !approvalsLoading && ( + + )} +
{/* Basic Info Card */} + {(type === 'add' || type === 'edit') && ( + +
+ <> + + + + + + +
+
+ )} + + {/* Combined Info Card for Detail View */} + {type === 'detail' && + initialValues && + (projectFlockKandangLookup || projectFlockKandangDetail) && ( + +
+ {initialValues.approval && ( +
+ + Status Approval + +
+ + {(() => { + const actionText = (() => { + switch (initialValues.approval.action) { + case 'APPROVED': + return 'Disetujui'; + case 'REJECTED': + return 'Ditolak'; + case 'CREATED': + return 'Dibuat'; + case 'UPDATED': + return 'Diperbarui'; + default: + return initialValues.approval.action; + } + })(); + + const stepName = initialValues.approval.step_name; + + if (stepName === actionText) { + return stepName; + } + + return `${stepName} - ${actionText}`; + })()} + +
+
+ )} +
+ Lokasi +

+ {projectFlockKandangLookup?.project_flock?.location + ?.name || + projectFlockKandangDetail?.project_flock?.location + ?.name || + '-'} +

+
+
+ Project Flock +

+ {projectFlockKandangLookup?.project_flock?.flock_name || + projectFlockKandangDetail?.project_flock?.flock_name || + '-'} +

+
+
+ Kandang +

+ {projectFlockKandangLookup?.kandang?.name || + projectFlockKandangDetail?.kandang?.name || + '-'} +

+
+
+ + Tanggal Recording + +

+ {formatDate( + initialValues.record_datetime || '', + 'DD MMMM YYYY' + )} +

+
+
+ Hari +

Hari ke-{initialValues.day}

+
+
+ Kategori +

+ + {initialValues.project_flock_category} + +

+
+
+ Periode +

+ + Periode{' '} + {projectFlockKandangLookup?.project_flock?.period || + projectFlockKandangDetail?.project_flock?.period || + '-'} + +

+
+
+
+ )} + + {/* Body Weights Table */} -
- { - locationChangeHandler(val); - setTimeout(() => { - formik.setFieldTouched('location', true); - formik.setFieldTouched('location_id', true); - }, 0); - }} - options={locationOptions} - onInputChange={setLocationSelectInputValue} - isLoading={isLoadingLocations} - isError={ - formik.touched.location_id && - Boolean(formik.errors.location_id) - } - errorMessage={formik.errors.location_id as string} - isDisabled={type === 'detail'} - isClearable - placeholder='Pilih lokasi terlebih dahulu' - /> - - { - const date = e.target.value ? new Date(e.target.value) : null; - formik.setFieldValue('recording_date', date); - }} - onBlur={formik.handleBlur} - isError={ - formik.touched.recording_date && - Boolean(formik.errors.recording_date) - } - errorMessage={formik.errors.recording_date as string} - readOnly={type === 'detail'} - /> - - { - flockChangeHandler(val); - setTimeout(() => { - formik.setFieldTouched('flock', true); - formik.setFieldTouched('flock_id', true); - }, 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' - } - /> - - { - coopChangeHandler(val); - setTimeout(() => { - formik.setFieldTouched('coop', true); - formik.setFieldTouched('coop_id', true); - }, 0); - }} - 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' - } - /> -
-
- - {/* Feed Data Table */} - {
- {type !== 'detail' && ( + {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( )} - - {type !== 'detail' && } + + {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( + + )} - {formik.values.feed_data?.map((feed, idx) => ( - - {type !== 'detail' && ( + {formik.values.body_weights?.map((bw, idx) => ( + + {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( )} - - - {type !== 'detail' && ( - - )} - - ))} - -
0 + formik.values.body_weights?.length === + selectedBodyWeights.length && + formik.values.body_weights?.length > 0 } onChange={( e: React.ChangeEvent ) => { if (e.target.checked) { - setSelectedFeed( - formik.values.feed_data?.map((_, idx) => idx) ?? - [] + setSelectedBodyWeights( + formik.values.body_weights?.map( + (_, idx) => idx + ) ?? [] ); } else { - setSelectedFeed([]); + setSelectedBodyWeights([]); } }} classNames={{ @@ -817,43 +1881,56 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { - Nama Pakan + Berat Ayam (gram) * Total Stock pada saat ini - Jumlah Stock yang digunakan + Jumlah Ayam * Action + Rata-rata Berat Ayam (gram) + + * + + Action
) => { if (e.target.checked) { - setSelectedFeed([...selectedFeed, idx]); + setSelectedBodyWeights([ + ...selectedBodyWeights, + idx, + ]); } else { - setSelectedFeed( - selectedFeed.filter((i) => i !== idx) + setSelectedBodyWeights( + selectedBodyWeights.filter((i) => i !== idx) ); } }} @@ -864,281 +1941,75 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { /> - - 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' && ( -
- {selectedFeed.length > 0 && ( - - )} - -
- )} - - - {/* Body Weight Table */} - -
- - - - {type !== 'detail' && ( - - )} - - - - {type !== 'detail' && } - - - - {formik.values.body_weight?.map((weight, idx) => ( - - {type !== 'detail' && ( - - )} - + - - {type !== 'detail' && ( + + {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
-
- 0 - } - onChange={( - e: React.ChangeEvent - ) => { - if (e.target.checked) { - setSelectedWeight( - formik.values.body_weight?.map( - (_, idx) => idx - ) ?? [] - ); - } else { - setSelectedWeight([]); - } - }} - classNames={{ - wrapper: - 'flex justify-center items-center h-full', - checkbox: 'checkbox checkbox-sm', - }} - /> -
-
- Berat (Gram) - - * - - - Jumlah Ayam - - * - - - Rata-rata berat Ayam - - * - - Action
- - ) => { - if (e.target.checked) { - setSelectedWeight([...selectedWeight, idx]); - } else { - setSelectedWeight( - selectedWeight.filter((i) => i !== idx) - ); - } - }} - classNames={{ - wrapper: 'flex justify-center items-center', - checkbox: 'checkbox checkbox-sm', - }} - /> - - + + { -
- -
-
-
+
- {type !== 'detail' && ( + {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
- {selectedWeight.length > 0 && ( + {selectedBodyWeights.length > 0 && ( )}
)}
- {/* Vaccination Table */} + {/* Stocks Table */} { - {type !== 'detail' && ( + {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( )} - - {type !== 'detail' && } + {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( + + )} - {formik.values.vaccination?.map((vaccine, idx) => ( - - {type !== 'detail' && ( + {formik.values.stocks?.map((stock, idx) => ( + + {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( - - {type !== 'detail' && ( + {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
-
- 0 + 0 + } + onChange={( + e: React.ChangeEvent + ) => { + if (e.target.checked) { + setSelectedStocks( + formik.values.stocks?.map((_, idx) => idx) ?? [] + ); + } else { + setSelectedStocks([]); } - onChange={( - e: React.ChangeEvent - ) => { - if (e.target.checked) { - setSelectedVaccine( - formik.values.vaccination?.map( - (_, idx) => idx - ) ?? [] - ); - } else { - setSelectedVaccine([]); - } - }} - classNames={{ - wrapper: - 'flex justify-center items-center h-full', - checkbox: 'checkbox checkbox-sm', - }} - /> -
+ }} + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} + />
- Name Vaksin + Persediaan * Total Stock pada saat ini - Jumlah Stock yang digunakan + Jumlah Pakai * ActionAction
) => { if (e.target.checked) { - setSelectedVaccine([...selectedVaccine, idx]); + setSelectedStocks([...selectedStocks, idx]); } else { - setSelectedVaccine( - selectedVaccine.filter((i) => i !== idx) + setSelectedStocks( + selectedStocks.filter((i) => i !== idx) ); } }} @@ -1353,131 +2191,94 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { - Number(opt.value) === Number(vaccine.vaccine_id) - ) ?? null + unifiedStockProducts.find( + (product) => + product.value === stock.product_warehouse_id + ) || null } - onChange={(val) => { - const productWarehouseId = - (val as OptionType)?.value ?? 0; - const stock = productWarehouseId - ? (ovkStockMap.get( - productWarehouseId as number - ) ?? '') - : ''; + onChange={(selectedOption) => { + const option = selectedOption as OptionType | null; formik.setFieldValue( - `vaccination.${idx}.vaccine`, - val + `stocks.${idx}.product_warehouse_id`, + option?.value || 0 ); - 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} + options={unifiedStockProducts} + placeholder='Pilih Produk' + isLoading={isLoadingStockProducts} isError={ isRepeaterInputError( - 'vaccination', - 'vaccine_id', + 'stocks', + 'product_warehouse_id', idx ).isError } errorMessage={ isRepeaterInputError( - 'vaccination', - 'vaccine_id', + 'stocks', + 'product_warehouse_id', idx ).errorMessage } + className={{ + wrapper: 'w-full min-w-48', + }} + isSearchable isDisabled={type === 'detail'} - isClearable - className={{ - wrapper: 'w-full min-w-52 md:min-w-72 lg:min-w-80', - }} - /> - - - +
+ + {(type as 'add' | 'edit' | 'detail') !== 'detail' && + getStockUsageAdornment(idx)} +
-
+
- {type !== 'detail' && ( + {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
- {selectedVaccine.length > 0 && ( + {selectedStocks.length > 0 && ( )}
)} - {/* Mortality Table */} + {/* Depletions Table */} { - {type !== 'detail' && ( + {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( )} - {type !== 'detail' && } + {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( + + )} - {formik.values.mortality?.map((mortality, idx) => ( - - {type !== 'detail' && ( + {formik.values.depletions?.map((depletion, idx) => ( + + {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( - {type !== 'detail' && ( + {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
-
- 0 + 0 + } + onChange={( + e: React.ChangeEvent + ) => { + if (e.target.checked) { + setSelectedDepletions( + formik.values.depletions?.map( + (_, idx) => idx + ) ?? [] + ); + } else { + setSelectedDepletions([]); } - onChange={( - e: React.ChangeEvent - ) => { - if (e.target.checked) { - setSelectedMortality( - formik.values.mortality?.map( - (_, idx) => idx - ) ?? [] - ); - } else { - setSelectedMortality([]); - } - }} - classNames={{ - wrapper: - 'flex justify-center items-center h-full', - checkbox: 'checkbox checkbox-sm', - }} - /> -
+ }} + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} + />
- Kondisi/Alasan Mortalitas + Kondisi * @@ -1577,34 +2371,36 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { Jumlah * ActionAction
) => { if (e.target.checked) { - setSelectedMortality([ - ...selectedMortality, + setSelectedDepletions([ + ...selectedDepletions, idx, ]); } else { - setSelectedMortality( - selectedMortality.filter((i) => i !== idx) + setSelectedDepletions( + selectedDepletions.filter((i) => i !== idx) ); } }} @@ -1617,69 +2413,80 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { )} opt.value === mortality.condition - )} - onChange={(val) => { - formik.setFieldTouched( - `mortality.${idx}.condition`, - true - ); + value={ + depletionProducts.find( + (product) => + product.value === depletion.product_warehouse_id + ) || null + } + onChange={(selectedOption) => { + const option = selectedOption as OptionType | null; formik.setFieldValue( - `mortality.${idx}.condition`, - (val as OptionType)?.value + `depletions.${idx}.product_warehouse_id`, + option?.value || 0 ); }} + options={depletionProducts} + placeholder='Pilih Kondisi' + isLoading={isLoadingDepletionProducts} isError={ - isRepeaterInputError('mortality', 'condition', idx) - .isError + isRepeaterInputError( + 'depletions', + 'product_warehouse_id', + idx + ).isError } errorMessage={ - isRepeaterInputError('mortality', 'condition', idx) - .errorMessage + isRepeaterInputError( + 'depletions', + 'product_warehouse_id', + idx + ).errorMessage } - options={RECORDING_FLAG_OPTIONS} isDisabled={type === 'detail'} - isClearable className={{ - wrapper: 'w-full min-w-52 md:min-w-72 lg:min-w-80', + wrapper: 'w-full min-w-48', }} + isSearchable + isClearable={type !== 'detail'} /> -
+
- {type !== 'detail' && ( + {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
- {selectedMortality.length > 0 && ( + {selectedDepletions.length > 0 && ( )}
)} - {/* Action buttons */} - - type={type} - formik={formik} - editUrl={ - initialValues - ? `/production/recording/detail/edit/?recordingId=${initialValues.id}` - : undefined - } - onDelete={deleteRecordingClickHandler} - /> + {/* Eggs Table - Only for LAYING Category */} + {isLayingCategory && ( + +
+ + + + {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( + + )} + + + {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( + + )} + + + + {(formik.values as RecordingLayingFormValues).eggs?.map( + (egg, idx) => ( + + {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( + + )} + + + {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( + + )} + + ) + )} + +
+ 0 + } + onChange={( + e: React.ChangeEvent + ) => { + if (e.target.checked) { + setSelectedEggs( + ( + formik.values as RecordingLayingFormValues + ).eggs?.map((_, idx) => idx) ?? [] + ); + } else { + setSelectedEggs([]); + } + }} + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} + /> + + Kondisi Telur + + * + + + Jumlah + + * + + Action
+ + ) => { + if (e.target.checked) { + setSelectedEggs([...selectedEggs, idx]); + } else { + setSelectedEggs( + selectedEggs.filter((i) => i !== idx) + ); + } + }} + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} + /> + + + product.value === egg.product_warehouse_id + ) || null + } + onChange={(selectedOption) => { + const option = + selectedOption as OptionType | null; + formik.setFieldValue( + `eggs.${idx}.product_warehouse_id`, + option?.value || 0 + ); + }} + options={eggProducts} + placeholder='Pilih Kondisi Telur' + isLoading={isLoadingEggProducts} + isError={ + isRepeaterInputError( + 'eggs', + 'product_warehouse_id', + idx + ).isError + } + errorMessage={ + isRepeaterInputError( + 'eggs', + 'product_warehouse_id', + idx + ).errorMessage + } + isDisabled={type === 'detail'} + className={{ + wrapper: 'w-full min-w-48', + }} + isSearchable + isClearable={type !== 'detail'} + /> + +
+ +
+
+
+ +
+
+
+ {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( +
+ {selectedEggs.length > 0 && ( + + )} + +
+ )} +
+ )} + {/* Action buttons */} +
+ {/* Left side - Detail & Edit actions */} +
+ {type === 'detail' && deleteRecordingClickHandler && ( + + )} + {type === 'detail' && initialValues && ( + + )} +
+ {/* Right side actions */} +
+ {type === 'detail' && isLayingCategory && ( + + + + )} + + {type === 'edit' && ( +
+ + +
+ )} + + {type === 'add' && ( +
+ + + {isLayingCategory && ( + + + + )} +
+ )} +
+
{recordingFormErrorMessage && (
{ - {type !== 'add' && ( - + {/* ===== MODALS ===== */} + {type === 'detail' && ( + <> + + + {/* Approve Confirmation Modal */} + {(type as 'add' | 'edit' | 'detail') === 'detail' && + !isRecordingApproved(initialValues) && + (!isLayingCategory || hasGradingData(initialValues)) && ( + setApprovalNotes(''), + }} + primaryButton={{ + text: 'Ya', + color: 'success', + isLoading: isApproveLoading, + onClick: approveHandler, + }} + placeholder='(Opsional) Tambahkan catatan untuk approval ini...' + rows={3} + /> + )} + + {/* Reject Confirmation Modal */} + {(type as 'add' | 'edit' | 'detail') === 'detail' && + !isRecordingApproved(initialValues) && + (!isLayingCategory || hasGradingData(initialValues)) && ( + setApprovalNotes(''), + }} + primaryButton={{ + text: 'Ya', + color: 'error', + isLoading: isRejectLoading, + onClick: rejectHandler, + }} + placeholder='(Opsional) Tambahkan catatan untuk reject ini...' + rows={3} + /> + )} + )} ); diff --git a/src/components/pages/production/recording/form/useRecordingFormHandlers.ts b/src/components/pages/production/recording/form/useRecordingFormHandlers.ts deleted file mode 100644 index 334b791d..00000000 --- a/src/components/pages/production/recording/form/useRecordingFormHandlers.ts +++ /dev/null @@ -1,70 +0,0 @@ -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/production'; -import { - CreateRecordingPayload, - UpdateRecordingPayload, -} from '@/types/api/production/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, - }; -}; diff --git a/src/components/pages/production/recording/grading/form/GradingForm.tsx b/src/components/pages/production/recording/grading/form/GradingForm.tsx new file mode 100644 index 00000000..9c3ba37a --- /dev/null +++ b/src/components/pages/production/recording/grading/form/GradingForm.tsx @@ -0,0 +1,1051 @@ +'use client'; + +import { useMemo, useState, useEffect, useCallback } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useFormik } from 'formik'; +import { Icon } from '@iconify/react'; + +import Button from '@/components/Button'; +import NumberInput from '@/components/input/NumberInput'; +import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import CheckboxInput from '@/components/input/CheckboxInput'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import Card from '@/components/Card'; +import Badge from '@/components/Badge'; + +import { + CreateGradingPayload, + UpdateGradingPayload, + RecordingEgg, + GradingEgg, + Recording, +} from '@/types/api/production/recording'; +import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; +import { type BaseApiResponse } from '@/types/api/api-general'; + +import { + RecordingGradingFormSchema, + RecordingGradingFormValues, + UpdateRecordingGradingFormSchema, + getRecordingGradingFormInitialValues, +} from '../../form/RecordingForm.schema'; + +import { cn, formatDate } from '@/lib/helper'; +import toast from 'react-hot-toast'; +import { isResponseError } from '@/lib/api-helper'; + +import { + RecordingApi, + ProjectFlockKandangApi, +} from '@/services/api/production'; + +import { useModal } from '@/components/Modal'; +import useSWR from 'swr'; + +// INTERFACES & PROPS +interface GradingFormProps { + type?: 'add' | 'edit' | 'detail'; + initialValues?: RecordingEgg & { + grading_eggs?: GradingEgg[]; + gradings?: { grade: string; qty: number }[]; + }; +} + +const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => { + // HOOKS & ROUTER + const router = useRouter(); + const searchParams = useSearchParams(); + const recordingId = searchParams.get('recording_id'); + + // STATE MANAGEMENT + const [selectedGradingItems, setSelectedGradingItems] = useState( + [] + ); + const [gradingFormErrorMessage, setGradingFormErrorMessage] = useState(''); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const deleteModal = useModal(); + + // API DATA FETCHING + const recordingUrl = useMemo(() => { + const recordingIdToUse = recordingId; + if (!recordingIdToUse) return null; + return `${RecordingApi.basePath}/${recordingIdToUse}`; + }, [recordingId]); + + const { data: recordingData } = useSWR( + recordingUrl, + recordingUrl ? RecordingApi.getAllFetcher : null + ); + + // DATA PROCESSING + const recording = + recordingData?.status === 'success' + ? (recordingData.data as unknown as Recording) + : undefined; + + const projectFlockKandangUrl = useMemo(() => { + if (!recording?.project_flock_kandang_id) return null; + return `${ProjectFlockKandangApi.basePath}/${recording.project_flock_kandang_id}`; + }, [recording?.project_flock_kandang_id]); + + const { data: projectFlockKandangData } = useSWR( + projectFlockKandangUrl, + projectFlockKandangUrl ? ProjectFlockKandangApi.getAllFetcher : null + ); + + const projectFlockKandang = + projectFlockKandangData?.status === 'success' + ? (projectFlockKandangData.data as unknown as ProjectFlockKandang) + : undefined; + + const konsumsiBaikEggData = useMemo(() => { + if (!recording?.eggs) return null; + + const konsumsiBaikEgg = recording.eggs.find((egg: RecordingEgg) => + egg.product_warehouse?.product?.name + ?.toLowerCase() + .includes('konsumsi baik') + ); + + return konsumsiBaikEgg || null; + }, [recording]); + + const totalKonsumsiBaikEggs = konsumsiBaikEggData?.qty || 0; + const konsumsiBaikEggId = konsumsiBaikEggData?.id; + + const isDataLoading = + !recording || + (totalKonsumsiBaikEggs === 0 && + recording?.project_flock_category === 'LAYING'); + + // FORM HANDLERS + const createGradingHandler = useCallback( + async (payload: CreateGradingPayload) => { + const res = (await RecordingApi.createGrading(payload)) as + | BaseApiResponse + | undefined; + + if (!res || isResponseError(res)) { + setGradingFormErrorMessage(res?.message || 'Failed to add Grading'); + return; + } + + toast.success(res?.message || 'Successfully added Grading!'); + router.push('/production/recording'); + }, + [router] + ); + + const updateGradingHandler = useCallback( + async (gradingId: number, payload: UpdateGradingPayload) => { + const res = (await RecordingApi.updateGrading(gradingId, payload)) as + | BaseApiResponse + | undefined; + + if (!res || isResponseError(res)) { + setGradingFormErrorMessage(res?.message || 'Failed to update Grading'); + return; + } + toast.success(res?.message || 'Successfully updated Grading!'); + router.refresh(); + router.push('/production/recording'); + }, + [router] + ); + + const deleteRecordingClickHandler = useCallback(() => { + deleteModal.openModal(); + }, [deleteModal]); + + const confirmationModalDeleteClickHandler = useCallback(async () => { + if (!initialValues?.id) return; + + setIsDeleteLoading(true); + try { + const res = (await RecordingApi.deleteGrading(initialValues.id)) as + | BaseApiResponse + | undefined; + + if (!res || isResponseError(res)) { + setGradingFormErrorMessage(res?.message || 'Failed to delete Grading'); + return; + } + deleteModal.closeModal(); + toast.success(res?.message || 'Successfully delete Grading!'); + router.push('/production/recording'); + } catch (err) { + console.error(err); + setGradingFormErrorMessage('Failed to delete Grading'); + } finally { + setIsDeleteLoading(false); + } + }, [deleteModal, initialValues?.id, router]); + + // FORMIK SETUP + const formikInitialValues = useMemo(() => { + let recordingEggId: number | undefined = konsumsiBaikEggId; + + if (!recordingEggId && initialValues?.id) { + recordingEggId = initialValues.id; + } + + if (!recordingEggId) { + recordingEggId = parseInt(recordingId || '0') || 0; + } + + let gradingData: { + recording_egg_id: number; + grade: string; + qty: number; + }[] = []; + + if (initialValues?.grading_eggs && initialValues.grading_eggs.length > 0) { + gradingData = initialValues.grading_eggs.map((grading: GradingEgg) => ({ + recording_egg_id: recordingEggId, + grade: grading.grade, + qty: grading.qty, + })); + } else if (initialValues?.gradings && initialValues.gradings.length > 0) { + gradingData = initialValues.gradings.map( + (grading: { grade: string; qty: number }) => ({ + recording_egg_id: recordingEggId, + grade: grading.grade, + qty: grading.qty, + }) + ); + } + + return getRecordingGradingFormInitialValues({ + recording_egg_id: recordingEggId, + eggs_grading: gradingData, + }); + }, [initialValues, recordingId, konsumsiBaikEggId]); + + const formik = useFormik({ + initialValues: formikInitialValues, + enableReinitialize: true, + validationSchema: (() => { + return type === 'edit' + ? UpdateRecordingGradingFormSchema + : RecordingGradingFormSchema; + })(), + validateOnChange: true, + validateOnBlur: true, + onSubmit: async (values) => { + const gradingPayload = { + eggs_grading: (values.eggs_grading ?? []).map((grading) => ({ + recording_egg_id: grading.recording_egg_id, + grade: grading.grade, + qty: grading.qty || 0, + })), + }; + + switch (type) { + case 'add': + await createGradingHandler(gradingPayload as CreateGradingPayload); + break; + case 'edit': + await updateGradingHandler( + initialValues?.id as number, + gradingPayload as UpdateGradingPayload + ); + break; + } + }, + }); + + const currentGradingTotal = useMemo(() => { + return (formik.values.eggs_grading || []).reduce((total, grading) => { + return total + (Number(grading.qty) || 0); + }, 0); + }, [formik.values.eggs_grading]); + + const isGradingExceedsAvailable = currentGradingTotal > totalKonsumsiBaikEggs; + const isGradingIncomplete = + currentGradingTotal < totalKonsumsiBaikEggs && totalKonsumsiBaikEggs > 0; + const hasUserStartedGrading = currentGradingTotal > 0; + + // GRADING HANDLERS + const addGrading = () => { + let recordingEggId: number | undefined = konsumsiBaikEggId; + + if (!recordingEggId && initialValues?.id) { + recordingEggId = initialValues.id; + } + + if (!recordingEggId) { + recordingEggId = parseInt(recordingId || '0') || 0; + } + + const newGrading = [ + ...(formik.values.eggs_grading || []), + { + recording_egg_id: recordingEggId, + grade: '', + qty: '', + }, + ]; + formik.setFieldValue('eggs_grading', newGrading); + }; + + const handleGradingGradeChangeWrapper = useCallback( + (idx: number) => (selectedOption: OptionType | OptionType[] | null) => { + const option = selectedOption as OptionType | null; + formik.setFieldValue(`eggs_grading.${idx}.grade`, option?.label || ''); + }, + [formik] + ); + + const handleGradingQtyChangeWrapper = useCallback( + (idx: number) => (e: React.ChangeEvent) => { + const value = parseFloat(e.target.value) || 0; + formik.setFieldValue(`eggs_grading.${idx}.qty`, value); + }, + [formik] + ); + + const removeGrading = (idx: number) => { + const updatedGrading = formik.values.eggs_grading?.filter( + (_, i) => i !== idx + ); + formik.setFieldValue('eggs_grading', updatedGrading); + }; + + const removeSelectedGrading = () => { + const updatedGrading = formik.values.eggs_grading?.filter( + (_, idx) => !selectedGradingItems.includes(idx) + ); + formik.setFieldValue('eggs_grading', updatedGrading); + setSelectedGradingItems([]); + }; + + // VALIDATION HELPERS + const isRepeaterInputError = ( + arrayName: 'eggs_grading', + column: string, + idx: number + ) => { + const touched = formik.touched as Record; + const errors = formik.errors as Record; + + if (!touched[arrayName] || !Array.isArray(touched[arrayName])) { + return { + isError: false, + errorMessage: '', + }; + } + + const touchedField = (touched[arrayName] as unknown[])?.[idx] as Record< + string, + unknown + >; + const errorField = (errors[arrayName] as unknown[])?.[idx] as Record< + string, + unknown + >; + + return { + isError: touchedField && Boolean(errorField?.[column]), + errorMessage: + touchedField && errorField?.[column] + ? (errorField[column] as string) + : '', + }; + }; + + // EFFECTS + useEffect(() => { + if (isDataLoading) { + toast.dismiss('grading-exceeds'); + toast.dismiss('grading-incomplete'); + return; + } + + if (isGradingExceedsAvailable && currentGradingTotal > 0) { + toast.error( + `Total grading (${currentGradingTotal}) melebihi telur yang tersedia (${totalKonsumsiBaikEggs})!`, + { + id: 'grading-exceeds', + duration: 3000, + } + ); + toast.dismiss('grading-incomplete'); + } else if (isGradingIncomplete && hasUserStartedGrading) { + toast.error( + `Total grading (${currentGradingTotal}) tidak sama dengan total telur konsumsi baik yang tersedia (${totalKonsumsiBaikEggs})! Semua telur harus digrading.`, + { + id: 'grading-incomplete', + duration: 3000, + } + ); + toast.dismiss('grading-exceeds'); + } else { + toast.dismiss('grading-exceeds'); + toast.dismiss('grading-incomplete'); + } + }, [ + isDataLoading, + isGradingExceedsAvailable, + isGradingIncomplete, + hasUserStartedGrading, + currentGradingTotal, + totalKonsumsiBaikEggs, + ]); + + useEffect(() => { + if ( + konsumsiBaikEggId && + formik.values.eggs_grading && + formik.values.eggs_grading.length === 0 + ) { + formik.setFieldValue('eggs_grading', [ + { recording_egg_id: konsumsiBaikEggId, grade: '', qty: '' }, + ]); + } + }, [konsumsiBaikEggId, formik.values.eggs_grading.length]); + + return ( + <> +
+
+ +

+ {type === 'add' && 'Tambah Grading'} + {type === 'edit' && 'Edit Grading'} + {type === 'detail' && 'Detail Grading'} +

+
+ +
+ {/* Basic Info Card */} + +
+ {/* Status Approval */} + {recording?.approval && ( +
+ Status Approval +
+ + {(() => { + const actionText = (() => { + switch (recording.approval.action) { + case 'APPROVED': + return 'Disetujui'; + case 'REJECTED': + return 'Ditolak'; + case 'CREATED': + return 'Dibuat'; + case 'UPDATED': + return 'Diperbarui'; + default: + return recording.approval.action; + } + })(); + + const stepName = recording.approval.step_name; + + if (stepName === actionText) { + return stepName; + } + + return `${stepName} - ${actionText}`; + })()} + +
+
+ )} + {/* Recording Info */} +
+ Lokasi +

+ {projectFlockKandang?.project_flock?.location?.name || '-'} +

+
+
+ Project Flock +

+ {projectFlockKandang?.project_flock?.flock_name || '-'} +

+
+
+ Kandang +

+ {projectFlockKandang?.kandang?.name || '-'} +

+
+
+ Tanggal Recording +

+ {recording + ? formatDate(recording.record_datetime, 'DD MMMM YYYY') + : '-'} +

+
+
+ Hari +

Hari ke-{recording?.day || '-'}

+
+
+ Kategori +

+ + {recording?.project_flock_category || '-'} + +

+
+
+ Periode +

+ + Periode {projectFlockKandang?.project_flock?.period || '-'} + +

+
+
+ +
+ {/* Additional Recording Info */} +
+
+
+ +
+ + Detail Recording + +
+
+
+

Area

+

+ {projectFlockKandang?.project_flock?.area?.name || '-'} +

+
+
+

Status Kandang

+

+ {projectFlockKandang?.kandang?.status || '-'} +

+
+
+
+ + {/* Total Telur Konsumsi Baik Info */} +
+
+
+

+ Total Telur Konsumsi Baik +

+
+

+ {isDataLoading ? ( + + ) : ( + totalKonsumsiBaikEggs + )}{' '} + + telur + +

+
+
+
+ +
+
+ + {/* Progress Bar */} +
+
+ Total yang digrading: + + {isDataLoading ? ( + + ) : ( + `${currentGradingTotal} / ${totalKonsumsiBaikEggs}` + )} + +
+
+
+
+ {!isDataLoading && isGradingExceedsAvailable && ( +
+ + Melebihi batas tersedia +
+ )} + {!isDataLoading && + isGradingIncomplete && + hasUserStartedGrading && ( +
+ + + Grading belum lengkap, semua telur harus digrading + +
+ )} + {isDataLoading && ( +
+ + Memuat data telur konsumsi baik... +
+ )} +
+
+
+ + + {/* Grading Table */} + +
+ + + + {type !== 'detail' && ( + + )} + + + {type !== 'detail' && } + + + + {formik.values.eggs_grading?.map((grading, idx) => ( + + {type !== 'detail' && ( + + )} + + + {type !== 'detail' && ( + + )} + + ))} + +
+ 0 + } + onChange={( + e: React.ChangeEvent + ) => { + if (e.target.checked) { + setSelectedGradingItems( + formik.values.eggs_grading?.map( + (_, idx) => idx + ) ?? [] + ); + } else { + setSelectedGradingItems([]); + } + }} + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} + /> + + Grade + + * + + + Jumlah + + * + + Action
+ + ) => { + if (e.target.checked) { + setSelectedGradingItems([ + ...selectedGradingItems, + idx, + ]); + } else { + setSelectedGradingItems( + selectedGradingItems.filter((i) => i !== idx) + ); + } + }} + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} + /> + + + + + +
+ +
+
+
+ {type !== 'detail' && ( +
+ {selectedGradingItems.length > 0 && ( + + )} + +
+ )} +
+ + {/* Action buttons */} +
+ {type !== 'add' && ( +
+ {deleteRecordingClickHandler && ( + + )} + {type !== 'edit' && initialValues && ( + + )} +
+ )} + {type !== 'detail' && ( +
+ + +
+ )} +
+ {gradingFormErrorMessage && ( +
+ + {gradingFormErrorMessage} +
+ )} + +
+ + {/* ===== MODALS ===== */} + {type !== 'add' && ( + <> + + + )} + + ); +}; + +export default GradingForm; diff --git a/src/config/approval-line.ts b/src/config/approval-line.ts index f0806bf8..1bc1c635 100644 --- a/src/config/approval-line.ts +++ b/src/config/approval-line.ts @@ -33,6 +33,51 @@ export const TRANSFER_TO_LAYING_APPROVAL_LINE: ApprovalLine = [ }, ] as const; +export const RECORDING_APPROVAL_LINE: ApprovalLine = [ + { + step_number: 1, + step_name: 'Grading-Telur', + }, + { + step_number: 2, + step_name: 'Pengajuan', + }, + { + step_number: 3, + step_name: 'Disetujui', + }, +] as const; + +export const GROWING_RECORDING_APPROVAL_LINE: ApprovalLine = [ + { + step_number: 1, + step_name: 'Grading-Telur', + }, + { + step_number: 2, + step_name: 'Pengajuan', + }, + { + step_number: 3, + step_name: 'Disetujui', + }, +] as const; + +export const LAYING_RECORDING_APPROVAL_LINE: ApprovalLine = [ + { + step_number: 1, + step_name: 'Grading-Telur', + }, + { + step_number: 2, + step_name: 'Pengajuan', + }, + { + step_number: 3, + step_name: 'Disetujui', + }, +] as const; + export const PURCHASE_ORDER_APPROVAL_LINE: ApprovalLine = [ { step_number: 1, diff --git a/src/config/constant.ts b/src/config/constant.ts index eaeb7c1a..ab85ffae 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -244,6 +244,39 @@ export const RECORDING_FLAG_OPTIONS = [ { label: 'Ayam Mati', value: 'Ayam Mati' }, ]; +export const APPROVAL_WORKFLOWS = [ + { + key: 'PROJECT_FLOCKS', + steps: [ + { + step_number: 1, + step_name: 'Pengajuan', + }, + { + step_number: 2, + step_name: 'Aktif', + }, + ], + }, + { + key: 'RECORDINGS', + steps: [ + { + step_number: 1, + step_name: 'Grading-Telur', + }, + { + step_number: 2, + step_name: 'Pengajuan', + }, + { + step_number: 3, + step_name: 'Disetujui', + }, + ], + }, +]; + export const ACCEPTED_FILE_TYPE = { PDF: { 'application/pdf': ['.pdf'], diff --git a/src/services/api/approval.ts b/src/services/api/approval.ts new file mode 100644 index 00000000..1debace3 --- /dev/null +++ b/src/services/api/approval.ts @@ -0,0 +1,6 @@ +import { BaseApiService } from '@/services/api/base'; +import { BaseApproval } from '@/types/api/api-general'; + +export const ApprovalApi = new BaseApiService( + '/approvals' +); diff --git a/src/services/api/production.ts b/src/services/api/production.ts index 11505dff..4266f6b7 100644 --- a/src/services/api/production.ts +++ b/src/services/api/production.ts @@ -1,8 +1,17 @@ import { BaseApiService } from './base'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { + CreateProjectFlockPayload, + ProjectFlock, + UpdateProjectFlockPayload, +} from '@/types/api/production/project-flock'; import { CreateRecordingPayload, Recording, UpdateRecordingPayload, + CreateGradingPayload, + UpdateGradingPayload, + NextDayRecording, } from '@/types/api/production/recording'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; @@ -11,8 +20,96 @@ export const ProjectFlockKandangApi = new BaseApiService< unknown, unknown >('/production/project-flock-kandangs'); -export const RecordingApi = new BaseApiService< +export const ProjectFlockApi = new BaseApiService< + ProjectFlock, + CreateProjectFlockPayload, + UpdateProjectFlockPayload +>('/production/project-flocks'); +export class RecordingService extends BaseApiService< Recording, CreateRecordingPayload, UpdateRecordingPayload ->('/production/recordings'); +> { + constructor(basePath: string = '') { + super(basePath); + } + + async approve( + idOrIds: number | number[], + notes?: string + ): Promise | undefined> { + const approvable_ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds]; + return await this.customRequest>('approvals', { + method: 'POST', + payload: { + action: 'APPROVED', + approvable_ids, + notes, + }, + }); + } + + async reject( + idOrIds: number | number[], + notes: string = '' + ): Promise | undefined> { + const approvable_ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds]; + return await this.customRequest>('approvals', { + method: 'POST', + payload: { + action: 'REJECTED', + approvable_ids, + notes, + }, + }); + } + + async createGrading( + payload: CreateGradingPayload + ): Promise | undefined> { + return await this.customRequest>('gradings', { + method: 'POST', + payload, + }); + } + + async updateGrading( + gradingId: number, + payload: UpdateGradingPayload + ): Promise | undefined> { + return await this.customRequest>( + `gradings/${gradingId}`, + { + method: 'PUT', + payload, + } + ); + } + + async deleteGrading( + gradingId: number + ): Promise | undefined> { + return await this.customRequest>( + `gradings/${gradingId}`, + { + method: 'DELETE', + } + ); + } + + async nextDayRecording( + projectFlockId: number + ): Promise | undefined> { + return await this.customRequest>( + `next-day`, + { + method: 'GET', + params: { + project_flock_kandang_id: projectFlockId, + }, + } + ); + } +} + +export const RecordingApi = new RecordingService('/production/recordings'); diff --git a/src/services/api/production/project-flock-kandang.ts b/src/services/api/production/project-flock-kandang.ts new file mode 100644 index 00000000..b7729325 --- /dev/null +++ b/src/services/api/production/project-flock-kandang.ts @@ -0,0 +1,11 @@ +import { BaseApiService } from '@/services/api/base'; +import { + BaseProjectFlockKandang, + ProjectFlockKandang, +} from '@/types/api/production/project-flock-kandang'; + +export const ProjectFlockKandangApi = new BaseApiService< + BaseProjectFlockKandang, + ProjectFlockKandang, + unknown +>('project-flock-kandang'); diff --git a/src/types/api/production/project-flock.d.ts b/src/types/api/production/project-flock.d.ts index 9e6e7c3c..54ae86f0 100644 --- a/src/types/api/production/project-flock.d.ts +++ b/src/types/api/production/project-flock.d.ts @@ -7,8 +7,8 @@ import { BaseApproval, BaseMetadata } from '@/types/api/api-general'; export type BaseProjectFlock = { id: number; - name: string; - flock_name: string; + name?: string; + flock_name?: string; status: string; flock?: Flock; flock_i?: number; @@ -52,6 +52,16 @@ export type ProjectFlockApprovalPayload = { approvable_ids: number[]; }; +export type ProjectFlockKandangLookup = { + id: number; + project_flock_kandang_id: number; + project_flock_id: number; + kandang_id: number; + kandang: Kandang; + project_flock: ProjectFlock; + quantity: number; +}; + export type ProjectFlockAvailableQuantity = { project_flock_id: number; flock_name: string; diff --git a/src/types/api/production/recording.d.ts b/src/types/api/production/recording.d.ts index 6fac0bc8..e7b28f47 100644 --- a/src/types/api/production/recording.d.ts +++ b/src/types/api/production/recording.d.ts @@ -1,61 +1,147 @@ -import { BaseMetadata } from '@/types/api/api-general'; -import { Location } from '@/types/api/master-data/location'; -import { Kandang } from '@/types/api/master-data/kandang'; -import { Flock } from '@/types/api/master-data/flock'; +import { BaseApproval, BaseMetadata, User } from '@/types/api/api-general'; +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; + total_chick_qty: number; + daily_depletion_rate?: number; + cum_depletion?: number; +}; export type BaseRecording = { id: number; - flock: Flock; - recording_date: string; - location: Location; - coop: Kandang; - feed_data: { - feed_name: string; - feed_qty: number; - feed_stock: number; - }[]; - body_weight: { - chicken_weight: number; - chicken_count: number; - average_chicken_weight: number; - }[]; - vaccination: { - vaccine_name: string; - total_stock: number; - used_stock: number; - }[]; - mortality: { - condition: string; - count: number; + project_flock_kandang_id: number; + record_datetime: string; + day: number; + created_by: User; +} & ProductionMetrics; + +export type RecordingBW = { + id: number; + recording_id: number; + avg_weight: number; + qty: number; + total_weight: number; +}; + +export type RecordingDepletion = { + id: number; + recording_id: number; + product_warehouse_id: number; + qty: number; + product_warehouse: ProductWarehouse; +}; + +export type RecordingStock = { + id: number; + recording_id: number; + product_warehouse_id: number; + usage_amount?: number; + usage_qty: number; + qty: number; + pending_qty: number; + product_warehouse: ProductWarehouse; +}; + +export type RecordingEgg = { + id: number; + recording_id: number; + product_warehouse_id: number; + qty: number; + created_by: User; + product_warehouse: ProductWarehouse; + gradings?: { + grade: string; + qty: number; }[]; }; -export type Recording = BaseMetadata & BaseRecording; +export type GradingEgg = { + id: number; + recording_egg_id: number; + qty: number; + grade: string; + created_by: User; +}; -export type CreateRecordingPayload = { - flock_id: number; - recording_date: string; - location_id: number; - coop_id: number; - feed_data: { - feed_id: string; - feed_qty: number; - feed_stock: number; +export type Recording = BaseMetadata & + BaseRecording & { + project_flock_category?: 'GROWING' | 'LAYING'; + approval?: BaseApproval; + egg_grading_status?: string | null; + egg_grading_pending_qty?: number | null; + egg_grading_completed_qty?: number | null; + body_weights?: RecordingBW[]; + depletions?: RecordingDepletion[]; + stocks?: RecordingStock[]; + eggs?: RecordingEgg[]; + recording_bws?: RecordingBW[]; + recording_depletions?: RecordingDepletion[]; + recording_stocks?: RecordingStock[]; + recording_eggs?: RecordingEgg[]; + grading_eggs?: GradingEgg[]; + }; + +export type NextDayRecording = { + project_flock_kandang_id: number; + next_day: number; +}; + +export type CreateGrowingRecordingPayload = { + project_flock_kandang_id: number; + body_weights: { + avg_weight: number; + qty: number; }[]; - body_weight: { - chicken_weight: number; - chicken_count: number; - average_chicken_weight: number; + stocks?: { + product_warehouse_id: number; + qty: number; }[]; - vaccination: { - vaccine_id: string; - total_stock: number; - used_stock: number; - }[]; - mortality: { - condition: string; - count: number; + depletions?: { + product_warehouse_id: number; + qty: number; }[]; }; +export type CreateGradingPayload = { + eggs_grading: { + recording_egg_id: number; + grade: string; + qty: number; + }[]; +}; + +export type UpdateGradingPayload = CreateGradingPayload; + +export type CreateGradingRecordingPayload = { + eggs_grading: { + recording_egg_id: number; + grade: string; + qty: number; + }[]; +}; + +export type CreateEggPayload = { + product_warehouse_id: number; + qty: number; +}; + +export type CreateLayingRecordingPayload = CreateGrowingRecordingPayload & { + eggs?: CreateEggPayload[]; +}; + +export type CreateRecordingPayload = + | CreateGrowingRecordingPayload + | CreateLayingRecordingPayload + | CreateGradingRecordingPayload; + +export type UpdateGrowingRecordingPayload = CreateGrowingRecordingPayload; +export type UpdateLayingRecordingPayload = CreateLayingRecordingPayload; +export type UpdateGradingRecordingPayload = CreateGradingRecordingPayload; + export type UpdateRecordingPayload = CreateRecordingPayload;