691 lines
27 KiB
TypeScript
691 lines
27 KiB
TypeScript
import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
import workflowApi from '@/services/workflowApi';
|
|
import { CUSTOM_REQUEST_DATABASE } from '@/utils/customRequestDatabase';
|
|
import { CLAIM_MANAGEMENT_DATABASE } from '@/utils/claimManagementDatabase';
|
|
import { getSocket } from '@/utils/socket';
|
|
|
|
/**
|
|
* 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: Loading state for initial fetch
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
// State: Access denied information
|
|
const [accessDenied, setAccessDenied] = useState<{ denied: boolean; message: string } | null>(null);
|
|
|
|
// 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
|
|
*
|
|
* Note: Wrapped in useCallback to allow use in Socket.io listeners
|
|
*/
|
|
const refreshDetails = useCallback(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) {
|
|
// TAT alerts loaded - logging removed
|
|
}
|
|
|
|
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';
|
|
})
|
|
: [];
|
|
|
|
/**
|
|
* Fetch: Get pause details if request is paused
|
|
* This is needed to show resume/retrigger buttons correctly
|
|
*/
|
|
let pauseInfo = null;
|
|
try {
|
|
const { getPauseDetails } = await import('@/services/workflowApi');
|
|
pauseInfo = await getPauseDetails(wf.requestId);
|
|
} catch (error) {
|
|
// Pause info not available or request not paused - ignore
|
|
console.debug('Pause details not available:', error);
|
|
}
|
|
|
|
/**
|
|
* 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 || 1,
|
|
// Store both raw and clamped values - raw for completion detection, clamped for display
|
|
currentStepRaw: summary?.currentLevel || wf.currentLevel || 1,
|
|
currentStep: Math.min(Math.max(1, summary?.currentLevel || wf.currentLevel || 1), wf.totalLevels || 1),
|
|
auditTrail: filteredActivities,
|
|
conclusionRemark: wf.conclusionRemark || null,
|
|
closureDate: wf.closureDate || null,
|
|
pauseInfo: pauseInfo || null, // Include pause info for resume/retrigger buttons
|
|
};
|
|
|
|
setApiRequest(updatedRequest);
|
|
|
|
/**
|
|
* Determine: Find the approval level assigned to current user
|
|
* Used to show approve/reject buttons only when user is the CURRENT active approver
|
|
* Conditions:
|
|
* 1. User email matches approverEmail
|
|
* 2. Status is PENDING or IN_PROGRESS
|
|
* 3. Approval level number matches the current active level in workflow
|
|
*/
|
|
const userEmail = (user as any)?.email?.toLowerCase();
|
|
const newCurrentLevel = approvals.find((a: any) => {
|
|
const st = (a.status || '').toString().toUpperCase();
|
|
const approverEmail = (a.approverEmail || '').toLowerCase();
|
|
const approvalLevelNumber = a.levelNumber || 0;
|
|
// Only show buttons if user is assigned to the CURRENT active level
|
|
// Include PAUSED status - paused level is still the current level
|
|
return (st === 'PENDING' || st === 'IN_PROGRESS' || st === 'PAUSED')
|
|
&& approverEmail === userEmail
|
|
&& approvalLevelNumber === currentLevel;
|
|
});
|
|
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);
|
|
}
|
|
}, [requestIdentifier, user]); // useCallback dependencies
|
|
|
|
/**
|
|
* Effect: Initial data fetch when component mounts or requestIdentifier changes
|
|
* This is the primary data loading mechanism
|
|
*/
|
|
useEffect(() => {
|
|
if (!requestIdentifier) {
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
let mounted = true;
|
|
setLoading(true);
|
|
setAccessDenied(null);
|
|
|
|
(async () => {
|
|
try {
|
|
const details = await workflowApi.getWorkflowDetails(requestIdentifier);
|
|
if (!mounted || !details) {
|
|
if (mounted) setLoading(false);
|
|
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 : [];
|
|
|
|
// TAT alerts received - logging removed
|
|
|
|
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 paused, show paused status (don't change it)
|
|
if (levelStatus === 'PAUSED') {
|
|
displayStatus = 'paused';
|
|
} else if (levelNumber > currentLevel && levelStatus !== 'APPROVED' && levelStatus !== 'REJECTED') {
|
|
displayStatus = 'waiting';
|
|
} else if (levelNumber === currentLevel && (levelStatus === 'PENDING' || levelStatus === 'IN_PROGRESS')) {
|
|
displayStatus = levelStatus === 'IN_PROGRESS' ? 'in-review' : '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),
|
|
// Use backend-calculated elapsedHours (working hours) for completed approvals
|
|
// Backend already calculates this correctly using calculateElapsedWorkingHours
|
|
actualHours: a.elapsedHours !== undefined && a.elapsedHours !== null
|
|
? Number(a.elapsedHours)
|
|
: 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';
|
|
})
|
|
: [];
|
|
|
|
// Fetch pause details
|
|
let pauseInfo = null;
|
|
try {
|
|
const { getPauseDetails } = await import('@/services/workflowApi');
|
|
pauseInfo = await getPauseDetails(wf.requestId);
|
|
} catch (error) {
|
|
// Pause info not available or request not paused - ignore
|
|
console.debug('Pause details not available:', error);
|
|
}
|
|
|
|
// 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 || 1,
|
|
// Store both raw and clamped values - raw for completion detection, clamped for display
|
|
currentStepRaw: summary?.currentLevel || wf.currentLevel || 1,
|
|
currentStep: Math.min(Math.max(1, summary?.currentLevel || wf.currentLevel || 1), wf.totalLevels || 1),
|
|
approvalFlow,
|
|
approvals,
|
|
documents: mappedDocuments,
|
|
spectators,
|
|
auditTrail: filteredActivities,
|
|
conclusionRemark: wf.conclusionRemark || null,
|
|
closureDate: wf.closureDate || null,
|
|
pauseInfo: pauseInfo || null,
|
|
};
|
|
|
|
setApiRequest(mapped);
|
|
|
|
// Find current user's approval level
|
|
// Only show approve/reject buttons if user is the CURRENT active approver
|
|
// Include PAUSED status - when paused, the paused level is still the current 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();
|
|
const approvalLevelNumber = a.levelNumber || 0;
|
|
// Only show buttons if user is assigned to the CURRENT active level
|
|
// Include PAUSED status - paused level is still the current level
|
|
return (status === 'PENDING' || status === 'IN_PROGRESS' || status === 'PAUSED')
|
|
&& approverEmail === userEmail
|
|
&& approvalLevelNumber === currentLevel;
|
|
});
|
|
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: any) {
|
|
console.error('[useRequestDetails] Error loading request details:', error);
|
|
if (mounted) {
|
|
// Check for 403 Forbidden (Access Denied)
|
|
if (error?.response?.status === 403) {
|
|
const message = error?.response?.data?.message ||
|
|
'You do not have permission to view this request. Access is restricted to the initiator, approvers, and spectators of this request.';
|
|
setAccessDenied({ denied: true, message });
|
|
}
|
|
setApiRequest(null);
|
|
}
|
|
} finally {
|
|
if (mounted) {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
})();
|
|
|
|
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]);
|
|
|
|
/**
|
|
* Effect: Listen for real-time request updates via Socket.io
|
|
*
|
|
* Purpose: Auto-refresh request details when other users take actions
|
|
*
|
|
* Listens for:
|
|
* - 'request:updated' - Any action that changes the request (approve, reject, pause, resume, skip, etc.)
|
|
*
|
|
* Behavior:
|
|
* - Silently refreshes data in background
|
|
* - Doesn't interrupt user actions
|
|
* - Updates all tabs with latest data
|
|
*/
|
|
useEffect(() => {
|
|
if (!requestIdentifier || !apiRequest) return;
|
|
|
|
const socket = getSocket();
|
|
if (!socket) {
|
|
console.warn('[useRequestDetails] Socket not available');
|
|
return;
|
|
}
|
|
|
|
console.log('[useRequestDetails] Setting up socket listener for request:', apiRequest.requestId);
|
|
|
|
/**
|
|
* Handler: Request updated by another user
|
|
* Silently refresh to show latest changes
|
|
*/
|
|
const handleRequestUpdated = (data: any) => {
|
|
console.log('[useRequestDetails] 📡 Received request:updated event:', data);
|
|
// Verify this update is for the current request
|
|
if (data?.requestId === apiRequest.requestId || data?.requestNumber === requestIdentifier) {
|
|
console.log('[useRequestDetails] 🔄 Request updated remotely, refreshing silently...');
|
|
// Silent refresh - no loading state, no user interruption
|
|
refreshDetails();
|
|
} else {
|
|
console.log('[useRequestDetails] ⚠️ Event for different request, ignoring');
|
|
}
|
|
};
|
|
|
|
// Register listener
|
|
socket.on('request:updated', handleRequestUpdated);
|
|
console.log('[useRequestDetails] ✅ Registered listener for request:updated');
|
|
|
|
// Cleanup on unmount
|
|
return () => {
|
|
console.log('[useRequestDetails] 🧹 Cleaning up socket listener');
|
|
socket.off('request:updated', handleRequestUpdated);
|
|
};
|
|
}, [requestIdentifier, apiRequest, refreshDetails]);
|
|
|
|
return {
|
|
request,
|
|
apiRequest,
|
|
loading,
|
|
refreshing,
|
|
refreshDetails,
|
|
currentApprovalLevel,
|
|
isSpectator,
|
|
isInitiator,
|
|
existingParticipants,
|
|
accessDenied
|
|
};
|
|
}
|
|
|