diff --git a/src/App.tsx b/src/App.tsx index 6fd5816..ff0ba94 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -28,6 +28,7 @@ import { CUSTOM_REQUEST_DATABASE } from '@/utils/customRequestDatabase'; import { AuthCallback } from '@/pages/Auth/AuthCallback'; import { createClaimRequest } from '@/services/dealerClaimApi'; import { ManagerSelectionModal } from '@/components/modals/ManagerSelectionModal'; +import { navigateToRequest } from '@/utils/requestNavigation'; import { TokenManager } from '@/utils/tokenManager'; interface AppProps { @@ -148,12 +149,11 @@ function AppRoutes({ onLogout }: AppProps) { } }; - const handleViewRequest = async (requestId: string, requestTitle?: string, status?: string, request?: any) => { + const handleViewRequest = (requestId: string, requestTitle?: string, status?: string, request?: any) => { setSelectedRequestId(requestId); setSelectedRequestTitle(requestTitle || 'Unknown Request'); // Use global navigation utility for consistent routing - const { navigateToRequest } = await import('@/utils/requestNavigation'); navigateToRequest({ requestId, requestTitle, diff --git a/src/assets/images/landing_page_image.jpg b/src/assets/images/landing_page_image.jpg new file mode 100644 index 0000000..2fcabe8 Binary files /dev/null and b/src/assets/images/landing_page_image.jpg differ diff --git a/src/assets/index.ts b/src/assets/index.ts index 487bec9..0d6c9f8 100644 --- a/src/assets/index.ts +++ b/src/assets/index.ts @@ -8,6 +8,7 @@ // Images export { default as ReLogo } from './images/Re_Logo.png'; export { default as RoyalEnfieldLogo } from './images/royal_enfield_logo.png'; +export { default as LandingPageImage } from './images/landing_page_image.jpg'; // Fonts // Add font exports here when fonts are added to the assets/fonts folder diff --git a/src/dealer-claim/components/request-detail/IOTab.tsx b/src/dealer-claim/components/request-detail/IOTab.tsx index a476e00..9c4317e 100644 --- a/src/dealer-claim/components/request-detail/IOTab.tsx +++ b/src/dealer-claim/components/request-detail/IOTab.tsx @@ -91,13 +91,7 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) { // Formula: remaining = availableBeforeBlock - blockedAmount const expectedRemaining = availableBeforeBlock - blockedAmt; - // Log for debugging backend calculation - console.log('[IOTab] Loading existing IO block:', { - availableBeforeBlock, - blockedAmount: blockedAmt, - expectedRemaining, - backendRemaining, - }); + // Loading existing IO block // Warn if remaining balance calculation seems incorrect (for backend debugging) if (Math.abs(backendRemaining - expectedRemaining) > 0.01) { @@ -249,15 +243,7 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) { return; } - // Log the amount being sent to backend for debugging - console.log('[IOTab] Blocking budget:', { - ioNumber: ioNumber.trim(), - originalInput: amountToBlock, - parsedAmount: blockAmountRaw, - roundedAmount: blockAmount, - fetchedAmount, - calculatedRemaining: fetchedAmount - blockAmount, - }); + // Blocking budget setBlockingBudget(true); try { @@ -272,7 +258,7 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) { ioRemainingBalance: fetchedAmount - blockAmount, // Calculated value (backend will use SAP's actual value) }; - console.log('[IOTab] Sending to backend:', payload); + // Sending to backend await updateIODetails(requestId, payload); @@ -287,16 +273,7 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) { // Calculate expected remaining balance for validation/debugging const expectedRemainingBalance = fetchedAmount - savedBlockedAmount; - // Log what was saved vs what we sent - console.log('[IOTab] Blocking result:', { - sentAmount: blockAmount, - savedBlockedAmount, - availableBalance: fetchedAmount, - expectedRemaining: expectedRemainingBalance, - backendRemaining: savedRemainingBalance, - difference: savedBlockedAmount - blockAmount, - remainingDifference: fetchedAmount - savedRemainingBalance, - }); + // Blocking result processed // Warn if the saved amount differs from what we sent if (Math.abs(savedBlockedAmount - blockAmount) > 0.01) { diff --git a/src/dealer-claim/components/request-detail/OverviewTab.tsx b/src/dealer-claim/components/request-detail/OverviewTab.tsx index e355b7d..956d1b2 100644 --- a/src/dealer-claim/components/request-detail/OverviewTab.tsx +++ b/src/dealer-claim/components/request-detail/OverviewTab.tsx @@ -86,16 +86,7 @@ export function ClaimManagementOverviewTab({ ); } - // Debug: Log mapped data for troubleshooting - console.debug('[ClaimManagementOverviewTab] Mapped claim data:', { - activityInfo: claimRequest.activityInfo, - dealerInfo: claimRequest.dealerInfo, - hasProposalDetails: !!claimRequest.proposalDetails, - closedExpenses: claimRequest.activityInfo?.closedExpenses, - closedExpensesBreakdown: claimRequest.activityInfo?.closedExpensesBreakdown, - hasDealerCode: !!claimRequest.dealerInfo?.dealerCode, - hasDealerName: !!claimRequest.dealerInfo?.dealerName, - }); + // Mapped claim data ready // Determine user's role const userRole: RequestRole = determineUserRole(apiRequest, currentUserId); @@ -103,13 +94,7 @@ export function ClaimManagementOverviewTab({ // Get visibility settings based on role const visibility = getRoleBasedVisibility(userRole); - console.debug('[ClaimManagementOverviewTab] User role and visibility:', { - userRole, - visibility, - currentUserId, - showDealerInfo: visibility.showDealerInfo, - dealerInfoPresent: !!(claimRequest.dealerInfo?.dealerCode || claimRequest.dealerInfo?.dealerName), - }); + // User role and visibility determined // Extract initiator info from request // The apiRequest has initiator object with displayName, email, department, phone, etc. @@ -121,20 +106,7 @@ export function ClaimManagementOverviewTab({ phone: apiRequest.initiator?.phone || apiRequest.initiator?.mobile, }; - // Debug: Log closure props to help troubleshoot - console.debug('[ClaimManagementOverviewTab] Closure setup check:', { - needsClosure, - requestStatus: apiRequest?.status, - requestStatusLower: (apiRequest?.status || '').toLowerCase(), - hasConclusionRemark: !!conclusionRemark, - conclusionRemarkLength: conclusionRemark?.length || 0, - conclusionLoading, - conclusionSubmitting, - aiGenerated, - hasHandleGenerate: !!handleGenerateConclusion, - hasHandleFinalize: !!handleFinalizeConclusion, - hasSetConclusion: !!setConclusionRemark, - }); + // Closure setup check completed return (
diff --git a/src/dealer-claim/pages/Dashboard.tsx b/src/dealer-claim/pages/Dashboard.tsx index 417e0a4..b9f5377 100644 --- a/src/dealer-claim/pages/Dashboard.tsx +++ b/src/dealer-claim/pages/Dashboard.tsx @@ -4,7 +4,6 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Progress } from '@/components/ui/progress'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { PieChart, Pie, Cell, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; import { getDealerDashboard, type DashboardKPIs as DashboardKPIsType, type CategoryData as CategoryDataType } from '@/services/dealerClaimApi'; import { RefreshCw } from 'lucide-react'; @@ -535,91 +534,76 @@ export function DealerDashboard({ onNavigate, onNewRequest: _onNewRequest }: Das
- - - Overview - Top Category 1 - Top Category 2 - - -
-

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 - +
+
+

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() {
- +
- Or + Or
@@ -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 ||