-
-
-
-
-
-
-
-
-
Work Notes
-
-
{requestInfo.title}
-
- {requestId}
-
-
+
+
+
+
+
Work Notes
+
+
{requestInfo.title}
+
+ {requestId}
+
@@ -1036,29 +1063,6 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on
{/* Main Chat Area */}
-
- {/* Tab Navigation - Fixed */}
-
-
-
-
- Messages
- Chat
-
-
-
- Files
-
-
-
- Activity
- Act
-
-
-
-
- {/* Chat Tab */}
-
{/* Search Bar - Fixed */}
@@ -1099,7 +1103,7 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on
{msg.content}
-
{msg.timestamp}
+
{formatDateTime(msg.timestamp)}
) : (
@@ -1400,129 +1404,6 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on
-
-
- {/* Files Tab */}
-
-
-
-
Shared Files
-
-
- Upload File
- Upload
-
-
-
- {sharedFiles.length === 0 ? (
-
-
-
No files shared yet
-
Files shared in chat will appear here
-
- ) : (
-
- {sharedFiles.map((file, index) => {
- const fileType = (file.type || '').toLowerCase();
- const displayType = fileType.includes('pdf') ? 'PDF' :
- fileType.includes('excel') || fileType.includes('spreadsheet') ? 'Excel' :
- fileType.includes('word') || fileType.includes('document') ? 'Word' :
- fileType.includes('powerpoint') || fileType.includes('presentation') ? 'PowerPoint' :
- fileType.includes('image') || fileType.includes('jpg') || fileType.includes('png') ? 'Image' :
- 'File';
-
- return (
-
-
-
-
-
-
-
-
- {file.name}
-
-
- {file.size ? formatFileSize(file.size) : 'Unknown size'} β’ {displayType}
-
-
- by {file.uploadedBy} β’ {formatDateTime(file.uploadedAt)}
-
-
-
-
- {
- if (file.attachmentId) {
- const previewUrl = getWorkNoteAttachmentPreviewUrl(file.attachmentId);
- setPreviewFile({
- fileName: file.name,
- fileType: file.type,
- fileUrl: previewUrl,
- fileSize: file.size,
- attachmentId: file.attachmentId
- });
- }
- }}
- disabled={!file.attachmentId}
- >
-
- View
-
- {
- if (!file.attachmentId) {
- alert('Cannot download: Attachment ID missing');
- return;
- }
- try {
- await downloadWorkNoteAttachment(file.attachmentId);
- } catch (error) {
- console.error('Download failed:', error);
- alert('Failed to download file');
- }
- }}
- disabled={!file.attachmentId}
- >
-
- Download
-
-
-
-
- );
- })}
-
- )}
-
-
-
- {/* Activity Tab */}
-
-
-
Recent Activity
-
- {messages.filter(msg => msg.isSystem).map((msg) => (
-
-
-
-
{msg.content}
-
{msg.timestamp}
-
-
- ))}
-
-
-
-
{/* Mobile Sidebar Overlay */}
diff --git a/src/hooks/useSLATracking.ts b/src/hooks/useSLATracking.ts
new file mode 100644
index 0000000..d255486
--- /dev/null
+++ b/src/hooks/useSLATracking.ts
@@ -0,0 +1,46 @@
+import { useState, useEffect } from 'react';
+import { getSLAStatus, SLAStatus } from '@/utils/slaTracker';
+
+/**
+ * Custom hook for real-time SLA tracking with working hours
+ * Automatically updates every minute and pauses during non-working hours
+ *
+ * @param startDate - When the SLA tracking started
+ * @param deadline - When the SLA should complete
+ * @param enabled - Whether tracking is enabled (default: true)
+ * @returns SLAStatus object with real-time updates
+ */
+export function useSLATracking(
+ startDate: string | Date | null | undefined,
+ deadline: string | Date | null | undefined,
+ enabled: boolean = true
+): SLAStatus | null {
+ const [slaStatus, setSlaStatus] = useState
(null);
+
+ useEffect(() => {
+ if (!enabled || !startDate || !deadline) {
+ setSlaStatus(null);
+ return;
+ }
+
+ // Initial calculation
+ const updateStatus = () => {
+ try {
+ const status = getSLAStatus(startDate, deadline);
+ setSlaStatus(status);
+ } catch (error) {
+ console.error('[useSLATracking] Error calculating SLA status:', error);
+ }
+ };
+
+ updateStatus();
+
+ // Update every minute
+ const interval = setInterval(updateStatus, 60000); // 60 seconds
+
+ return () => clearInterval(interval);
+ }, [startDate, deadline, enabled]);
+
+ return slaStatus;
+}
+
diff --git a/src/pages/MyRequests/MyRequests.tsx b/src/pages/MyRequests/MyRequests.tsx
index 5a1fd53..e8714fe 100644
--- a/src/pages/MyRequests/MyRequests.tsx
+++ b/src/pages/MyRequests/MyRequests.tsx
@@ -20,7 +20,7 @@ import {
} from 'lucide-react';
import { motion } from 'framer-motion';
import workflowApi from '@/services/workflowApi';
-import { formatDateShort } from '@/utils/dateFormatter';
+import { SLATracker } from '@/components/sla/SLATracker';
interface MyRequestsProps {
onViewRequest: (requestId: string, requestTitle?: string) => void;
@@ -359,7 +359,7 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
>
onViewRequest(request.id, request.title, request.status)}
+ onClick={() => onViewRequest(request.id, request.title)}
>
@@ -420,12 +420,18 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
-
-
- Estimated completion: {request.dueDate ? formatDateShort(request.dueDate) : 'Not set'}
-
-
+
+ {/* SLA Tracker with Working Hours */}
+ {request.createdAt && request.dueDate && request.status !== 'approved' && request.status !== 'rejected' && (
+
+
+
+ )}
diff --git a/src/pages/RequestDetail/RequestDetail.tsx b/src/pages/RequestDetail/RequestDetail.tsx
index 0025125..666845a 100644
--- a/src/pages/RequestDetail/RequestDetail.tsx
+++ b/src/pages/RequestDetail/RequestDetail.tsx
@@ -10,7 +10,7 @@ import { formatDateTime, formatDateShort } from '@/utils/dateFormatter';
import { FilePreview } from '@/components/common/FilePreview';
import { CUSTOM_REQUEST_DATABASE } from '@/utils/customRequestDatabase';
import { CLAIM_MANAGEMENT_DATABASE } from '@/utils/claimManagementDatabase';
-import workflowApi, { approveLevel, rejectLevel, addApprover, addSpectator, downloadDocument, getDocumentPreviewUrl } from '@/services/workflowApi';
+import workflowApi, { approveLevel, rejectLevel, addApprover, addApproverAtLevel, skipApprover, addSpectator, downloadDocument, getDocumentPreviewUrl } from '@/services/workflowApi';
import { uploadDocument } from '@/services/documentApi';
import { ApprovalModal } from '@/components/approval/ApprovalModal/ApprovalModal';
import { RejectionModal } from '@/components/approval/RejectionModal/RejectionModal';
@@ -19,6 +19,8 @@ import { AddSpectatorModal } from '@/components/participant/AddSpectatorModal';
import { WorkNoteChat } from '@/components/workNote/WorkNoteChat/WorkNoteChat';
import { useAuth } from '@/contexts/AuthContext';
import { getSocket, joinRequestRoom, leaveRequestRoom } from '@/utils/socket';
+import { getWorkNotes } from '@/services/workflowApi';
+import { Component, ErrorInfo, ReactNode } from 'react';
import {
ArrowLeft,
Clock,
@@ -38,9 +40,52 @@ import {
UserPlus,
ClipboardList,
Paperclip,
- AlertTriangle
+ AlertTriangle,
+ AlertCircle
} from 'lucide-react';
+// Simple Error Boundary for RequestDetail
+class RequestDetailErrorBoundary extends Component<
+ { children: ReactNode },
+ { hasError: boolean; error: Error | null }
+> {
+ constructor(props: { children: ReactNode }) {
+ super(props);
+ this.state = { hasError: false, error: null };
+ }
+
+ static getDerivedStateFromError(error: Error) {
+ return { hasError: true, error };
+ }
+
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
+ console.error('RequestDetail Error:', error, errorInfo);
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return (
+
+
+
+
Error Loading Request
+
+ {this.state.error?.message || 'An unexpected error occurred'}
+
+
window.location.reload()} className="mr-2">
+ Reload Page
+
+
window.history.back()}>
+ Go Back
+
+
+
+ );
+ }
+ return this.props.children;
+ }
+}
+
interface RequestDetailProps {
requestId: string;
onBack?: () => void;
@@ -165,7 +210,7 @@ const getActionTypeIcon = (type: string) => {
}
};
-export function RequestDetail({
+function RequestDetailInner({
requestId: propRequestId,
onBack,
dynamicRequests = []
@@ -186,6 +231,8 @@ export function RequestDetail({
const [previewDocument, setPreviewDocument] = useState<{ fileName: string; fileType: string; documentId: string; fileSize?: number } | null>(null);
const [uploadingDocument, setUploadingDocument] = useState(false);
const [unreadWorkNotes, setUnreadWorkNotes] = useState(0);
+ const [mergedMessages, setMergedMessages] = useState
([]);
+ const [workNoteAttachments, setWorkNoteAttachments] = useState([]);
const fileInputRef = useState(null)[0];
const { user } = useAuth();
@@ -227,14 +274,18 @@ export function RequestDetail({
// Shared refresh routine
const refreshDetails = async () => {
- const details = await workflowApi.getWorkflowDetails(requestIdentifier);
- if (!details) return;
- 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 : [];
+ try {
+ const details = await workflowApi.getWorkflowDetails(requestIdentifier);
+ if (!details) {
+ console.warn('[RequestDetail] No details returned from API');
+ return;
+ }
+ 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 to console
if (tatAlerts.length > 0) {
@@ -392,6 +443,10 @@ export function RequestDetail({
} else {
setIsSpectator(false);
}
+ } catch (error) {
+ console.error('[RequestDetail] Error refreshing details:', error);
+ alert('Failed to refresh request details. Please try again.');
+ }
};
// Work notes load
@@ -422,19 +477,31 @@ export function RequestDetail({
(window as any)?.toast?.('Rejected successfully');
}
- // Add approver modal handler
- async function handleAddApprover(email: string) {
+ // Add approver modal handler (enhanced with level and TAT)
+ async function handleAddApprover(email: string, tatHours: number, level: number) {
try {
- await addApprover(requestIdentifier, email);
+ await addApproverAtLevel(requestIdentifier, email, tatHours, level);
await refreshDetails();
setShowAddApproverModal(false);
- alert('Approver added successfully');
+ alert(`Approver added successfully at Level ${level} with ${tatHours}h TAT`);
} catch (error: any) {
alert(error?.response?.data?.error || 'Failed to add approver');
throw error;
}
}
+ // Skip approver handler
+ async function handleSkipApprover(levelId: string, reason: string) {
+ try {
+ await skipApprover(requestIdentifier, levelId, reason);
+ await refreshDetails();
+ alert('Approver skipped successfully');
+ } catch (error: any) {
+ alert(error?.response?.data?.error || 'Failed to skip approver');
+ throw error;
+ }
+ }
+
// Add spectator modal handler
async function handleAddSpectator(email: string) {
try {
@@ -563,6 +630,33 @@ export function RequestDetail({
};
}, [requestIdentifier, user]);
+ // Fetch and merge work notes with activities
+ useEffect(() => {
+ if (!requestIdentifier || !apiRequest) return;
+
+ (async () => {
+ try {
+ const workNotes = await getWorkNotes(requestIdentifier);
+ const activities = apiRequest.auditTrail || [];
+
+ // Merge work notes and activities
+ const merged = [...workNotes, ...activities];
+
+ // Sort by timestamp
+ merged.sort((a, b) => {
+ const timeA = new Date(a.createdAt || a.created_at || a.timestamp || 0).getTime();
+ const timeB = new Date(b.createdAt || b.created_at || b.timestamp || 0).getTime();
+ return timeA - timeB;
+ });
+
+ setMergedMessages(merged);
+ console.log(`[RequestDetail] Merged ${workNotes.length} work notes with ${activities.length} activities`);
+ } catch (error) {
+ console.error('[RequestDetail] Failed to fetch and merge messages:', error);
+ }
+ })();
+ }, [requestIdentifier, apiRequest]);
+
// Separate effect to listen for new work notes and update badge
useEffect(() => {
if (!requestIdentifier) return;
@@ -579,6 +673,22 @@ export function RequestDetail({
if (activeTab !== 'worknotes') {
setUnreadWorkNotes(prev => prev + 1);
}
+
+ // Refresh merged messages when new work note arrives
+ (async () => {
+ try {
+ const workNotes = await getWorkNotes(requestIdentifier);
+ const activities = apiRequest?.auditTrail || [];
+ const merged = [...workNotes, ...activities].sort((a, b) => {
+ const timeA = new Date(a.createdAt || a.created_at || a.timestamp || 0).getTime();
+ const timeB = new Date(b.createdAt || b.created_at || b.timestamp || 0).getTime();
+ return timeA - timeB;
+ });
+ setMergedMessages(merged);
+ } catch (error) {
+ console.error('[RequestDetail] Failed to refresh messages:', error);
+ }
+ })();
};
socket.on('noteHandler', handleNewWorkNote);
@@ -589,7 +699,7 @@ export function RequestDetail({
socket.off('noteHandler', handleNewWorkNote);
socket.off('worknote:new', handleNewWorkNote);
};
- }, [requestIdentifier, activeTab]);
+ }, [requestIdentifier, activeTab, apiRequest]);
// Clear unread count when switching to work notes tab
useEffect(() => {
@@ -760,6 +870,12 @@ export function RequestDetail({
} else {
setIsSpectator(false);
}
+ } catch (error) {
+ console.error('[RequestDetail] Error loading request details:', error);
+ if (mounted) {
+ // Set a minimal request object to prevent complete failure
+ setApiRequest(null);
+ }
} finally {
}
@@ -846,6 +962,18 @@ export function RequestDetail({
return participants;
}, [request]);
+ // Loading state
+ if (!request && !apiRequest) {
+ return (
+
+
+
+
Loading request details...
+
+
+ );
+ }
+
if (!request) {
return (
@@ -861,9 +989,9 @@ export function RequestDetail({
);
}
- const priorityConfig = getPriorityConfig(request.priority);
- const statusConfig = getStatusConfig(request.status);
- const slaConfig = getSLAConfig(request.slaProgress);
+ const priorityConfig = getPriorityConfig(request.priority || 'standard');
+ const statusConfig = getStatusConfig(request.status || 'pending');
+ const slaConfig = getSLAConfig(request.slaProgress || 0);
return (
<>
@@ -890,7 +1018,7 @@ export function RequestDetail({
-
{request.id}
+
{request.id || 'N/A'}
{priorityConfig.label}
@@ -1317,6 +1445,35 @@ export function RequestDetail({
{isCompleted ? 'Approved' : isRejected ? 'Rejected' : 'Actioned'} on {formatDateTime(step.timestamp)}
)}
+
+ {/* Skip Approver Button - Only show for pending/in-review levels */}
+ {(isActive || step.status === 'pending') && !isCompleted && !isRejected && step.levelId && (
+
+
{
+ if (!step.levelId) {
+ alert('Level ID not available');
+ return;
+ }
+ const reason = prompt('Please provide a reason for skipping this approver:');
+ if (reason !== null && reason.trim()) {
+ handleSkipApprover(step.levelId, reason.trim()).catch(err => {
+ console.error('Skip approver failed:', err);
+ });
+ }
+ }}
+ >
+
+ Skip This Approver
+
+
+ Skip if approver is unavailable and move to next level
+
+
+ )}