mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 15:55:48 +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 { 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
|
||||||
|
|||||||
Reference in New Issue
Block a user