mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-24 23:35:45 +00:00
feat(FE-170,174): add GradingForm component for managing grading records
This commit is contained in:
@@ -0,0 +1,44 @@
|
|||||||
|
'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' recordingData={isResponseSuccess(recording) ? recording.data : undefined} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddGrading;
|
||||||
@@ -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 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.recording_eggs?.find(egg => egg.id === parseInt(gradingId || '0'))}
|
||||||
|
recordingData={recording.data}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditGrading;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
|
const Layout = ({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) => {
|
||||||
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
'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 { isResponseError, 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.recording_eggs?.find(egg => egg.id === parseInt(gradingId))}
|
||||||
|
recordingData={grading.data}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailGrading;
|
||||||
@@ -0,0 +1,780 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo, useState, useEffect, useCallback } from 'react';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
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 { FormHeader } from '@/components/helper/form/FormHeader';
|
||||||
|
import { RecordingApi } from '@/services/api/production';
|
||||||
|
import {
|
||||||
|
CreateGradingPayload,
|
||||||
|
Recording,
|
||||||
|
RecordingEgg,
|
||||||
|
GradingEgg,
|
||||||
|
} from '@/types/api/production/recording';
|
||||||
|
import { type BaseApiResponse, FormStepStatus } from '@/types/api/api-general';
|
||||||
|
import {
|
||||||
|
RecordingGradingFormSchema,
|
||||||
|
RecordingGradingFormValues,
|
||||||
|
UpdateRecordingGradingFormSchema,
|
||||||
|
getRecordingGradingFormInitialValues,
|
||||||
|
} from '../../form/RecordingForm.schema';
|
||||||
|
import { useRecordingFormHandlers } from '../../form/useRecordingFormHandlers';
|
||||||
|
import { ProductWarehouseApi } from '@/services/api/inventory';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
import { useModal } from '@/components/Modal';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import Badge from '@/components/Badge';
|
||||||
|
import StepItem from '@/components/steps/StepItem';
|
||||||
|
|
||||||
|
interface GradingFormProps {
|
||||||
|
type?: 'add' | 'edit' | 'detail';
|
||||||
|
initialValues?: RecordingEgg & { grading_eggs?: GradingEgg[] };
|
||||||
|
recordingData?: Recording;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GradingForm = ({
|
||||||
|
type = 'add',
|
||||||
|
initialValues,
|
||||||
|
recordingData,
|
||||||
|
}: GradingFormProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const recordingId = searchParams.get('recording_id');
|
||||||
|
const [selectedGradingItems, setSelectedGradingItems] = useState<number[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
|
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||||
|
const [formSteps, setFormSteps] = useState<FormStepStatus[] | null>(null);
|
||||||
|
|
||||||
|
const approveModal = useModal();
|
||||||
|
const rejectModal = useModal();
|
||||||
|
|
||||||
|
// ===== API DATA FETCHING =====
|
||||||
|
const eggProductsUrl = useMemo(() => {
|
||||||
|
if (!recordingData?.project_flock_kandangs_id) return null;
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
search: '',
|
||||||
|
location_id: recordingData.project_flock_kandangs_id.toString(),
|
||||||
|
});
|
||||||
|
return `${ProductWarehouseApi.basePath}?${params.toString()}`;
|
||||||
|
}, [recordingData]);
|
||||||
|
|
||||||
|
const { data: eggProductsData, isLoading: isLoadingEggProducts } = useSWR(
|
||||||
|
eggProductsUrl,
|
||||||
|
ProductWarehouseApi.getAllFetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== DATA PROCESSING =====
|
||||||
|
const eggProducts = useMemo(() => {
|
||||||
|
const options: OptionType[] = [];
|
||||||
|
if (isResponseSuccess(eggProductsData)) {
|
||||||
|
eggProductsData.data.forEach((product) => {
|
||||||
|
const productName = product.product.name;
|
||||||
|
|
||||||
|
if (
|
||||||
|
productName.toLowerCase().includes('telur') ||
|
||||||
|
productName.toLowerCase().includes('egg')
|
||||||
|
) {
|
||||||
|
options.push({
|
||||||
|
value: product.id,
|
||||||
|
label: product.product.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}, [eggProductsData]);
|
||||||
|
|
||||||
|
// ===== FORM HANDLERS =====
|
||||||
|
const {
|
||||||
|
deleteModal,
|
||||||
|
recordingFormErrorMessage,
|
||||||
|
isDeleteLoading,
|
||||||
|
createRecordingHandler,
|
||||||
|
updateRecordingHandler,
|
||||||
|
deleteRecordingClickHandler,
|
||||||
|
confirmationModalDeleteClickHandler,
|
||||||
|
} = useRecordingFormHandlers(initialValues?.id);
|
||||||
|
|
||||||
|
const formikInitialValues = useMemo(() => {
|
||||||
|
return getRecordingGradingFormInitialValues({
|
||||||
|
recording_egg_id: initialValues?.id || parseInt(recordingId || '0') || 0,
|
||||||
|
eggs_grading:
|
||||||
|
initialValues?.grading_eggs?.map((grading: GradingEgg) => ({
|
||||||
|
grade: grading.grade,
|
||||||
|
qty: grading.qty,
|
||||||
|
})) || [],
|
||||||
|
});
|
||||||
|
}, [initialValues, recordingId]);
|
||||||
|
|
||||||
|
const formik = useFormik<RecordingGradingFormValues>({
|
||||||
|
initialValues: formikInitialValues,
|
||||||
|
validationSchema: (() => {
|
||||||
|
return type === 'edit'
|
||||||
|
? UpdateRecordingGradingFormSchema
|
||||||
|
: RecordingGradingFormSchema;
|
||||||
|
})(),
|
||||||
|
validateOnChange: true,
|
||||||
|
validateOnBlur: true,
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
const gradingPayload = {
|
||||||
|
recording_egg_id: values.recording_egg_id,
|
||||||
|
eggs_grading: (values.eggs_grading ?? []).map((grading) => ({
|
||||||
|
grade: grading.grade,
|
||||||
|
qty: grading.qty || 0,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'add':
|
||||||
|
await createRecordingHandler(gradingPayload as CreateGradingPayload);
|
||||||
|
break;
|
||||||
|
case 'edit':
|
||||||
|
await updateRecordingHandler(
|
||||||
|
initialValues?.id as number,
|
||||||
|
gradingPayload as CreateGradingPayload
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== EVENT HANDLERS =====
|
||||||
|
const approveHandler = async () => {
|
||||||
|
setIsApproveLoading(true);
|
||||||
|
|
||||||
|
const approveResponse = await RecordingApi.customRequest<
|
||||||
|
BaseApiResponse<Recording[]>
|
||||||
|
>('approvals', {
|
||||||
|
method: 'POST',
|
||||||
|
payload: {
|
||||||
|
action: 'APPROVED',
|
||||||
|
approvable_ids: [initialValues?.id as number],
|
||||||
|
notes: 'Approved via Grading Form',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isResponseSuccess(approveResponse)) {
|
||||||
|
toast.success('Grading berhasil disetujui!');
|
||||||
|
approveModal.closeModal();
|
||||||
|
router.push('/production/recording');
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
(approveResponse?.message as string) || 'Gagal menyetujui grading'
|
||||||
|
);
|
||||||
|
approveModal.closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsApproveLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rejectHandler = async () => {
|
||||||
|
setIsRejectLoading(true);
|
||||||
|
|
||||||
|
const rejectResponse = await RecordingApi.customRequest<
|
||||||
|
BaseApiResponse<Recording[]>
|
||||||
|
>('approvals', {
|
||||||
|
method: 'POST',
|
||||||
|
payload: {
|
||||||
|
action: 'REJECTED',
|
||||||
|
approvable_ids: [initialValues?.id as number],
|
||||||
|
notes: 'Rejected via Grading Form',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isResponseSuccess(rejectResponse)) {
|
||||||
|
toast.success('Grading berhasil ditolak!');
|
||||||
|
rejectModal.closeModal();
|
||||||
|
router.push('/production/recording');
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
(rejectResponse?.message as string) || 'Gagal menolak grading'
|
||||||
|
);
|
||||||
|
rejectModal.closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRejectLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Grading Handlers
|
||||||
|
const addGrading = () => {
|
||||||
|
const newGrading = [
|
||||||
|
...(formik.values.eggs_grading || []),
|
||||||
|
{
|
||||||
|
grade: '',
|
||||||
|
qty: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
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<HTMLInputElement>) => {
|
||||||
|
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([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isRepeaterInputError = (
|
||||||
|
arrayName: 'eggs_grading',
|
||||||
|
column: string,
|
||||||
|
idx: number
|
||||||
|
) => {
|
||||||
|
const touched = formik.touched as Record<string, unknown>;
|
||||||
|
const errors = formik.errors as Record<string, unknown>;
|
||||||
|
|
||||||
|
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(() => {
|
||||||
|
const steps: FormStepStatus[] = [
|
||||||
|
{
|
||||||
|
name: 'Recording',
|
||||||
|
isCompleted: true,
|
||||||
|
isCurrent: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Grading',
|
||||||
|
isCompleted: false,
|
||||||
|
isCurrent: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
setFormSteps(steps);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (formik.values.eggs_grading && formik.values.eggs_grading.length === 0) {
|
||||||
|
formik.setFieldValue('eggs_grading', [{ grade: '', qty: 0 }]);
|
||||||
|
}
|
||||||
|
}, [formik]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className='w-full'>
|
||||||
|
<FormHeader
|
||||||
|
type={type}
|
||||||
|
title='Grading Telur'
|
||||||
|
backUrl='/production/recording'
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Project Flock Info Card */}
|
||||||
|
<div className='flex items-center gap-2 mb-4'>
|
||||||
|
{/* Form Steps */}
|
||||||
|
{formSteps && (
|
||||||
|
<div className='flex-1 mt-4'>
|
||||||
|
<div className='w-full'>
|
||||||
|
<ul className='steps w-full'>
|
||||||
|
{formSteps.map((step, idx) => (
|
||||||
|
<StepItem
|
||||||
|
key={idx}
|
||||||
|
color={
|
||||||
|
step.isCompleted
|
||||||
|
? 'success'
|
||||||
|
: step.isCurrent
|
||||||
|
? 'primary'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
className={
|
||||||
|
step.isCurrent && idx > 0 ? 'step-primary' : undefined
|
||||||
|
}
|
||||||
|
icon={
|
||||||
|
step.isCompleted ? (
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:check-rounded'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
idx + 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{step.name}
|
||||||
|
</StepItem>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action buttons for multi-form navigation */}
|
||||||
|
{type === 'detail' && (
|
||||||
|
<div className='mt-4 flex gap-2'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='primary'
|
||||||
|
onClick={() => {
|
||||||
|
toast.success('Semua form telah selesai!');
|
||||||
|
router.push('/production/recording');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Kembali ke Recording
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
className='w-full mt-8 flex flex-col gap-6'
|
||||||
|
>
|
||||||
|
{/* Basic Info Card */}
|
||||||
|
<Card
|
||||||
|
title='Informasi Grading'
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full mb-4 shadow',
|
||||||
|
body: 'flex flex-col gap-6',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
type === 'detail'
|
||||||
|
? 'flex flex-col gap-6'
|
||||||
|
: 'grid grid-cols-3 gap-4'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{type === 'detail' && recordingData ? (
|
||||||
|
<>
|
||||||
|
<span>Recording ID</span>#{recordingData.id}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span>Recording Egg ID</span>#{formik.values.recording_egg_id}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Grading Table */}
|
||||||
|
<Card
|
||||||
|
title='Grading Telur'
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full mb-4 shadow',
|
||||||
|
title: 'mb-4',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='overflow-x-auto'>
|
||||||
|
<table className='table'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<th>
|
||||||
|
<CheckboxInput
|
||||||
|
name='select-all-grading'
|
||||||
|
checked={
|
||||||
|
formik.values.eggs_grading?.length ===
|
||||||
|
selectedGradingItems.length &&
|
||||||
|
formik.values.eggs_grading?.length > 0
|
||||||
|
}
|
||||||
|
onChange={(
|
||||||
|
e: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedGradingItems(
|
||||||
|
formik.values.eggs_grading?.map(
|
||||||
|
(_, idx) => idx
|
||||||
|
) ?? []
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedGradingItems([]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
classNames={{
|
||||||
|
wrapper: 'flex justify-center',
|
||||||
|
checkbox: 'checkbox checkbox-sm',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
)}
|
||||||
|
<th>
|
||||||
|
Grade
|
||||||
|
<span
|
||||||
|
className='tooltip tooltip-error tooltip-bottom z-[9999]'
|
||||||
|
data-tip='required'
|
||||||
|
>
|
||||||
|
<span className='text-error'>*</span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Jumlah
|
||||||
|
<span
|
||||||
|
className='tooltip tooltip-error tooltip-bottom z-[9999]'
|
||||||
|
data-tip='required'
|
||||||
|
>
|
||||||
|
<span className='text-error'>*</span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
{type !== 'detail' && <th>Action</th>}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{formik.values.eggs_grading?.map((grading, idx) => (
|
||||||
|
<tr key={`grading-${idx}`}>
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<td className='!align-middle'>
|
||||||
|
<CheckboxInput
|
||||||
|
name={`grading-${idx}`}
|
||||||
|
checked={selectedGradingItems.includes(idx)}
|
||||||
|
onChange={(
|
||||||
|
e: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedGradingItems([
|
||||||
|
...selectedGradingItems,
|
||||||
|
idx,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
setSelectedGradingItems(
|
||||||
|
selectedGradingItems.filter((i) => i !== idx)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
classNames={{
|
||||||
|
wrapper: 'flex justify-center',
|
||||||
|
checkbox: 'checkbox checkbox-sm',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td>
|
||||||
|
<SelectInput
|
||||||
|
required
|
||||||
|
value={
|
||||||
|
grading.grade
|
||||||
|
? { value: grading.grade, label: grading.grade }
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
onChange={handleGradingGradeChangeWrapper(idx)}
|
||||||
|
options={[
|
||||||
|
{ value: 'Grade A', label: 'Grade A' },
|
||||||
|
{ value: 'Grade B', label: 'Grade B' },
|
||||||
|
{ value: 'Grade C', label: 'Grade C' },
|
||||||
|
{ value: 'Grade D', label: 'Grade D' },
|
||||||
|
{ value: 'Extra', label: 'Extra' },
|
||||||
|
]}
|
||||||
|
placeholder='Pilih Grade'
|
||||||
|
isError={
|
||||||
|
isRepeaterInputError('eggs_grading', 'grade', idx)
|
||||||
|
.isError
|
||||||
|
}
|
||||||
|
errorMessage={
|
||||||
|
isRepeaterInputError('eggs_grading', 'grade', idx)
|
||||||
|
.errorMessage
|
||||||
|
}
|
||||||
|
isDisabled={type === 'detail'}
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full min-w-32',
|
||||||
|
}}
|
||||||
|
isSearchable={false}
|
||||||
|
isClearable={type !== 'detail'}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<NumberInput
|
||||||
|
required
|
||||||
|
name={`eggs_grading.${idx}.qty`}
|
||||||
|
value={grading.qty}
|
||||||
|
onChange={handleGradingQtyChangeWrapper(idx)}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
decimalScale={0}
|
||||||
|
allowNegative={false}
|
||||||
|
thousandSeparator=','
|
||||||
|
decimalSeparator='.'
|
||||||
|
isError={
|
||||||
|
isRepeaterInputError('eggs_grading', 'qty', idx)
|
||||||
|
.isError
|
||||||
|
}
|
||||||
|
errorMessage={
|
||||||
|
isRepeaterInputError('eggs_grading', 'qty', idx)
|
||||||
|
.errorMessage
|
||||||
|
}
|
||||||
|
readOnly={type === 'detail'}
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full min-w-24',
|
||||||
|
}}
|
||||||
|
placeholder='Jumlah'
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<td>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='error'
|
||||||
|
onClick={() => removeGrading(idx)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='mdi:trash-can'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<div className='flex justify-center items-center mt-4 gap-4'>
|
||||||
|
{selectedGradingItems.length > 0 && (
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='error'
|
||||||
|
onClick={removeSelectedGrading}
|
||||||
|
disabled={selectedGradingItems.length === 0}
|
||||||
|
className='w-fit'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:trash-can' width={24} height={24} />
|
||||||
|
Hapus Terpilih ({selectedGradingItems.length})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='success'
|
||||||
|
onClick={addGrading}
|
||||||
|
className='w-fit'
|
||||||
|
>
|
||||||
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
|
Tambah Grading
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
|
{type !== 'add' && (
|
||||||
|
<div className='flex flex-row justify-start gap-2'>
|
||||||
|
{deleteRecordingClickHandler && (
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='error'
|
||||||
|
onClick={deleteRecordingClickHandler}
|
||||||
|
className='px-4'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:delete-outline-rounded'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className='justify-start text-sm'
|
||||||
|
/>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{type !== 'edit' && initialValues && (
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='warning'
|
||||||
|
href={`/production/recording/grading/edit/?gradingId=${initialValues.id}`}
|
||||||
|
className='px-4'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:edit-outline'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className='justify-start text-sm'
|
||||||
|
/>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{type === 'detail' && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='success'
|
||||||
|
onClick={() => approveModal.openModal()}
|
||||||
|
className='px-4'
|
||||||
|
isLoading={isApproveLoading}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:check-circle-outline'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className='justify-start text-sm'
|
||||||
|
/>
|
||||||
|
Approve
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='error'
|
||||||
|
onClick={() => rejectModal.openModal()}
|
||||||
|
className='px-4'
|
||||||
|
isLoading={isRejectLoading}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:cancel-outline'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className='justify-start text-sm'
|
||||||
|
/>
|
||||||
|
Reject
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<div
|
||||||
|
className={cn('flex flex-row justify-end gap-2', {
|
||||||
|
'w-full': type === 'add',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type='reset'
|
||||||
|
color='warning'
|
||||||
|
className='px-4'
|
||||||
|
onClick={(e) => {
|
||||||
|
formik.handleReset(e);
|
||||||
|
formik.validateForm();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
color='primary'
|
||||||
|
className='px-4'
|
||||||
|
isLoading={formik.isSubmitting}
|
||||||
|
disabled={!formik.isValid || formik.isSubmitting}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{recordingFormErrorMessage && (
|
||||||
|
<div role='alert' className='alert alert-error'>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:error-outline'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
<span>{recordingFormErrorMessage}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{type !== 'add' && (
|
||||||
|
<>
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={deleteModal.ref}
|
||||||
|
type='error'
|
||||||
|
text='Apakah anda yakin ingin menghapus data Grading ini?'
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'error',
|
||||||
|
isLoading: isDeleteLoading,
|
||||||
|
onClick: confirmationModalDeleteClickHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Approve Confirmation Modal */}
|
||||||
|
{type === 'detail' && (
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={approveModal.ref}
|
||||||
|
type='success'
|
||||||
|
text='Apakah anda yakin ingin menyetujui data Grading ini?'
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'success',
|
||||||
|
isLoading: isApproveLoading,
|
||||||
|
onClick: approveHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Reject Confirmation Modal */}
|
||||||
|
{type === 'detail' && (
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={rejectModal.ref}
|
||||||
|
type='error'
|
||||||
|
text='Apakah anda yakin ingin menolak data Grading ini?'
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'error',
|
||||||
|
isLoading: isRejectLoading,
|
||||||
|
onClick: rejectHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GradingForm;
|
||||||
Reference in New Issue
Block a user