+
+
Activity Type Value Comparison
+
+
+
+
+ formatCurrency(value)} />
+ formatCurrency(value)}
+ labelFormatter={(label) => label}
+ />
+
+
+
+
+
+
+
+
+ {categoryData.slice(0, 3).map((cat, index) => (
+
+
+
+ {cat.activityType}
+
+ {cat.approvalRate.toFixed(1)}% approved
+
+
+
+
+
+
+ Raised:
+ {formatNumber(cat.raised)} ({formatCurrency(cat.raisedValue)})
-
-
-
-
- Raised:
- {formatNumber(cat.raised)} ({formatCurrency(cat.raisedValue)})
-
-
- Approved:
- {formatNumber(cat.approved)} ({formatCurrency(cat.approvedValue)})
-
-
- Rejected:
- {formatNumber(cat.rejected)} ({formatCurrency(cat.rejectedValue)})
-
-
- Pending:
- {formatNumber(cat.pending)} ({formatCurrency(cat.pendingValue)})
-
-
-
- Credited:
- {formatNumber(cat.credited)} ({formatCurrency(cat.creditedValue)})
-
-
- Pending Credit:
- {formatNumber(cat.pendingCredit)} ({formatCurrency(cat.pendingCreditValue)})
-
+
+ Approved:
+ {formatNumber(cat.approved)} ({formatCurrency(cat.approvedValue)})
-
-
- Credit Rate
- {cat.creditRate.toFixed(1)}%
-
-
+
+ Rejected:
+ {formatNumber(cat.rejected)} ({formatCurrency(cat.rejectedValue)})
-
-
- ))}
-
-
-
- {/* Category 1 details */}
- Detailed view for top category 1
-
-
- {/* Category 2 details */}
- Detailed view for top category 2
-
-
+
+ Pending:
+ {formatNumber(cat.pending)} ({formatCurrency(cat.pendingValue)})
+
+
+
+ Credited:
+ {formatNumber(cat.credited)} ({formatCurrency(cat.creditedValue)})
+
+
+ Pending Credit:
+ {formatNumber(cat.pendingCredit)} ({formatCurrency(cat.pendingCreditValue)})
+
+
+
+
+ Credit Rate
+ {cat.creditRate.toFixed(1)}%
+
+
+
+
+
+ ))}
+
+
diff --git a/src/dealer-claim/pages/RequestDetail.tsx b/src/dealer-claim/pages/RequestDetail.tsx
index 2c4eb65..678ca43 100644
--- a/src/dealer-claim/pages/RequestDetail.tsx
+++ b/src/dealer-claim/pages/RequestDetail.tsx
@@ -245,14 +245,7 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
const requestStatus = (request?.status || apiRequest?.status || '').toLowerCase();
const needsClosure = (requestStatus === 'approved' || requestStatus === 'rejected') && isInitiator;
- // Debug logging
- console.debug('[DealerClaimRequestDetail] Closure check:', {
- requestStatus,
- requestStatusRaw: request?.status,
- apiRequestStatusRaw: apiRequest?.status,
- isInitiator,
- needsClosure,
- });
+ // Closure check completed
const {
conclusionRemark,
setConclusionRemark,
diff --git a/src/hooks/useConclusionRemark.ts b/src/hooks/useConclusionRemark.ts
index 80e9080..0489ff3 100644
--- a/src/hooks/useConclusionRemark.ts
+++ b/src/hooks/useConclusionRemark.ts
@@ -50,26 +50,46 @@ export function useConclusionRemark(
* Use Case: When request is approved, final approver generates conclusion.
* Initiator needs to review and finalize it before closing request.
*
+ * Optimization: Check request object first before making API call
* Process:
- * 1. Dynamically import conclusion API service
- * 2. Fetch conclusion by request ID
+ * 1. Check if conclusion data is already in request object
+ * 2. If not available, fetch from API
* 3. Load into state if exists
* 4. Mark as AI-generated if applicable
*/
const fetchExistingConclusion = async () => {
+ // Optimization: Check if conclusion data is already in request object
+ // Request detail response includes conclusionRemark and aiGeneratedConclusion fields
+ const existingConclusion = request?.conclusionRemark || request?.conclusion_remark;
+ const existingAiConclusion = request?.aiGeneratedConclusion || request?.ai_generated_conclusion;
+
+ if (existingConclusion || existingAiConclusion) {
+ // Use data from request object - no API call needed
+ setConclusionRemark(existingConclusion || existingAiConclusion);
+ setAiGenerated(!!existingAiConclusion);
+ return;
+ }
+
+ // Only fetch from API if not available in request object
+ // This handles cases where request object might not have been refreshed yet
try {
// Lazy load: Import conclusion API only when needed
const { getConclusion } = await import('@/services/conclusionApi');
- // API Call: Fetch existing conclusion
+ // API Call: Fetch existing conclusion (returns null if not found)
const result = await getConclusion(request.requestId || requestIdentifier);
- if (result && result.aiGeneratedRemark) {
+ if (result && (result.aiGeneratedRemark || result.finalRemark)) {
// Load: Set the AI-generated or final remark
- setConclusionRemark(result.finalRemark || result.aiGeneratedRemark);
+ // Handle null values by providing empty string fallback
+ setConclusionRemark(result.finalRemark || result.aiGeneratedRemark || '');
setAiGenerated(!!result.aiGeneratedRemark);
}
} catch (err) {
+ // Only log non-404 errors (404 is handled gracefully in API)
+ if ((err as any)?.response?.status !== 404) {
+ console.error('[useConclusionRemark] Error fetching conclusion:', err);
+ }
// No conclusion yet - this is expected for newly approved requests
}
};
@@ -218,16 +238,36 @@ export function useConclusionRemark(
};
/**
- * Effect: Auto-fetch existing conclusion when request becomes approved or rejected
+ * Effect: Auto-load existing conclusion when request becomes approved, rejected, or closed
*
- * Trigger: When request status changes to "approved" or "rejected" and user is initiator
+ * Trigger: When request status changes to "approved", "rejected", or "closed" and user is initiator
* Purpose: Load any conclusion generated by final approver (for approved) or AI (for rejected)
+ *
+ * Optimization:
+ * 1. First check if conclusion data is already in request object (no API call needed)
+ * 2. Only fetch from API if not available in request object
*/
useEffect(() => {
- if ((request?.status === 'approved' || request?.status === 'rejected') && isInitiator && !conclusionRemark) {
+ const status = request?.status?.toLowerCase();
+ const shouldLoad = (status === 'approved' || status === 'rejected' || status === 'closed')
+ && isInitiator
+ && !conclusionRemark;
+
+ if (!shouldLoad) return;
+
+ // Check if conclusion data is already in request object
+ const existingConclusion = request?.conclusionRemark || request?.conclusion_remark;
+ const existingAiConclusion = request?.aiGeneratedConclusion || request?.ai_generated_conclusion;
+
+ if (existingConclusion || existingAiConclusion) {
+ // Use data from request object - no API call needed
+ setConclusionRemark(existingConclusion || existingAiConclusion);
+ setAiGenerated(!!existingAiConclusion);
+ } else {
+ // Only fetch from API if not available in request object
fetchExistingConclusion();
}
- }, [request?.status, isInitiator]);
+ }, [request?.status, request?.conclusionRemark, request?.aiGeneratedConclusion, isInitiator, conclusionRemark]);
return {
conclusionRemark,
diff --git a/src/hooks/useRequestDetails.ts b/src/hooks/useRequestDetails.ts
index 8b5d439..ee5aa7d 100644
--- a/src/hooks/useRequestDetails.ts
+++ b/src/hooks/useRequestDetails.ts
@@ -219,15 +219,18 @@ export function useRequestDetails(
: [];
/**
- * Fetch: Get pause details if request is paused
- * This is needed to show resume/retrigger buttons correctly
+ * Fetch: Get pause details only if request is actually paused
+ * Use request-level isPaused field from workflow response
*/
let pauseInfo = null;
- try {
- pauseInfo = await getPauseDetails(wf.requestId);
- } catch (error) {
- // Pause info not available or request not paused - ignore
- console.debug('Pause details not available:', error);
+ const isPaused = (wf as any).isPaused || false;
+
+ if (isPaused) {
+ try {
+ pauseInfo = await getPauseDetails(wf.requestId);
+ } catch (error) {
+ // Pause info not available - ignore
+ }
}
/**
@@ -240,24 +243,9 @@ export function useRequestDetails(
if (wf.workflowType === 'CLAIM_MANAGEMENT' || wf.templateType === 'claim-management') {
try {
- console.debug('[useRequestDetails] Fetching claim details for requestId:', wf.requestId);
const claimResponse = await apiClient.get(`/dealer-claims/${wf.requestId}`);
- console.debug('[useRequestDetails] Claim API response:', {
- status: claimResponse.status,
- hasData: !!claimResponse.data,
- dataKeys: claimResponse.data ? Object.keys(claimResponse.data) : [],
- fullResponse: claimResponse.data,
- });
const claimData = claimResponse.data?.data || claimResponse.data;
- console.debug('[useRequestDetails] Extracted claimData:', {
- hasClaimData: !!claimData,
- claimDataKeys: claimData ? Object.keys(claimData) : [],
- hasClaimDetails: !!(claimData?.claimDetails || claimData?.claim_details),
- hasProposalDetails: !!(claimData?.proposalDetails || claimData?.proposal_details),
- hasCompletionDetails: !!(claimData?.completionDetails || claimData?.completion_details),
- hasInternalOrder: !!(claimData?.internalOrder || claimData?.internal_order),
- });
if (claimData) {
claimDetails = claimData.claimDetails || claimData.claim_details;
@@ -278,24 +266,7 @@ export function useRequestDetails(
(claimDetails as any).completionExpenses = completionExpenses;
}
- console.debug('[useRequestDetails] Extracted details:', {
- claimDetails: claimDetails ? {
- hasActivityName: !!(claimDetails.activityName || claimDetails.activity_name),
- hasActivityType: !!(claimDetails.activityType || claimDetails.activity_type),
- hasLocation: !!(claimDetails.location),
- activityName: claimDetails.activityName || claimDetails.activity_name,
- activityType: claimDetails.activityType || claimDetails.activity_type,
- location: claimDetails.location,
- allKeys: Object.keys(claimDetails),
- } : null,
- hasProposalDetails: !!proposalDetails,
- hasCompletionDetails: !!completionDetails,
- hasInternalOrder: !!internalOrder,
- hasBudgetTracking: !!budgetTracking,
- hasInvoice: !!invoice,
- hasCreditNote: !!creditNote,
- hasCompletionExpenses: Array.isArray(completionExpenses) && completionExpenses.length > 0,
- });
+ // Extracted details processed
} else {
console.warn('[useRequestDetails] No claimData found in response');
}
@@ -528,13 +499,17 @@ export function useRequestDetails(
})
: [];
- // Fetch pause details
+ // Fetch pause details only if request is actually paused
+ // Use request-level isPaused field from workflow response
let pauseInfo = null;
- try {
- pauseInfo = await getPauseDetails(wf.requestId);
- } catch (error) {
- // Pause info not available or request not paused - ignore
- console.debug('Pause details not available:', error);
+ const isPaused = (wf as any).isPaused || false;
+
+ if (isPaused) {
+ try {
+ pauseInfo = await getPauseDetails(wf.requestId);
+ } catch (error) {
+ // Pause info not available - ignore
+ }
}
/**
@@ -547,13 +522,7 @@ export function useRequestDetails(
if (wf.workflowType === 'CLAIM_MANAGEMENT' || wf.templateType === 'claim-management') {
try {
- console.debug('[useRequestDetails] Initial load - Fetching claim details for requestId:', wf.requestId);
const claimResponse = await apiClient.get(`/dealer-claims/${wf.requestId}`);
- console.debug('[useRequestDetails] Initial load - Claim API response:', {
- status: claimResponse.status,
- hasData: !!claimResponse.data,
- dataKeys: claimResponse.data ? Object.keys(claimResponse.data) : [],
- });
const claimData = claimResponse.data?.data || claimResponse.data;
if (claimData) {
@@ -575,17 +544,7 @@ export function useRequestDetails(
(claimDetails as any).completionExpenses = completionExpenses;
}
- console.debug('[useRequestDetails] Initial load - Extracted details:', {
- hasClaimDetails: !!claimDetails,
- claimDetailsKeys: claimDetails ? Object.keys(claimDetails) : [],
- hasProposalDetails: !!proposalDetails,
- hasCompletionDetails: !!completionDetails,
- hasInternalOrder: !!internalOrder,
- hasBudgetTracking: !!budgetTracking,
- hasInvoice: !!invoice,
- hasCreditNote: !!creditNote,
- hasCompletionExpenses: Array.isArray(completionExpenses) && completionExpenses.length > 0,
- });
+ // Initial load - Extracted details processed
}
} catch (error: any) {
// Claim details not available - request might not be fully initialized yet
diff --git a/src/pages/ApproverPerformance/components/ApproverPerformanceRequestList.tsx b/src/pages/ApproverPerformance/components/ApproverPerformanceRequestList.tsx
index 0c66871..7e83a83 100644
--- a/src/pages/ApproverPerformance/components/ApproverPerformanceRequestList.tsx
+++ b/src/pages/ApproverPerformance/components/ApproverPerformanceRequestList.tsx
@@ -11,6 +11,7 @@ import { Pagination } from '@/components/common/Pagination';
import { getPriorityConfig, getStatusConfig, getSLAConfig } from '../utils/configMappers';
import { formatDate, formatDateTime } from '../utils/formatters';
import { formatHoursMinutes } from '@/utils/slaTracker';
+import { navigateToRequest } from '@/utils/requestNavigation';
import type { ApproverPerformanceRequest } from '../types/approverPerformance.types';
interface ApproverPerformanceRequestListProps {
@@ -69,7 +70,6 @@ export function ApproverPerformanceRequestList({
key={request.requestId}
className="hover:shadow-md transition-shadow cursor-pointer"
onClick={() => {
- const { navigateToRequest } = require('@/utils/requestNavigation');
navigateToRequest({
requestId: request.requestId,
requestTitle: request.title,
@@ -166,7 +166,6 @@ export function ApproverPerformanceRequestList({
size="sm"
onClick={(e) => {
e.stopPropagation();
- const { navigateToRequest } = require('@/utils/requestNavigation');
navigateToRequest({
requestId: request.requestId,
requestTitle: request.title,
diff --git a/src/pages/Auth/Auth.tsx b/src/pages/Auth/Auth.tsx
index 08f1498..6563bc5 100644
--- a/src/pages/Auth/Auth.tsx
+++ b/src/pages/Auth/Auth.tsx
@@ -1,14 +1,28 @@
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import { useAuth } from '@/contexts/AuthContext';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { LogIn, Shield } from 'lucide-react';
-import { ReLogo } from '@/assets';
+import { ReLogo, LandingPageImage } from '@/assets';
import { initiateTanflowLogin } from '@/services/tanflowAuth';
export function Auth() {
const { login, isLoading, error } = useAuth();
const [tanflowLoading, setTanflowLoading] = useState(false);
+ const [imageLoaded, setImageLoaded] = useState(false);
+
+ // Preload the background image
+ useEffect(() => {
+ const img = new Image();
+ img.src = LandingPageImage;
+ img.onload = () => {
+ setImageLoaded(true);
+ };
+ // If image is already cached, trigger load immediately
+ if (img.complete) {
+ setImageLoaded(true);
+ }
+ }, []);
const handleOKTALogin = async () => {
// Clear any existing session data
@@ -51,8 +65,24 @@ export function Auth() {
}
return (
-
-
+
+ {/* Fallback background while image loads */}
+ {!imageLoaded && (
+
+ )}
+ {/* Overlay for better readability */}
+
+
+
![Royal Enfield Logo]()
-
Approval Portal
+
Approval Portal
{error && (
-
+
Authentication Error
{error.message}
@@ -96,10 +126,10 @@ export function Auth() {
@@ -125,9 +155,9 @@ export function Auth() {
-
+
Secure Single Sign-On
-
Choose your authentication provider
+
Choose your authentication provider
diff --git a/src/pages/DetailedReports/DetailedReports.tsx b/src/pages/DetailedReports/DetailedReports.tsx
index 633a7cb..06d7424 100644
--- a/src/pages/DetailedReports/DetailedReports.tsx
+++ b/src/pages/DetailedReports/DetailedReports.tsx
@@ -1,5 +1,6 @@
import { useState, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
+import { navigateToRequest } from '@/utils/requestNavigation';
// Components
import { DetailedReportsHeader } from './components/DetailedReportsHeader';
@@ -69,7 +70,6 @@ export function DetailedReports({ onBack }: DetailedReportsProps) {
}, [onBack, navigate]);
const handleViewRequest = useCallback((requestId: string) => {
- const { navigateToRequest } = require('@/utils/requestNavigation');
navigateToRequest({
requestId,
navigate,
diff --git a/src/pages/Notifications/Notifications.tsx b/src/pages/Notifications/Notifications.tsx
index 8c95ad2..2ff02a7 100644
--- a/src/pages/Notifications/Notifications.tsx
+++ b/src/pages/Notifications/Notifications.tsx
@@ -42,11 +42,15 @@ export function Notifications({ onNavigate }: NotificationsProps) {
const result = await notificationApi.list({ page, limit: ITEMS_PER_PAGE, unreadOnly });
const notifs = result.data?.notifications || [];
- const total = result.data?.total || 0;
+ // Extract pagination data from the response
+ const pagination = result.data?.pagination || {};
+ const total = pagination.total || 0;
+ const totalPages = pagination.totalPages || 1;
setNotifications(notifs);
setTotalCount(total);
- setTotalPages(Math.ceil(total / ITEMS_PER_PAGE));
+ setTotalPages(totalPages);
+ setCurrentPage(page); // Update current page to match what was fetched
} catch (error) {
console.error('[Notifications] Failed to fetch:', error);
} finally {
@@ -56,6 +60,7 @@ export function Notifications({ onNavigate }: NotificationsProps) {
};
useEffect(() => {
+ setCurrentPage(1); // Reset to page 1 when filter changes
fetchNotifications(1, filter === 'unread');
}, [filter]);
@@ -82,6 +87,11 @@ export function Notifications({ onNavigate }: NotificationsProps) {
navigationUrl += '?tab=worknotes';
}
+ // Document added notifications should open Documents tab
+ if (notification.notificationType === 'document_added') {
+ navigationUrl += '?tab=documents';
+ }
+
onNavigate(navigationUrl);
}
}
@@ -131,6 +141,8 @@ export function Notifications({ onNavigate }: NotificationsProps) {
return
;
case 'worknote':
return
;
+ case 'document_added':
+ return
;
case 'assignment':
return
;
case 'approval':
diff --git a/src/services/conclusionApi.ts b/src/services/conclusionApi.ts
index 40e7e2a..0ae189b 100644
--- a/src/services/conclusionApi.ts
+++ b/src/services/conclusionApi.ts
@@ -57,9 +57,19 @@ export async function finalizeConclusion(requestId: string, finalRemark: string)
/**
* Get conclusion for a request
+ * Returns null if conclusion doesn't exist (404) instead of throwing error
*/
-export async function getConclusion(requestId: string): Promise
{
- const response = await apiClient.get(`/conclusions/${requestId}`);
- return response.data.data;
+export async function getConclusion(requestId: string): Promise {
+ try {
+ const response = await apiClient.get(`/conclusions/${requestId}`);
+ return response.data.data;
+ } catch (error: any) {
+ // Handle 404 gracefully - conclusion doesn't exist yet, which is normal
+ if (error.response?.status === 404) {
+ return null;
+ }
+ // Re-throw other errors
+ throw error;
+ }
}
diff --git a/src/utils/claimDataMapper.ts b/src/utils/claimDataMapper.ts
index 8d93578..392930b 100644
--- a/src/utils/claimDataMapper.ts
+++ b/src/utils/claimDataMapper.ts
@@ -115,18 +115,7 @@ export function mapToClaimManagementRequest(
const creditNote = apiRequest.creditNote || apiRequest.credit_note || {};
const completionExpenses = apiRequest.completionExpenses || apiRequest.completion_expenses || [];
- // Debug: Log raw claim details to help troubleshoot
- console.debug('[claimDataMapper] Raw claimDetails:', claimDetails);
- console.debug('[claimDataMapper] Raw apiRequest:', {
- hasClaimDetails: !!apiRequest.claimDetails,
- hasProposalDetails: !!apiRequest.proposalDetails,
- hasCompletionDetails: !!apiRequest.completionDetails,
- hasBudgetTracking: !!budgetTracking,
- hasInvoice: !!invoice,
- hasCreditNote: !!creditNote,
- hasCompletionExpenses: Array.isArray(completionExpenses) && completionExpenses.length > 0,
- workflowType: apiRequest.workflowType,
- });
+ // Raw claim details processed
// Map activity information (matching ActivityInformationCard expectations)
// Handle both camelCase and snake_case field names from Sequelize
@@ -137,14 +126,7 @@ export function mapToClaimManagementRequest(
const activityType = claimDetails.activityType || claimDetails.activity_type || '';
const location = claimDetails.location || '';
- console.debug('[claimDataMapper] Mapped activity fields:', {
- activityName,
- activityType,
- location,
- hasActivityName: !!activityName,
- hasActivityType: !!activityType,
- hasLocation: !!location,
- });
+ // Activity fields mapped
// Get budget values from budgetTracking table (new source of truth)
const estimatedBudget = budgetTracking.proposalEstimatedBudget ||