Compare commits

..

2 Commits

15 changed files with 220 additions and 261 deletions

View File

@ -28,6 +28,7 @@ import { CUSTOM_REQUEST_DATABASE } from '@/utils/customRequestDatabase';
import { AuthCallback } from '@/pages/Auth/AuthCallback'; import { AuthCallback } from '@/pages/Auth/AuthCallback';
import { createClaimRequest } from '@/services/dealerClaimApi'; import { createClaimRequest } from '@/services/dealerClaimApi';
import { ManagerSelectionModal } from '@/components/modals/ManagerSelectionModal'; import { ManagerSelectionModal } from '@/components/modals/ManagerSelectionModal';
import { navigateToRequest } from '@/utils/requestNavigation';
import { TokenManager } from '@/utils/tokenManager'; import { TokenManager } from '@/utils/tokenManager';
interface AppProps { 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); setSelectedRequestId(requestId);
setSelectedRequestTitle(requestTitle || 'Unknown Request'); setSelectedRequestTitle(requestTitle || 'Unknown Request');
// Use global navigation utility for consistent routing // Use global navigation utility for consistent routing
const { navigateToRequest } = await import('@/utils/requestNavigation');
navigateToRequest({ navigateToRequest({
requestId, requestId,
requestTitle, requestTitle,

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -8,6 +8,7 @@
// Images // Images
export { default as ReLogo } from './images/Re_Logo.png'; export { default as ReLogo } from './images/Re_Logo.png';
export { default as RoyalEnfieldLogo } from './images/royal_enfield_logo.png'; export { default as RoyalEnfieldLogo } from './images/royal_enfield_logo.png';
export { default as LandingPageImage } from './images/landing_page_image.jpg';
// Fonts // Fonts
// Add font exports here when fonts are added to the assets/fonts folder // Add font exports here when fonts are added to the assets/fonts folder

View File

@ -91,13 +91,7 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
// Formula: remaining = availableBeforeBlock - blockedAmount // Formula: remaining = availableBeforeBlock - blockedAmount
const expectedRemaining = availableBeforeBlock - blockedAmt; const expectedRemaining = availableBeforeBlock - blockedAmt;
// Log for debugging backend calculation // Loading existing IO block
console.log('[IOTab] Loading existing IO block:', {
availableBeforeBlock,
blockedAmount: blockedAmt,
expectedRemaining,
backendRemaining,
});
// Warn if remaining balance calculation seems incorrect (for backend debugging) // Warn if remaining balance calculation seems incorrect (for backend debugging)
if (Math.abs(backendRemaining - expectedRemaining) > 0.01) { if (Math.abs(backendRemaining - expectedRemaining) > 0.01) {
@ -249,15 +243,7 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
return; return;
} }
// Log the amount being sent to backend for debugging // Blocking budget
console.log('[IOTab] Blocking budget:', {
ioNumber: ioNumber.trim(),
originalInput: amountToBlock,
parsedAmount: blockAmountRaw,
roundedAmount: blockAmount,
fetchedAmount,
calculatedRemaining: fetchedAmount - blockAmount,
});
setBlockingBudget(true); setBlockingBudget(true);
try { try {
@ -272,7 +258,7 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
ioRemainingBalance: fetchedAmount - blockAmount, // Calculated value (backend will use SAP's actual value) 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); await updateIODetails(requestId, payload);
@ -287,16 +273,7 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
// Calculate expected remaining balance for validation/debugging // Calculate expected remaining balance for validation/debugging
const expectedRemainingBalance = fetchedAmount - savedBlockedAmount; const expectedRemainingBalance = fetchedAmount - savedBlockedAmount;
// Log what was saved vs what we sent // Blocking result processed
console.log('[IOTab] Blocking result:', {
sentAmount: blockAmount,
savedBlockedAmount,
availableBalance: fetchedAmount,
expectedRemaining: expectedRemainingBalance,
backendRemaining: savedRemainingBalance,
difference: savedBlockedAmount - blockAmount,
remainingDifference: fetchedAmount - savedRemainingBalance,
});
// Warn if the saved amount differs from what we sent // Warn if the saved amount differs from what we sent
if (Math.abs(savedBlockedAmount - blockAmount) > 0.01) { if (Math.abs(savedBlockedAmount - blockAmount) > 0.01) {

View File

@ -86,16 +86,7 @@ export function ClaimManagementOverviewTab({
); );
} }
// Debug: Log mapped data for troubleshooting // Mapped claim data ready
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,
});
// Determine user's role // Determine user's role
const userRole: RequestRole = determineUserRole(apiRequest, currentUserId); const userRole: RequestRole = determineUserRole(apiRequest, currentUserId);
@ -103,13 +94,7 @@ export function ClaimManagementOverviewTab({
// Get visibility settings based on role // Get visibility settings based on role
const visibility = getRoleBasedVisibility(userRole); const visibility = getRoleBasedVisibility(userRole);
console.debug('[ClaimManagementOverviewTab] User role and visibility:', { // User role and visibility determined
userRole,
visibility,
currentUserId,
showDealerInfo: visibility.showDealerInfo,
dealerInfoPresent: !!(claimRequest.dealerInfo?.dealerCode || claimRequest.dealerInfo?.dealerName),
});
// Extract initiator info from request // Extract initiator info from request
// The apiRequest has initiator object with displayName, email, department, phone, etc. // 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, phone: apiRequest.initiator?.phone || apiRequest.initiator?.mobile,
}; };
// Debug: Log closure props to help troubleshoot // Closure setup check completed
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,
});
return ( return (
<div className={`space-y-6 ${className}`}> <div className={`space-y-6 ${className}`}>

View File

@ -4,7 +4,6 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress'; 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 { 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 { getDealerDashboard, type DashboardKPIs as DashboardKPIsType, type CategoryData as CategoryDataType } from '@/services/dealerClaimApi';
import { RefreshCw } from 'lucide-react'; import { RefreshCw } from 'lucide-react';
@ -535,91 +534,76 @@ export function DealerDashboard({ onNavigate, onNewRequest: _onNewRequest }: Das
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Tabs defaultValue="overview" className="w-full"> <div className="space-y-4">
<TabsList className="grid w-full grid-cols-3"> <div>
<TabsTrigger value="overview">Overview</TabsTrigger> <h3 className="text-lg mb-4 text-gray-900">Activity Type Value Comparison</h3>
<TabsTrigger value="category-1">Top Category 1</TabsTrigger> <ResponsiveContainer width="100%" height={350}>
<TabsTrigger value="category-2">Top Category 2</TabsTrigger> <BarChart data={valueComparisonData}>
</TabsList> <CartesianGrid strokeDasharray="3 3" />
<TabsContent value="overview" className="space-y-4 mt-6"> <XAxis dataKey="name" />
<div> <YAxis tickFormatter={(value) => formatCurrency(value)} />
<h3 className="text-lg mb-4 text-gray-900">Activity Type Value Comparison</h3> <Tooltip
<ResponsiveContainer width="100%" height={350}> formatter={(value: number) => formatCurrency(value)}
<BarChart data={valueComparisonData}> labelFormatter={(label) => label}
<CartesianGrid strokeDasharray="3 3" /> />
<XAxis dataKey="name" /> <Legend />
<YAxis tickFormatter={(value) => formatCurrency(value)} /> <Bar dataKey="Raised" fill="#3b82f6" />
<Tooltip <Bar dataKey="Approved" fill="#22c55e" />
formatter={(value: number) => formatCurrency(value)} <Bar dataKey="Credited" fill="#10b981" />
labelFormatter={(label) => label} </BarChart>
/> </ResponsiveContainer>
<Legend /> </div>
<Bar dataKey="Raised" fill="#3b82f6" /> <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
<Bar dataKey="Approved" fill="#22c55e" /> {categoryData.slice(0, 3).map((cat, index) => (
<Bar dataKey="Credited" fill="#10b981" /> <Card key={index} className="shadow-md hover:shadow-lg transition-shadow">
</BarChart> <CardHeader className="pb-3">
</ResponsiveContainer> <div className="flex items-center justify-between">
</div> <CardTitle className="text-base">{cat.activityType}</CardTitle>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6"> <Badge className="bg-emerald-50 text-emerald-700 border-emerald-200">
{categoryData.slice(0, 3).map((cat, index) => ( {cat.approvalRate.toFixed(1)}% approved
<Card key={index} className="shadow-md hover:shadow-lg transition-shadow"> </Badge>
<CardHeader className="pb-3"> </div>
<div className="flex items-center justify-between"> </CardHeader>
<CardTitle className="text-base">{cat.activityType}</CardTitle> <CardContent className="space-y-3">
<Badge className="bg-emerald-50 text-emerald-700 border-emerald-200"> <div className="space-y-2">
{cat.approvalRate.toFixed(1)}% approved <div className="flex justify-between text-sm">
</Badge> <span className="text-gray-600">Raised:</span>
<span className="text-gray-900">{formatNumber(cat.raised)} ({formatCurrency(cat.raisedValue)})</span>
</div> </div>
</CardHeader> <div className="flex justify-between text-sm">
<CardContent className="space-y-3"> <span className="text-gray-600">Approved:</span>
<div className="space-y-2"> <span className="text-green-600">{formatNumber(cat.approved)} ({formatCurrency(cat.approvedValue)})</span>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Raised:</span>
<span className="text-gray-900">{formatNumber(cat.raised)} ({formatCurrency(cat.raisedValue)})</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Approved:</span>
<span className="text-green-600">{formatNumber(cat.approved)} ({formatCurrency(cat.approvedValue)})</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Rejected:</span>
<span className="text-red-600">{formatNumber(cat.rejected)} ({formatCurrency(cat.rejectedValue)})</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Pending:</span>
<span className="text-orange-600">{formatNumber(cat.pending)} ({formatCurrency(cat.pendingValue)})</span>
</div>
<div className="h-px bg-gray-200 my-2" />
<div className="flex justify-between text-sm">
<span className="text-gray-600">Credited:</span>
<span className="text-emerald-600">{formatNumber(cat.credited)} ({formatCurrency(cat.creditedValue)})</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Pending Credit:</span>
<span className="text-amber-600">{formatNumber(cat.pendingCredit)} ({formatCurrency(cat.pendingCreditValue)})</span>
</div>
</div> </div>
<div className="pt-2"> <div className="flex justify-between text-sm">
<div className="flex justify-between text-xs text-gray-600 mb-1"> <span className="text-gray-600">Rejected:</span>
<span>Credit Rate</span> <span className="text-red-600">{formatNumber(cat.rejected)} ({formatCurrency(cat.rejectedValue)})</span>
<span>{cat.creditRate.toFixed(1)}%</span>
</div>
<Progress value={cat.creditRate} className="h-2" />
</div> </div>
</CardContent> <div className="flex justify-between text-sm">
</Card> <span className="text-gray-600">Pending:</span>
))} <span className="text-orange-600">{formatNumber(cat.pending)} ({formatCurrency(cat.pendingValue)})</span>
</div> </div>
</TabsContent> <div className="h-px bg-gray-200 my-2" />
<TabsContent value="category-1" className="space-y-4"> <div className="flex justify-between text-sm">
{/* Category 1 details */} <span className="text-gray-600">Credited:</span>
<p className="text-gray-600">Detailed view for top category 1</p> <span className="text-emerald-600">{formatNumber(cat.credited)} ({formatCurrency(cat.creditedValue)})</span>
</TabsContent> </div>
<TabsContent value="category-2" className="space-y-4"> <div className="flex justify-between text-sm">
{/* Category 2 details */} <span className="text-gray-600">Pending Credit:</span>
<p className="text-gray-600">Detailed view for top category 2</p> <span className="text-amber-600">{formatNumber(cat.pendingCredit)} ({formatCurrency(cat.pendingCreditValue)})</span>
</TabsContent> </div>
</Tabs> </div>
<div className="pt-2">
<div className="flex justify-between text-xs text-gray-600 mb-1">
<span>Credit Rate</span>
<span>{cat.creditRate.toFixed(1)}%</span>
</div>
<Progress value={cat.creditRate} className="h-2" />
</div>
</CardContent>
</Card>
))}
</div>
</div>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -245,14 +245,7 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
const requestStatus = (request?.status || apiRequest?.status || '').toLowerCase(); const requestStatus = (request?.status || apiRequest?.status || '').toLowerCase();
const needsClosure = (requestStatus === 'approved' || requestStatus === 'rejected') && isInitiator; const needsClosure = (requestStatus === 'approved' || requestStatus === 'rejected') && isInitiator;
// Debug logging // Closure check completed
console.debug('[DealerClaimRequestDetail] Closure check:', {
requestStatus,
requestStatusRaw: request?.status,
apiRequestStatusRaw: apiRequest?.status,
isInitiator,
needsClosure,
});
const { const {
conclusionRemark, conclusionRemark,
setConclusionRemark, setConclusionRemark,

View File

@ -50,26 +50,46 @@ export function useConclusionRemark(
* Use Case: When request is approved, final approver generates conclusion. * Use Case: When request is approved, final approver generates conclusion.
* Initiator needs to review and finalize it before closing request. * Initiator needs to review and finalize it before closing request.
* *
* Optimization: Check request object first before making API call
* Process: * Process:
* 1. Dynamically import conclusion API service * 1. Check if conclusion data is already in request object
* 2. Fetch conclusion by request ID * 2. If not available, fetch from API
* 3. Load into state if exists * 3. Load into state if exists
* 4. Mark as AI-generated if applicable * 4. Mark as AI-generated if applicable
*/ */
const fetchExistingConclusion = async () => { 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 { try {
// Lazy load: Import conclusion API only when needed // Lazy load: Import conclusion API only when needed
const { getConclusion } = await import('@/services/conclusionApi'); 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); 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 // 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); setAiGenerated(!!result.aiGeneratedRemark);
} }
} catch (err) { } 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 // 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) * 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(() => { 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(); fetchExistingConclusion();
} }
}, [request?.status, isInitiator]); }, [request?.status, request?.conclusionRemark, request?.aiGeneratedConclusion, isInitiator, conclusionRemark]);
return { return {
conclusionRemark, conclusionRemark,

View File

@ -219,15 +219,18 @@ export function useRequestDetails(
: []; : [];
/** /**
* Fetch: Get pause details if request is paused * Fetch: Get pause details only if request is actually paused
* This is needed to show resume/retrigger buttons correctly * Use request-level isPaused field from workflow response
*/ */
let pauseInfo = null; let pauseInfo = null;
try { const isPaused = (wf as any).isPaused || false;
pauseInfo = await getPauseDetails(wf.requestId);
} catch (error) { if (isPaused) {
// Pause info not available or request not paused - ignore try {
console.debug('Pause details not available:', error); 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') { if (wf.workflowType === 'CLAIM_MANAGEMENT' || wf.templateType === 'claim-management') {
try { try {
console.debug('[useRequestDetails] Fetching claim details for requestId:', wf.requestId);
const claimResponse = await apiClient.get(`/dealer-claims/${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; 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) { if (claimData) {
claimDetails = claimData.claimDetails || claimData.claim_details; claimDetails = claimData.claimDetails || claimData.claim_details;
@ -278,24 +266,7 @@ export function useRequestDetails(
(claimDetails as any).completionExpenses = completionExpenses; (claimDetails as any).completionExpenses = completionExpenses;
} }
console.debug('[useRequestDetails] Extracted details:', { // Extracted details processed
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,
});
} else { } else {
console.warn('[useRequestDetails] No claimData found in response'); 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; let pauseInfo = null;
try { const isPaused = (wf as any).isPaused || false;
pauseInfo = await getPauseDetails(wf.requestId);
} catch (error) { if (isPaused) {
// Pause info not available or request not paused - ignore try {
console.debug('Pause details not available:', error); 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') { if (wf.workflowType === 'CLAIM_MANAGEMENT' || wf.templateType === 'claim-management') {
try { try {
console.debug('[useRequestDetails] Initial load - Fetching claim details for requestId:', wf.requestId);
const claimResponse = await apiClient.get(`/dealer-claims/${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; const claimData = claimResponse.data?.data || claimResponse.data;
if (claimData) { if (claimData) {
@ -575,17 +544,7 @@ export function useRequestDetails(
(claimDetails as any).completionExpenses = completionExpenses; (claimDetails as any).completionExpenses = completionExpenses;
} }
console.debug('[useRequestDetails] Initial load - Extracted details:', { // Initial load - Extracted details processed
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,
});
} }
} catch (error: any) { } catch (error: any) {
// Claim details not available - request might not be fully initialized yet // Claim details not available - request might not be fully initialized yet

View File

@ -11,6 +11,7 @@ import { Pagination } from '@/components/common/Pagination';
import { getPriorityConfig, getStatusConfig, getSLAConfig } from '../utils/configMappers'; import { getPriorityConfig, getStatusConfig, getSLAConfig } from '../utils/configMappers';
import { formatDate, formatDateTime } from '../utils/formatters'; import { formatDate, formatDateTime } from '../utils/formatters';
import { formatHoursMinutes } from '@/utils/slaTracker'; import { formatHoursMinutes } from '@/utils/slaTracker';
import { navigateToRequest } from '@/utils/requestNavigation';
import type { ApproverPerformanceRequest } from '../types/approverPerformance.types'; import type { ApproverPerformanceRequest } from '../types/approverPerformance.types';
interface ApproverPerformanceRequestListProps { interface ApproverPerformanceRequestListProps {
@ -69,7 +70,6 @@ export function ApproverPerformanceRequestList({
key={request.requestId} key={request.requestId}
className="hover:shadow-md transition-shadow cursor-pointer" className="hover:shadow-md transition-shadow cursor-pointer"
onClick={() => { onClick={() => {
const { navigateToRequest } = require('@/utils/requestNavigation');
navigateToRequest({ navigateToRequest({
requestId: request.requestId, requestId: request.requestId,
requestTitle: request.title, requestTitle: request.title,
@ -166,7 +166,6 @@ export function ApproverPerformanceRequestList({
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
const { navigateToRequest } = require('@/utils/requestNavigation');
navigateToRequest({ navigateToRequest({
requestId: request.requestId, requestId: request.requestId,
requestTitle: request.title, requestTitle: request.title,

View File

@ -1,14 +1,28 @@
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { LogIn, Shield } from 'lucide-react'; import { LogIn, Shield } from 'lucide-react';
import { ReLogo } from '@/assets'; import { ReLogo, LandingPageImage } from '@/assets';
import { initiateTanflowLogin } from '@/services/tanflowAuth'; import { initiateTanflowLogin } from '@/services/tanflowAuth';
export function Auth() { export function Auth() {
const { login, isLoading, error } = useAuth(); const { login, isLoading, error } = useAuth();
const [tanflowLoading, setTanflowLoading] = useState(false); 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 () => { const handleOKTALogin = async () => {
// Clear any existing session data // Clear any existing session data
@ -51,8 +65,24 @@ export function Auth() {
} }
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 p-4"> <div
<Card className="w-full max-w-md shadow-xl"> className="min-h-screen flex items-center justify-center p-4 relative"
style={{
backgroundImage: imageLoaded ? `url(${LandingPageImage})` : 'none',
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
transition: 'background-image 0.3s ease-in-out'
}}
>
{/* Fallback background while image loads */}
{!imageLoaded && (
<div className="absolute inset-0 bg-gradient-to-br from-slate-900 to-slate-800"></div>
)}
{/* Overlay for better readability */}
<div className="absolute inset-0 bg-black/40"></div>
<Card className="w-full max-w-md shadow-xl relative z-10 bg-black backdrop-blur-sm border-gray-800">
<CardHeader className="space-y-1 text-center pb-6"> <CardHeader className="space-y-1 text-center pb-6">
<div className="flex flex-col items-center justify-center mb-4"> <div className="flex flex-col items-center justify-center mb-4">
<img <img
@ -60,13 +90,13 @@ export function Auth() {
alt="Royal Enfield Logo" alt="Royal Enfield Logo"
className="h-10 w-auto max-w-[168px] object-contain mb-2" className="h-10 w-auto max-w-[168px] object-contain mb-2"
/> />
<p className="text-xs text-gray-400 text-center truncate">Approval Portal</p> <p className="text-xs text-gray-300 text-center truncate">Approval Portal</p>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{error && ( {error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg"> <div className="bg-red-900/50 border border-red-700 text-red-200 px-4 py-3 rounded-lg">
<p className="text-sm font-medium">Authentication Error</p> <p className="text-sm font-medium">Authentication Error</p>
<p className="text-sm">{error.message}</p> <p className="text-sm">{error.message}</p>
</div> </div>
@ -96,10 +126,10 @@ export function Auth() {
<div className="relative"> <div className="relative">
<div className="absolute inset-0 flex items-center"> <div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-gray-300"></span> <span className="w-full border-t border-gray-700"></span>
</div> </div>
<div className="relative flex justify-center text-xs uppercase"> <div className="relative flex justify-center text-xs uppercase">
<span className="bg-white px-2 text-gray-500">Or</span> <span className="bg-gray-900 px-2 text-gray-400">Or</span>
</div> </div>
</div> </div>
@ -125,9 +155,9 @@ export function Auth() {
</Button> </Button>
</div> </div>
<div className="text-center text-sm text-gray-500 mt-4"> <div className="text-center text-sm text-gray-400 mt-4">
<p>Secure Single Sign-On</p> <p>Secure Single Sign-On</p>
<p className="text-xs mt-1">Choose your authentication provider</p> <p className="text-xs mt-1 text-gray-500">Choose your authentication provider</p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -1,5 +1,6 @@
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { navigateToRequest } from '@/utils/requestNavigation';
// Components // Components
import { DetailedReportsHeader } from './components/DetailedReportsHeader'; import { DetailedReportsHeader } from './components/DetailedReportsHeader';
@ -69,7 +70,6 @@ export function DetailedReports({ onBack }: DetailedReportsProps) {
}, [onBack, navigate]); }, [onBack, navigate]);
const handleViewRequest = useCallback((requestId: string) => { const handleViewRequest = useCallback((requestId: string) => {
const { navigateToRequest } = require('@/utils/requestNavigation');
navigateToRequest({ navigateToRequest({
requestId, requestId,
navigate, navigate,

View File

@ -42,11 +42,15 @@ export function Notifications({ onNavigate }: NotificationsProps) {
const result = await notificationApi.list({ page, limit: ITEMS_PER_PAGE, unreadOnly }); const result = await notificationApi.list({ page, limit: ITEMS_PER_PAGE, unreadOnly });
const notifs = result.data?.notifications || []; 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); setNotifications(notifs);
setTotalCount(total); setTotalCount(total);
setTotalPages(Math.ceil(total / ITEMS_PER_PAGE)); setTotalPages(totalPages);
setCurrentPage(page); // Update current page to match what was fetched
} catch (error) { } catch (error) {
console.error('[Notifications] Failed to fetch:', error); console.error('[Notifications] Failed to fetch:', error);
} finally { } finally {
@ -56,6 +60,7 @@ export function Notifications({ onNavigate }: NotificationsProps) {
}; };
useEffect(() => { useEffect(() => {
setCurrentPage(1); // Reset to page 1 when filter changes
fetchNotifications(1, filter === 'unread'); fetchNotifications(1, filter === 'unread');
}, [filter]); }, [filter]);
@ -82,6 +87,11 @@ export function Notifications({ onNavigate }: NotificationsProps) {
navigationUrl += '?tab=worknotes'; navigationUrl += '?tab=worknotes';
} }
// Document added notifications should open Documents tab
if (notification.notificationType === 'document_added') {
navigationUrl += '?tab=documents';
}
onNavigate(navigationUrl); onNavigate(navigationUrl);
} }
} }
@ -131,6 +141,8 @@ export function Notifications({ onNavigate }: NotificationsProps) {
return <MessageSquare className={`${iconClass} text-blue-600`} />; return <MessageSquare className={`${iconClass} text-blue-600`} />;
case 'worknote': case 'worknote':
return <FileText className={`${iconClass} text-purple-600`} />; return <FileText className={`${iconClass} text-purple-600`} />;
case 'document_added':
return <FileText className={`${iconClass} text-teal-600`} />;
case 'assignment': case 'assignment':
return <UserPlus className={`${iconClass} text-indigo-600`} />; return <UserPlus className={`${iconClass} text-indigo-600`} />;
case 'approval': case 'approval':

View File

@ -57,9 +57,19 @@ export async function finalizeConclusion(requestId: string, finalRemark: string)
/** /**
* Get conclusion for a request * Get conclusion for a request
* Returns null if conclusion doesn't exist (404) instead of throwing error
*/ */
export async function getConclusion(requestId: string): Promise<ConclusionRemark> { export async function getConclusion(requestId: string): Promise<ConclusionRemark | null> {
const response = await apiClient.get(`/conclusions/${requestId}`); try {
return response.data.data; 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;
}
} }

View File

@ -115,18 +115,7 @@ export function mapToClaimManagementRequest(
const creditNote = apiRequest.creditNote || apiRequest.credit_note || {}; const creditNote = apiRequest.creditNote || apiRequest.credit_note || {};
const completionExpenses = apiRequest.completionExpenses || apiRequest.completion_expenses || []; const completionExpenses = apiRequest.completionExpenses || apiRequest.completion_expenses || [];
// Debug: Log raw claim details to help troubleshoot // Raw claim details processed
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,
});
// Map activity information (matching ActivityInformationCard expectations) // Map activity information (matching ActivityInformationCard expectations)
// Handle both camelCase and snake_case field names from Sequelize // 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 activityType = claimDetails.activityType || claimDetails.activity_type || '';
const location = claimDetails.location || ''; const location = claimDetails.location || '';
console.debug('[claimDataMapper] Mapped activity fields:', { // Activity fields mapped
activityName,
activityType,
location,
hasActivityName: !!activityName,
hasActivityType: !!activityType,
hasLocation: !!location,
});
// Get budget values from budgetTracking table (new source of truth) // Get budget values from budgetTracking table (new source of truth)
const estimatedBudget = budgetTracking.proposalEstimatedBudget || const estimatedBudget = budgetTracking.proposalEstimatedBudget ||