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, deleteClickHandler,
approveClickHandler, approveClickHandler,
rejectClickHandler, rejectClickHandler,
isGradingCompleted,
}: { }: {
type: 'dropdown' | 'collapse'; type: 'dropdown' | 'collapse';
props: CellContext<Recording, unknown>; props: CellContext<Recording, unknown>;
deleteClickHandler: () => void; deleteClickHandler: () => void;
approveClickHandler: () => void; approveClickHandler: () => void;
rejectClickHandler: () => void; rejectClickHandler: () => void;
isGradingCompleted: (recording: Recording) => boolean;
}) => { }) => {
const isLayingCategory =
props.row.original.project_flock_category === 'LAYING';
const isRecordingApproved = (recording: Recording) => { const isRecordingApproved = (recording: Recording) => {
return ( return (
recording.approval?.action === 'APPROVED' && recording.approval?.action === 'APPROVED' &&
recording.approval?.step_name === 'Disetujui' && recording.approval?.step_number === 2 &&
recording.approval?.step_number === 3 recording.approval?.step_name === 'Disetujui'
); );
}; };
const isApproved = isRecordingApproved(props.row.original); const isApproved = isRecordingApproved(props.row.original);
const isGradingDone = isGradingCompleted(props.row.original);
return ( return (
<RowOptionsMenuWrapper type={type}> <RowOptionsMenuWrapper type={type}>
@@ -78,7 +72,7 @@ const RowOptionsMenu = ({
<Icon icon='mdi:pencil-outline' width={16} height={16} /> <Icon icon='mdi:pencil-outline' width={16} height={16} />
Edit Edit
</Button> </Button>
{!isApproved && !(isLayingCategory && !isGradingDone) && ( {!isApproved && (
<Button <Button
onClick={approveClickHandler} onClick={approveClickHandler}
variant='ghost' variant='ghost'
@@ -89,7 +83,7 @@ const RowOptionsMenu = ({
Approve Approve
</Button> </Button>
)} )}
{!isApproved && !(isLayingCategory && !isGradingDone) && ( {!isApproved && (
<Button <Button
onClick={rejectClickHandler} onClick={rejectClickHandler}
variant='ghost' variant='ghost'
@@ -386,33 +380,10 @@ const RecordingTable = () => {
RecordingApi.getAllFetcher RecordingApi.getAllFetcher
); );
const isRecordingFullyApproved = useCallback( const isRecordingApproved = useCallback((recording: Recording): boolean => {
(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 ( return (
recording.egg_grading_status === 'COMPLETED' || recording.approval?.action === 'APPROVED' &&
(recording.approval?.action === 'UPDATED' && recording.approval?.step_name === 'Disetujui'
recording.approval?.step_number === 2)
); );
}, []); }, []);
@@ -506,19 +477,9 @@ const RecordingTable = () => {
if (!isResponseSuccess(recordings) || !recordings.data) return []; if (!isResponseSuccess(recordings) || !recordings.data) return [];
return selectedRowIds.filter((id) => { return selectedRowIds.filter((id) => {
const recording = recordings.data.find((r) => r.id === id); const recording = recordings.data.find((r) => r.id === id);
if (!recording || isRecordingApproved(recording)) return false; return recording && !isRecordingApproved(recording);
if (recording.project_flock_category === 'GROWING') {
return true;
}
if (recording.project_flock_category === 'LAYING') {
return isGradingCompleted(recording);
}
return false;
}); });
}, [selectedRowIds, recordings, isRecordingApproved, isGradingCompleted]); }, [selectedRowIds, recordings, isRecordingApproved]);
useEffect(() => { useEffect(() => {
if (isResponseSuccess(recordings) && recordings.data) { if (isResponseSuccess(recordings) && recordings.data) {
@@ -530,14 +491,7 @@ const RecordingTable = () => {
(r) => r.id === parseInt(rowId) (r) => r.id === parseInt(rowId)
); );
if (recording && !isRecordingApproved(recording)) { if (recording && !isRecordingApproved(recording)) {
if (recording.project_flock_category === 'GROWING') { newSelection[rowId] = true;
newSelection[rowId] = true;
} else if (
recording.project_flock_category === 'LAYING' &&
isGradingCompleted(recording)
) {
newSelection[rowId] = true;
}
} }
} }
}); });
@@ -548,13 +502,7 @@ const RecordingTable = () => {
setRowSelection(newSelection); setRowSelection(newSelection);
} }
} }
}, [ }, [recordings, rowSelection, isRecordingApproved, setRowSelection]);
recordings,
rowSelection,
isRecordingApproved,
isGradingCompleted,
setRowSelection,
]);
return ( return (
<div className='w-full p-0 sm:p-4'> <div className='w-full p-0 sm:p-4'>
@@ -640,40 +588,28 @@ const RecordingTable = () => {
id: 'select', id: 'select',
header: ({ table }) => { header: ({ table }) => {
const allRows = table.getRowModel().rows; const allRows = table.getRowModel().rows;
const selectableRows = allRows.filter((row) => {
const selectableGrowingRows = allRows.filter((row) => {
const recording = row.original; const recording = row.original;
return ( return !isRecordingApproved(recording);
recording.project_flock_category === 'GROWING' &&
!isRecordingApproved(recording)
);
}); });
const hasNoSelectableGrowing = selectableGrowingRows.length === 0; const hasNoSelectableRows = selectableRows.length === 0;
const handleSelectAllGrowing = () => { const handleSelectAll = () => {
const isAllSelected = selectableGrowingRows.every((row) => const isAllSelected = selectableRows.every((row) =>
row.getIsSelected() row.getIsSelected()
); );
allRows.forEach((row) => { selectableRows.forEach((row) => {
const recording = row.original; row.toggleSelected(!isAllSelected);
if (
recording.project_flock_category === 'GROWING' &&
!isRecordingApproved(recording)
) {
row.toggleSelected(!isAllSelected);
} else if (recording.project_flock_category === 'LAYING') {
row.toggleSelected(false);
}
}); });
}; };
const isAllGrowingSelected = const isAllSelected =
selectableGrowingRows.length > 0 && selectableRows.length > 0 &&
selectableGrowingRows.every((row) => row.getIsSelected()); selectableRows.every((row) => row.getIsSelected());
const isSomeGrowingSelected = selectableGrowingRows.some((row) => const isSomeSelected = selectableRows.some((row) =>
row.getIsSelected() row.getIsSelected()
); );
@@ -681,33 +617,20 @@ const RecordingTable = () => {
<div className='w-full flex flex-row justify-center'> <div className='w-full flex flex-row justify-center'>
<CheckboxInput <CheckboxInput
name='allRow' name='allRow'
checked={isAllGrowingSelected} checked={isAllSelected}
indeterminate={ indeterminate={isSomeSelected && !isAllSelected}
isSomeGrowingSelected && !isAllGrowingSelected onChange={handleSelectAll}
} disabled={hasNoSelectableRows}
onChange={handleSelectAllGrowing}
disabled={hasNoSelectableGrowing}
/> />
</div> </div>
); );
}, },
cell: ({ row }) => { 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 ( return (
<div> <div>
<CheckboxInput <CheckboxInput
name='row' name='row'
checked={row.getIsSelected()} checked={row.getIsSelected()}
disabled={isDisabled}
indeterminate={row.getIsSomeSelected()} indeterminate={row.getIsSomeSelected()}
onChange={row.getToggleSelectedHandler()} onChange={row.getToggleSelectedHandler()}
/> />
@@ -883,7 +806,6 @@ const RecordingTable = () => {
deleteClickHandler={deleteClickHandler} deleteClickHandler={deleteClickHandler}
approveClickHandler={approveClickHandler} approveClickHandler={approveClickHandler}
rejectClickHandler={rejectClickHandler} rejectClickHandler={rejectClickHandler}
isGradingCompleted={isGradingCompleted}
/> />
</RowDropdownOptions> </RowDropdownOptions>
)} )}
@@ -896,7 +818,6 @@ const RecordingTable = () => {
deleteClickHandler={deleteClickHandler} deleteClickHandler={deleteClickHandler}
approveClickHandler={approveClickHandler} approveClickHandler={approveClickHandler}
rejectClickHandler={rejectClickHandler} rejectClickHandler={rejectClickHandler}
isGradingCompleted={isGradingCompleted}
/> />
</RowCollapseOptions> </RowCollapseOptions>
)} )}
@@ -4,7 +4,6 @@ import {
CreateGrowingRecordingPayload, CreateGrowingRecordingPayload,
CreateLayingRecordingPayload, CreateLayingRecordingPayload,
CreateEggPayload, CreateEggPayload,
CreateGradingPayload,
} from '@/types/api/production/recording'; } from '@/types/api/production/recording';
type RecordingGrowingFormSchemaType = { type RecordingGrowingFormSchemaType = {
@@ -32,14 +31,7 @@ type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & {
eggs: { eggs: {
product_warehouse_id: number; product_warehouse_id: number;
qty: number | string; qty: number | string;
}[]; weight: number | string;
};
type RecordingGradingFormSchemaType = {
eggs_grading: {
recording_egg_id: number;
grade: string;
qty: number | string;
}[]; }[];
}; };
@@ -62,6 +54,7 @@ export type DepletionSchema = {
export type EggSchema = { export type EggSchema = {
product_warehouse_id: number; product_warehouse_id: number;
qty: number | string; qty: number | string;
weight: number | string;
}; };
const BodyWeightObjectSchema: Yup.ObjectSchema<BodyWeightSchema> = Yup.object({ const BodyWeightObjectSchema: Yup.ObjectSchema<BodyWeightSchema> = Yup.object({
@@ -109,6 +102,10 @@ const EggObjectSchema: Yup.ObjectSchema<EggSchema> = Yup.object({
.required('Jumlah telur wajib diisi!') .required('Jumlah telur wajib diisi!')
.min(1, 'Jumlah telur tidak boleh 0!') .min(1, 'Jumlah telur tidak boleh 0!')
.typeError('Jumlah telur harus berupa angka!'), .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> = export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSchemaType> =
@@ -190,30 +187,6 @@ export const UpdateRecordingLayingFormSchema = RecordingLayingFormSchema.shape({
.required('Project Flock Kandang wajib diisi!'), .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< export type RecordingGrowingFormValues = Yup.InferType<
typeof RecordingGrowingFormSchema typeof RecordingGrowingFormSchema
>; >;
@@ -222,10 +195,6 @@ export type RecordingLayingFormValues = Yup.InferType<
typeof RecordingLayingFormSchema typeof RecordingLayingFormSchema
>; >;
export type RecordingGradingFormValues = Yup.InferType<
typeof RecordingGradingFormSchema
>;
type RecordingFormData = Partial<Recording> & { type RecordingFormData = Partial<Recording> & {
body_weights?: CreateGrowingRecordingPayload['body_weights']; body_weights?: CreateGrowingRecordingPayload['body_weights'];
stocks?: CreateGrowingRecordingPayload['stocks'] | Recording['stocks']; stocks?: CreateGrowingRecordingPayload['stocks'] | Recording['stocks'];
@@ -295,26 +264,12 @@ export const getRecordingLayingFormInitialValues = (
eggs: initialValues?.eggs?.map((egg: CreateEggPayload) => ({ eggs: initialValues?.eggs?.map((egg: CreateEggPayload) => ({
product_warehouse_id: egg.product_warehouse_id, product_warehouse_id: egg.product_warehouse_id,
qty: egg.qty, qty: egg.qty,
weight: egg.weight,
})) ?? [ })) ?? [
{ {
product_warehouse_id: 0, product_warehouse_id: 0,
qty: '', qty: '',
}, weight: '',
],
});
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: '',
}, },
], ],
}); });
@@ -16,7 +16,6 @@ import CheckboxInput from '@/components/input/CheckboxInput';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import Tooltip from '@/components/Tooltip';
import { import {
ProjectFlockKandangApi, ProjectFlockKandangApi,
@@ -98,9 +97,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const [recordingFormErrorMessage, setRecordingFormErrorMessage] = const [recordingFormErrorMessage, setRecordingFormErrorMessage] =
useState(''); useState('');
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [newRecordingData, setNewRecordingData] = useState<Recording | null>( const [, setNewRecordingData] = useState<Recording | null>(null);
null
);
const [nextDayRecording, setNextDayRecording] = const [nextDayRecording, setNextDayRecording] =
useState<NextDayRecording | null>(null); useState<NextDayRecording | null>(null);
@@ -111,18 +108,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const isRecordingApproved = useCallback((recording?: Recording) => { const isRecordingApproved = useCallback((recording?: Recording) => {
return ( return (
recording?.approval?.action === 'APPROVED' && recording?.approval?.action === 'APPROVED' &&
recording?.approval?.step_name === 'Disetujui' && 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)
); );
}, []); }, []);
@@ -181,6 +167,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
eggs: (values.eggs ?? []).map((egg) => ({ eggs: (values.eggs ?? []).map((egg) => ({
product_warehouse_id: egg.product_warehouse_id, product_warehouse_id: egg.product_warehouse_id,
qty: Number(egg.qty) || 0, 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] [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( const updateRecordingHandler = useCallback(
async ( async (
recordingId: number, recordingId: number,
@@ -650,7 +611,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const hasPakanFlag = product.product.flags?.includes('PAKAN'); const hasPakanFlag = product.product.flags?.includes('PAKAN');
const hasOvkFlag = product.product.flags?.includes('OVK'); const hasOvkFlag = product.product.flags?.includes('OVK');
// Only include products that are in the same location as the selected kandang
if (hasPakanFlag || hasOvkFlag) { if (hasPakanFlag || hasOvkFlag) {
options.push({ options.push({
value: product.id, value: product.id,
@@ -690,7 +650,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
depletionProductsData.data.forEach((product) => { depletionProductsData.data.forEach((product) => {
const productName = product.product.name; const productName = product.product.name;
// Filter for depletion-related products (culling, mati, afkir)
if ( if (
productName.toLowerCase().includes('culling') || productName.toLowerCase().includes('culling') ||
productName.toLowerCase().includes('mati') || productName.toLowerCase().includes('mati') ||
@@ -732,7 +691,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
eggProductsData.data.forEach((product) => { eggProductsData.data.forEach((product) => {
const productName = product.product.name; const productName = product.product.name;
// Filter for egg-related products
if ( if (
productName.toLowerCase().includes('telur') || productName.toLowerCase().includes('telur') ||
productName.toLowerCase().includes('egg') || productName.toLowerCase().includes('egg') ||
@@ -1019,54 +977,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
); );
}, [formik.values.stocks, getStockUsageError, type]); }, [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 = ( const isRepeaterInputError = (
arrayName: 'body_weights' | 'stocks' | 'depletions' | 'eggs', arrayName: 'body_weights' | 'stocks' | 'depletions' | 'eggs',
column: string, column: string,
@@ -1148,7 +1058,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
if (hasSameDayRecording) { if (hasSameDayRecording) {
toast.error( 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.` Tidak bisa membuat recording duplikat, mohon perbarui recording yang sudah ada terlebih dahulu.`
); );
return; return;
@@ -1278,7 +1188,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
setIsRejectLoading(false); setIsRejectLoading(false);
}; };
// Body Weights Handlers
const addBodyWeight = () => { const addBodyWeight = () => {
const newBodyWeights = [ const newBodyWeights = [
...(formik.values.body_weights || []), ...(formik.values.body_weights || []),
@@ -1397,7 +1306,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
setSelectedBodyWeights([]); setSelectedBodyWeights([]);
}; };
// Stocks Handlers
const addStock = () => { const addStock = () => {
const newStocks = [ const newStocks = [
...(formik.values.stocks || []), ...(formik.values.stocks || []),
@@ -1430,7 +1338,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
setSelectedStocks([]); setSelectedStocks([]);
}; };
// Depletions Handlers
const addDepletion = () => { const addDepletion = () => {
const newDepletions = [ const newDepletions = [
...(formik.values.depletions || []), ...(formik.values.depletions || []),
@@ -1465,7 +1372,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
setSelectedDepletions([]); setSelectedDepletions([]);
}; };
// Eggs Handlers
const addEgg = () => { const addEgg = () => {
const newEggs = [ const newEggs = [
...((formik.values as RecordingLayingFormValues).eggs || []), ...((formik.values as RecordingLayingFormValues).eggs || []),
@@ -1485,6 +1391,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
[formik] [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 removeEgg = (idx: number) => {
const updatedEggs = ( const updatedEggs = (
formik.values as RecordingLayingFormValues formik.values as RecordingLayingFormValues
@@ -1569,47 +1483,37 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
Kembali Kembali
</Button> </Button>
{type === 'detail' && {type === 'detail' && !isRecordingApproved(initialValues) && (
!isRecordingApproved(initialValues) && <div className='flex flex-row gap-2'>
(!isLayingCategory || hasGradingData(initialValues)) && ( <Button
<div className='flex flex-row gap-2'> variant='outline'
<Button color='success'
variant='outline' onClick={() => {
color='success' setApprovalNotes('');
onClick={() => { approveModal.openModal();
setApprovalNotes(''); }}
approveModal.openModal(); isLoading={isApproveLoading}
}} className='w-full sm:w-fit'
isLoading={isApproveLoading} >
className='w-full sm:w-fit' <Icon icon='material-symbols:check' width={24} height={24} />
> Approve
<Icon </Button>
icon='material-symbols:check'
width={24}
height={24}
/>
Approve
</Button>
<Button <Button
variant='outline' variant='outline'
color='error' color='error'
onClick={() => { onClick={() => {
setApprovalNotes(''); setApprovalNotes('');
rejectModal.openModal(); rejectModal.openModal();
}} }}
isLoading={isRejectLoading} isLoading={isRejectLoading}
className='w-full sm:w-fit' className='w-full sm:w-fit'
> >
<Icon <Icon icon='material-symbols:close' width={24} height={24} />
icon='material-symbols:close' Reject
width={24} </Button>
height={24} </div>
/> )}
Reject
</Button>
</div>
)}
</div> </div>
<h1 className='text-2xl font-bold text-center'> <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) => ( {formik.values.body_weights?.map((bw, idx) => (
<tr key={`body-weight-${idx}`}> <tr key={`body-weight-${idx}`}>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<td className='!align-middle'> <td className='align-middle!'>
<CheckboxInput <CheckboxInput
name={`body-weight-${idx}`} name={`body-weight-${idx}`}
checked={selectedBodyWeights.includes(idx)} checked={selectedBodyWeights.includes(idx)}
@@ -2166,7 +2070,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{formik.values.stocks?.map((stock, idx) => ( {formik.values.stocks?.map((stock, idx) => (
<tr key={`stock-${idx}`}> <tr key={`stock-${idx}`}>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<td className='!align-middle'> <td className='align-middle!'>
<CheckboxInput <CheckboxInput
name={`stock-${idx}`} name={`stock-${idx}`}
checked={selectedStocks.includes(idx)} checked={selectedStocks.includes(idx)}
@@ -2386,7 +2290,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{formik.values.depletions?.map((depletion, idx) => ( {formik.values.depletions?.map((depletion, idx) => (
<tr key={`depletion-${idx}`}> <tr key={`depletion-${idx}`}>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<td className='!align-middle'> <td className='align-middle!'>
<CheckboxInput <CheckboxInput
name={`depletion-${idx}`} name={`depletion-${idx}`}
checked={selectedDepletions.includes(idx)} checked={selectedDepletions.includes(idx)}
@@ -2587,6 +2491,15 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<span className='text-error'>*</span> <span className='text-error'>*</span>
</span> </span>
</th> </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' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<th>Action</th> <th>Action</th>
)} )}
@@ -2597,7 +2510,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
(egg, idx) => ( (egg, idx) => (
<tr key={`egg-${idx}`}> <tr key={`egg-${idx}`}>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<td className='!align-middle'> <td className='align-middle!'>
<CheckboxInput <CheckboxInput
name={`egg-${idx}`} name={`egg-${idx}`}
checked={selectedEggs.includes(idx)} checked={selectedEggs.includes(idx)}
@@ -2662,32 +2575,55 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
/> />
</td> </td>
<td> <td>
<div className='flex flex-col gap-1'> <NumberInput
<NumberInput required
required name={`eggs.${idx}.qty`}
name={`eggs.${idx}.qty`} value={egg.qty ?? ''}
value={egg.qty ?? ''} onChange={handleEggQtyChangeWrapper(idx)}
onChange={handleEggQtyChangeWrapper(idx)} onBlur={formik.handleBlur}
onBlur={formik.handleBlur} decimalScale={0}
decimalScale={0} allowNegative={false}
allowNegative={false} thousandSeparator=','
thousandSeparator=',' decimalSeparator='.'
decimalSeparator='.' isError={
isError={ isRepeaterInputError('eggs', 'qty', idx).isError
isRepeaterInputError('eggs', 'qty', idx) }
.isError errorMessage={
} isRepeaterInputError('eggs', 'qty', idx)
errorMessage={ .errorMessage
isRepeaterInputError('eggs', 'qty', idx) }
.errorMessage readOnly={type === 'detail'}
} className={{
readOnly={type === 'detail'} wrapper: 'w-full min-w-24',
className={{ }}
wrapper: 'w-full min-w-24', placeholder='Masukkan jumlah telur'
}} />
placeholder='Masukkan jumlah telur' </td>
/> <td>
</div> <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> </td>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<td> <td>
@@ -2779,46 +2715,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</div> </div>
{/* Right side actions */} {/* Right side actions */}
<div className='flex flex-col sm:flex-row sm:justify-end gap-2 w-full sm:w-auto'> <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' && ( {type === 'edit' && (
<div className='flex flex-col sm:flex-row gap-2 w-full sm:w-auto'> <div className='flex flex-col sm:flex-row gap-2 w-full sm:w-auto'>
<Button <Button
@@ -2870,78 +2766,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
> >
Submit Submit
</Button> </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>
)} )}
</div> </div>
@@ -2979,8 +2803,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{/* Approve Confirmation Modal */} {/* Approve Confirmation Modal */}
{(type as 'add' | 'edit' | 'detail') === 'detail' && {(type as 'add' | 'edit' | 'detail') === 'detail' &&
!isRecordingApproved(initialValues) && !isRecordingApproved(initialValues) && (
(!isLayingCategory || hasGradingData(initialValues)) && (
<ConfirmationModalWithNotes <ConfirmationModalWithNotes
ref={approveModal.ref} ref={approveModal.ref}
type='success' type='success'
@@ -3002,8 +2825,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{/* Reject Confirmation Modal */} {/* Reject Confirmation Modal */}
{(type as 'add' | 'edit' | 'detail') === 'detail' && {(type as 'add' | 'edit' | 'detail') === 'detail' &&
!isRecordingApproved(initialValues) && !isRecordingApproved(initialValues) && (
(!isLayingCategory || hasGradingData(initialValues)) && (
<ConfirmationModalWithNotes <ConfirmationModalWithNotes
ref={rejectModal.ref} ref={rejectModal.ref}
type='error' 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 = [ export const RECORDING_APPROVAL_LINE: ApprovalLine = [
{ {
step_number: 1, step_number: 1,
step_name: 'Grading-Telur',
},
{
step_number: 2,
step_name: 'Pengajuan', step_name: 'Pengajuan',
}, },
{ {
step_number: 3, step_number: 2,
step_name: 'Disetujui', step_name: 'Disetujui',
}, },
] as const; ] as const;
@@ -66,14 +62,10 @@ export const RECORDING_APPROVAL_LINE: ApprovalLine = [
export const GROWING_RECORDING_APPROVAL_LINE: ApprovalLine = [ export const GROWING_RECORDING_APPROVAL_LINE: ApprovalLine = [
{ {
step_number: 1, step_number: 1,
step_name: 'Grading-Telur',
},
{
step_number: 2,
step_name: 'Pengajuan', step_name: 'Pengajuan',
}, },
{ {
step_number: 3, step_number: 2,
step_name: 'Disetujui', step_name: 'Disetujui',
}, },
] as const; ] as const;
@@ -81,14 +73,10 @@ export const GROWING_RECORDING_APPROVAL_LINE: ApprovalLine = [
export const LAYING_RECORDING_APPROVAL_LINE: ApprovalLine = [ export const LAYING_RECORDING_APPROVAL_LINE: ApprovalLine = [
{ {
step_number: 1, step_number: 1,
step_name: 'Grading-Telur',
},
{
step_number: 2,
step_name: 'Pengajuan', step_name: 'Pengajuan',
}, },
{ {
step_number: 3, step_number: 2,
step_name: 'Disetujui', step_name: 'Disetujui',
}, },
] as const; ] as const;
+1 -5
View File
@@ -233,14 +233,10 @@ export const APPROVAL_WORKFLOWS = [
steps: [ steps: [
{ {
step_number: 1, step_number: 1,
step_name: 'Grading-Telur',
},
{
step_number: 2,
step_name: 'Pengajuan', step_name: 'Pengajuan',
}, },
{ {
step_number: 3, step_number: 2,
step_name: 'Disetujui', step_name: 'Disetujui',
}, },
], ],
-24
View File
@@ -9,8 +9,6 @@ import {
CreateRecordingPayload, CreateRecordingPayload,
Recording, Recording,
UpdateRecordingPayload, UpdateRecordingPayload,
CreateGradingPayload,
UpdateGradingPayload,
NextDayRecording, NextDayRecording,
} from '@/types/api/production/recording'; } from '@/types/api/production/recording';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; 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( async deleteGrading(
gradingId: number gradingId: number
): Promise<BaseApiResponse<unknown> | undefined> { ): Promise<BaseApiResponse<unknown> | undefined> {
+6 -42
View File
@@ -9,8 +9,7 @@ export type ProductionMetrics = {
cum_intake: number; cum_intake: number;
fcr_value: number; fcr_value: number;
total_chick_qty: number; total_chick_qty: number;
daily_depletion_rate?: number; cum_depletion: number;
cum_depletion?: number;
}; };
export type BaseRecording = { export type BaseRecording = {
@@ -18,42 +17,33 @@ export type BaseRecording = {
project_flock_kandang_id: number; project_flock_kandang_id: number;
record_datetime: string; record_datetime: string;
day: number; day: number;
created_by: User; project_flock_category?: 'GROWING' | 'LAYING';
} & ProductionMetrics; } & ProductionMetrics;
export type RecordingBW = { export type RecordingBW = {
id: number;
recording_id: number;
avg_weight: number; avg_weight: number;
qty: number; qty: number;
total_weight: number; total_weight: number;
}; };
export type RecordingDepletion = { export type RecordingDepletion = {
id: number;
recording_id: number;
product_warehouse_id: number; product_warehouse_id: number;
qty: number; qty: number;
product_warehouse: ProductWarehouse; product_warehouse: ProductWarehouse;
}; };
export type RecordingStock = { export type RecordingStock = {
id: number;
recording_id: number;
product_warehouse_id: number; product_warehouse_id: number;
usage_amount?: number; usage_amount?: number;
usage_qty: number;
qty: number;
pending_qty: number; pending_qty: number;
product_warehouse: ProductWarehouse; product_warehouse: ProductWarehouse;
}; };
export type RecordingEgg = { export type RecordingEgg = {
id: number; id: number;
recording_id: number;
product_warehouse_id: number; product_warehouse_id: number;
qty: number; qty: number;
created_by: User; weight: number;
product_warehouse: ProductWarehouse; product_warehouse: ProductWarehouse;
gradings?: { gradings?: {
grade: string; grade: string;
@@ -71,19 +61,12 @@ export type GradingEgg = {
export type Recording = BaseMetadata & export type Recording = BaseMetadata &
BaseRecording & { BaseRecording & {
project_flock_category?: 'GROWING' | 'LAYING';
approval?: BaseApproval; approval?: BaseApproval;
egg_grading_status?: string | null; created_user: User;
egg_grading_pending_qty?: number | null;
egg_grading_completed_qty?: number | null;
body_weights?: RecordingBW[]; body_weights?: RecordingBW[];
depletions?: RecordingDepletion[]; depletions?: RecordingDepletion[];
stocks?: RecordingStock[]; stocks?: RecordingStock[];
eggs?: RecordingEgg[]; eggs?: RecordingEgg[];
recording_bws?: RecordingBW[];
recording_depletions?: RecordingDepletion[];
recording_stocks?: RecordingStock[];
recording_eggs?: RecordingEgg[];
grading_eggs?: GradingEgg[]; 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 = { export type CreateEggPayload = {
product_warehouse_id: number; product_warehouse_id: number;
qty: number; qty: number;
weight: number;
}; };
export type CreateLayingRecordingPayload = CreateGrowingRecordingPayload & { export type CreateLayingRecordingPayload = CreateGrowingRecordingPayload & {
@@ -137,11 +103,9 @@ export type CreateLayingRecordingPayload = CreateGrowingRecordingPayload & {
export type CreateRecordingPayload = export type CreateRecordingPayload =
| CreateGrowingRecordingPayload | CreateGrowingRecordingPayload
| CreateLayingRecordingPayload | CreateLayingRecordingPayload;
| CreateGradingRecordingPayload;
export type UpdateGrowingRecordingPayload = CreateGrowingRecordingPayload; export type UpdateGrowingRecordingPayload = CreateGrowingRecordingPayload;
export type UpdateLayingRecordingPayload = CreateLayingRecordingPayload; export type UpdateLayingRecordingPayload = CreateLayingRecordingPayload;
export type UpdateGradingRecordingPayload = CreateGradingRecordingPayload;
export type UpdateRecordingPayload = CreateRecordingPayload; export type UpdateRecordingPayload = CreateRecordingPayload;