Merge branch 'feat/FE/US-282/egg-grading-adjustment' into 'development'

[FEAT/FE][US#282] Adjustment Recording Egg Grading

See merge request mbugroup/lti-web-client!85
This commit is contained in:
Adnan Zahir
2025-12-10 23:18:20 +07:00
12 changed files with 153 additions and 1746 deletions
@@ -1,49 +0,0 @@
'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 (
<div className='w-full p-4 flex flex-row justify-center'>
{recordingId && recordingId !== 'new' && isLoadingRecording && (
<span className='loading loading-spinner loading-xl' />
)}
{(!recordingId ||
recordingId === 'new' ||
(!isLoadingRecording && recording && isResponseSuccess(recording))) && (
<GradingForm
type='add'
initialValues={
isResponseSuccess(recording) ? recording.data?.eggs?.[0] : undefined
}
/>
)}
</div>
);
};
export default AddGrading;
@@ -1,53 +0,0 @@
'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 (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (!isLoadingRecording && (!recording || !isResponseSuccess(recording))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingRecording && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingRecording && recording && isResponseSuccess(recording) && (
<GradingForm
type='edit'
initialValues={recording.data.eggs?.find(
(egg) => egg.id === parseInt(gradingId || '0')
)}
/>
)}
</div>
);
};
export default EditGrading;
@@ -1,52 +0,0 @@
'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 (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (!isLoadingGrading && (!grading || !isResponseSuccess(grading))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingGrading && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingGrading && grading && isResponseSuccess(grading) && (
<GradingForm
type='detail'
initialValues={grading.data.eggs?.find(
(egg) => egg.id === parseInt(gradingId)
)}
/>
)}
</div>
);
};
export default DetailGrading;
@@ -1,11 +0,0 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
@@ -35,28 +35,22 @@ const RowOptionsMenu = ({
deleteClickHandler,
approveClickHandler,
rejectClickHandler,
isGradingCompleted,
}: {
type: 'dropdown' | 'collapse';
props: CellContext<Recording, unknown>;
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
recording.approval?.step_number === 2 &&
recording.approval?.step_name === 'Disetujui'
);
};
const isApproved = isRecordingApproved(props.row.original);
const isGradingDone = isGradingCompleted(props.row.original);
return (
<RowOptionsMenuWrapper type={type}>
@@ -78,7 +72,7 @@ const RowOptionsMenu = ({
<Icon icon='mdi:pencil-outline' width={16} height={16} />
Edit
</Button>
{!isApproved && !(isLayingCategory && !isGradingDone) && (
{!isApproved && (
<Button
onClick={approveClickHandler}
variant='ghost'
@@ -89,7 +83,7 @@ const RowOptionsMenu = ({
Approve
</Button>
)}
{!isApproved && !(isLayingCategory && !isGradingDone) && (
{!isApproved && (
<Button
onClick={rejectClickHandler}
variant='ghost'
@@ -386,33 +380,10 @@ const RecordingTable = () => {
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;
}
const isRecordingApproved = useCallback((recording: Recording): boolean => {
return (
recording.egg_grading_status === 'COMPLETED' ||
(recording.approval?.action === 'UPDATED' &&
recording.approval?.step_number === 2)
recording.approval?.action === 'APPROVED' &&
recording.approval?.step_name === 'Disetujui'
);
}, []);
@@ -506,19 +477,9 @@ const RecordingTable = () => {
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;
return recording && !isRecordingApproved(recording);
});
}, [selectedRowIds, recordings, isRecordingApproved, isGradingCompleted]);
}, [selectedRowIds, recordings, isRecordingApproved]);
useEffect(() => {
if (isResponseSuccess(recordings) && recordings.data) {
@@ -530,14 +491,7 @@ const RecordingTable = () => {
(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;
}
newSelection[rowId] = true;
}
}
});
@@ -548,13 +502,7 @@ const RecordingTable = () => {
setRowSelection(newSelection);
}
}
}, [
recordings,
rowSelection,
isRecordingApproved,
isGradingCompleted,
setRowSelection,
]);
}, [recordings, rowSelection, isRecordingApproved, setRowSelection]);
return (
<div className='w-full p-0 sm:p-4'>
@@ -640,40 +588,28 @@ const RecordingTable = () => {
id: 'select',
header: ({ table }) => {
const allRows = table.getRowModel().rows;
const selectableGrowingRows = allRows.filter((row) => {
const selectableRows = allRows.filter((row) => {
const recording = row.original;
return (
recording.project_flock_category === 'GROWING' &&
!isRecordingApproved(recording)
);
return !isRecordingApproved(recording);
});
const hasNoSelectableGrowing = selectableGrowingRows.length === 0;
const hasNoSelectableRows = selectableRows.length === 0;
const handleSelectAllGrowing = () => {
const isAllSelected = selectableGrowingRows.every((row) =>
const handleSelectAll = () => {
const isAllSelected = selectableRows.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);
}
selectableRows.forEach((row) => {
row.toggleSelected(!isAllSelected);
});
};
const isAllGrowingSelected =
selectableGrowingRows.length > 0 &&
selectableGrowingRows.every((row) => row.getIsSelected());
const isAllSelected =
selectableRows.length > 0 &&
selectableRows.every((row) => row.getIsSelected());
const isSomeGrowingSelected = selectableGrowingRows.some((row) =>
const isSomeSelected = selectableRows.some((row) =>
row.getIsSelected()
);
@@ -681,33 +617,20 @@ const RecordingTable = () => {
<div className='w-full flex flex-row justify-center'>
<CheckboxInput
name='allRow'
checked={isAllGrowingSelected}
indeterminate={
isSomeGrowingSelected && !isAllGrowingSelected
}
onChange={handleSelectAllGrowing}
disabled={hasNoSelectableGrowing}
checked={isAllSelected}
indeterminate={isSomeSelected && !isAllSelected}
onChange={handleSelectAll}
disabled={hasNoSelectableRows}
/>
</div>
);
},
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 (
<div>
<CheckboxInput
name='row'
checked={row.getIsSelected()}
disabled={isDisabled}
indeterminate={row.getIsSomeSelected()}
onChange={row.getToggleSelectedHandler()}
/>
@@ -883,7 +806,6 @@ const RecordingTable = () => {
deleteClickHandler={deleteClickHandler}
approveClickHandler={approveClickHandler}
rejectClickHandler={rejectClickHandler}
isGradingCompleted={isGradingCompleted}
/>
</RowDropdownOptions>
)}
@@ -896,7 +818,6 @@ const RecordingTable = () => {
deleteClickHandler={deleteClickHandler}
approveClickHandler={approveClickHandler}
rejectClickHandler={rejectClickHandler}
isGradingCompleted={isGradingCompleted}
/>
</RowCollapseOptions>
)}
@@ -4,7 +4,6 @@ import {
CreateGrowingRecordingPayload,
CreateLayingRecordingPayload,
CreateEggPayload,
CreateGradingPayload,
} from '@/types/api/production/recording';
type RecordingGrowingFormSchemaType = {
@@ -32,14 +31,7 @@ type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & {
eggs: {
product_warehouse_id: number;
qty: number | string;
}[];
};
type RecordingGradingFormSchemaType = {
eggs_grading: {
recording_egg_id: number;
grade: string;
qty: number | string;
weight: number | string;
}[];
};
@@ -62,6 +54,7 @@ export type DepletionSchema = {
export type EggSchema = {
product_warehouse_id: number;
qty: number | string;
weight: number | string;
};
const BodyWeightObjectSchema: Yup.ObjectSchema<BodyWeightSchema> = Yup.object({
@@ -109,6 +102,10 @@ const EggObjectSchema: Yup.ObjectSchema<EggSchema> = Yup.object({
.required('Jumlah telur wajib diisi!')
.min(1, 'Jumlah telur tidak boleh 0!')
.typeError('Jumlah telur harus berupa angka!'),
weight: Yup.number()
.required('Berat telur wajib diisi!')
.min(1, 'Berat telur minimal 1 gram!')
.typeError('Berat telur harus berupa angka!'),
});
export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSchemaType> =
@@ -190,30 +187,6 @@ export const UpdateRecordingLayingFormSchema = RecordingLayingFormSchema.shape({
.required('Project Flock Kandang wajib diisi!'),
});
export const RecordingGradingFormSchema: Yup.ObjectSchema<RecordingGradingFormSchemaType> =
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
>;
@@ -222,10 +195,6 @@ export type RecordingLayingFormValues = Yup.InferType<
typeof RecordingLayingFormSchema
>;
export type RecordingGradingFormValues = Yup.InferType<
typeof RecordingGradingFormSchema
>;
type RecordingFormData = Partial<Recording> & {
body_weights?: CreateGrowingRecordingPayload['body_weights'];
stocks?: CreateGrowingRecordingPayload['stocks'] | Recording['stocks'];
@@ -295,26 +264,12 @@ export const getRecordingLayingFormInitialValues = (
eggs: initialValues?.eggs?.map((egg: CreateEggPayload) => ({
product_warehouse_id: egg.product_warehouse_id,
qty: egg.qty,
weight: egg.weight,
})) ?? [
{
product_warehouse_id: 0,
qty: '',
},
],
});
export const getRecordingGradingFormInitialValues = (
initialValues?: Partial<CreateGradingPayload> & { 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: '',
weight: '',
},
],
});
@@ -16,7 +16,6 @@ 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,
@@ -98,9 +97,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const [recordingFormErrorMessage, setRecordingFormErrorMessage] =
useState('');
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [newRecordingData, setNewRecordingData] = useState<Recording | null>(
null
);
const [, setNewRecordingData] = useState<Recording | null>(null);
const [nextDayRecording, setNextDayRecording] =
useState<NextDayRecording | null>(null);
@@ -111,18 +108,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
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)
recording?.approval?.step_name === 'Disetujui'
);
}, []);
@@ -181,6 +167,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
eggs: (values.eggs ?? []).map((egg) => ({
product_warehouse_id: egg.product_warehouse_id,
qty: Number(egg.qty) || 0,
weight:
typeof egg.weight === 'number'
? egg.weight
: parseFloat(String(egg.weight)) || 0,
})),
};
},
@@ -203,35 +193,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
[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,
@@ -650,7 +611,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const hasPakanFlag = product.product.flags?.includes('PAKAN');
const hasOvkFlag = product.product.flags?.includes('OVK');
// Only include products that are in the same location as the selected kandang
if (hasPakanFlag || hasOvkFlag) {
options.push({
value: product.id,
@@ -690,7 +650,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
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') ||
@@ -732,7 +691,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
eggProductsData.data.forEach((product) => {
const productName = product.product.name;
// Filter for egg-related products
if (
productName.toLowerCase().includes('telur') ||
productName.toLowerCase().includes('egg') ||
@@ -1019,54 +977,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
);
}, [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,
@@ -1148,7 +1058,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
if (hasSameDayRecording) {
toast.error(
`Recording untuk hari ${nextDayRecording.next_day} sudah ada.
`Recording untuk hari ${nextDayRecording.next_day} sudah ada.
Tidak bisa membuat recording duplikat, mohon perbarui recording yang sudah ada terlebih dahulu.`
);
return;
@@ -1278,7 +1188,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
setIsRejectLoading(false);
};
// Body Weights Handlers
const addBodyWeight = () => {
const newBodyWeights = [
...(formik.values.body_weights || []),
@@ -1397,7 +1306,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
setSelectedBodyWeights([]);
};
// Stocks Handlers
const addStock = () => {
const newStocks = [
...(formik.values.stocks || []),
@@ -1430,7 +1338,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
setSelectedStocks([]);
};
// Depletions Handlers
const addDepletion = () => {
const newDepletions = [
...(formik.values.depletions || []),
@@ -1465,7 +1372,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
setSelectedDepletions([]);
};
// Eggs Handlers
const addEgg = () => {
const newEggs = [
...((formik.values as RecordingLayingFormValues).eggs || []),
@@ -1485,6 +1391,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
[formik]
);
const handleEggWeightChangeWrapper = useCallback(
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseFloat(e.target.value) || 0;
formik.setFieldValue(`eggs.${idx}.weight`, value);
},
[formik]
);
const removeEgg = (idx: number) => {
const updatedEggs = (
formik.values as RecordingLayingFormValues
@@ -1569,47 +1483,37 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
Kembali
</Button>
{type === 'detail' &&
!isRecordingApproved(initialValues) &&
(!isLayingCategory || hasGradingData(initialValues)) && (
<div className='flex flex-row gap-2'>
<Button
variant='outline'
color='success'
onClick={() => {
setApprovalNotes('');
approveModal.openModal();
}}
isLoading={isApproveLoading}
className='w-full sm:w-fit'
>
<Icon
icon='material-symbols:check'
width={24}
height={24}
/>
Approve
</Button>
{type === 'detail' && !isRecordingApproved(initialValues) && (
<div className='flex flex-row gap-2'>
<Button
variant='outline'
color='success'
onClick={() => {
setApprovalNotes('');
approveModal.openModal();
}}
isLoading={isApproveLoading}
className='w-full sm:w-fit'
>
<Icon icon='material-symbols:check' width={24} height={24} />
Approve
</Button>
<Button
variant='outline'
color='error'
onClick={() => {
setApprovalNotes('');
rejectModal.openModal();
}}
isLoading={isRejectLoading}
className='w-full sm:w-fit'
>
<Icon
icon='material-symbols:close'
width={24}
height={24}
/>
Reject
</Button>
</div>
)}
<Button
variant='outline'
color='error'
onClick={() => {
setApprovalNotes('');
rejectModal.openModal();
}}
isLoading={isRejectLoading}
className='w-full sm:w-fit'
>
<Icon icon='material-symbols:close' width={24} height={24} />
Reject
</Button>
</div>
)}
</div>
<h1 className='text-2xl font-bold text-center'>
@@ -1916,7 +1820,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{formik.values.body_weights?.map((bw, idx) => (
<tr key={`body-weight-${idx}`}>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<td className='!align-middle'>
<td className='align-middle!'>
<CheckboxInput
name={`body-weight-${idx}`}
checked={selectedBodyWeights.includes(idx)}
@@ -2166,7 +2070,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{formik.values.stocks?.map((stock, idx) => (
<tr key={`stock-${idx}`}>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<td className='!align-middle'>
<td className='align-middle!'>
<CheckboxInput
name={`stock-${idx}`}
checked={selectedStocks.includes(idx)}
@@ -2386,7 +2290,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{formik.values.depletions?.map((depletion, idx) => (
<tr key={`depletion-${idx}`}>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<td className='!align-middle'>
<td className='align-middle!'>
<CheckboxInput
name={`depletion-${idx}`}
checked={selectedDepletions.includes(idx)}
@@ -2587,6 +2491,15 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<span className='text-error'>*</span>
</span>
</th>
<th>
Berat (gram)
<span
className='tooltip tooltip-error tooltip-bottom '
data-tip='required'
>
<span className='text-error'>*</span>
</span>
</th>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<th>Action</th>
)}
@@ -2597,7 +2510,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
(egg, idx) => (
<tr key={`egg-${idx}`}>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<td className='!align-middle'>
<td className='align-middle!'>
<CheckboxInput
name={`egg-${idx}`}
checked={selectedEggs.includes(idx)}
@@ -2662,32 +2575,55 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
/>
</td>
<td>
<div className='flex flex-col gap-1'>
<NumberInput
required
name={`eggs.${idx}.qty`}
value={egg.qty ?? ''}
onChange={handleEggQtyChangeWrapper(idx)}
onBlur={formik.handleBlur}
decimalScale={0}
allowNegative={false}
thousandSeparator=','
decimalSeparator='.'
isError={
isRepeaterInputError('eggs', 'qty', idx)
.isError
}
errorMessage={
isRepeaterInputError('eggs', 'qty', idx)
.errorMessage
}
readOnly={type === 'detail'}
className={{
wrapper: 'w-full min-w-24',
}}
placeholder='Masukkan jumlah telur'
/>
</div>
<NumberInput
required
name={`eggs.${idx}.qty`}
value={egg.qty ?? ''}
onChange={handleEggQtyChangeWrapper(idx)}
onBlur={formik.handleBlur}
decimalScale={0}
allowNegative={false}
thousandSeparator=','
decimalSeparator='.'
isError={
isRepeaterInputError('eggs', 'qty', idx).isError
}
errorMessage={
isRepeaterInputError('eggs', 'qty', idx)
.errorMessage
}
readOnly={type === 'detail'}
className={{
wrapper: 'w-full min-w-24',
}}
placeholder='Masukkan jumlah telur'
/>
</td>
<td>
<NumberInput
required
name={`eggs.${idx}.weight`}
value={egg.weight ?? ''}
onChange={handleEggWeightChangeWrapper(idx)}
onBlur={formik.handleBlur}
decimalScale={0}
allowNegative={false}
thousandSeparator=','
decimalSeparator='.'
isError={
isRepeaterInputError('eggs', 'weight', idx)
.isError
}
errorMessage={
isRepeaterInputError('eggs', 'weight', idx)
.errorMessage
}
readOnly={type === 'detail'}
className={{
wrapper: 'w-full min-w-24',
}}
placeholder='Masukkan berat telur (gram)...'
/>
</td>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<td>
@@ -2779,46 +2715,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</div>
{/* Right side actions */}
<div className='flex flex-col sm:flex-row sm:justify-end gap-2 w-full sm:w-auto'>
{type === 'detail' && isLayingCategory && (
<Tooltip
content={
hasConsumableEggsInCurrentRecording
? 'Lanjut ke proses grading untuk telur konsumsi baik'
: 'Hanya bisa melanjutkan ke grading jika ada Telur Konsumsi Baik'
}
position='left'
color={
hasConsumableEggsInCurrentRecording ? 'info' : 'warning'
}
>
<Button
type='button'
color='primary'
disabled={!hasConsumableEggsInCurrentRecording}
className='w-full sm:w-auto'
onClick={() => {
const recordingId =
newRecordingData?.id || initialValues?.id;
if (recordingId) {
router.push(
`/production/recording/grading/add?recording_id=${recordingId}`
);
} else {
toast.error(
'Recording ID tidak ditemukan. Silakan refresh halaman.'
);
}
}}
>
<Icon icon='material-symbols:egg' width={24} height={24} />
{hasGradingData(initialValues) ||
hasGradingData(newRecordingData || undefined)
? 'Edit Grading'
: 'Lanjut ke Grading'}
</Button>
</Tooltip>
)}
{type === 'edit' && (
<div className='flex flex-col sm:flex-row gap-2 w-full sm:w-auto'>
<Button
@@ -2870,78 +2766,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
>
Submit
</Button>
{isLayingCategory && (
<Tooltip
content={
hasConsumableEggs
? 'Lanjut ke proses grading untuk telur konsumsi baik'
: 'Hanya bisa melanjutkan ke grading jika ada Telur Konsumsi Baik'
}
position='left'
color={hasConsumableEggs ? 'info' : 'warning'}
>
<Button
type='button'
color='info'
className='px-4'
isLoading={formik.isSubmitting}
disabled={
hasExceededStock ||
!formik.isValid ||
formik.isSubmitting ||
!hasConsumableEggs
}
onClick={async () => {
if (!formik.isValid) {
await formik.validateForm();
return;
}
setRecordingFormErrorMessage('');
formik.setSubmitting(true);
try {
if (isLayingCategory) {
const layingValues =
formik.values as RecordingLayingFormValues;
const layingPayload =
createLayingPayload(layingValues);
const recordingData =
await createRecordingHandlerWithRedirect(
layingPayload as CreateLayingRecordingPayload,
true
);
if (recordingData?.id) {
toast.success(
'Recording berhasil disimpan! Mengalihkan ke form Grading...'
);
setTimeout(() => {
router.push(
`/production/recording/grading/add?recording_id=${recordingData.id}`
);
}, 1000);
}
}
} catch {
toast.error(
'Gagal membuat recording. Silakan coba lagi.'
);
} finally {
formik.setSubmitting(false);
}
}}
>
<Icon
icon='material-symbols:egg'
width={24}
height={24}
/>
Next Step: Grading
</Button>
</Tooltip>
)}
</div>
)}
</div>
@@ -2979,8 +2803,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{/* Approve Confirmation Modal */}
{(type as 'add' | 'edit' | 'detail') === 'detail' &&
!isRecordingApproved(initialValues) &&
(!isLayingCategory || hasGradingData(initialValues)) && (
!isRecordingApproved(initialValues) && (
<ConfirmationModalWithNotes
ref={approveModal.ref}
type='success'
@@ -3002,8 +2825,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{/* Reject Confirmation Modal */}
{(type as 'add' | 'edit' | 'detail') === 'detail' &&
!isRecordingApproved(initialValues) &&
(!isLayingCategory || hasGradingData(initialValues)) && (
!isRecordingApproved(initialValues) && (
<ConfirmationModalWithNotes
ref={rejectModal.ref}
type='error'
File diff suppressed because it is too large Load Diff
+3 -15
View File
@@ -51,14 +51,10 @@ export const MARKETING_APPROVAL_LINE: ApprovalLine = [
export const RECORDING_APPROVAL_LINE: ApprovalLine = [
{
step_number: 1,
step_name: 'Grading-Telur',
},
{
step_number: 2,
step_name: 'Pengajuan',
},
{
step_number: 3,
step_number: 2,
step_name: 'Disetujui',
},
] as const;
@@ -66,14 +62,10 @@ export const RECORDING_APPROVAL_LINE: ApprovalLine = [
export const GROWING_RECORDING_APPROVAL_LINE: ApprovalLine = [
{
step_number: 1,
step_name: 'Grading-Telur',
},
{
step_number: 2,
step_name: 'Pengajuan',
},
{
step_number: 3,
step_number: 2,
step_name: 'Disetujui',
},
] as const;
@@ -81,14 +73,10 @@ export const GROWING_RECORDING_APPROVAL_LINE: ApprovalLine = [
export const LAYING_RECORDING_APPROVAL_LINE: ApprovalLine = [
{
step_number: 1,
step_name: 'Grading-Telur',
},
{
step_number: 2,
step_name: 'Pengajuan',
},
{
step_number: 3,
step_number: 2,
step_name: 'Disetujui',
},
] as const;
+1 -5
View File
@@ -233,14 +233,10 @@ export const APPROVAL_WORKFLOWS = [
steps: [
{
step_number: 1,
step_name: 'Grading-Telur',
},
{
step_number: 2,
step_name: 'Pengajuan',
},
{
step_number: 3,
step_number: 2,
step_name: 'Disetujui',
},
],
-24
View File
@@ -9,8 +9,6 @@ import {
CreateRecordingPayload,
Recording,
UpdateRecordingPayload,
CreateGradingPayload,
UpdateGradingPayload,
NextDayRecording,
} from '@/types/api/production/recording';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
@@ -64,28 +62,6 @@ export class RecordingService extends BaseApiService<
});
}
async createGrading(
payload: CreateGradingPayload
): Promise<BaseApiResponse<unknown> | undefined> {
return await this.customRequest<BaseApiResponse<unknown>>('gradings', {
method: 'POST',
payload,
});
}
async updateGrading(
gradingId: number,
payload: UpdateGradingPayload
): Promise<BaseApiResponse<unknown> | undefined> {
return await this.customRequest<BaseApiResponse<unknown>>(
`gradings/${gradingId}`,
{
method: 'PUT',
payload,
}
);
}
async deleteGrading(
gradingId: number
): Promise<BaseApiResponse<unknown> | undefined> {
+6 -42
View File
@@ -9,8 +9,7 @@ export type ProductionMetrics = {
cum_intake: number;
fcr_value: number;
total_chick_qty: number;
daily_depletion_rate?: number;
cum_depletion?: number;
cum_depletion: number;
};
export type BaseRecording = {
@@ -18,42 +17,33 @@ export type BaseRecording = {
project_flock_kandang_id: number;
record_datetime: string;
day: number;
created_by: User;
project_flock_category?: 'GROWING' | 'LAYING';
} & 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;
weight: number;
product_warehouse: ProductWarehouse;
gradings?: {
grade: string;
@@ -71,19 +61,12 @@ export type GradingEgg = {
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;
created_user: User;
body_weights?: RecordingBW[];
depletions?: RecordingDepletion[];
stocks?: RecordingStock[];
eggs?: RecordingEgg[];
recording_bws?: RecordingBW[];
recording_depletions?: RecordingDepletion[];
recording_stocks?: RecordingStock[];
recording_eggs?: RecordingEgg[];
grading_eggs?: GradingEgg[];
};
@@ -108,27 +91,10 @@ export type CreateGrowingRecordingPayload = {
}[];
};
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;
weight: number;
};
export type CreateLayingRecordingPayload = CreateGrowingRecordingPayload & {
@@ -137,11 +103,9 @@ export type CreateLayingRecordingPayload = CreateGrowingRecordingPayload & {
export type CreateRecordingPayload =
| CreateGrowingRecordingPayload
| CreateLayingRecordingPayload
| CreateGradingRecordingPayload;
| CreateLayingRecordingPayload;
export type UpdateGrowingRecordingPayload = CreateGrowingRecordingPayload;
export type UpdateLayingRecordingPayload = CreateLayingRecordingPayload;
export type UpdateGradingRecordingPayload = CreateGradingRecordingPayload;
export type UpdateRecordingPayload = CreateRecordingPayload;