Compare commits
No commits in common. "94b7c34a7a5ceb8514decb4bedf2be702bfabd94" and "22d3e8a38834ff97e4a76448496a06b22eb805d9" have entirely different histories.
94b7c34a7a
...
22d3e8a388
@ -28,7 +28,6 @@ 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 {
|
||||
@ -149,11 +148,12 @@ function AppRoutes({ onLogout }: AppProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewRequest = (requestId: string, requestTitle?: string, status?: string, request?: any) => {
|
||||
const handleViewRequest = async (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,
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB |
@ -8,7 +8,6 @@
|
||||
// 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
|
||||
|
||||
@ -91,7 +91,13 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
// Formula: remaining = availableBeforeBlock - blockedAmount
|
||||
const expectedRemaining = availableBeforeBlock - blockedAmt;
|
||||
|
||||
// Loading existing IO block
|
||||
// Log for debugging backend calculation
|
||||
console.log('[IOTab] Loading existing IO block:', {
|
||||
availableBeforeBlock,
|
||||
blockedAmount: blockedAmt,
|
||||
expectedRemaining,
|
||||
backendRemaining,
|
||||
});
|
||||
|
||||
// Warn if remaining balance calculation seems incorrect (for backend debugging)
|
||||
if (Math.abs(backendRemaining - expectedRemaining) > 0.01) {
|
||||
@ -243,7 +249,15 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Blocking budget
|
||||
// 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,
|
||||
});
|
||||
|
||||
setBlockingBudget(true);
|
||||
try {
|
||||
@ -258,7 +272,7 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
ioRemainingBalance: fetchedAmount - blockAmount, // Calculated value (backend will use SAP's actual value)
|
||||
};
|
||||
|
||||
// Sending to backend
|
||||
console.log('[IOTab] Sending to backend:', payload);
|
||||
|
||||
await updateIODetails(requestId, payload);
|
||||
|
||||
@ -273,7 +287,16 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
// Calculate expected remaining balance for validation/debugging
|
||||
const expectedRemainingBalance = fetchedAmount - savedBlockedAmount;
|
||||
|
||||
// Blocking result processed
|
||||
// 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,
|
||||
});
|
||||
|
||||
// Warn if the saved amount differs from what we sent
|
||||
if (Math.abs(savedBlockedAmount - blockAmount) > 0.01) {
|
||||
|
||||
@ -86,7 +86,16 @@ export function ClaimManagementOverviewTab({
|
||||
);
|
||||
}
|
||||
|
||||
// Mapped claim data ready
|
||||
// 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,
|
||||
});
|
||||
|
||||
// Determine user's role
|
||||
const userRole: RequestRole = determineUserRole(apiRequest, currentUserId);
|
||||
@ -94,7 +103,13 @@ export function ClaimManagementOverviewTab({
|
||||
// Get visibility settings based on role
|
||||
const visibility = getRoleBasedVisibility(userRole);
|
||||
|
||||
// User role and visibility determined
|
||||
console.debug('[ClaimManagementOverviewTab] User role and visibility:', {
|
||||
userRole,
|
||||
visibility,
|
||||
currentUserId,
|
||||
showDealerInfo: visibility.showDealerInfo,
|
||||
dealerInfoPresent: !!(claimRequest.dealerInfo?.dealerCode || claimRequest.dealerInfo?.dealerName),
|
||||
});
|
||||
|
||||
// Extract initiator info from request
|
||||
// The apiRequest has initiator object with displayName, email, department, phone, etc.
|
||||
@ -106,7 +121,20 @@ export function ClaimManagementOverviewTab({
|
||||
phone: apiRequest.initiator?.phone || apiRequest.initiator?.mobile,
|
||||
};
|
||||
|
||||
// Closure setup check completed
|
||||
// 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,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={`space-y-6 ${className}`}>
|
||||
|
||||
@ -4,6 +4,7 @@ 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';
|
||||
@ -534,76 +535,91 @@ export function DealerDashboard({ onNavigate, onNewRequest: _onNewRequest }: Das
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg mb-4 text-gray-900">Activity Type Value Comparison</h3>
|
||||
<ResponsiveContainer width="100%" height={350}>
|
||||
<BarChart data={valueComparisonData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis tickFormatter={(value) => formatCurrency(value)} />
|
||||
<Tooltip
|
||||
formatter={(value: number) => formatCurrency(value)}
|
||||
labelFormatter={(label) => label}
|
||||
/>
|
||||
<Legend />
|
||||
<Bar dataKey="Raised" fill="#3b82f6" />
|
||||
<Bar dataKey="Approved" fill="#22c55e" />
|
||||
<Bar dataKey="Credited" fill="#10b981" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
|
||||
{categoryData.slice(0, 3).map((cat, index) => (
|
||||
<Card key={index} className="shadow-md hover:shadow-lg transition-shadow">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base">{cat.activityType}</CardTitle>
|
||||
<Badge className="bg-emerald-50 text-emerald-700 border-emerald-200">
|
||||
{cat.approvalRate.toFixed(1)}% approved
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<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>
|
||||
<Tabs defaultValue="overview" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="category-1">Top Category 1</TabsTrigger>
|
||||
<TabsTrigger value="category-2">Top Category 2</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="overview" className="space-y-4 mt-6">
|
||||
<div>
|
||||
<h3 className="text-lg mb-4 text-gray-900">Activity Type Value Comparison</h3>
|
||||
<ResponsiveContainer width="100%" height={350}>
|
||||
<BarChart data={valueComparisonData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis tickFormatter={(value) => formatCurrency(value)} />
|
||||
<Tooltip
|
||||
formatter={(value: number) => formatCurrency(value)}
|
||||
labelFormatter={(label) => label}
|
||||
/>
|
||||
<Legend />
|
||||
<Bar dataKey="Raised" fill="#3b82f6" />
|
||||
<Bar dataKey="Approved" fill="#22c55e" />
|
||||
<Bar dataKey="Credited" fill="#10b981" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
|
||||
{categoryData.slice(0, 3).map((cat, index) => (
|
||||
<Card key={index} className="shadow-md hover:shadow-lg transition-shadow">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base">{cat.activityType}</CardTitle>
|
||||
<Badge className="bg-emerald-50 text-emerald-700 border-emerald-200">
|
||||
{cat.approvalRate.toFixed(1)}% approved
|
||||
</Badge>
|
||||
</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>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<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 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 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>
|
||||
<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 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>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="category-1" className="space-y-4">
|
||||
{/* Category 1 details */}
|
||||
<p className="text-gray-600">Detailed view for top category 1</p>
|
||||
</TabsContent>
|
||||
<TabsContent value="category-2" className="space-y-4">
|
||||
{/* Category 2 details */}
|
||||
<p className="text-gray-600">Detailed view for top category 2</p>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@ -245,7 +245,14 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
|
||||
const requestStatus = (request?.status || apiRequest?.status || '').toLowerCase();
|
||||
const needsClosure = (requestStatus === 'approved' || requestStatus === 'rejected') && isInitiator;
|
||||
|
||||
// Closure check completed
|
||||
// Debug logging
|
||||
console.debug('[DealerClaimRequestDetail] Closure check:', {
|
||||
requestStatus,
|
||||
requestStatusRaw: request?.status,
|
||||
apiRequestStatusRaw: apiRequest?.status,
|
||||
isInitiator,
|
||||
needsClosure,
|
||||
});
|
||||
const {
|
||||
conclusionRemark,
|
||||
setConclusionRemark,
|
||||
|
||||
@ -50,46 +50,26 @@ 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. Check if conclusion data is already in request object
|
||||
* 2. If not available, fetch from API
|
||||
* 1. Dynamically import conclusion API service
|
||||
* 2. Fetch conclusion by request ID
|
||||
* 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 (returns null if not found)
|
||||
// API Call: Fetch existing conclusion
|
||||
const result = await getConclusion(request.requestId || requestIdentifier);
|
||||
|
||||
if (result && (result.aiGeneratedRemark || result.finalRemark)) {
|
||||
if (result && result.aiGeneratedRemark) {
|
||||
// Load: Set the AI-generated or final remark
|
||||
// Handle null values by providing empty string fallback
|
||||
setConclusionRemark(result.finalRemark || result.aiGeneratedRemark || '');
|
||||
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
|
||||
}
|
||||
};
|
||||
@ -238,36 +218,16 @@ export function useConclusionRemark(
|
||||
};
|
||||
|
||||
/**
|
||||
* Effect: Auto-load existing conclusion when request becomes approved, rejected, or closed
|
||||
* Effect: Auto-fetch existing conclusion when request becomes approved or rejected
|
||||
*
|
||||
* Trigger: When request status changes to "approved", "rejected", or "closed" and user is initiator
|
||||
* Trigger: When request status changes to "approved" or "rejected" 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(() => {
|
||||
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
|
||||
if ((request?.status === 'approved' || request?.status === 'rejected') && isInitiator && !conclusionRemark) {
|
||||
fetchExistingConclusion();
|
||||
}
|
||||
}, [request?.status, request?.conclusionRemark, request?.aiGeneratedConclusion, isInitiator, conclusionRemark]);
|
||||
}, [request?.status, isInitiator]);
|
||||
|
||||
return {
|
||||
conclusionRemark,
|
||||
|
||||
@ -219,18 +219,15 @@ export function useRequestDetails(
|
||||
: [];
|
||||
|
||||
/**
|
||||
* Fetch: Get pause details only if request is actually paused
|
||||
* Use request-level isPaused field from workflow response
|
||||
* Fetch: Get pause details if request is paused
|
||||
* This is needed to show resume/retrigger buttons correctly
|
||||
*/
|
||||
let pauseInfo = null;
|
||||
const isPaused = (wf as any).isPaused || false;
|
||||
|
||||
if (isPaused) {
|
||||
try {
|
||||
pauseInfo = await getPauseDetails(wf.requestId);
|
||||
} catch (error) {
|
||||
// Pause info not available - ignore
|
||||
}
|
||||
try {
|
||||
pauseInfo = await getPauseDetails(wf.requestId);
|
||||
} catch (error) {
|
||||
// Pause info not available or request not paused - ignore
|
||||
console.debug('Pause details not available:', error);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -243,9 +240,24 @@ 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;
|
||||
@ -266,7 +278,24 @@ export function useRequestDetails(
|
||||
(claimDetails as any).completionExpenses = completionExpenses;
|
||||
}
|
||||
|
||||
// Extracted details processed
|
||||
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,
|
||||
});
|
||||
} else {
|
||||
console.warn('[useRequestDetails] No claimData found in response');
|
||||
}
|
||||
@ -499,17 +528,13 @@ export function useRequestDetails(
|
||||
})
|
||||
: [];
|
||||
|
||||
// Fetch pause details only if request is actually paused
|
||||
// Use request-level isPaused field from workflow response
|
||||
// Fetch pause details
|
||||
let pauseInfo = null;
|
||||
const isPaused = (wf as any).isPaused || false;
|
||||
|
||||
if (isPaused) {
|
||||
try {
|
||||
pauseInfo = await getPauseDetails(wf.requestId);
|
||||
} catch (error) {
|
||||
// Pause info not available - ignore
|
||||
}
|
||||
try {
|
||||
pauseInfo = await getPauseDetails(wf.requestId);
|
||||
} catch (error) {
|
||||
// Pause info not available or request not paused - ignore
|
||||
console.debug('Pause details not available:', error);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -522,7 +547,13 @@ 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) {
|
||||
@ -544,7 +575,17 @@ export function useRequestDetails(
|
||||
(claimDetails as any).completionExpenses = completionExpenses;
|
||||
}
|
||||
|
||||
// Initial load - Extracted details processed
|
||||
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,
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Claim details not available - request might not be fully initialized yet
|
||||
|
||||
@ -11,7 +11,6 @@ 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 {
|
||||
@ -70,6 +69,7 @@ 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,6 +166,7 @@ export function ApproverPerformanceRequestList({
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const { navigateToRequest } = require('@/utils/requestNavigation');
|
||||
navigateToRequest({
|
||||
requestId: request.requestId,
|
||||
requestTitle: request.title,
|
||||
|
||||
@ -1,28 +1,14 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState } 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, LandingPageImage } from '@/assets';
|
||||
import { ReLogo } 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
|
||||
@ -65,24 +51,8 @@ export function Auth() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
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">
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 p-4">
|
||||
<Card className="w-full max-w-md shadow-xl">
|
||||
<CardHeader className="space-y-1 text-center pb-6">
|
||||
<div className="flex flex-col items-center justify-center mb-4">
|
||||
<img
|
||||
@ -90,13 +60,13 @@ export function Auth() {
|
||||
alt="Royal Enfield Logo"
|
||||
className="h-10 w-auto max-w-[168px] object-contain mb-2"
|
||||
/>
|
||||
<p className="text-xs text-gray-300 text-center truncate">Approval Portal</p>
|
||||
<p className="text-xs text-gray-400 text-center truncate">Approval Portal</p>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
{error && (
|
||||
<div className="bg-red-900/50 border border-red-700 text-red-200 px-4 py-3 rounded-lg">
|
||||
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg">
|
||||
<p className="text-sm font-medium">Authentication Error</p>
|
||||
<p className="text-sm">{error.message}</p>
|
||||
</div>
|
||||
@ -126,10 +96,10 @@ export function Auth() {
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t border-gray-700"></span>
|
||||
<span className="w-full border-t border-gray-300"></span>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-gray-900 px-2 text-gray-400">Or</span>
|
||||
<span className="bg-white px-2 text-gray-500">Or</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -155,9 +125,9 @@ export function Auth() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm text-gray-400 mt-4">
|
||||
<div className="text-center text-sm text-gray-500 mt-4">
|
||||
<p>Secure Single Sign-On</p>
|
||||
<p className="text-xs mt-1 text-gray-500">Choose your authentication provider</p>
|
||||
<p className="text-xs mt-1">Choose your authentication provider</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { navigateToRequest } from '@/utils/requestNavigation';
|
||||
|
||||
// Components
|
||||
import { DetailedReportsHeader } from './components/DetailedReportsHeader';
|
||||
@ -70,6 +69,7 @@ export function DetailedReports({ onBack }: DetailedReportsProps) {
|
||||
}, [onBack, navigate]);
|
||||
|
||||
const handleViewRequest = useCallback((requestId: string) => {
|
||||
const { navigateToRequest } = require('@/utils/requestNavigation');
|
||||
navigateToRequest({
|
||||
requestId,
|
||||
navigate,
|
||||
|
||||
@ -42,15 +42,11 @@ export function Notifications({ onNavigate }: NotificationsProps) {
|
||||
const result = await notificationApi.list({ page, limit: ITEMS_PER_PAGE, unreadOnly });
|
||||
|
||||
const notifs = result.data?.notifications || [];
|
||||
// Extract pagination data from the response
|
||||
const pagination = result.data?.pagination || {};
|
||||
const total = pagination.total || 0;
|
||||
const totalPages = pagination.totalPages || 1;
|
||||
const total = result.data?.total || 0;
|
||||
|
||||
setNotifications(notifs);
|
||||
setTotalCount(total);
|
||||
setTotalPages(totalPages);
|
||||
setCurrentPage(page); // Update current page to match what was fetched
|
||||
setTotalPages(Math.ceil(total / ITEMS_PER_PAGE));
|
||||
} catch (error) {
|
||||
console.error('[Notifications] Failed to fetch:', error);
|
||||
} finally {
|
||||
@ -60,7 +56,6 @@ export function Notifications({ onNavigate }: NotificationsProps) {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPage(1); // Reset to page 1 when filter changes
|
||||
fetchNotifications(1, filter === 'unread');
|
||||
}, [filter]);
|
||||
|
||||
@ -87,11 +82,6 @@ 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);
|
||||
}
|
||||
}
|
||||
@ -141,8 +131,6 @@ export function Notifications({ onNavigate }: NotificationsProps) {
|
||||
return <MessageSquare className={`${iconClass} text-blue-600`} />;
|
||||
case 'worknote':
|
||||
return <FileText className={`${iconClass} text-purple-600`} />;
|
||||
case 'document_added':
|
||||
return <FileText className={`${iconClass} text-teal-600`} />;
|
||||
case 'assignment':
|
||||
return <UserPlus className={`${iconClass} text-indigo-600`} />;
|
||||
case 'approval':
|
||||
|
||||
@ -57,19 +57,9 @@ 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<ConclusionRemark | null> {
|
||||
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;
|
||||
}
|
||||
export async function getConclusion(requestId: string): Promise<ConclusionRemark> {
|
||||
const response = await apiClient.get(`/conclusions/${requestId}`);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
|
||||
@ -115,7 +115,18 @@ export function mapToClaimManagementRequest(
|
||||
const creditNote = apiRequest.creditNote || apiRequest.credit_note || {};
|
||||
const completionExpenses = apiRequest.completionExpenses || apiRequest.completion_expenses || [];
|
||||
|
||||
// Raw claim details processed
|
||||
// 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,
|
||||
});
|
||||
|
||||
// Map activity information (matching ActivityInformationCard expectations)
|
||||
// Handle both camelCase and snake_case field names from Sequelize
|
||||
@ -126,7 +137,14 @@ export function mapToClaimManagementRequest(
|
||||
const activityType = claimDetails.activityType || claimDetails.activity_type || '';
|
||||
const location = claimDetails.location || '';
|
||||
|
||||
// Activity fields mapped
|
||||
console.debug('[claimDataMapper] Mapped activity fields:', {
|
||||
activityName,
|
||||
activityType,
|
||||
location,
|
||||
hasActivityName: !!activityName,
|
||||
hasActivityType: !!activityType,
|
||||
hasLocation: !!location,
|
||||
});
|
||||
|
||||
// Get budget values from budgetTracking table (new source of truth)
|
||||
const estimatedBudget = budgetTracking.proposalEstimatedBudget ||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user