mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 21:41:57 +00:00
feat(FE-170,175): enhance GradingForm with additional recording details and improve UI for grading information
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user