Re_Figma_Code/src/hooks/useRequestDetails.ts

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
};
}