From 478ca186d3594c1ecafa2241176aa5649db9e11f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 12 Nov 2025 23:41:22 +0700 Subject: [PATCH] feat(FE-170,175): add approval steps and status display in GradingForm and RecordingForm --- .../recording/grading/form/GradingForm.tsx | 371 +++++++++++++++--- 1 file changed, 312 insertions(+), 59 deletions(-) diff --git a/src/components/pages/production/recording/grading/form/GradingForm.tsx b/src/components/pages/production/recording/grading/form/GradingForm.tsx index 0f7d5390..f046c2c4 100644 --- a/src/components/pages/production/recording/grading/form/GradingForm.tsx +++ b/src/components/pages/production/recording/grading/form/GradingForm.tsx @@ -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 = {}; + 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 */}
- {/* Form Steps */} - {formSteps && ( + {/* Approval Steps */} + {type === 'detail' && approvalStepsData.length > 0 && (
-
-
    - {formSteps.map((step, idx) => ( - 0 ? 'step-primary' : undefined - } - icon={ - step.isCompleted ? ( - - ) : ( - idx + 1 - ) - } - > - {step.name} - - ))} -
-
- - {/* Action buttons for multi-form navigation */} - {type === 'detail' && ( -
- -
- )} + +
+ )} + {/* Default approval steps for add/edit modes */} + {type !== 'detail' && ( +
+
)}
@@ -482,11 +691,55 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => { }} >
+ {/* Status Approval */} + {recording?.approval && ( +
+ Status Approval +
+ + {(() => { + 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}`; + })()} + +
+
+ )} {/* Recording Info */} -
- Recording ID -

#{recording?.id || '-'}

-
Lokasi