561 lines
22 KiB
TypeScript
561 lines
22 KiB
TypeScript
import { useState, useEffect, useMemo } from 'react';
|
|
import workflowApi from '@/services/workflowApi';
|
|
import { CUSTOM_REQUEST_DATABASE } from '@/utils/customRequestDatabase';
|
|
import { CLAIM_MANAGEMENT_DATABASE } from '@/utils/claimManagementDatabase';
|
|
|
|
/**
|
|
* Custom Hook: useRequestDetails
|
|
*
|
|
* Purpose: Manages request data fetching, transformation, and state management
|
|
*
|
|
* Responsibilities:
|
|
* - Fetches workflow details from API using request identifier (request number or UUID)
|
|
* - Transforms backend data structure to frontend format
|
|
* - Maps approval levels with TAT alerts
|
|
* - Handles spectators and participants
|
|
* - Provides refresh functionality
|
|
* - Falls back to static databases when API fails
|
|
*
|
|
* @param requestIdentifier - Request number or UUID to fetch
|
|
* @param dynamicRequests - Optional array of dynamic requests for fallback
|
|
* @param user - Current authenticated user object
|
|
* @returns Object containing request data, loading state, refresh function, etc.
|
|
*/
|
|
export function useRequestDetails(
|
|
requestIdentifier: string,
|
|
dynamicRequests: any[] = [],
|
|
user: any
|
|
) {
|
|
// State: Stores the fetched and transformed request data
|
|
const [apiRequest, setApiRequest] = useState<any | null>(null);
|
|
|
|
// State: Indicates if data is currently being fetched
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
|
// State: Stores the current approval level for the logged-in user
|
|
const [currentApprovalLevel, setCurrentApprovalLevel] = useState<any | null>(null);
|
|
|
|
// State: Indicates if the current user is a spectator (view-only access)
|
|
const [isSpectator, setIsSpectator] = useState(false);
|
|
|
|
/**
|
|
* Helper: Convert name/email to initials for avatar display
|
|
* Example: "John Doe" → "JD", "john@email.com" → "JO"
|
|
*/
|
|
const toInitials = (name?: string, email?: string) => {
|
|
const base = (name || email || 'NA').toString();
|
|
return base.split(' ').map(s => s[0]).filter(Boolean).join('').slice(0, 2).toUpperCase();
|
|
};
|
|
|
|
/**
|
|
* Helper: Map backend status strings to frontend display format
|
|
* Converts: IN_PROGRESS → in-review, PENDING → pending, etc.
|
|
*/
|
|
const statusMap = (s: string) => {
|
|
const val = (s || '').toUpperCase();
|
|
if (val === 'IN_PROGRESS') return 'in-review';
|
|
if (val === 'PENDING') return 'pending';
|
|
if (val === 'APPROVED') return 'approved';
|
|
if (val === 'REJECTED') return 'rejected';
|
|
if (val === 'CLOSED') return 'closed';
|
|
if (val === 'SKIPPED') return 'skipped';
|
|
return (s || '').toLowerCase();
|
|
};
|
|
|
|
/**
|
|
* Function: refreshDetails
|
|
*
|
|
* Purpose: Fetch the latest request data from backend and update all state
|
|
*
|
|
* Process:
|
|
* 1. Fetch workflow details from API
|
|
* 2. Extract and validate data arrays (approvals, participants, documents, TAT alerts)
|
|
* 3. Transform approval levels with TAT alerts
|
|
* 4. Map spectators and documents
|
|
* 5. Filter out TAT warning activities from audit trail
|
|
* 6. Update all state with transformed data
|
|
* 7. Determine current user's approval level and spectator status
|
|
*/
|
|
const refreshDetails = async () => {
|
|
setRefreshing(true);
|
|
try {
|
|
// API Call: Fetch complete workflow details including approvals, documents, participants
|
|
const details = await workflowApi.getWorkflowDetails(requestIdentifier);
|
|
if (!details) {
|
|
console.warn('[useRequestDetails] No details returned from API');
|
|
return;
|
|
}
|
|
|
|
// Extract: Separate data structures from API response
|
|
const wf = details.workflow || {};
|
|
const approvals = Array.isArray(details.approvals) ? details.approvals : [];
|
|
const participants = Array.isArray(details.participants) ? details.participants : [];
|
|
const documents = Array.isArray(details.documents) ? details.documents : [];
|
|
const summary = details.summary || {};
|
|
const tatAlerts = Array.isArray(details.tatAlerts) ? details.tatAlerts : [];
|
|
|
|
// Debug: Log TAT alerts for monitoring
|
|
if (tatAlerts.length > 0) {
|
|
console.log(`[useRequestDetails] Found ${tatAlerts.length} TAT alerts:`, tatAlerts);
|
|
}
|
|
|
|
const currentLevel = summary?.currentLevel || wf.currentLevel || 1;
|
|
|
|
/**
|
|
* Transform: Map approval levels to UI format with TAT alerts
|
|
* Each approval level includes:
|
|
* - Display status (waiting, pending, in-review, approved, rejected, skipped)
|
|
* - TAT information (hours, elapsed, remaining, percentage)
|
|
* - TAT alerts specific to this level
|
|
* - Approver details
|
|
*/
|
|
const approvalFlow = approvals.map((a: any) => {
|
|
const levelNumber = a.levelNumber || 0;
|
|
const levelStatus = (a.status || '').toString().toUpperCase();
|
|
const levelId = a.levelId || a.level_id;
|
|
|
|
// Determine display status based on workflow progress
|
|
let displayStatus = statusMap(a.status);
|
|
|
|
// Future levels that haven't been reached yet show as "waiting"
|
|
if (levelNumber > currentLevel && levelStatus !== 'APPROVED' && levelStatus !== 'REJECTED') {
|
|
displayStatus = 'waiting';
|
|
}
|
|
// Current level with pending status shows as "pending"
|
|
else if (levelNumber === currentLevel && levelStatus === 'PENDING') {
|
|
displayStatus = 'pending';
|
|
}
|
|
|
|
// Filter: Get TAT alerts that belong to this specific approval level
|
|
const levelAlerts = tatAlerts.filter((alert: any) => alert.levelId === levelId);
|
|
|
|
return {
|
|
step: levelNumber,
|
|
levelId,
|
|
role: a.levelName || a.approverName || 'Approver',
|
|
status: displayStatus,
|
|
approver: a.approverName || a.approverEmail,
|
|
approverId: a.approverId || a.approver_id,
|
|
approverEmail: a.approverEmail,
|
|
tatHours: Number(a.tatHours || 0),
|
|
elapsedHours: Number(a.elapsedHours || 0),
|
|
remainingHours: Number(a.remainingHours || 0),
|
|
tatPercentageUsed: Number(a.tatPercentageUsed || 0),
|
|
// Calculate actual hours taken if level is completed
|
|
actualHours: a.levelEndTime && a.levelStartTime
|
|
? Math.max(0, (new Date(a.levelEndTime).getTime() - new Date(a.levelStartTime).getTime()) / (1000 * 60 * 60))
|
|
: undefined,
|
|
comment: a.comments || undefined,
|
|
timestamp: a.actionDate || undefined,
|
|
levelStartTime: a.levelStartTime || a.tatStartTime,
|
|
tatAlerts: levelAlerts,
|
|
skipReason: a.skipReason || undefined,
|
|
isSkipped: levelStatus === 'SKIPPED' || a.isSkipped || false,
|
|
};
|
|
});
|
|
|
|
/**
|
|
* Transform: Map spectators from participants array
|
|
* Spectators have view-only access to the request
|
|
*/
|
|
const spectators = participants
|
|
.filter((p: any) => (p.participantType || p.participant_type || '').toUpperCase() === 'SPECTATOR')
|
|
.map((p: any) => ({
|
|
name: p.userName || p.user_name || p.userEmail || p.user_email,
|
|
role: 'Spectator',
|
|
email: p.userEmail || p.user_email,
|
|
avatar: toInitials(p.userName || p.user_name, p.userEmail || p.user_email),
|
|
}));
|
|
|
|
/**
|
|
* Helper: Get participant name by userId
|
|
* Used for document upload attribution
|
|
*/
|
|
const participantNameById = (uid?: string) => {
|
|
if (!uid) return undefined;
|
|
const p = participants.find((x: any) => x.userId === uid || x.user_id === uid);
|
|
if (p?.userName || p?.user_name) return p.userName || p.user_name;
|
|
if (wf.initiatorId === uid) return wf.initiator?.displayName || wf.initiator?.email;
|
|
return uid;
|
|
};
|
|
|
|
/**
|
|
* Transform: Map documents with file size conversion and uploader details
|
|
* Converts bytes to MB for better readability
|
|
*/
|
|
const mappedDocuments = documents.map((d: any) => {
|
|
const sizeBytes = Number(d.fileSize || d.file_size || 0);
|
|
const sizeMb = (sizeBytes / (1024 * 1024)).toFixed(2) + ' MB';
|
|
return {
|
|
documentId: d.documentId || d.document_id,
|
|
name: d.originalFileName || d.fileName || d.file_name,
|
|
fileType: d.fileType || d.file_type || '',
|
|
size: sizeMb,
|
|
sizeBytes: sizeBytes,
|
|
uploadedBy: participantNameById(d.uploadedBy || d.uploaded_by),
|
|
uploadedAt: d.uploadedAt || d.uploaded_at,
|
|
};
|
|
});
|
|
|
|
/**
|
|
* Filter: Remove TAT breach activities from audit trail
|
|
* TAT warnings are already shown in approval steps, no need to duplicate in timeline
|
|
*/
|
|
const filteredActivities = Array.isArray(details.activities)
|
|
? details.activities.filter((activity: any) => {
|
|
const activityType = (activity.type || '').toLowerCase();
|
|
return activityType !== 'sla_warning';
|
|
})
|
|
: [];
|
|
|
|
/**
|
|
* Build: Complete request object with all transformed data
|
|
* This object is used throughout the UI
|
|
*/
|
|
const updatedRequest = {
|
|
...wf,
|
|
id: wf.requestNumber || wf.requestId,
|
|
requestId: wf.requestId, // UUID for API calls
|
|
requestNumber: wf.requestNumber, // Human-readable number for display
|
|
title: wf.title,
|
|
description: wf.description,
|
|
status: statusMap(wf.status),
|
|
priority: (wf.priority || '').toString().toLowerCase(),
|
|
approvalFlow,
|
|
approvals, // Raw approvals for SLA calculations
|
|
participants,
|
|
documents: mappedDocuments,
|
|
spectators,
|
|
summary, // Backend-provided SLA summary
|
|
initiator: {
|
|
name: wf.initiator?.displayName || wf.initiator?.email,
|
|
role: wf.initiator?.designation || undefined,
|
|
department: wf.initiator?.department || undefined,
|
|
email: wf.initiator?.email || undefined,
|
|
phone: wf.initiator?.phone || undefined,
|
|
avatar: toInitials(wf.initiator?.displayName, wf.initiator?.email),
|
|
},
|
|
createdAt: wf.createdAt,
|
|
updatedAt: wf.updatedAt,
|
|
totalSteps: wf.totalLevels,
|
|
currentStep: summary?.currentLevel || wf.currentLevel,
|
|
auditTrail: filteredActivities,
|
|
conclusionRemark: wf.conclusionRemark || null,
|
|
closureDate: wf.closureDate || null,
|
|
};
|
|
|
|
setApiRequest(updatedRequest);
|
|
|
|
/**
|
|
* Determine: Find the approval level assigned to current user
|
|
* Used to show approve/reject buttons only when user has pending approval
|
|
*/
|
|
const userEmail = (user as any)?.email?.toLowerCase();
|
|
const newCurrentLevel = approvals.find((a: any) => {
|
|
const st = (a.status || '').toString().toUpperCase();
|
|
const approverEmail = (a.approverEmail || '').toLowerCase();
|
|
return (st === 'PENDING' || st === 'IN_PROGRESS') && approverEmail === userEmail;
|
|
});
|
|
setCurrentApprovalLevel(newCurrentLevel || null);
|
|
|
|
/**
|
|
* Determine: Check if current user is a spectator
|
|
* Spectators can only view and comment, cannot approve/reject
|
|
*/
|
|
const viewerId = (user as any)?.userId;
|
|
if (viewerId) {
|
|
const isSpec = participants.some((p: any) =>
|
|
(p.participantType || p.participant_type || '').toUpperCase() === 'SPECTATOR' &&
|
|
(p.userId || p.user_id) === viewerId
|
|
);
|
|
setIsSpectator(isSpec);
|
|
} else {
|
|
setIsSpectator(false);
|
|
}
|
|
} catch (error) {
|
|
console.error('[useRequestDetails] Error refreshing details:', error);
|
|
alert('Failed to refresh request details. Please try again.');
|
|
} finally {
|
|
setRefreshing(false);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Effect: Initial data fetch when component mounts or requestIdentifier changes
|
|
* This is the primary data loading mechanism
|
|
*/
|
|
useEffect(() => {
|
|
if (!requestIdentifier) return;
|
|
|
|
let mounted = true;
|
|
|
|
(async () => {
|
|
try {
|
|
const details = await workflowApi.getWorkflowDetails(requestIdentifier);
|
|
if (!mounted || !details) return;
|
|
|
|
// Use the same transformation logic as refreshDetails
|
|
const wf = details.workflow || {};
|
|
const approvals = Array.isArray(details.approvals) ? details.approvals : [];
|
|
const participants = Array.isArray(details.participants) ? details.participants : [];
|
|
const documents = Array.isArray(details.documents) ? details.documents : [];
|
|
const summary = details.summary || {};
|
|
const tatAlerts = Array.isArray(details.tatAlerts) ? details.tatAlerts : [];
|
|
|
|
console.log('[useRequestDetails] TAT Alerts received:', tatAlerts.length, tatAlerts);
|
|
|
|
const priority = (wf.priority || '').toString().toLowerCase();
|
|
const currentLevel = summary?.currentLevel || wf.currentLevel || 1;
|
|
|
|
// Transform approval flow (same logic as refreshDetails)
|
|
const approvalFlow = approvals.map((a: any) => {
|
|
const levelNumber = a.levelNumber || 0;
|
|
const levelStatus = (a.status || '').toString().toUpperCase();
|
|
const levelId = a.levelId || a.level_id;
|
|
|
|
let displayStatus = statusMap(a.status);
|
|
|
|
if (levelNumber > currentLevel && levelStatus !== 'APPROVED' && levelStatus !== 'REJECTED') {
|
|
displayStatus = 'waiting';
|
|
} else if (levelNumber === currentLevel && levelStatus === 'PENDING') {
|
|
displayStatus = 'pending';
|
|
}
|
|
|
|
const levelAlerts = tatAlerts.filter((alert: any) => alert.levelId === levelId);
|
|
|
|
return {
|
|
step: levelNumber,
|
|
levelId,
|
|
role: a.levelName || a.approverName || 'Approver',
|
|
status: displayStatus,
|
|
approver: a.approverName || a.approverEmail,
|
|
approverId: a.approverId || a.approver_id,
|
|
approverEmail: a.approverEmail,
|
|
tatHours: Number(a.tatHours || 0),
|
|
elapsedHours: Number(a.elapsedHours || 0),
|
|
remainingHours: Number(a.remainingHours || 0),
|
|
tatPercentageUsed: Number(a.tatPercentageUsed || 0),
|
|
actualHours: a.levelEndTime && a.levelStartTime
|
|
? Math.max(0, (new Date(a.levelEndTime).getTime() - new Date(a.levelStartTime).getTime()) / (1000 * 60 * 60))
|
|
: undefined,
|
|
comment: a.comments || undefined,
|
|
timestamp: a.actionDate || undefined,
|
|
levelStartTime: a.levelStartTime || a.tatStartTime,
|
|
tatAlerts: levelAlerts,
|
|
};
|
|
});
|
|
|
|
// Map spectators
|
|
const spectators = participants
|
|
.filter((p: any) => (p.participantType || '').toUpperCase() === 'SPECTATOR')
|
|
.map((p: any) => ({
|
|
name: p.userName || p.userEmail,
|
|
role: 'Spectator',
|
|
avatar: toInitials(p.userName, p.userEmail),
|
|
}));
|
|
|
|
// Helper to get participant name by ID
|
|
const participantNameById = (uid?: string) => {
|
|
if (!uid) return undefined;
|
|
const p = participants.find((x: any) => x.userId === uid);
|
|
if (p?.userName) return p.userName;
|
|
if (wf.initiatorId === uid) return wf.initiator?.displayName || wf.initiator?.email;
|
|
return uid;
|
|
};
|
|
|
|
// Map documents with size conversion
|
|
const mappedDocuments = documents.map((d: any) => {
|
|
const sizeBytes = Number(d.fileSize || 0);
|
|
const sizeMb = (sizeBytes / (1024 * 1024)).toFixed(2) + ' MB';
|
|
return {
|
|
documentId: d.documentId || d.document_id,
|
|
name: d.originalFileName || d.fileName,
|
|
fileType: d.fileType || d.file_type || '',
|
|
size: sizeMb,
|
|
sizeBytes: sizeBytes,
|
|
uploadedBy: participantNameById(d.uploadedBy),
|
|
uploadedAt: d.uploadedAt,
|
|
};
|
|
});
|
|
|
|
// Filter out TAT warnings from activities
|
|
const filteredActivities = Array.isArray(details.activities)
|
|
? details.activities.filter((activity: any) => {
|
|
const activityType = (activity.type || '').toLowerCase();
|
|
return activityType !== 'sla_warning';
|
|
})
|
|
: [];
|
|
|
|
// Build complete request object
|
|
const mapped = {
|
|
id: wf.requestNumber || wf.requestId,
|
|
requestId: wf.requestId,
|
|
title: wf.title,
|
|
description: wf.description,
|
|
priority,
|
|
status: statusMap(wf.status),
|
|
summary,
|
|
initiator: {
|
|
name: wf.initiator?.displayName || wf.initiator?.email,
|
|
role: wf.initiator?.designation || undefined,
|
|
department: wf.initiator?.department || undefined,
|
|
email: wf.initiator?.email || undefined,
|
|
phone: wf.initiator?.phone || undefined,
|
|
avatar: toInitials(wf.initiator?.displayName, wf.initiator?.email),
|
|
},
|
|
createdAt: wf.createdAt,
|
|
updatedAt: wf.updatedAt,
|
|
totalSteps: wf.totalLevels,
|
|
currentStep: summary?.currentLevel || wf.currentLevel,
|
|
approvalFlow,
|
|
approvals,
|
|
documents: mappedDocuments,
|
|
spectators,
|
|
auditTrail: filteredActivities,
|
|
conclusionRemark: wf.conclusionRemark || null,
|
|
closureDate: wf.closureDate || null,
|
|
};
|
|
|
|
setApiRequest(mapped);
|
|
|
|
// Find current user's approval level
|
|
const userEmail = (user as any)?.email?.toLowerCase();
|
|
const userCurrentLevel = approvals.find((a: any) => {
|
|
const status = (a.status || '').toString().toUpperCase();
|
|
const approverEmail = (a.approverEmail || '').toLowerCase();
|
|
return (status === 'PENDING' || status === 'IN_PROGRESS') && approverEmail === userEmail;
|
|
});
|
|
setCurrentApprovalLevel(userCurrentLevel || null);
|
|
|
|
// Check spectator status
|
|
const viewerId = (user as any)?.userId;
|
|
if (viewerId) {
|
|
const isSpec = participants.some((p: any) =>
|
|
(p.participantType || '').toUpperCase() === 'SPECTATOR' && p.userId === viewerId
|
|
);
|
|
setIsSpectator(isSpec);
|
|
} else {
|
|
setIsSpectator(false);
|
|
}
|
|
} catch (error) {
|
|
console.error('[useRequestDetails] Error loading request details:', error);
|
|
if (mounted) {
|
|
setApiRequest(null);
|
|
}
|
|
}
|
|
})();
|
|
|
|
return () => { mounted = false; };
|
|
}, [requestIdentifier, user]);
|
|
|
|
/**
|
|
* Computed: Get final request object with fallback to static databases
|
|
* Priority: API data → Custom DB → Claim DB → Dynamic props → null
|
|
*/
|
|
const request = useMemo(() => {
|
|
// Primary source: API data
|
|
if (apiRequest) return apiRequest;
|
|
|
|
// Fallback 1: Static custom request database
|
|
const customRequest = CUSTOM_REQUEST_DATABASE[requestIdentifier];
|
|
if (customRequest) return customRequest;
|
|
|
|
// Fallback 2: Static claim management database
|
|
const claimRequest = CLAIM_MANAGEMENT_DATABASE[requestIdentifier];
|
|
if (claimRequest) return claimRequest;
|
|
|
|
// Fallback 3: Dynamic requests passed as props
|
|
const dynamicRequest = dynamicRequests.find((req: any) =>
|
|
req.id === requestIdentifier ||
|
|
req.requestNumber === requestIdentifier ||
|
|
req.request_number === requestIdentifier
|
|
);
|
|
if (dynamicRequest) return dynamicRequest;
|
|
|
|
return null;
|
|
}, [requestIdentifier, dynamicRequests, apiRequest]);
|
|
|
|
/**
|
|
* Computed: Check if current user is the request initiator
|
|
* Initiators have special permissions (add approvers, skip approvers, close request)
|
|
*/
|
|
const isInitiator = useMemo(() => {
|
|
if (!request || !user) return false;
|
|
const userEmail = (user as any)?.email?.toLowerCase();
|
|
const initiatorEmail = request.initiator?.email?.toLowerCase();
|
|
return userEmail === initiatorEmail;
|
|
}, [request, user]);
|
|
|
|
/**
|
|
* Computed: Get all existing participants for validation
|
|
* Used when adding new approvers/spectators to prevent duplicates
|
|
*/
|
|
const existingParticipants = useMemo(() => {
|
|
if (!request) return [];
|
|
|
|
const participants: Array<{ email: string; participantType: string; name?: string }> = [];
|
|
|
|
// Add initiator
|
|
if (request.initiator?.email) {
|
|
participants.push({
|
|
email: request.initiator.email.toLowerCase(),
|
|
participantType: 'INITIATOR',
|
|
name: request.initiator.name
|
|
});
|
|
}
|
|
|
|
// Add approvers from approval flow
|
|
if (request.approvalFlow && Array.isArray(request.approvalFlow)) {
|
|
request.approvalFlow.forEach((approval: any) => {
|
|
if (approval.approverEmail) {
|
|
participants.push({
|
|
email: approval.approverEmail.toLowerCase(),
|
|
participantType: 'APPROVER',
|
|
name: approval.approver
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add spectators
|
|
if (request.spectators && Array.isArray(request.spectators)) {
|
|
request.spectators.forEach((spectator: any) => {
|
|
if (spectator.email) {
|
|
participants.push({
|
|
email: spectator.email.toLowerCase(),
|
|
participantType: 'SPECTATOR',
|
|
name: spectator.name
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add from participants array
|
|
if (request.participants && Array.isArray(request.participants)) {
|
|
request.participants.forEach((p: any) => {
|
|
const email = (p.userEmail || p.email || '').toLowerCase();
|
|
const participantType = (p.participantType || p.participant_type || '').toUpperCase();
|
|
const name = p.userName || p.user_name || p.name;
|
|
|
|
if (email && participantType && !participants.find(x => x.email === email)) {
|
|
participants.push({ email, participantType, name });
|
|
}
|
|
});
|
|
}
|
|
|
|
return participants;
|
|
}, [request]);
|
|
|
|
return {
|
|
request,
|
|
apiRequest,
|
|
refreshing,
|
|
refreshDetails,
|
|
currentApprovalLevel,
|
|
isSpectator,
|
|
isInitiator,
|
|
existingParticipants
|
|
};
|
|
}
|
|
|