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