Files
lti-web-client/src/components/pages/ApprovalSteps.tsx
T

448 lines
13 KiB
TypeScript

import { Icon } from '@iconify/react';
import Steps from '@/components/steps/Steps';
import StepItem from '@/components/steps/StepItem';
import Tooltip from '@/components/Tooltip';
import { cn, formatDate } from '@/lib/helper';
import {
BaseApiResponse,
BaseApproval,
BaseGroupedApproval,
} from '@/types/api/api-general';
import { ApprovalLine } from '@/types/config/constant';
import useSWR from 'swr';
import { httpClientFetcher } from '@/services/http/client';
import { isResponseSuccess } from '@/lib/api-helper';
import { useCallback, useMemo } from 'react';
export type ApprovalStepStatus = 'APPROVED' | 'REJECTED' | 'WAITING' | 'IDLE';
export type ApprovalStepLog = {
action: string;
action_by?: string;
date?: string;
notes?: string | null;
};
interface ApprovalStepsProps {
approvals: {
name?: string;
status: ApprovalStepStatus;
logs?: ApprovalStepLog[];
}[];
}
const ApprovalSteps = ({ approvals }: ApprovalStepsProps) => {
return (
<Steps direction='vertical' className='w-full md:steps-horizontal'>
{approvals.map((approval, idx) => {
const stepItemColor =
approval.status === 'APPROVED'
? 'success'
: approval.status === 'REJECTED'
? 'error'
: approval.status === 'WAITING'
? 'warning'
: undefined;
const stepItemIcon =
approval.status === 'APPROVED'
? 'material-symbols:check-rounded'
: approval.status === 'REJECTED'
? 'material-symbols:close-rounded'
: approval.status === 'WAITING'
? 'pajamas:dash-circle'
: approval.logs && approval.logs.length > 0
? 'material-symbols:info-outline-rounded'
: 'bxs:hourglass';
return (
<StepItem
key={idx}
color={stepItemColor}
icon={
<Tooltip
color={stepItemColor}
position='right'
className={{
wrapper: 'md:tooltip-bottom',
content: 'p-0 rounded overflow-hidden',
}}
content={
<>
{approval.logs && approval.logs.length > 0 && (
<div className='flex flex-col gap-0'>
{approval.logs?.map((approvalLog, logIdx) => {
const action =
approvalLog.action === 'CREATED'
? 'Dibuat'
: approvalLog.action === 'UPDATED'
? 'Diperbarui'
: approvalLog.action === 'APPROVED'
? 'Disetujui'
: approvalLog.action === 'REJECTED'
? 'Ditolak'
: '-';
return (
<div
key={logIdx}
className={cn(
'p-2 flex flex-col text-base text-start',
{
'bg-success text-success-content':
approvalLog.action === 'APPROVED',
'bg-error text-error-content':
approvalLog.action === 'REJECTED',
'bg-info text-info-content':
approvalLog.action === 'CREATED',
'bg-warning text-warning-content':
approvalLog.action === 'UPDATED',
}
)}
>
{approvalLog.date && (
<span>
{formatDate(
approvalLog.date,
'YYYY-MM-DD, HH:mm:ss'
)}
</span>
)}
<span>Aksi: {action}</span>
<span>Oleh: {approvalLog.action_by ?? '-'}</span>
<span>Catatan: {approvalLog.notes ?? '-'}</span>
</div>
);
})}
</div>
)}
</>
}
>
<Icon
icon={stepItemIcon}
width={24}
height={24}
className={cn({
invisible:
approval.status === 'IDLE' &&
(!approval.logs ||
(approval.logs && approval.logs.length === 0)),
})}
/>
</Tooltip>
}
>
{approval.name}
</StepItem>
);
})}
</Steps>
);
};
export const formatGroupedApprovalsToApprovalSteps = (
approvalLine: ApprovalLine,
groupedApprovals: BaseGroupedApproval[] | undefined,
latestApproval: BaseApproval | undefined
): ApprovalStepsProps['approvals'] => {
const formattedApprovalSteps: ApprovalStepsProps['approvals'] =
approvalLine.map((approvalLineItem) => {
const approvalGroup = groupedApprovals?.find(
(approvalGroupItem) =>
approvalGroupItem.step_number === approvalLineItem.step_number
);
const currentStepNumber = approvalLineItem.step_number;
const lastStepNumber =
groupedApprovals?.[groupedApprovals.length - 1]?.step_number;
const isLatestApprovalRejected = latestApproval?.action === 'REJECTED';
// Only throw error if we have a valid lastStepNumber to compare against
if (
!approvalGroup &&
lastStepNumber !== undefined &&
currentStepNumber <= lastStepNumber
) {
// throw new Error(
// `Approval dengan ${approvalLineItem.step_name} tidak ditemukan!`
// );
}
if (!approvalGroup) {
// Check if this step is waiting (only if we have latestApproval)
const isWaiting =
latestApproval?.step_number !== undefined &&
currentStepNumber === latestApproval.step_number + 1;
// Check if previous approval was rejected
const isPreviousApprovalRejected =
groupedApprovals &&
groupedApprovals.length > 0 &&
groupedApprovals[groupedApprovals.length - 1]?.approvals?.[0]
?.action === 'REJECTED';
return {
name: approvalLineItem.step_name,
status: isPreviousApprovalRejected
? 'IDLE'
: isWaiting
? 'WAITING'
: 'IDLE',
};
}
let approvalStatus: ApprovalStepStatus = 'IDLE';
// Only compare if latestApproval and its step_number exist
if (
latestApproval?.step_number !== undefined &&
approvalGroup.step_number <= latestApproval.step_number
) {
if (approvalGroup.approvals) {
switch (approvalGroup?.approvals[0]?.action) {
case 'CREATED':
case 'UPDATED':
case 'APPROVED':
approvalStatus = 'APPROVED';
break;
case 'REJECTED':
approvalStatus = 'REJECTED';
break;
default:
approvalStatus = 'IDLE';
break;
}
}
} else if (
latestApproval?.step_number !== undefined &&
approvalGroup.step_number === latestApproval.step_number + 1 &&
!isLatestApprovalRejected
) {
approvalStatus = 'WAITING';
} else {
approvalStatus = 'IDLE';
}
const approvalLogs: ApprovalStepLog[] = approvalGroup.approvals
? approvalGroup.approvals.map((approval) => ({
action_by: approval.action_by.name,
date: approval.action_at,
notes: approval.notes,
action: approval.action,
}))
: [];
return {
name: approvalGroup.step_name,
status: approvalStatus,
logs: approvalLogs,
};
});
return formattedApprovalSteps;
};
export default ApprovalSteps;
/**
* Mengubah array BaseApproval (datar) menjadi BaseGroupedApproval (berkelompok).
*/
const groupApprovalsByStep = (
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);
};
/**
* Mengubah array BaseGroupedApproval (berkelompok) kembali menjadi BaseApproval[] (datar).
*/
const flattenGroupedApprovals = (
groupedApprovals: BaseGroupedApproval[]
): BaseApproval[] => {
return groupedApprovals.flatMap((group) => group.approvals);
};
/**
* Type guard untuk memeriksa apakah data adalah BaseGroupedApproval[].
*/
const isGroupedApprovalData = (
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)
);
};
const useApprovalSteps = ({
latestApproval,
approvalLines,
moduleName,
moduleId,
params,
}: {
latestApproval: BaseApproval | undefined;
approvalLines: ApprovalLine;
moduleName: string;
moduleId: string;
params?: {
page?: number;
limit?: number | string;
search?: string;
group_step_number?: boolean;
order_by_date?: 'ASC' | 'DESC';
};
}) => {
// Membuat URL Parameters
const paramString = new URLSearchParams({
page: params?.page?.toString() || '',
limit: params?.limit?.toString() || '',
search: params?.search || '',
group_step_number: params?.group_step_number?.toString() || '',
order_by_date: params?.order_by_date || '',
}).toString();
// fetching data approvals
const SWR_KEY_APPROVALS =
moduleName && moduleId
? `/approvals?module_name=${moduleName}&module_id=${moduleId}${
params ? `&${paramString}` : ''
}`
: null;
const {
data: approvalData,
isLoading: approvalIsLoading,
mutate: mutateApprovals,
} = useSWR(SWR_KEY_APPROVALS, async (url) => {
return await httpClientFetcher<
BaseApiResponse<BaseApproval[] | BaseGroupedApproval[]>
>(url);
});
// Fungsi Refresh
const refresh = useCallback(async () => {
await mutateApprovals();
}, [mutateApprovals]);
const { groupedApprovals } = useMemo(() => {
const rawData = isResponseSuccess(approvalData)
? approvalData.data
: undefined;
let processedGroupedApprovals: BaseGroupedApproval[] = [];
if (rawData) {
if (isGroupedApprovalData(rawData)) {
processedGroupedApprovals = rawData;
} else {
processedGroupedApprovals = groupApprovalsByStep(
rawData as BaseApproval[]
);
}
}
return {
groupedApprovals: processedGroupedApprovals,
};
}, [approvalData]);
const isLoading = approvalIsLoading;
// Formatting Akhir
const approvals = useMemo(() => {
if (isLoading || !approvalLines.length) {
return [];
}
// Try to derive latestApproval from groupedApprovals if not provided
let effectiveLatestApproval = latestApproval;
if (!effectiveLatestApproval && groupedApprovals.length > 0) {
// Get all approvals from grouped data
const allApprovals = groupedApprovals.flatMap((group) => group.approvals);
if (allApprovals.length > 0) {
// Use the most recent approval (last in array)
effectiveLatestApproval = allApprovals[allApprovals.length - 1];
}
}
// If still no latestApproval, return empty
if (!effectiveLatestApproval) {
return [];
}
try {
return formatGroupedApprovalsToApprovalSteps(
approvalLines,
groupedApprovals,
effectiveLatestApproval
);
} catch (error) {
console.warn('Gagal memformat approval steps:', error);
return [];
}
}, [isLoading, approvalLines, groupedApprovals, latestApproval]);
// Raw Data Approvals
const rawDataApprovals = useMemo(() => {
const rawData = isResponseSuccess(approvalData)
? approvalData.data
: undefined;
if (!rawData) {
return undefined;
}
const isDataCurrentlyGrouped = isGroupedApprovalData(rawData);
const wantsGrouped = params?.group_step_number !== false;
if (wantsGrouped) {
if (isDataCurrentlyGrouped) {
return rawData as BaseGroupedApproval[];
} else {
return groupApprovalsByStep(rawData as BaseApproval[]);
}
} else {
if (isDataCurrentlyGrouped) {
return flattenGroupedApprovals(rawData as BaseGroupedApproval[]);
} else {
return rawData as BaseApproval[];
}
}
}, [approvalData, params?.group_step_number]);
// Return Hook
return {
approvals,
isLoading,
rawDataApprovals: rawDataApprovals,
refresh,
};
};
export { useApprovalSteps };