feat(FE-170,174): add loading state and improve validation messages in GradingForm

This commit is contained in:
rstubryan
2025-11-19 19:17:32 +07:00
parent c876824c8f
commit 7e58e46254
2 changed files with 106 additions and 83 deletions
@@ -37,7 +37,6 @@ const RowOptionsMenu = ({
approveClickHandler, approveClickHandler,
rejectClickHandler, rejectClickHandler,
isGradingCompleted, isGradingCompleted,
hasConsumableEggs,
}: { }: {
type: 'dropdown' | 'collapse'; type: 'dropdown' | 'collapse';
props: CellContext<Recording, unknown>; props: CellContext<Recording, unknown>;
@@ -45,7 +44,6 @@ const RowOptionsMenu = ({
approveClickHandler: () => void; approveClickHandler: () => void;
rejectClickHandler: () => void; rejectClickHandler: () => void;
isGradingCompleted: (recording: Recording) => boolean; isGradingCompleted: (recording: Recording) => boolean;
hasConsumableEggs: (recording: Recording) => boolean;
}) => { }) => {
const isLayingCategory = const isLayingCategory =
props.row.original.project_flock_category === 'LAYING'; props.row.original.project_flock_category === 'LAYING';
@@ -60,7 +58,6 @@ const RowOptionsMenu = ({
const isApproved = isRecordingApproved(props.row.original); const isApproved = isRecordingApproved(props.row.original);
const isGradingDone = isGradingCompleted(props.row.original); const isGradingDone = isGradingCompleted(props.row.original);
const hasConsumable = hasConsumableEggs(props.row.original);
const getApprovalTooltip = () => { const getApprovalTooltip = () => {
if (isLayingCategory && !isGradingDone) { if (isLayingCategory && !isGradingDone) {
@@ -95,24 +92,9 @@ const RowOptionsMenu = ({
<Button <Button
variant='ghost' variant='ghost'
color='info' color='info'
className={`justify-start text-sm ${!hasConsumable ? 'cursor-not-allowed opacity-50' : ''}`} className={`justify-start text-sm`}
onClick={(e) => { href={`/production/recording/grading/add?recording_id=${props.row.original.id}`}
if (!hasConsumable) { title={'Lanjut ke proses grading untuk telur konsumsi baik'}
e.preventDefault();
toast.error(
'Tidak bisa melakukan grading karena tidak ada Telur Konsumsi Baik'
);
} else {
router.push(
`/production/recording/grading/add?recording_id=${props.row.original.id}`
);
}
}}
title={
hasConsumable
? 'Lanjut ke proses grading untuk telur konsumsi baik'
: 'Tidak bisa melakukan grading karena tidak ada Telur Konsumsi Baik'
}
> >
<Icon icon='material-symbols:egg' width={16} height={16} /> <Icon icon='material-symbols:egg' width={16} height={16} />
Grading Grading
@@ -461,23 +443,6 @@ const RecordingTable = () => {
); );
}, []); }, []);
const hasConsumableEggs = useCallback((recording: Recording): boolean => {
if (!recording.eggs || recording.eggs.length === 0) return false;
return recording.eggs.some((egg) => {
if (!egg.product_warehouse || !egg.product_warehouse.product)
return false;
if (Number(egg.qty) <= 0) return false;
const productName = egg.product_warehouse.product.name.toLowerCase();
return (
productName.includes('konsumsi') &&
productName.includes('baik') &&
Number(egg.qty) > 0
);
});
}, []);
const searchChangeHandler = useCallback( const searchChangeHandler = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => { (e: React.ChangeEvent<HTMLInputElement>) => {
updateFilter('search', e.target.value); updateFilter('search', e.target.value);
@@ -910,7 +875,6 @@ const RecordingTable = () => {
approveClickHandler={approveClickHandler} approveClickHandler={approveClickHandler}
rejectClickHandler={rejectClickHandler} rejectClickHandler={rejectClickHandler}
isGradingCompleted={isGradingCompleted} isGradingCompleted={isGradingCompleted}
hasConsumableEggs={hasConsumableEggs}
/> />
</RowDropdownOptions> </RowDropdownOptions>
)} )}
@@ -924,7 +888,6 @@ const RecordingTable = () => {
approveClickHandler={approveClickHandler} approveClickHandler={approveClickHandler}
rejectClickHandler={rejectClickHandler} rejectClickHandler={rejectClickHandler}
isGradingCompleted={isGradingCompleted} isGradingCompleted={isGradingCompleted}
hasConsumableEggs={hasConsumableEggs}
/> />
</RowCollapseOptions> </RowCollapseOptions>
)} )}
@@ -113,6 +113,11 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
const totalKonsumsiBaikEggs = konsumsiBaikEggData?.qty || 0; const totalKonsumsiBaikEggs = konsumsiBaikEggData?.qty || 0;
const konsumsiBaikEggId = konsumsiBaikEggData?.id; const konsumsiBaikEggId = konsumsiBaikEggData?.id;
const isDataLoading =
!recording ||
(totalKonsumsiBaikEggs === 0 &&
recording?.project_flock_category === 'LAYING');
// FORM HANDLERS // FORM HANDLERS
const createGradingHandler = useCallback( const createGradingHandler = useCallback(
async (payload: CreateGradingPayload) => { async (payload: CreateGradingPayload) => {
@@ -256,7 +261,8 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
}, [formik.values.eggs_grading]); }, [formik.values.eggs_grading]);
const isGradingExceedsAvailable = currentGradingTotal > totalKonsumsiBaikEggs; const isGradingExceedsAvailable = currentGradingTotal > totalKonsumsiBaikEggs;
const isGradingIncomplete = currentGradingTotal < totalKonsumsiBaikEggs && totalKonsumsiBaikEggs > 0; const isGradingIncomplete =
currentGradingTotal < totalKonsumsiBaikEggs && totalKonsumsiBaikEggs > 0;
const hasUserStartedGrading = currentGradingTotal > 0; const hasUserStartedGrading = currentGradingTotal > 0;
// GRADING HANDLERS // GRADING HANDLERS
@@ -349,6 +355,12 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
// EFFECTS // EFFECTS
useEffect(() => { useEffect(() => {
if (isDataLoading) {
toast.dismiss('grading-exceeds');
toast.dismiss('grading-incomplete');
return;
}
if (isGradingExceedsAvailable && currentGradingTotal > 0) { if (isGradingExceedsAvailable && currentGradingTotal > 0) {
toast.error( toast.error(
`Total grading (${currentGradingTotal}) melebihi telur yang tersedia (${totalKonsumsiBaikEggs})!`, `Total grading (${currentGradingTotal}) melebihi telur yang tersedia (${totalKonsumsiBaikEggs})!`,
@@ -371,7 +383,14 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
toast.dismiss('grading-exceeds'); toast.dismiss('grading-exceeds');
toast.dismiss('grading-incomplete'); toast.dismiss('grading-incomplete');
} }
}, [isGradingExceedsAvailable, isGradingIncomplete, hasUserStartedGrading, currentGradingTotal, totalKonsumsiBaikEggs]); }, [
isDataLoading,
isGradingExceedsAvailable,
isGradingIncomplete,
hasUserStartedGrading,
currentGradingTotal,
totalKonsumsiBaikEggs,
]);
useEffect(() => { useEffect(() => {
if ( if (
@@ -570,11 +589,13 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
{/* Total Telur Konsumsi Baik Info */} {/* Total Telur Konsumsi Baik Info */}
<div <div
className={`rounded-lg p-4 border-2 ${ className={`rounded-lg p-4 border-2 ${
isGradingExceedsAvailable isDataLoading
? 'bg-red-50 border-red-200' ? 'bg-gray-50 border-gray-200'
: isGradingIncomplete && hasUserStartedGrading : isGradingExceedsAvailable
? 'bg-yellow-50 border-yellow-200' ? 'bg-red-50 border-red-200'
: 'bg-green-50 border-green-200' : isGradingIncomplete && hasUserStartedGrading
? 'bg-yellow-50 border-yellow-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'>
@@ -584,7 +605,11 @@ 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}{' '} {isDataLoading ? (
<span className='loading loading-spinner loading-sm'></span>
) : (
totalKonsumsiBaikEggs
)}{' '}
<span className='text-sm text-gray-500 font-medium'> <span className='text-sm text-gray-500 font-medium'>
telur telur
</span> </span>
@@ -593,29 +618,35 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
</div> </div>
<div <div
className={`rounded-full p-2 ${ className={`rounded-full p-2 ${
isGradingExceedsAvailable isDataLoading
? 'bg-red-100' ? 'bg-gray-100'
: isGradingIncomplete && hasUserStartedGrading : isGradingExceedsAvailable
? 'bg-yellow-100' ? 'bg-red-100'
: 'bg-green-100' : isGradingIncomplete && hasUserStartedGrading
? 'bg-yellow-100'
: 'bg-green-100'
}`} }`}
> >
<Icon <Icon
icon={ icon={
isGradingExceedsAvailable isDataLoading
? 'material-symbols:error' ? 'material-symbols:hourglass-empty'
: isGradingIncomplete && hasUserStartedGrading : isGradingExceedsAvailable
? 'material-symbols:warning' ? 'material-symbols:error'
: 'material-symbols:check-circle' : isGradingIncomplete && hasUserStartedGrading
? 'material-symbols:warning'
: 'material-symbols:check-circle'
} }
width={20} width={20}
height={20} height={20}
className={ className={
isGradingExceedsAvailable isDataLoading
? 'text-red-600' ? 'text-gray-500'
: isGradingIncomplete && hasUserStartedGrading : isGradingExceedsAvailable
? 'text-yellow-600' ? 'text-red-600'
: 'text-green-600' : isGradingIncomplete && hasUserStartedGrading
? 'text-yellow-600'
: 'text-green-600'
} }
/> />
</div> </div>
@@ -627,31 +658,41 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
<span className='text-gray-600'>Total yang digrading:</span> <span className='text-gray-600'>Total yang digrading:</span>
<span <span
className={`font-semibold ${ className={`font-semibold ${
isGradingExceedsAvailable isDataLoading
? 'text-red-600' ? 'text-gray-500'
: isGradingIncomplete && hasUserStartedGrading : isGradingExceedsAvailable
? 'text-yellow-600' ? 'text-red-600'
: 'text-green-600' : isGradingIncomplete && hasUserStartedGrading
? 'text-yellow-600'
: 'text-green-600'
}`} }`}
> >
{currentGradingTotal} / {totalKonsumsiBaikEggs} {isDataLoading ? (
<span className='loading loading-spinner loading-xs'></span>
) : (
`${currentGradingTotal} / ${totalKonsumsiBaikEggs}`
)}
</span> </span>
</div> </div>
<div className='w-full bg-gray-200 rounded-full h-2 overflow-hidden'> <div className='w-full bg-gray-200 rounded-full h-2 overflow-hidden'>
<div <div
className={`h-full transition-all duration-300 ${ className={`h-full transition-all duration-300 ${
isGradingExceedsAvailable isDataLoading
? 'bg-red-500' ? 'bg-gray-400'
: isGradingIncomplete && hasUserStartedGrading : isGradingExceedsAvailable
? 'bg-yellow-500' ? 'bg-red-500'
: 'bg-green-500' : isGradingIncomplete && hasUserStartedGrading
? 'bg-yellow-500'
: 'bg-green-500'
}`} }`}
style={{ style={{
width: `${Math.min((currentGradingTotal / totalKonsumsiBaikEggs) * 100, 100)}%`, width: isDataLoading
? '0%'
: `${Math.min((currentGradingTotal / totalKonsumsiBaikEggs) * 100, 100)}%`,
}} }}
/> />
</div> </div>
{isGradingExceedsAvailable && ( {!isDataLoading && isGradingExceedsAvailable && (
<div className='flex items-center gap-1 text-xs text-red-600 mt-1'> <div className='flex items-center gap-1 text-xs text-red-600 mt-1'>
<Icon <Icon
icon='material-symbols:warning' icon='material-symbols:warning'
@@ -661,14 +702,28 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
<span>Melebihi batas tersedia</span> <span>Melebihi batas tersedia</span>
</div> </div>
)} )}
{isGradingIncomplete && hasUserStartedGrading && ( {!isDataLoading &&
<div className='flex items-center gap-1 text-xs text-yellow-600 mt-1'> isGradingIncomplete &&
hasUserStartedGrading && (
<div className='flex items-center gap-1 text-xs text-yellow-600 mt-1'>
<Icon
icon='material-symbols:info'
width={12}
height={12}
/>
<span>
Grading belum lengkap, semua telur harus digrading
</span>
</div>
)}
{isDataLoading && (
<div className='flex items-center gap-1 text-xs text-gray-500 mt-1'>
<Icon <Icon
icon='material-symbols:info' icon='material-symbols:hourglass-empty'
width={12} width={12}
height={12} height={12}
/> />
<span>Grading belum lengkap, semua telur harus digrading</span> <span>Memuat data telur konsumsi baik...</span>
</div> </div>
)} )}
</div> </div>
@@ -813,14 +868,18 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
isError={ isError={
isRepeaterInputError('eggs_grading', 'qty', idx) isRepeaterInputError('eggs_grading', 'qty', idx)
.isError || .isError ||
(isGradingExceedsAvailable || (isGradingIncomplete && hasUserStartedGrading)) (!isDataLoading &&
(isGradingExceedsAvailable ||
(isGradingIncomplete && hasUserStartedGrading)))
} }
errorMessage={ errorMessage={
isRepeaterInputError('eggs_grading', 'qty', idx) isRepeaterInputError('eggs_grading', 'qty', idx)
.errorMessage || .errorMessage ||
(isGradingExceedsAvailable (!isDataLoading && isGradingExceedsAvailable
? `Total grading melebihi telur yang tersedia (${totalKonsumsiBaikEggs})` ? `Total grading melebihi telur yang tersedia (${totalKonsumsiBaikEggs})`
: isGradingIncomplete && hasUserStartedGrading : !isDataLoading &&
isGradingIncomplete &&
hasUserStartedGrading
? `Total grading (${currentGradingTotal}) harus sama dengan total telur konsumsi baik (${totalKonsumsiBaikEggs})` ? `Total grading (${currentGradingTotal}) harus sama dengan total telur konsumsi baik (${totalKonsumsiBaikEggs})`
: undefined) : undefined)
} }
@@ -943,6 +1002,7 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
disabled={ disabled={
!formik.isValid || !formik.isValid ||
formik.isSubmitting || formik.isSubmitting ||
isDataLoading ||
isGradingExceedsAvailable || isGradingExceedsAvailable ||
(isGradingIncomplete && hasUserStartedGrading) (isGradingIncomplete && hasUserStartedGrading)
} }