feat(FE-170,174,175): implement approval steps in RecordingForm and remove unused form step status

This commit is contained in:
rstubryan
2025-11-12 23:12:22 +07:00
parent e2249cf73a
commit 47262adaf1
3 changed files with 326 additions and 84 deletions
@@ -23,6 +23,7 @@ import {
} from '@/services/api/production';
import { LocationApi } from '@/services/api/master-data';
import { ProductWarehouseApi } from '@/services/api/inventory';
import { ApprovalApi } from '@/services/api/approval';
import {
CreateGrowingRecordingPayload,
@@ -31,7 +32,11 @@ import {
UpdateLayingRecordingPayload,
Recording,
} from '@/types/api/production/recording';
import { type BaseApiResponse, FormStepStatus } from '@/types/api/api-general';
import {
type BaseApiResponse,
BaseApproval,
BaseGroupedApproval,
} from '@/types/api/api-general';
import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
import { Kandang } from '@/types/api/master-data/kandang';
@@ -50,7 +55,8 @@ import {
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
import { formatDate } from '@/lib/helper';
import toast from 'react-hot-toast';
import StepItem from '@/components/steps/StepItem';
import ApprovalSteps from '@/components/pages/ApprovalSteps';
import { RECORDING_APPROVAL_LINE } from '@/config/approval-line';
interface RecordingFormProps {
type?: 'add' | 'edit' | 'detail';
@@ -85,7 +91,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const [isApproveLoading, setIsApproveLoading] = useState(false);
const [isRejectLoading, setIsRejectLoading] = useState(false);
const [formSteps, setFormSteps] = useState<FormStepStatus[] | null>(null);
const [recordingFormErrorMessage, setRecordingFormErrorMessage] =
useState('');
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
@@ -330,7 +335,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
location_id: selectedLocation.value.toString(),
});
// Add kandang_id parameter if available from lookup
if (projectFlockKandangLookup?.kandang?.id) {
params.append(
'kandang_id',
@@ -351,7 +355,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
location_id: selectedLocation.value.toString(),
});
// Add kandang_id parameter if available from lookup
if (projectFlockKandangLookup?.kandang?.id) {
params.append(
'kandang_id',
@@ -366,7 +369,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const today = new Date().toISOString().split('T')[0];
const existingRecordingsUrl = useMemo(() => {
return `${RecordingApi.basePath}?record_date=${today}`;
const params = new URLSearchParams({
record_date: today,
});
return `${RecordingApi.basePath}?${params.toString()}`;
}, [today]);
const { data: existingRecordings } = useSWR(
@@ -407,6 +413,231 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
ProductWarehouseApi.getAllFetcher
);
// ===== APPROVAL DATA FETCHING =====
const approvalHistoryUrl = useMemo(() => {
if (!initialValues?.id || type !== 'detail') return null;
const params = new URLSearchParams({
module_name: 'RECORDINGS',
module_id: initialValues.id.toString(),
group_step_number: 'true',
});
return `${ApprovalApi.basePath}?${params.toString()}`;
}, [initialValues?.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 = initialValues?.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,
initialValues?.approval,
isGroupedApprovalData,
groupApprovalsByStep,
]);
// Format approval steps for display
const approvalStepsData = useMemo(() => {
if (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]);
// ===== DATA PROCESSING =====
const locationOptions = useMemo(() => {
let options: OptionType[] = [];
@@ -1058,26 +1289,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
setIsRejectLoading(false);
};
useEffect(() => {
if (isLayingCategory) {
const steps: FormStepStatus[] = [
{
name: 'Recording',
isCompleted: type === 'detail',
isCurrent: type !== 'detail',
},
{
name: 'Grading',
isCompleted: false,
isCurrent: type === 'detail',
},
];
setFormSteps(steps);
} else {
setFormSteps(null);
}
}, [isLayingCategory, type]);
// Body Weights Handlers
const addBodyWeight = () => {
const newBodyWeights = [
@@ -1311,23 +1522,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
}, [isLayingCategory, type]);
useEffect(() => {
if (isLayingCategory) {
const steps: FormStepStatus[] = [
{
name: 'Recording',
isCompleted: type === 'detail',
isCurrent: type !== 'detail',
},
{
name: 'Grading',
isCompleted: false,
isCurrent: type === 'detail',
},
];
setFormSteps(steps);
} else {
setFormSteps(null);
}
if (type !== 'add') {
setNewRecordingData(null);
}
@@ -1434,38 +1628,31 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{/* Project Flock Info Card */}
{(projectFlockKandangLookup || projectFlockKandangDetail) && (
<div className='flex items-center gap-2 mb-4'>
{/* Form Steps for LAYING Category */}
{formSteps && (
{/* Approval Steps for all categories */}
{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
}
icon={
step.isCompleted ? (
<Icon
icon='material-symbols:check-rounded'
width={24}
height={24}
/>
) : (
idx + 1
)
}
>
{step.name}
</StepItem>
))}
</ul>
</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: 'WAITING',
},
{
name: RECORDING_APPROVAL_LINE[2].step_name,
status: 'IDLE',
},
]}
/>
</div>
)}
</div>
@@ -1558,10 +1745,56 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
}}
>
<div className='grid grid-cols-2 md:grid-cols-4 gap-4'>
<div>
<span className='text-sm text-gray-600'>Recording ID</span>
<p className='font-semibold'>#{initialValues.id}</p>
</div>
{initialValues.approval && (
<div>
<span className='text-sm text-gray-600'>
Status Approval
</span>
<div className='mt-1'>
<Badge
variant='soft'
color={
initialValues.approval.action === 'APPROVED'
? 'success'
: initialValues.approval.action === 'REJECTED'
? 'error'
: initialValues.approval.action === 'UPDATED'
? 'warning'
: 'info'
}
className={{
badge:
'whitespace-nowrap font-semibold text-xs px-2',
}}
>
{(() => {
const actionText = (() => {
switch (initialValues.approval.action) {
case 'APPROVED':
return 'Disetujui';
case 'REJECTED':
return 'Ditolak';
case 'CREATED':
return 'Dibuat';
case 'UPDATED':
return 'Diperbarui';
default:
return initialValues.approval.action;
}
})();
const stepName = initialValues.approval.step_name;
if (stepName === actionText) {
return stepName;
}
return `${stepName} - ${actionText}`;
})()}
</Badge>
</div>
</div>
)}
<div>
<span className='text-sm text-gray-600'>Lokasi</span>
<p className='font-semibold'>
+15
View File
@@ -10,3 +10,18 @@ export const PROJECT_FLOCK_APPROVAL_LINE: ApprovalLine = [
step_name: 'Aktif',
},
] as const;
export const RECORDING_APPROVAL_LINE: ApprovalLine = [
{
step_number: 1,
step_name: 'Grading-Telur',
},
{
step_number: 2,
step_name: 'Pengajuan',
},
{
step_number: 3,
step_name: 'Disetujui',
},
] as const;
-6
View File
@@ -112,12 +112,6 @@ export type BaseGroupedApproval = {
approvals: BaseApproval[];
};
export type FormStepStatus = {
name: string;
isCompleted: boolean;
isCurrent: boolean;
};
export type Approvals = BaseApiResponse<BaseApproval>;
export type GroupedApprovals = BaseApiResponse<BaseGroupedApproval[]>;