mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat(FE-170,175): add approval steps and status display in GradingForm and RecordingForm
This commit is contained in:
@@ -23,8 +23,9 @@ import {
|
||||
} from '@/types/api/production/recording';
|
||||
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
||||
import {
|
||||
type FormStepStatus,
|
||||
type BaseApiResponse,
|
||||
BaseApproval,
|
||||
BaseGroupedApproval,
|
||||
} from '@/types/api/api-general';
|
||||
|
||||
import {
|
||||
@@ -36,15 +37,24 @@ import {
|
||||
|
||||
import { cn, formatDate } from '@/lib/helper';
|
||||
import toast from 'react-hot-toast';
|
||||
import { isResponseError } from '@/lib/api-helper';
|
||||
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
|
||||
|
||||
import {
|
||||
RecordingApi,
|
||||
ProjectFlockKandangApi,
|
||||
} from '@/services/api/production';
|
||||
import { ApprovalApi } from '@/services/api/approval';
|
||||
|
||||
import { useModal } from '@/components/Modal';
|
||||
import useSWR from 'swr';
|
||||
import ApprovalSteps from '@/components/pages/ApprovalSteps';
|
||||
import { RECORDING_APPROVAL_LINE } from '@/config/approval-line';
|
||||
|
||||
type FormStepStatus = {
|
||||
name: string;
|
||||
isCompleted: boolean;
|
||||
isCurrent: boolean;
|
||||
};
|
||||
|
||||
// INTERFACES & PROPS
|
||||
interface GradingFormProps {
|
||||
@@ -103,6 +113,231 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
|
||||
? (projectFlockKandangData.data as unknown as ProjectFlockKandang)
|
||||
: undefined;
|
||||
|
||||
// ===== APPROVAL DATA FETCHING =====
|
||||
const approvalHistoryUrl = useMemo(() => {
|
||||
if (!recording?.id || type !== 'detail') return null;
|
||||
const params = new URLSearchParams({
|
||||
module_name: 'RECORDINGS',
|
||||
module_id: recording.id.toString(),
|
||||
group_step_number: 'true',
|
||||
});
|
||||
return `${ApprovalApi.basePath}?${params.toString()}`;
|
||||
}, [recording?.id, type]);
|
||||
|
||||
const { data: approvalHistoryData } = useSWR(
|
||||
approvalHistoryUrl,
|
||||
approvalHistoryUrl ? ApprovalApi.getAllFetcher : null
|
||||
);
|
||||
|
||||
// Helper functions for approval data processing
|
||||
const groupApprovalsByStep = useCallback(
|
||||
(approvals: BaseApproval[]): BaseGroupedApproval[] => {
|
||||
const groups: Record<number, BaseGroupedApproval> = {};
|
||||
for (const approval of approvals) {
|
||||
if (!groups[approval.step_number]) {
|
||||
groups[approval.step_number] = {
|
||||
step_number: approval.step_number,
|
||||
step_name: approval.step_name,
|
||||
approvals: [],
|
||||
};
|
||||
}
|
||||
groups[approval.step_number].approvals.push(approval);
|
||||
}
|
||||
return Object.values(groups);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const isGroupedApprovalData = useCallback(
|
||||
(
|
||||
data: BaseApproval[] | BaseGroupedApproval[]
|
||||
): data is BaseGroupedApproval[] => {
|
||||
if (!data || data.length === 0) {
|
||||
return true;
|
||||
}
|
||||
const firstElement = data[0];
|
||||
return (
|
||||
typeof firstElement === 'object' &&
|
||||
firstElement !== null &&
|
||||
'approvals' in firstElement &&
|
||||
Array.isArray(firstElement.approvals)
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Process approval data
|
||||
const { groupedApprovals, latestApproval } = useMemo(() => {
|
||||
const latest = recording?.approval;
|
||||
|
||||
const rawData = isResponseSuccess(approvalHistoryData)
|
||||
? approvalHistoryData.data
|
||||
: undefined;
|
||||
|
||||
let processedGroupedApprovals: BaseGroupedApproval[] = [];
|
||||
|
||||
if (rawData) {
|
||||
if (isGroupedApprovalData(rawData)) {
|
||||
processedGroupedApprovals = rawData as BaseGroupedApproval[];
|
||||
} else {
|
||||
processedGroupedApprovals = groupApprovalsByStep(
|
||||
rawData as BaseApproval[]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
latest &&
|
||||
!processedGroupedApprovals.find(
|
||||
(g) => g.step_number === latest.step_number
|
||||
)
|
||||
) {
|
||||
processedGroupedApprovals.push({
|
||||
step_number: latest.step_number,
|
||||
step_name: latest.step_name,
|
||||
approvals: [latest],
|
||||
});
|
||||
|
||||
processedGroupedApprovals.sort((a, b) => a.step_number - b.step_number);
|
||||
}
|
||||
|
||||
return {
|
||||
groupedApprovals: processedGroupedApprovals,
|
||||
latestApproval: latest,
|
||||
};
|
||||
}, [
|
||||
approvalHistoryData,
|
||||
recording?.approval,
|
||||
isGroupedApprovalData,
|
||||
groupApprovalsByStep,
|
||||
]);
|
||||
|
||||
// Format approval steps for display
|
||||
const approvalStepsData = useMemo(() => {
|
||||
if (!latestApproval || type !== 'detail') {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return RECORDING_APPROVAL_LINE.map(
|
||||
(
|
||||
approvalLineItem
|
||||
): {
|
||||
name: string;
|
||||
status: 'APPROVED' | 'REJECTED' | 'WAITING' | 'IDLE';
|
||||
logs: Array<{
|
||||
action_by?: string;
|
||||
date?: string;
|
||||
notes?: string | null;
|
||||
}>;
|
||||
} => {
|
||||
const approvalGroup = groupedApprovals.find(
|
||||
(approvalGroupItem) =>
|
||||
approvalGroupItem.step_number === approvalLineItem.step_number
|
||||
);
|
||||
|
||||
if (!latestApproval) {
|
||||
return {
|
||||
name: approvalLineItem.step_name,
|
||||
status: 'IDLE',
|
||||
logs: approvalGroup
|
||||
? approvalGroup.approvals.map((approval) => ({
|
||||
action_by: approval.action_by.name,
|
||||
date: approval.action_at,
|
||||
notes: approval.notes,
|
||||
}))
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
const currentStepNumber = approvalLineItem.step_number;
|
||||
const latestStepNumber = latestApproval.step_number;
|
||||
|
||||
if (!approvalGroup) {
|
||||
if (currentStepNumber === latestStepNumber + 1) {
|
||||
return {
|
||||
name: approvalLineItem.step_name,
|
||||
status: 'WAITING',
|
||||
logs: [],
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: approvalLineItem.step_name,
|
||||
status: 'IDLE',
|
||||
logs: [],
|
||||
};
|
||||
}
|
||||
|
||||
let approvalStatus: 'APPROVED' | 'REJECTED' | 'WAITING' | 'IDLE' =
|
||||
'IDLE';
|
||||
|
||||
if (currentStepNumber <= latestStepNumber) {
|
||||
if (approvalGroup.approvals && approvalGroup.approvals.length > 0) {
|
||||
const latestApprovalInGroup = approvalGroup.approvals.sort(
|
||||
(a, b) =>
|
||||
new Date(b.action_at).getTime() -
|
||||
new Date(a.action_at).getTime()
|
||||
)[0];
|
||||
|
||||
switch (latestApprovalInGroup.action) {
|
||||
case 'CREATED':
|
||||
case 'APPROVED':
|
||||
approvalStatus = 'APPROVED';
|
||||
break;
|
||||
case 'REJECTED':
|
||||
approvalStatus = 'REJECTED';
|
||||
break;
|
||||
case 'UPDATED':
|
||||
if (currentStepNumber === latestStepNumber) {
|
||||
switch (latestApproval.action) {
|
||||
case 'CREATED':
|
||||
case 'APPROVED':
|
||||
approvalStatus = 'APPROVED';
|
||||
break;
|
||||
case 'REJECTED':
|
||||
approvalStatus = 'REJECTED';
|
||||
break;
|
||||
default:
|
||||
approvalStatus = 'WAITING';
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
approvalStatus = 'APPROVED';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
approvalStatus =
|
||||
currentStepNumber === latestStepNumber
|
||||
? 'WAITING'
|
||||
: 'APPROVED';
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (currentStepNumber === latestStepNumber + 1) {
|
||||
approvalStatus = 'WAITING';
|
||||
} else {
|
||||
approvalStatus = 'IDLE';
|
||||
}
|
||||
|
||||
const approvalLogs = approvalGroup.approvals.map((approval) => ({
|
||||
action_by: approval.action_by.name,
|
||||
date: approval.action_at,
|
||||
notes: approval.notes,
|
||||
}));
|
||||
|
||||
return {
|
||||
name: approvalGroup.step_name,
|
||||
status: approvalStatus,
|
||||
logs: approvalLogs,
|
||||
};
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('Gagal memformat approval steps:', error);
|
||||
return [];
|
||||
}
|
||||
}, [groupedApprovals, latestApproval, type]);
|
||||
|
||||
const konsumsiBaikEggData = useMemo(() => {
|
||||
if (!recording?.eggs) return null;
|
||||
|
||||
@@ -360,12 +595,12 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
|
||||
},
|
||||
{
|
||||
name: 'Grading',
|
||||
isCompleted: false,
|
||||
isCurrent: true,
|
||||
isCompleted: type !== 'add',
|
||||
isCurrent: type === 'add',
|
||||
},
|
||||
];
|
||||
setFormSteps(steps);
|
||||
}, []);
|
||||
}, [type]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isGradingExceedsAvailable && currentGradingTotal > 0) {
|
||||
@@ -414,57 +649,31 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
|
||||
|
||||
{/* Project Flock Info Card */}
|
||||
<div className='flex items-center gap-2 mb-4'>
|
||||
{/* Form Steps */}
|
||||
{formSteps && (
|
||||
{/* Approval Steps */}
|
||||
{type === 'detail' && approvalStepsData.length > 0 && (
|
||||
<div className='flex-1 mt-4'>
|
||||
<div className='w-full'>
|
||||
<ul className='steps w-full'>
|
||||
{formSteps.map((step, idx) => (
|
||||
<StepItem
|
||||
key={idx}
|
||||
color={
|
||||
step.isCompleted
|
||||
? 'success'
|
||||
: step.isCurrent
|
||||
? 'primary'
|
||||
: undefined
|
||||
}
|
||||
className={
|
||||
step.isCurrent && idx > 0 ? 'step-primary' : undefined
|
||||
}
|
||||
icon={
|
||||
step.isCompleted ? (
|
||||
<Icon
|
||||
icon='material-symbols:check-rounded'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
) : (
|
||||
idx + 1
|
||||
)
|
||||
}
|
||||
>
|
||||
{step.name}
|
||||
</StepItem>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Action buttons for multi-form navigation */}
|
||||
{type === 'detail' && (
|
||||
<div className='mt-4 flex gap-2'>
|
||||
<Button
|
||||
type='button'
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
toast.success('Semua form telah selesai!');
|
||||
router.push('/production/recording');
|
||||
}}
|
||||
>
|
||||
Kembali ke Recording
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<ApprovalSteps approvals={approvalStepsData} />
|
||||
</div>
|
||||
)}
|
||||
{/* Default approval steps for add/edit modes */}
|
||||
{type !== 'detail' && (
|
||||
<div className='flex-1 mt-4'>
|
||||
<ApprovalSteps
|
||||
approvals={[
|
||||
{
|
||||
name: RECORDING_APPROVAL_LINE[0].step_name,
|
||||
status: 'APPROVED',
|
||||
},
|
||||
{
|
||||
name: RECORDING_APPROVAL_LINE[1].step_name,
|
||||
status: 'APPROVED',
|
||||
},
|
||||
{
|
||||
name: RECORDING_APPROVAL_LINE[2].step_name,
|
||||
status: 'WAITING',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -482,11 +691,55 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
|
||||
}}
|
||||
>
|
||||
<div className='grid grid-cols-2 md:grid-cols-4 gap-4 mb-6'>
|
||||
{/* Status Approval */}
|
||||
{recording?.approval && (
|
||||
<div>
|
||||
<span className='text-sm text-gray-600'>Status Approval</span>
|
||||
<div className='mt-1'>
|
||||
<Badge
|
||||
variant='soft'
|
||||
color={
|
||||
recording.approval.action === 'APPROVED'
|
||||
? 'success'
|
||||
: recording.approval.action === 'REJECTED'
|
||||
? 'error'
|
||||
: recording.approval.action === 'UPDATED'
|
||||
? 'warning'
|
||||
: 'info'
|
||||
}
|
||||
className={{
|
||||
badge: 'whitespace-nowrap font-semibold text-xs px-2',
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
const actionText = (() => {
|
||||
switch (recording.approval.action) {
|
||||
case 'APPROVED':
|
||||
return 'Disetujui';
|
||||
case 'REJECTED':
|
||||
return 'Ditolak';
|
||||
case 'CREATED':
|
||||
return 'Dibuat';
|
||||
case 'UPDATED':
|
||||
return 'Diperbarui';
|
||||
default:
|
||||
return recording.approval.action;
|
||||
}
|
||||
})();
|
||||
|
||||
const stepName = recording.approval.step_name;
|
||||
|
||||
if (stepName === actionText) {
|
||||
return stepName;
|
||||
}
|
||||
|
||||
return `${stepName} - ${actionText}`;
|
||||
})()}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* 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'>
|
||||
|
||||
Reference in New Issue
Block a user