mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
448 lines
13 KiB
TypeScript
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 };
|