feat(FE-170,175): enhance GradingForm with additional recording details and improve UI for grading information

This commit is contained in:
rstubryan
2025-11-06 15:12:04 +07:00
parent de9ec716f5
commit 90dd26064d
@@ -4,12 +4,16 @@ import { useMemo, useState, useEffect, useCallback } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useFormik } from 'formik';
import { Icon } from '@iconify/react';
import Button from '@/components/Button';
import Button from '@/components/Button';
import NumberInput from '@/components/input/NumberInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import CheckboxInput from '@/components/input/CheckboxInput';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import Card from '@/components/Card';
import StepItem from '@/components/steps/StepItem';
import Badge from '@/components/Badge';
import {
CreateGradingPayload,
UpdateGradingPayload,
@@ -17,45 +21,53 @@ import {
GradingEgg,
Recording,
} from '@/types/api/production/recording';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
import {
type FormStepStatus,
type BaseApiResponse,
} from '@/types/api/api-general';
import {
RecordingGradingFormSchema,
RecordingGradingFormValues,
UpdateRecordingGradingFormSchema,
getRecordingGradingFormInitialValues,
} from '../../form/RecordingForm.schema';
import { cn } from '@/lib/helper';
import { cn, formatDate } from '@/lib/helper';
import toast from 'react-hot-toast';
import { RecordingApi } from '@/services/api/production';
import { isResponseError } from '@/lib/api-helper';
import {
RecordingApi,
ProjectFlockKandangApi,
} from '@/services/api/production';
import { useModal } from '@/components/Modal';
import useSWR from 'swr';
import Card from '@/components/Card';
import StepItem from '@/components/steps/StepItem';
// INTERFACES & PROPS
interface GradingFormProps {
type?: 'add' | 'edit' | 'detail';
initialValues?: RecordingEgg & { grading_eggs?: GradingEgg[] };
}
const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
// HOOKS & ROUTER
const router = useRouter();
const searchParams = useSearchParams();
const recordingId = searchParams.get('recording_id');
// STATE MANAGEMENT
const [selectedGradingItems, setSelectedGradingItems] = useState<number[]>(
[]
);
const [formSteps, setFormSteps] = useState<FormStepStatus[] | null>(null);
const [gradingFormErrorMessage, setGradingFormErrorMessage] = useState('');
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const deleteModal = useModal();
// ===== API DATA FETCHING =====
// API DATA FETCHING
const recordingUrl = useMemo(() => {
const recordingIdToUse = recordingId;
if (!recordingIdToUse) return null;
@@ -67,12 +79,27 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
recordingUrl ? RecordingApi.getAllFetcher : null
);
// ===== DATA PROCESSING =====
// DATA PROCESSING
const recording =
recordingData?.status === 'success'
? (recordingData.data as unknown as Recording)
: undefined;
const projectFlockKandangUrl = useMemo(() => {
if (!recording?.project_flock_kandang_id) return null;
return `${ProjectFlockKandangApi.basePath}/${recording.project_flock_kandang_id}`;
}, [recording?.project_flock_kandang_id]);
const { data: projectFlockKandangData } = useSWR(
projectFlockKandangUrl,
projectFlockKandangUrl ? ProjectFlockKandangApi.getAllFetcher : null
);
const projectFlockKandang =
projectFlockKandangData?.status === 'success'
? (projectFlockKandangData.data as unknown as ProjectFlockKandang)
: undefined;
const konsumsiBaikEggData = useMemo(() => {
if (!recording?.eggs) return null;
@@ -87,7 +114,7 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
const totalKonsumsiBaikEggs = konsumsiBaikEggData?.qty || 0;
// ===== FORM HANDLERS =====
// FORM HANDLERS
const createGradingHandler = useCallback(
async (payload: CreateGradingPayload) => {
const res = (await RecordingApi.createGrading(payload)) as
@@ -150,6 +177,7 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
}
}, [deleteModal, initialValues?.id, router]);
// FORMIK SETUP
const formikInitialValues = useMemo(() => {
let recordingEggId: number | undefined = initialValues?.id;
@@ -208,6 +236,7 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
const isGradingExceedsAvailable = currentGradingTotal > totalKonsumsiBaikEggs;
// GRADING HANDLERS
const addGrading = () => {
let recordingEggId: number | undefined = initialValues?.id;
@@ -257,6 +286,7 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
setSelectedGradingItems([]);
};
// VALIDATION HELPERS
const isRepeaterInputError = (
arrayName: 'eggs_grading',
column: string,
@@ -290,7 +320,7 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
};
};
// ===== EFFECTS =====
// EFFECTS
useEffect(() => {
const steps: FormStepStatus[] = [
{
@@ -307,7 +337,6 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
setFormSteps(steps);
}, []);
// Show toast when grading exceeds available eggs
useEffect(() => {
if (isGradingExceedsAvailable && currentGradingTotal > 0) {
toast.error(
@@ -334,7 +363,7 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
{ recording_egg_id: recordingEggId, grade: '', qty: '' },
]);
}
}, [formik]);
}, []);
return (
<>
@@ -354,6 +383,7 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
{type === 'detail' && 'Detail Grading'}
</h1>
</header>
{/* Project Flock Info Card */}
<div className='flex items-center gap-2 mb-4'>
{/* Form Steps */}
@@ -417,12 +447,81 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
>
{/* Basic Info Card */}
<Card
title='Informasi Grading'
title='Informasi Recording'
className={{
wrapper: 'w-full mb-6 shadow-sm',
body: 'p-6',
}}
>
<div className='grid grid-cols-2 md:grid-cols-4 gap-4 mb-6'>
{/* Recording Info */}
<div>
<span className='text-sm text-gray-600'>Recording ID</span>
<p className='font-semibold'>#{recording?.id || '-'}</p>
</div>
<div>
<span className='text-sm text-gray-600'>Lokasi</span>
<p className='font-semibold'>
{projectFlockKandang?.project_flock?.location?.name || '-'}
</p>
</div>
<div>
<span className='text-sm text-gray-600'>Project Flock</span>
<p className='font-semibold'>
{projectFlockKandang?.project_flock?.flock_name || '-'}
</p>
</div>
<div>
<span className='text-sm text-gray-600'>Kandang</span>
<p className='font-semibold'>
{projectFlockKandang?.kandang?.name || '-'}
</p>
</div>
<div>
<span className='text-sm text-gray-600'>Tanggal Recording</span>
<p className='font-semibold'>
{recording
? formatDate(recording.record_datetime, 'DD MMMM YYYY')
: '-'}
</p>
</div>
<div>
<span className='text-sm text-gray-600'>Hari</span>
<p className='font-semibold'>Hari ke-{recording?.day || '-'}</p>
</div>
<div>
<span className='text-sm text-gray-600'>Kategori</span>
<p className='font-semibold'>
<Badge
variant='soft'
color={
recording?.project_flock_category === 'LAYING'
? 'info'
: 'warning'
}
size='sm'
>
{recording?.project_flock_category || '-'}
</Badge>
</p>
</div>
<div>
<span className='text-sm text-gray-600'>Periode</span>
<p className='font-semibold'>
<Badge
variant='soft'
color='neutral'
size='sm'
className={{
badge: 'whitespace-nowrap font-semibold text-xs px-2',
}}
>
Periode {projectFlockKandang?.project_flock?.period || '-'}
</Badge>
</p>
</div>
</div>
<div
className={
type === 'detail'
@@ -430,18 +529,9 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
: 'grid grid-cols-1 md:grid-cols-2 gap-6'
}
>
{/* Recording Egg ID */}
<div className='bg-gray-50 rounded-lg p-4 border border-gray-200'>
<div className='flex items-center justify-between'>
<div>
<p className='text-sm font-medium text-gray-600 mb-1'>Recording ID</p>
<p className='text-xl font-bold text-gray-900'>
#{type === 'detail' && initialValues ?
initialValues.id :
(formik.values.eggs_grading?.[0]?.recording_egg_id || '-')
}
</p>
</div>
{/* Additional Recording Info */}
<div className='bg-blue-50 rounded-lg p-4 border border-blue-200'>
<div className='flex items-center justify-between mb-3'>
<div className='bg-blue-100 rounded-full p-2'>
<Icon
icon='material-symbols:info'
@@ -450,15 +540,34 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
className='text-blue-600'
/>
</div>
<span className='text-xs text-blue-600 font-medium uppercase tracking-wide'>
Detail Recording
</span>
</div>
<div className='space-y-2'>
<div>
<p className='text-xs text-gray-600'>Area</p>
<p className='font-semibold text-blue-900'>
{projectFlockKandang?.project_flock?.area?.name || '-'}
</p>
</div>
<div>
<p className='text-xs text-gray-600'>Status Kandang</p>
<p className='font-semibold text-blue-900'>
{projectFlockKandang?.kandang?.status || '-'}
</p>
</div>
</div>
</div>
{/* Total Telur Konsumsi Baik Info */}
<div className={`rounded-lg p-4 border-2 ${
isGradingExceedsAvailable
? 'bg-red-50 border-red-200'
: 'bg-green-50 border-green-200'
}`}>
<div
className={`rounded-lg p-4 border-2 ${
isGradingExceedsAvailable
? 'bg-red-50 border-red-200'
: 'bg-green-50 border-green-200'
}`}
>
<div className='flex items-center justify-between mb-3'>
<div>
<p className='text-sm font-medium text-gray-600 mb-1'>
@@ -466,28 +575,30 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
</p>
<div className='flex items-baseline gap-2'>
<p className='text-2xl font-bold text-gray-900'>
{totalKonsumsiBaikEggs}
{totalKonsumsiBaikEggs}{' '}
<span className='text-sm text-gray-500 font-medium'>
telur
</span>
</p>
<span className='text-sm text-gray-500 font-medium'>
telur
</span>
</div>
</div>
<div className={`rounded-full p-2 ${
isGradingExceedsAvailable
? 'bg-red-100'
: 'bg-green-100'
}`}>
<div
className={`rounded-full p-2 ${
isGradingExceedsAvailable ? 'bg-red-100' : 'bg-green-100'
}`}
>
<Icon
icon={isGradingExceedsAvailable ?
'material-symbols:error' :
'material-symbols:check-circle'
icon={
isGradingExceedsAvailable
? 'material-symbols:error'
: 'material-symbols:check-circle'
}
width={20}
height={20}
className={isGradingExceedsAvailable ?
'text-red-600' :
'text-green-600'
className={
isGradingExceedsAvailable
? 'text-red-600'
: 'text-green-600'
}
/>
</div>
@@ -497,9 +608,13 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
<div className='space-y-2'>
<div className='flex items-center justify-between text-sm'>
<span className='text-gray-600'>Total yang digrading:</span>
<span className={`font-semibold ${
isGradingExceedsAvailable ? 'text-red-600' : 'text-green-600'
}`}>
<span
className={`font-semibold ${
isGradingExceedsAvailable
? 'text-red-600'
: 'text-green-600'
}`}
>
{currentGradingTotal} / {totalKonsumsiBaikEggs}
</span>
</div>
@@ -511,7 +626,7 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
: 'bg-green-500'
}`}
style={{
width: `${Math.min((currentGradingTotal / totalKonsumsiBaikEggs) * 100, 100)}%`
width: `${Math.min((currentGradingTotal / totalKonsumsiBaikEggs) * 100, 100)}%`,
}}
/>
</div>
@@ -815,6 +930,7 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
</form>
</section>
{/* ===== MODALS ===== */}
{type !== 'add' && (
<>
<ConfirmationModal