after altering the tables rechecked the all steps working as expected
This commit is contained in:
parent
69c7e99d18
commit
a4f9962c38
@ -264,6 +264,19 @@ export function useRequestDetails(
|
|||||||
proposalDetails = claimData.proposalDetails || claimData.proposal_details;
|
proposalDetails = claimData.proposalDetails || claimData.proposal_details;
|
||||||
completionDetails = claimData.completionDetails || claimData.completion_details;
|
completionDetails = claimData.completionDetails || claimData.completion_details;
|
||||||
internalOrder = claimData.internalOrder || claimData.internal_order || null;
|
internalOrder = claimData.internalOrder || claimData.internal_order || null;
|
||||||
|
// New normalized tables
|
||||||
|
const budgetTracking = claimData.budgetTracking || claimData.budget_tracking || null;
|
||||||
|
const invoice = claimData.invoice || null;
|
||||||
|
const creditNote = claimData.creditNote || claimData.credit_note || null;
|
||||||
|
const completionExpenses = claimData.completionExpenses || claimData.completion_expenses || null;
|
||||||
|
|
||||||
|
// Store new fields in claimDetails for backward compatibility and easy access
|
||||||
|
if (claimDetails) {
|
||||||
|
(claimDetails as any).budgetTracking = budgetTracking;
|
||||||
|
(claimDetails as any).invoice = invoice;
|
||||||
|
(claimDetails as any).creditNote = creditNote;
|
||||||
|
(claimDetails as any).completionExpenses = completionExpenses;
|
||||||
|
}
|
||||||
|
|
||||||
console.debug('[useRequestDetails] Extracted details:', {
|
console.debug('[useRequestDetails] Extracted details:', {
|
||||||
claimDetails: claimDetails ? {
|
claimDetails: claimDetails ? {
|
||||||
@ -278,6 +291,10 @@ export function useRequestDetails(
|
|||||||
hasProposalDetails: !!proposalDetails,
|
hasProposalDetails: !!proposalDetails,
|
||||||
hasCompletionDetails: !!completionDetails,
|
hasCompletionDetails: !!completionDetails,
|
||||||
hasInternalOrder: !!internalOrder,
|
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');
|
||||||
@ -337,6 +354,11 @@ export function useRequestDetails(
|
|||||||
proposalDetails: proposalDetails || null,
|
proposalDetails: proposalDetails || null,
|
||||||
completionDetails: completionDetails || null,
|
completionDetails: completionDetails || null,
|
||||||
internalOrder: internalOrder || null,
|
internalOrder: internalOrder || null,
|
||||||
|
// New normalized tables (also available via claimDetails for backward compatibility)
|
||||||
|
budgetTracking: (claimDetails as any)?.budgetTracking || null,
|
||||||
|
invoice: (claimDetails as any)?.invoice || null,
|
||||||
|
creditNote: (claimDetails as any)?.creditNote || null,
|
||||||
|
completionExpenses: (claimDetails as any)?.completionExpenses || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
setApiRequest(updatedRequest);
|
setApiRequest(updatedRequest);
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import {
|
|||||||
ShieldX,
|
ShieldX,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
|
DollarSign,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
|
||||||
@ -51,7 +52,9 @@ import { DocumentsTab } from './components/tabs/DocumentsTab';
|
|||||||
import { ActivityTab } from './components/tabs/ActivityTab';
|
import { ActivityTab } from './components/tabs/ActivityTab';
|
||||||
import { WorkNotesTab } from './components/tabs/WorkNotesTab';
|
import { WorkNotesTab } from './components/tabs/WorkNotesTab';
|
||||||
import { SummaryTab } from './components/tabs/SummaryTab';
|
import { SummaryTab } from './components/tabs/SummaryTab';
|
||||||
|
import { IOTab } from './components/tabs/IOTab';
|
||||||
import { isClaimManagementRequest } from '@/utils/claimRequestUtils';
|
import { isClaimManagementRequest } from '@/utils/claimRequestUtils';
|
||||||
|
import { determineUserRole } from '@/utils/claimDataMapper';
|
||||||
import { QuickActionsSidebar } from './components/QuickActionsSidebar';
|
import { QuickActionsSidebar } from './components/QuickActionsSidebar';
|
||||||
import { RequestDetailModals } from './components/RequestDetailModals';
|
import { RequestDetailModals } from './components/RequestDetailModals';
|
||||||
import { RequestDetailProps } from './types/requestDetail.types';
|
import { RequestDetailProps } from './types/requestDetail.types';
|
||||||
@ -133,6 +136,42 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
|
|||||||
accessDenied,
|
accessDenied,
|
||||||
} = useRequestDetails(requestIdentifier, dynamicRequests, user);
|
} = useRequestDetails(requestIdentifier, dynamicRequests, user);
|
||||||
|
|
||||||
|
// Determine if user is initiator (from overview tab initiator info)
|
||||||
|
const currentUserId = (user as any)?.userId || '';
|
||||||
|
const currentUserEmail = (user as any)?.email?.toLowerCase() || '';
|
||||||
|
const initiatorUserId = apiRequest?.initiator?.userId;
|
||||||
|
const initiatorEmail = apiRequest?.initiator?.email?.toLowerCase();
|
||||||
|
const isUserInitiator = apiRequest?.initiator && (
|
||||||
|
(initiatorUserId && initiatorUserId === currentUserId) ||
|
||||||
|
(initiatorEmail && initiatorEmail === currentUserEmail)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Determine if user is department lead (whoever is in step 3 / approval level 3)
|
||||||
|
const approvalLevels = apiRequest?.approvalLevels || [];
|
||||||
|
const step3Level = approvalLevels.find((level: any) =>
|
||||||
|
(level.levelNumber || level.level_number) === 3
|
||||||
|
);
|
||||||
|
const deptLeadUserId = step3Level?.approverId || step3Level?.approver?.userId;
|
||||||
|
const isDeptLead = deptLeadUserId && deptLeadUserId === currentUserId;
|
||||||
|
|
||||||
|
// Check if IO tab should be visible (for initiator and department lead in claim management requests)
|
||||||
|
const showIOTab = isClaimManagementRequest(apiRequest) &&
|
||||||
|
(isUserInitiator || isDeptLead);
|
||||||
|
|
||||||
|
// Debug logging for troubleshooting
|
||||||
|
console.debug('[RequestDetail] IO Tab visibility:', {
|
||||||
|
isClaimManagement: isClaimManagementRequest(apiRequest),
|
||||||
|
isUserInitiator,
|
||||||
|
isDeptLead,
|
||||||
|
currentUserId,
|
||||||
|
currentUserEmail,
|
||||||
|
initiatorUserId,
|
||||||
|
initiatorEmail,
|
||||||
|
step3Level: step3Level ? { levelNumber: step3Level.levelNumber || step3Level.level_number, approverId: step3Level.approverId || step3Level.approver?.userId } : null,
|
||||||
|
deptLeadUserId,
|
||||||
|
showIOTab,
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mergedMessages,
|
mergedMessages,
|
||||||
unreadWorkNotes,
|
unreadWorkNotes,
|
||||||
@ -433,6 +472,16 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
|
|||||||
<TrendingUp className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
<TrendingUp className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
||||||
<span className="truncate">Workflow</span>
|
<span className="truncate">Workflow</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
{showIOTab && (
|
||||||
|
<TabsTrigger
|
||||||
|
value="io"
|
||||||
|
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
|
||||||
|
data-testid="tab-io"
|
||||||
|
>
|
||||||
|
<DollarSign className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
||||||
|
<span className="truncate">IO</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
)}
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="documents"
|
value="documents"
|
||||||
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
|
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
|
||||||
@ -547,6 +596,16 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
|
|||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
{showIOTab && (
|
||||||
|
<TabsContent value="io" className="mt-0">
|
||||||
|
<IOTab
|
||||||
|
request={request}
|
||||||
|
apiRequest={apiRequest}
|
||||||
|
onRefresh={refreshDetails}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
|
||||||
<TabsContent value="documents" className="mt-0">
|
<TabsContent value="documents" className="mt-0">
|
||||||
<DocumentsTab
|
<DocumentsTab
|
||||||
request={request}
|
request={request}
|
||||||
@ -595,6 +654,7 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
|
|||||||
refreshTrigger={sharedRecipientsRefreshTrigger}
|
refreshTrigger={sharedRecipientsRefreshTrigger}
|
||||||
pausedByUserId={request?.pauseInfo?.pausedBy?.userId}
|
pausedByUserId={request?.pauseInfo?.pausedBy?.userId}
|
||||||
currentUserId={(user as any)?.userId}
|
currentUserId={(user as any)?.userId}
|
||||||
|
apiRequest={apiRequest}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
* Quick Actions Sidebar Component
|
* Quick Actions Sidebar Component
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||||
@ -10,6 +10,9 @@ import { UserPlus, Eye, CheckCircle, XCircle, Share2, Pause, Play, AlertCircle }
|
|||||||
import { getSharedRecipients, type SharedRecipient } from '@/services/summaryApi';
|
import { getSharedRecipients, type SharedRecipient } from '@/services/summaryApi';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import notificationApi, { type Notification } from '@/services/notificationApi';
|
import notificationApi, { type Notification } from '@/services/notificationApi';
|
||||||
|
import { ProcessDetailsCard } from './claim-cards';
|
||||||
|
import { isClaimManagementRequest } from '@/utils/claimRequestUtils';
|
||||||
|
import { determineUserRole, getRoleBasedVisibility, mapToClaimManagementRequest } from '@/utils/claimDataMapper';
|
||||||
|
|
||||||
interface QuickActionsSidebarProps {
|
interface QuickActionsSidebarProps {
|
||||||
request: any;
|
request: any;
|
||||||
@ -27,6 +30,8 @@ interface QuickActionsSidebarProps {
|
|||||||
refreshTrigger?: number; // Trigger to refresh shared recipients list
|
refreshTrigger?: number; // Trigger to refresh shared recipients list
|
||||||
pausedByUserId?: string; // User ID of the approver who paused (kept for backwards compatibility)
|
pausedByUserId?: string; // User ID of the approver who paused (kept for backwards compatibility)
|
||||||
currentUserId?: string; // Current user's ID (kept for backwards compatibility)
|
currentUserId?: string; // Current user's ID (kept for backwards compatibility)
|
||||||
|
apiRequest?: any;
|
||||||
|
onEditClaimAmount?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QuickActionsSidebar({
|
export function QuickActionsSidebar({
|
||||||
@ -43,6 +48,10 @@ export function QuickActionsSidebar({
|
|||||||
onRetrigger,
|
onRetrigger,
|
||||||
summaryId,
|
summaryId,
|
||||||
refreshTrigger,
|
refreshTrigger,
|
||||||
|
pausedByUserId: pausedByUserIdProp,
|
||||||
|
currentUserId: currentUserIdProp,
|
||||||
|
apiRequest,
|
||||||
|
onEditClaimAmount,
|
||||||
}: QuickActionsSidebarProps) {
|
}: QuickActionsSidebarProps) {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [sharedRecipients, setSharedRecipients] = useState<SharedRecipient[]>([]);
|
const [sharedRecipients, setSharedRecipients] = useState<SharedRecipient[]>([]);
|
||||||
@ -50,8 +59,8 @@ export function QuickActionsSidebar({
|
|||||||
const [hasRetriggerNotification, setHasRetriggerNotification] = useState(false);
|
const [hasRetriggerNotification, setHasRetriggerNotification] = useState(false);
|
||||||
const isClosed = request?.status === 'closed';
|
const isClosed = request?.status === 'closed';
|
||||||
const isPaused = request?.pauseInfo?.isPaused || false;
|
const isPaused = request?.pauseInfo?.isPaused || false;
|
||||||
const pausedByUserId = request?.pauseInfo?.pausedBy?.userId;
|
const pausedByUserId = pausedByUserIdProp || request?.pauseInfo?.pausedBy?.userId;
|
||||||
const currentUserId = (user as any)?.userId || '';
|
const currentUserId = currentUserIdProp || (user as any)?.userId || '';
|
||||||
|
|
||||||
// Both approver AND initiator can pause (when not already paused and not closed)
|
// Both approver AND initiator can pause (when not already paused and not closed)
|
||||||
const canPause = !isPaused && !isClosed && (currentApprovalLevel || isInitiator);
|
const canPause = !isPaused && !isClosed && (currentApprovalLevel || isInitiator);
|
||||||
@ -117,6 +126,16 @@ export function QuickActionsSidebar({
|
|||||||
fetchSharedRecipients();
|
fetchSharedRecipients();
|
||||||
}, [isClosed, summaryId, isInitiator, refreshTrigger]);
|
}, [isClosed, summaryId, isInitiator, refreshTrigger]);
|
||||||
|
|
||||||
|
// Claim details for sidebar (only for claim management requests)
|
||||||
|
const claimSidebarData = useMemo(() => {
|
||||||
|
if (!apiRequest || !isClaimManagementRequest(apiRequest)) return null;
|
||||||
|
const claimRequest = mapToClaimManagementRequest(apiRequest, currentUserId);
|
||||||
|
if (!claimRequest) return null;
|
||||||
|
const userRole = determineUserRole(apiRequest, currentUserId);
|
||||||
|
const visibility = getRoleBasedVisibility(userRole);
|
||||||
|
return { claimRequest, visibility };
|
||||||
|
}, [apiRequest, currentUserId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 sm:space-y-6">
|
<div className="space-y-4 sm:space-y-6">
|
||||||
{/* Quick Actions Card - Hide entire card for spectators and closed requests */}
|
{/* Quick Actions Card - Hide entire card for spectators and closed requests */}
|
||||||
@ -339,6 +358,19 @@ export function QuickActionsSidebar({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Process details anchored at the bottom of the action sidebar for claim workflows */}
|
||||||
|
{claimSidebarData && (
|
||||||
|
<ProcessDetailsCard
|
||||||
|
ioDetails={claimSidebarData.claimRequest.ioDetails}
|
||||||
|
dmsDetails={claimSidebarData.claimRequest.dmsDetails}
|
||||||
|
claimAmount={claimSidebarData.claimRequest.claimAmount}
|
||||||
|
estimatedBudgetBreakdown={claimSidebarData.claimRequest.proposalDetails?.costBreakup}
|
||||||
|
closedExpensesBreakdown={claimSidebarData.claimRequest.activityInfo?.closedExpensesBreakdown}
|
||||||
|
visibility={claimSidebarData.visibility}
|
||||||
|
onEditClaimAmount={onEditClaimAmount}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -105,8 +105,8 @@ export function ActivityInformationCard({ activityInfo, className }: ActivityInf
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Closed Expenses */}
|
{/* Closed Expenses - Show if value exists (including 0) */}
|
||||||
{activityInfo.closedExpenses !== undefined && (
|
{activityInfo.closedExpenses !== undefined && activityInfo.closedExpenses !== null && (
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-medium text-gray-500 uppercase tracking-wide">
|
<label className="text-xs font-medium text-gray-500 uppercase tracking-wide">
|
||||||
Closed Expenses
|
Closed Expenses
|
||||||
|
|||||||
@ -8,15 +8,44 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Activity, Receipt, DollarSign, Pen } from 'lucide-react';
|
import { Activity, Receipt, DollarSign, Pen } from 'lucide-react';
|
||||||
import {
|
|
||||||
IODetails,
|
|
||||||
DMSDetails,
|
|
||||||
ClaimAmountDetails,
|
|
||||||
CostBreakdownItem,
|
|
||||||
RoleBasedVisibility,
|
|
||||||
} from '../../types/claimManagement.types';
|
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
|
// Local minimal types to avoid external dependency issues
|
||||||
|
interface IODetails {
|
||||||
|
ioNumber?: string;
|
||||||
|
remarks?: string;
|
||||||
|
availableBalance?: number;
|
||||||
|
blockedAmount?: number;
|
||||||
|
remainingBalance?: number;
|
||||||
|
blockedByName?: string;
|
||||||
|
blockedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DMSDetails {
|
||||||
|
dmsNumber?: string;
|
||||||
|
remarks?: string;
|
||||||
|
createdByName?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClaimAmountDetails {
|
||||||
|
amount: number;
|
||||||
|
lastUpdatedBy?: string;
|
||||||
|
lastUpdatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CostBreakdownItem {
|
||||||
|
description: string;
|
||||||
|
amount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RoleBasedVisibility {
|
||||||
|
showIODetails: boolean;
|
||||||
|
showDMSDetails: boolean;
|
||||||
|
showClaimAmount: boolean;
|
||||||
|
canEditClaimAmount: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface ProcessDetailsCardProps {
|
interface ProcessDetailsCardProps {
|
||||||
ioDetails?: IODetails;
|
ioDetails?: IODetails;
|
||||||
dmsDetails?: DMSDetails;
|
dmsDetails?: DMSDetails;
|
||||||
@ -38,29 +67,34 @@ export function ProcessDetailsCard({
|
|||||||
onEditClaimAmount,
|
onEditClaimAmount,
|
||||||
className,
|
className,
|
||||||
}: ProcessDetailsCardProps) {
|
}: ProcessDetailsCardProps) {
|
||||||
const formatCurrency = (amount: number) => {
|
const formatCurrency = (amount?: number | null) => {
|
||||||
|
if (amount === undefined || amount === null || Number.isNaN(amount)) {
|
||||||
|
return '₹0.00';
|
||||||
|
}
|
||||||
return `₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
return `₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
const formatDate = (dateString?: string | null) => {
|
||||||
|
if (!dateString) return '';
|
||||||
try {
|
try {
|
||||||
return format(new Date(dateString), 'MMM d, yyyy, h:mm a');
|
return format(new Date(dateString), 'MMM d, yyyy, h:mm a');
|
||||||
} catch {
|
} catch {
|
||||||
return dateString;
|
return dateString || '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateTotal = (items: CostBreakdownItem[]) => {
|
const calculateTotal = (items?: CostBreakdownItem[]) => {
|
||||||
return items.reduce((sum, item) => sum + item.amount, 0);
|
if (!items || items.length === 0) return 0;
|
||||||
|
return items.reduce((sum, item) => sum + (item.amount ?? 0), 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Don't render if nothing to show
|
// Don't render if nothing to show
|
||||||
const hasContent =
|
const hasContent =
|
||||||
(visibility.showIODetails && ioDetails) ||
|
(visibility.showIODetails && ioDetails) ||
|
||||||
(visibility.showDMSDetails && dmsDetails) ||
|
(visibility.showDMSDetails && dmsDetails) ||
|
||||||
(visibility.showClaimAmount && claimAmount) ||
|
(visibility.showClaimAmount && claimAmount && claimAmount.amount !== undefined && claimAmount.amount !== null) ||
|
||||||
estimatedBudgetBreakdown ||
|
(estimatedBudgetBreakdown && estimatedBudgetBreakdown.length > 0) ||
|
||||||
closedExpensesBreakdown;
|
(closedExpensesBreakdown && closedExpensesBreakdown.length > 0);
|
||||||
|
|
||||||
if (!hasContent) {
|
if (!hasContent) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -5,32 +5,50 @@
|
|||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
import { Receipt, Calendar } from 'lucide-react';
|
import { Receipt, Calendar } from 'lucide-react';
|
||||||
import { ProposalDetails } from '../../types/claimManagement.types';
|
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
|
// Minimal local types to avoid missing imports during runtime
|
||||||
|
interface ProposalCostItem {
|
||||||
|
description: string;
|
||||||
|
amount?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProposalDetails {
|
||||||
|
costBreakup: ProposalCostItem[];
|
||||||
|
estimatedBudgetTotal?: number | null;
|
||||||
|
timelineForClosure?: string | null;
|
||||||
|
dealerComments?: string | null;
|
||||||
|
submittedOn?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
interface ProposalDetailsCardProps {
|
interface ProposalDetailsCardProps {
|
||||||
proposalDetails: ProposalDetails;
|
proposalDetails: ProposalDetails;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProposalDetailsCard({ proposalDetails, className }: ProposalDetailsCardProps) {
|
export function ProposalDetailsCard({ proposalDetails, className }: ProposalDetailsCardProps) {
|
||||||
const formatCurrency = (amount: number) => {
|
const formatCurrency = (amount?: number | null) => {
|
||||||
|
if (amount === undefined || amount === null || Number.isNaN(amount)) {
|
||||||
|
return '₹0.00';
|
||||||
|
}
|
||||||
return `₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
return `₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
const formatDate = (dateString?: string | null) => {
|
||||||
|
if (!dateString) return '';
|
||||||
try {
|
try {
|
||||||
return format(new Date(dateString), 'MMM d, yyyy, h:mm a');
|
return format(new Date(dateString), 'MMM d, yyyy, h:mm a');
|
||||||
} catch {
|
} catch {
|
||||||
return dateString;
|
return dateString || '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatTimelineDate = (dateString: string) => {
|
const formatTimelineDate = (dateString?: string | null) => {
|
||||||
|
if (!dateString) return '-';
|
||||||
try {
|
try {
|
||||||
return format(new Date(dateString), 'MMM d, yyyy');
|
return format(new Date(dateString), 'MMM d, yyyy');
|
||||||
} catch {
|
} catch {
|
||||||
return dateString;
|
return dateString || '-';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -66,7 +84,7 @@ export function ProposalDetailsCard({ proposalDetails, className }: ProposalDeta
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody className="divide-y divide-gray-200">
|
||||||
{proposalDetails.costBreakup.map((item, index) => (
|
{(proposalDetails.costBreakup || []).map((item, index) => (
|
||||||
<tr key={index} className="hover:bg-gray-50">
|
<tr key={index} className="hover:bg-gray-50">
|
||||||
<td className="px-4 py-3 text-sm text-gray-900">
|
<td className="px-4 py-3 text-sm text-gray-900">
|
||||||
{item.description}
|
{item.description}
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import {
|
|||||||
ActivityInformationCard,
|
ActivityInformationCard,
|
||||||
DealerInformationCard,
|
DealerInformationCard,
|
||||||
ProposalDetailsCard,
|
ProposalDetailsCard,
|
||||||
ProcessDetailsCard,
|
|
||||||
RequestInitiatorCard,
|
RequestInitiatorCard,
|
||||||
} from '../claim-cards';
|
} from '../claim-cards';
|
||||||
import { isClaimManagementRequest } from '@/utils/claimRequestUtils';
|
import { isClaimManagementRequest } from '@/utils/claimRequestUtils';
|
||||||
@ -70,6 +69,10 @@ export function ClaimManagementOverviewTab({
|
|||||||
activityInfo: claimRequest.activityInfo,
|
activityInfo: claimRequest.activityInfo,
|
||||||
dealerInfo: claimRequest.dealerInfo,
|
dealerInfo: claimRequest.dealerInfo,
|
||||||
hasProposalDetails: !!claimRequest.proposalDetails,
|
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
|
||||||
@ -82,50 +85,35 @@ export function ClaimManagementOverviewTab({
|
|||||||
userRole,
|
userRole,
|
||||||
visibility,
|
visibility,
|
||||||
currentUserId,
|
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.
|
||||||
const initiatorInfo = {
|
const initiatorInfo = {
|
||||||
name: apiRequest.requestedBy?.name || apiRequest.createdByName || 'Unknown',
|
name: apiRequest.initiator?.name || apiRequest.initiator?.displayName || apiRequest.initiator?.email || 'Unknown',
|
||||||
role: 'initiator',
|
role: apiRequest.initiator?.role || apiRequest.initiator?.designation || 'Initiator',
|
||||||
department: apiRequest.requestedBy?.department || apiRequest.department || '',
|
department: apiRequest.initiator?.department || apiRequest.department || '',
|
||||||
email: apiRequest.requestedBy?.email || 'N/A',
|
email: apiRequest.initiator?.email || 'N/A',
|
||||||
phone: apiRequest.requestedBy?.phone || apiRequest.requestedBy?.mobile,
|
phone: apiRequest.initiator?.phone || apiRequest.initiator?.mobile,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`grid grid-cols-1 lg:grid-cols-3 gap-6 ${className}`}>
|
<div className={`space-y-6 ${className}`}>
|
||||||
{/* Left Column: Main Information Cards */}
|
{/* Activity Information - Always visible */}
|
||||||
<div className="lg:col-span-2 space-y-6">
|
<ActivityInformationCard activityInfo={claimRequest.activityInfo} />
|
||||||
{/* Activity Information - Always visible */}
|
|
||||||
<ActivityInformationCard activityInfo={claimRequest.activityInfo} />
|
|
||||||
|
|
||||||
{/* Dealer Information - Visible based on role */}
|
{/* Dealer Information - Always visible */}
|
||||||
{visibility.showDealerInfo && (
|
<DealerInformationCard dealerInfo={claimRequest.dealerInfo} />
|
||||||
<DealerInformationCard dealerInfo={claimRequest.dealerInfo} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Proposal Details - Only shown after dealer submits proposal */}
|
{/* Proposal Details - Only shown after dealer submits proposal */}
|
||||||
{visibility.showProposalDetails && claimRequest.proposalDetails && (
|
{visibility.showProposalDetails && claimRequest.proposalDetails && (
|
||||||
<ProposalDetailsCard proposalDetails={claimRequest.proposalDetails} />
|
<ProposalDetailsCard proposalDetails={claimRequest.proposalDetails} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Request Initiator */}
|
{/* Request Initiator */}
|
||||||
<RequestInitiatorCard initiatorInfo={initiatorInfo} />
|
<RequestInitiatorCard initiatorInfo={initiatorInfo} />
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Column: Process Details Sidebar */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
<ProcessDetailsCard
|
|
||||||
ioDetails={claimRequest.ioDetails}
|
|
||||||
dmsDetails={claimRequest.dmsDetails}
|
|
||||||
claimAmount={claimRequest.claimAmount}
|
|
||||||
estimatedBudgetBreakdown={claimRequest.proposalDetails?.costBreakup}
|
|
||||||
closedExpensesBreakdown={claimRequest.activityInfo.closedExpensesBreakdown}
|
|
||||||
visibility={visibility}
|
|
||||||
onEditClaimAmount={onEditClaimAmount}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1099,7 +1099,8 @@ export function DealerClaimWorkflowTab({
|
|||||||
}
|
}
|
||||||
// Call API to push to DMS (this will auto-generate e-invoice)
|
// Call API to push to DMS (this will auto-generate e-invoice)
|
||||||
// eInvoiceDate is required, so we pass current date
|
// eInvoiceDate is required, so we pass current date
|
||||||
const today = new Date().toISOString().split('T')[0];
|
// Use slice to avoid undefined with strict index checks
|
||||||
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
await updateEInvoice(requestId as string, {
|
await updateEInvoice(requestId as string, {
|
||||||
eInvoiceDate: today,
|
eInvoiceDate: today,
|
||||||
});
|
});
|
||||||
@ -1213,9 +1214,26 @@ export function DealerClaimWorkflowTab({
|
|||||||
toast.info('Send to dealer functionality will be implemented');
|
toast.info('Send to dealer functionality will be implemented');
|
||||||
}}
|
}}
|
||||||
creditNoteData={{
|
creditNoteData={{
|
||||||
creditNoteNumber: (request as any)?.claimDetails?.creditNoteNumber || (request as any)?.claimDetails?.credit_note_number,
|
creditNoteNumber: (request as any)?.creditNote?.creditNoteNumber ||
|
||||||
creditNoteDate: (request as any)?.claimDetails?.creditNoteDate || (request as any)?.claimDetails?.credit_note_date,
|
(request as any)?.creditNote?.credit_note_number ||
|
||||||
creditNoteAmount: (request as any)?.claimDetails?.creditNoteAmount || (request as any)?.claimDetails?.credit_note_amount,
|
(request as any)?.claimDetails?.creditNote?.creditNoteNumber ||
|
||||||
|
(request as any)?.claimDetails?.creditNoteNumber ||
|
||||||
|
(request as any)?.claimDetails?.credit_note_number,
|
||||||
|
creditNoteDate: (request as any)?.creditNote?.creditNoteDate ||
|
||||||
|
(request as any)?.creditNote?.credit_note_date ||
|
||||||
|
(request as any)?.claimDetails?.creditNote?.creditNoteDate ||
|
||||||
|
(request as any)?.claimDetails?.creditNoteDate ||
|
||||||
|
(request as any)?.claimDetails?.credit_note_date,
|
||||||
|
creditNoteAmount: (request as any)?.creditNote?.creditNoteAmount ?
|
||||||
|
Number((request as any)?.creditNote?.creditNoteAmount) :
|
||||||
|
((request as any)?.creditNote?.credit_note_amount ?
|
||||||
|
Number((request as any)?.creditNote?.credit_note_amount) :
|
||||||
|
((request as any)?.claimDetails?.creditNote?.creditNoteAmount ?
|
||||||
|
Number((request as any)?.claimDetails?.creditNote?.creditNoteAmount) :
|
||||||
|
((request as any)?.claimDetails?.creditNoteAmount ?
|
||||||
|
Number((request as any)?.claimDetails?.creditNoteAmount) :
|
||||||
|
((request as any)?.claimDetails?.credit_note_amount ?
|
||||||
|
Number((request as any)?.claimDetails?.credit_note_amount) : undefined)))),
|
||||||
status: 'APPROVED',
|
status: 'APPROVED',
|
||||||
}}
|
}}
|
||||||
dealerInfo={{
|
dealerInfo={{
|
||||||
|
|||||||
@ -15,16 +15,7 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { DollarSign, Download, CircleCheckBig, AlertCircle } from 'lucide-react';
|
import { DollarSign, Download, CircleCheckBig, AlertCircle } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { mockApi } from '@/services/mockApi';
|
import { updateIODetails, getClaimDetails } from '@/services/dealerClaimApi';
|
||||||
|
|
||||||
// Helper to extract data from API response and handle errors
|
|
||||||
const handleApiResponse = <T>(response: any): T => {
|
|
||||||
if (!response.success) {
|
|
||||||
const errorMsg = response.error?.message || 'Operation failed';
|
|
||||||
throw new Error(errorMsg);
|
|
||||||
}
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IOTabProps {
|
interface IOTabProps {
|
||||||
request: any;
|
request: any;
|
||||||
@ -43,41 +34,46 @@ interface IOBlockedDetails {
|
|||||||
|
|
||||||
export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||||
const requestId = apiRequest?.requestId || request?.requestId;
|
const requestId = apiRequest?.requestId || request?.requestId;
|
||||||
const [ioNumber, setIoNumber] = useState(request?.ioNumber || '');
|
|
||||||
|
// Load existing IO data from apiRequest or request
|
||||||
|
const internalOrder = apiRequest?.internalOrder || apiRequest?.internal_order || null;
|
||||||
|
const existingIONumber = internalOrder?.ioNumber || internalOrder?.io_number || request?.ioNumber || '';
|
||||||
|
const existingBlockedAmount = internalOrder?.ioBlockedAmount || internalOrder?.io_blocked_amount || 0;
|
||||||
|
const existingAvailableBalance = internalOrder?.ioAvailableBalance || internalOrder?.io_available_balance || 0;
|
||||||
|
const existingRemainingBalance = internalOrder?.ioRemainingBalance || internalOrder?.io_remaining_balance || 0;
|
||||||
|
const sapDocNumber = internalOrder?.sapDocumentNumber || internalOrder?.sap_document_number || '';
|
||||||
|
|
||||||
|
const [ioNumber, setIoNumber] = useState(existingIONumber);
|
||||||
const [fetchingAmount, setFetchingAmount] = useState(false);
|
const [fetchingAmount, setFetchingAmount] = useState(false);
|
||||||
const [fetchedAmount, setFetchedAmount] = useState<number | null>(null);
|
const [fetchedAmount, setFetchedAmount] = useState<number | null>(null);
|
||||||
const [blockedDetails, setBlockedDetails] = useState<IOBlockedDetails | null>(null);
|
const [blockedDetails, setBlockedDetails] = useState<IOBlockedDetails | null>(null);
|
||||||
const [blockingBudget, setBlockingBudget] = useState(false);
|
const [blockingBudget, setBlockingBudget] = useState(false);
|
||||||
|
|
||||||
// Load existing IO block from mock API
|
// Load existing IO block details from apiRequest
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (requestId) {
|
if (internalOrder && existingIONumber) {
|
||||||
mockApi.getIOBlock(requestId).then(response => {
|
setBlockedDetails({
|
||||||
try {
|
ioNumber: existingIONumber,
|
||||||
const ioBlock = handleApiResponse<any>(response);
|
blockedAmount: Number(existingBlockedAmount) || 0,
|
||||||
if (ioBlock) {
|
availableBalance: Number(existingAvailableBalance) || 0,
|
||||||
setBlockedDetails({
|
blockedDate: internalOrder.organizedAt || internalOrder.organized_at || new Date().toISOString(),
|
||||||
ioNumber: ioBlock.ioNumber,
|
sapDocumentNumber: sapDocNumber,
|
||||||
blockedAmount: ioBlock.blockedAmount,
|
status: (internalOrder.status === 'BLOCKED' ? 'blocked' :
|
||||||
availableBalance: ioBlock.availableBalance,
|
internalOrder.status === 'RELEASED' ? 'released' : 'blocked') as 'blocked' | 'released' | 'failed',
|
||||||
blockedDate: ioBlock.blockedDate,
|
|
||||||
sapDocumentNumber: ioBlock.sapDocumentNumber,
|
|
||||||
status: ioBlock.status,
|
|
||||||
});
|
|
||||||
setIoNumber(ioBlock.ioNumber);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// IO block not found is expected for new requests
|
|
||||||
console.debug('No IO block found for request:', requestId);
|
|
||||||
}
|
|
||||||
}).catch(error => {
|
|
||||||
console.warn('Error loading IO block:', error);
|
|
||||||
});
|
});
|
||||||
|
setIoNumber(existingIONumber);
|
||||||
|
|
||||||
|
// Set fetched amount if available balance exists
|
||||||
|
if (existingAvailableBalance > 0) {
|
||||||
|
setFetchedAmount(Number(existingAvailableBalance));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [requestId]);
|
}, [internalOrder, existingIONumber, existingBlockedAmount, existingAvailableBalance, existingRemainingBalance, sapDocNumber]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch available budget from SAP
|
* Fetch available budget from SAP
|
||||||
|
* Validates IO number and gets available balance
|
||||||
|
* Calls updateIODetails with blockedAmount=0 to validate without blocking
|
||||||
*/
|
*/
|
||||||
const handleFetchAmount = async () => {
|
const handleFetchAmount = async () => {
|
||||||
if (!ioNumber.trim()) {
|
if (!ioNumber.trim()) {
|
||||||
@ -85,23 +81,44 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!requestId) {
|
||||||
|
toast.error('Request ID not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setFetchingAmount(true);
|
setFetchingAmount(true);
|
||||||
try {
|
try {
|
||||||
// TODO: Replace with actual SAP API integration
|
// Validate IO number by calling updateIODetails with blockedAmount=0
|
||||||
// const response = await fetch(`/api/sap/io/${ioNumber}/budget`);
|
// This validates the IO with SAP and returns available balance without blocking
|
||||||
// const data = await response.json();
|
// The backend will validate the IO number and return the availableBalance from SAP
|
||||||
|
await updateIODetails(requestId, {
|
||||||
|
ioNumber: ioNumber.trim(),
|
||||||
|
ioAvailableBalance: 0, // Will be fetched from SAP validation by backend
|
||||||
|
ioBlockedAmount: 0, // No blocking, just validation
|
||||||
|
ioRemainingBalance: 0, // Will be calculated by backend
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch updated claim details to get the validated IO data
|
||||||
|
const claimData = await getClaimDetails(requestId);
|
||||||
|
const updatedInternalOrder = claimData?.internalOrder || claimData?.internal_order;
|
||||||
|
|
||||||
// Mock API call - simulate SAP integration
|
if (updatedInternalOrder) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
const availableBalance = Number(updatedInternalOrder.ioAvailableBalance || updatedInternalOrder.io_available_balance || 0);
|
||||||
|
if (availableBalance > 0) {
|
||||||
// Mock response
|
setFetchedAmount(availableBalance);
|
||||||
const mockAvailableAmount = 50000; // ₹50,000
|
toast.success(`IO validated successfully. Available balance: ₹${availableBalance.toLocaleString('en-IN')}`);
|
||||||
|
} else {
|
||||||
setFetchedAmount(mockAvailableAmount);
|
toast.error('No available balance found for this IO number');
|
||||||
toast.success('IO budget fetched successfully from SAP');
|
setFetchedAmount(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error('Failed to fetch IO details after validation');
|
||||||
|
setFetchedAmount(null);
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Failed to fetch IO budget:', error);
|
console.error('Failed to fetch IO budget:', error);
|
||||||
toast.error(error.message || 'Failed to fetch IO budget from SAP');
|
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to validate IO number or fetch budget from SAP';
|
||||||
|
toast.error(errorMessage);
|
||||||
setFetchedAmount(null);
|
setFetchedAmount(null);
|
||||||
} finally {
|
} finally {
|
||||||
setFetchingAmount(false);
|
setFetchingAmount(false);
|
||||||
@ -112,12 +129,23 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
|||||||
* Block budget in SAP system
|
* Block budget in SAP system
|
||||||
*/
|
*/
|
||||||
const handleBlockBudget = async () => {
|
const handleBlockBudget = async () => {
|
||||||
if (!ioNumber.trim() || !fetchedAmount) {
|
if (!ioNumber.trim() || fetchedAmount === null) {
|
||||||
toast.error('Please fetch IO amount first');
|
toast.error('Please fetch IO amount first');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const claimAmount = request?.claimAmount || request?.amount || 0;
|
if (!requestId) {
|
||||||
|
toast.error('Request ID not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get claim amount from budget tracking or proposal details
|
||||||
|
const claimAmount = apiRequest?.budgetTracking?.proposalEstimatedBudget ||
|
||||||
|
apiRequest?.proposalDetails?.totalEstimatedBudget ||
|
||||||
|
apiRequest?.claimDetails?.estimatedBudget ||
|
||||||
|
request?.claimAmount ||
|
||||||
|
request?.amount ||
|
||||||
|
0;
|
||||||
|
|
||||||
if (claimAmount > fetchedAmount) {
|
if (claimAmount > fetchedAmount) {
|
||||||
toast.error('Claim amount exceeds available IO budget');
|
toast.error('Claim amount exceeds available IO budget');
|
||||||
@ -126,76 +154,41 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
|||||||
|
|
||||||
setBlockingBudget(true);
|
setBlockingBudget(true);
|
||||||
try {
|
try {
|
||||||
// TODO: Replace with actual SAP API integration
|
// Call updateIODetails with blockedAmount to block budget in SAP
|
||||||
// const response = await fetch(`/api/sap/io/block`, {
|
await updateIODetails(requestId, {
|
||||||
// method: 'POST',
|
ioNumber: ioNumber.trim(),
|
||||||
// headers: { 'Content-Type': 'application/json' },
|
ioAvailableBalance: fetchedAmount,
|
||||||
// body: JSON.stringify({
|
ioBlockedAmount: claimAmount,
|
||||||
// ioNumber,
|
ioRemainingBalance: fetchedAmount - claimAmount,
|
||||||
// amount: claimAmount,
|
});
|
||||||
// requestId: apiRequest?.requestId,
|
|
||||||
// }),
|
|
||||||
// });
|
|
||||||
// const data = await response.json();
|
|
||||||
|
|
||||||
// Mock API call
|
// Fetch updated claim details to get the blocked IO data
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
const claimData = await getClaimDetails(requestId);
|
||||||
|
const updatedInternalOrder = claimData?.internalOrder || claimData?.internal_order;
|
||||||
|
|
||||||
// Mock blocked details
|
if (updatedInternalOrder) {
|
||||||
const blocked: IOBlockedDetails = {
|
const blocked: IOBlockedDetails = {
|
||||||
ioNumber,
|
ioNumber: updatedInternalOrder.ioNumber || updatedInternalOrder.io_number || ioNumber,
|
||||||
blockedAmount: claimAmount,
|
blockedAmount: Number(updatedInternalOrder.ioBlockedAmount || updatedInternalOrder.io_blocked_amount || claimAmount),
|
||||||
availableBalance: fetchedAmount - claimAmount,
|
availableBalance: Number(updatedInternalOrder.ioAvailableBalance || updatedInternalOrder.io_available_balance || fetchedAmount),
|
||||||
blockedDate: new Date().toISOString(),
|
blockedDate: updatedInternalOrder.organizedAt || updatedInternalOrder.organized_at || new Date().toISOString(),
|
||||||
sapDocumentNumber: `SAP-${Date.now()}`,
|
sapDocumentNumber: updatedInternalOrder.sapDocumentNumber || updatedInternalOrder.sap_document_number || '',
|
||||||
status: 'blocked',
|
status: 'blocked',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save to mock API
|
setBlockedDetails(blocked);
|
||||||
if (requestId) {
|
toast.success('IO budget blocked successfully in SAP');
|
||||||
try {
|
|
||||||
const ioBlockResponse = await mockApi.createIOBlock(requestId, {
|
// Refresh request details
|
||||||
id: `io-${Date.now()}`,
|
onRefresh?.();
|
||||||
ioNumber: blocked.ioNumber,
|
} else {
|
||||||
blockedAmount: blocked.blockedAmount,
|
toast.error('IO blocked but failed to fetch updated details');
|
||||||
availableBalance: blocked.availableBalance,
|
onRefresh?.();
|
||||||
blockedDate: blocked.blockedDate,
|
|
||||||
sapDocumentNumber: blocked.sapDocumentNumber,
|
|
||||||
status: blocked.status,
|
|
||||||
});
|
|
||||||
handleApiResponse(ioBlockResponse);
|
|
||||||
|
|
||||||
// Update request with IO number
|
|
||||||
const updateResponse = await mockApi.updateRequest(requestId, {
|
|
||||||
ioNumber: blocked.ioNumber,
|
|
||||||
ioBlockedAmount: blocked.blockedAmount,
|
|
||||||
sapDocumentNumber: blocked.sapDocumentNumber,
|
|
||||||
});
|
|
||||||
handleApiResponse(updateResponse);
|
|
||||||
|
|
||||||
// Create activity log
|
|
||||||
const activityResponse = await mockApi.createActivity(requestId, {
|
|
||||||
id: `act-${Date.now()}`,
|
|
||||||
type: 'io_blocked',
|
|
||||||
action: 'IO Budget Blocked',
|
|
||||||
details: `IO ${ioNumber} budget of ₹${claimAmount.toLocaleString('en-IN')} blocked in SAP`,
|
|
||||||
user: 'System',
|
|
||||||
message: `IO budget blocked: ${blocked.sapDocumentNumber}`,
|
|
||||||
});
|
|
||||||
handleApiResponse(activityResponse);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save IO block to database:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setBlockedDetails(blocked);
|
|
||||||
toast.success('IO budget blocked successfully in SAP');
|
|
||||||
|
|
||||||
// Refresh request details
|
|
||||||
onRefresh?.();
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Failed to block IO budget:', error);
|
console.error('Failed to block IO budget:', error);
|
||||||
toast.error(error.message || 'Failed to block IO budget in SAP');
|
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to block IO budget in SAP';
|
||||||
|
toast.error(errorMessage);
|
||||||
} finally {
|
} finally {
|
||||||
setBlockingBudget(false);
|
setBlockingBudget(false);
|
||||||
}
|
}
|
||||||
@ -203,60 +196,53 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Release blocked budget
|
* Release blocked budget
|
||||||
|
* Note: This functionality may need a separate backend endpoint for releasing IO budget
|
||||||
|
* For now, we'll call updateIODetails with blockedAmount=0 to release
|
||||||
*/
|
*/
|
||||||
const handleReleaseBudget = async () => {
|
const handleReleaseBudget = async () => {
|
||||||
if (!blockedDetails || !requestId) return;
|
if (!blockedDetails || !requestId) {
|
||||||
|
toast.error('No blocked budget to release');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ioNumber.trim()) {
|
||||||
|
toast.error('IO number not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: Replace with actual SAP API integration
|
// Release budget by setting blockedAmount to 0
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
// Note: Backend may need a dedicated release endpoint for proper SAP integration
|
||||||
|
await updateIODetails(requestId, {
|
||||||
// Update IO block in mock API
|
ioNumber: ioNumber.trim(),
|
||||||
try {
|
ioAvailableBalance: blockedDetails.availableBalance + blockedDetails.blockedAmount,
|
||||||
const ioBlockResponse = await mockApi.getIOBlock(requestId);
|
ioBlockedAmount: 0,
|
||||||
const ioBlock = handleApiResponse<any>(ioBlockResponse);
|
ioRemainingBalance: blockedDetails.availableBalance + blockedDetails.blockedAmount,
|
||||||
if (ioBlock) {
|
});
|
||||||
const updateIOResponse = await mockApi.updateIOBlock(requestId, {
|
|
||||||
status: 'released',
|
|
||||||
releasedDate: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
handleApiResponse(updateIOResponse);
|
|
||||||
|
|
||||||
// Update request
|
|
||||||
const updateRequestResponse = await mockApi.updateRequest(requestId, {
|
|
||||||
ioBlockedAmount: null,
|
|
||||||
sapDocumentNumber: null,
|
|
||||||
});
|
|
||||||
handleApiResponse(updateRequestResponse);
|
|
||||||
|
|
||||||
// Create activity log
|
|
||||||
const activityResponse = await mockApi.createActivity(requestId, {
|
|
||||||
id: `act-${Date.now()}`,
|
|
||||||
type: 'io_released',
|
|
||||||
action: 'IO Budget Released',
|
|
||||||
details: `IO ${blockedDetails.ioNumber} budget released`,
|
|
||||||
user: 'System',
|
|
||||||
message: 'IO budget released',
|
|
||||||
});
|
|
||||||
handleApiResponse(activityResponse);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to update IO block in database:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Clear local state
|
||||||
setBlockedDetails(null);
|
setBlockedDetails(null);
|
||||||
setFetchedAmount(null);
|
setFetchedAmount(null);
|
||||||
setIoNumber('');
|
setIoNumber('');
|
||||||
|
|
||||||
toast.success('IO budget released successfully');
|
toast.success('IO budget released successfully');
|
||||||
|
|
||||||
|
// Refresh request details
|
||||||
onRefresh?.();
|
onRefresh?.();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Failed to release IO budget:', error);
|
console.error('Failed to release IO budget:', error);
|
||||||
toast.error(error.message || 'Failed to release IO budget');
|
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to release IO budget';
|
||||||
|
toast.error(errorMessage);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const claimAmount = request?.claimAmount || request?.amount || 0;
|
// Get claim amount for display purposes (from budget tracking or proposal details)
|
||||||
|
const claimAmount = apiRequest?.budgetTracking?.proposalEstimatedBudget ||
|
||||||
|
apiRequest?.proposalDetails?.totalEstimatedBudget ||
|
||||||
|
apiRequest?.claimDetails?.estimatedBudget ||
|
||||||
|
request?.claimAmount ||
|
||||||
|
request?.amount ||
|
||||||
|
0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
|||||||
@ -108,6 +108,12 @@ export function mapToClaimManagementRequest(
|
|||||||
const proposalDetails = apiRequest.proposalDetails || {};
|
const proposalDetails = apiRequest.proposalDetails || {};
|
||||||
const completionDetails = apiRequest.completionDetails || {};
|
const completionDetails = apiRequest.completionDetails || {};
|
||||||
const internalOrder = apiRequest.internalOrder || apiRequest.internal_order || {};
|
const internalOrder = apiRequest.internalOrder || apiRequest.internal_order || {};
|
||||||
|
|
||||||
|
// Extract new normalized tables
|
||||||
|
const budgetTracking = apiRequest.budgetTracking || apiRequest.budget_tracking || {};
|
||||||
|
const invoice = apiRequest.invoice || {};
|
||||||
|
const creditNote = apiRequest.creditNote || apiRequest.credit_note || {};
|
||||||
|
const completionExpenses = apiRequest.completionExpenses || apiRequest.completion_expenses || [];
|
||||||
|
|
||||||
// Debug: Log raw claim details to help troubleshoot
|
// Debug: Log raw claim details to help troubleshoot
|
||||||
console.debug('[claimDataMapper] Raw claimDetails:', claimDetails);
|
console.debug('[claimDataMapper] Raw claimDetails:', claimDetails);
|
||||||
@ -115,6 +121,10 @@ export function mapToClaimManagementRequest(
|
|||||||
hasClaimDetails: !!apiRequest.claimDetails,
|
hasClaimDetails: !!apiRequest.claimDetails,
|
||||||
hasProposalDetails: !!apiRequest.proposalDetails,
|
hasProposalDetails: !!apiRequest.proposalDetails,
|
||||||
hasCompletionDetails: !!apiRequest.completionDetails,
|
hasCompletionDetails: !!apiRequest.completionDetails,
|
||||||
|
hasBudgetTracking: !!budgetTracking,
|
||||||
|
hasInvoice: !!invoice,
|
||||||
|
hasCreditNote: !!creditNote,
|
||||||
|
hasCompletionExpenses: Array.isArray(completionExpenses) && completionExpenses.length > 0,
|
||||||
workflowType: apiRequest.workflowType,
|
workflowType: apiRequest.workflowType,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -136,6 +146,37 @@ export function mapToClaimManagementRequest(
|
|||||||
hasLocation: !!location,
|
hasLocation: !!location,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get budget values from budgetTracking table (new source of truth)
|
||||||
|
const estimatedBudget = budgetTracking.proposalEstimatedBudget ||
|
||||||
|
budgetTracking.proposal_estimated_budget ||
|
||||||
|
budgetTracking.initialEstimatedBudget ||
|
||||||
|
budgetTracking.initial_estimated_budget ||
|
||||||
|
claimDetails.estimatedBudget ||
|
||||||
|
claimDetails.estimated_budget;
|
||||||
|
|
||||||
|
// Get closed expenses - check multiple sources with proper number conversion
|
||||||
|
const closedExpensesRaw = budgetTracking?.closedExpenses ||
|
||||||
|
budgetTracking?.closed_expenses ||
|
||||||
|
completionDetails?.totalClosedExpenses ||
|
||||||
|
completionDetails?.total_closed_expenses ||
|
||||||
|
claimDetails?.closedExpenses ||
|
||||||
|
claimDetails?.closed_expenses;
|
||||||
|
// Convert to number and handle 0 as valid value
|
||||||
|
const closedExpenses = closedExpensesRaw !== null && closedExpensesRaw !== undefined
|
||||||
|
? Number(closedExpensesRaw)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// Get closed expenses breakdown from new completionExpenses table
|
||||||
|
const closedExpensesBreakdown = Array.isArray(completionExpenses) && completionExpenses.length > 0
|
||||||
|
? completionExpenses.map((exp: any) => ({
|
||||||
|
description: exp.description || exp.itemDescription || '',
|
||||||
|
amount: Number(exp.amount) || 0
|
||||||
|
}))
|
||||||
|
: (completionDetails?.closedExpenses ||
|
||||||
|
completionDetails?.closed_expenses ||
|
||||||
|
completionDetails?.closedExpensesBreakdown ||
|
||||||
|
[]);
|
||||||
|
|
||||||
const activityInfo = {
|
const activityInfo = {
|
||||||
activityName,
|
activityName,
|
||||||
activityType,
|
activityType,
|
||||||
@ -145,24 +186,34 @@ export function mapToClaimManagementRequest(
|
|||||||
startDate: periodStartDate,
|
startDate: periodStartDate,
|
||||||
endDate: periodEndDate,
|
endDate: periodEndDate,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
estimatedBudget: claimDetails.estimatedBudget || claimDetails.estimated_budget,
|
estimatedBudget,
|
||||||
closedExpenses: claimDetails.closedExpenses || claimDetails.closed_expenses,
|
closedExpenses,
|
||||||
closedExpensesBreakdown: completionDetails.closedExpenses ||
|
closedExpensesBreakdown,
|
||||||
completionDetails.closed_expenses ||
|
|
||||||
completionDetails.closedExpensesBreakdown ||
|
|
||||||
[],
|
|
||||||
description: apiRequest.description || '', // Get description from workflow request
|
description: apiRequest.description || '', // Get description from workflow request
|
||||||
};
|
};
|
||||||
|
|
||||||
// Map dealer information (matching DealerInformationCard expectations)
|
// Map dealer information (matching DealerInformationCard expectations)
|
||||||
|
// Dealer info should always be available from claimDetails (created during claim request creation)
|
||||||
|
// Handle both camelCase and snake_case from Sequelize JSON serialization
|
||||||
const dealerInfo = {
|
const dealerInfo = {
|
||||||
dealerCode: claimDetails.dealerCode || claimDetails.dealer_code || '',
|
dealerCode: claimDetails?.dealerCode || claimDetails?.dealer_code || claimDetails?.DealerCode || '',
|
||||||
dealerName: claimDetails.dealerName || claimDetails.dealer_name || '',
|
dealerName: claimDetails?.dealerName || claimDetails?.dealer_name || claimDetails?.DealerName || '',
|
||||||
email: claimDetails.dealerEmail || claimDetails.dealer_email || '',
|
email: claimDetails?.dealerEmail || claimDetails?.dealer_email || claimDetails?.DealerEmail || '',
|
||||||
phone: claimDetails.dealerPhone || claimDetails.dealer_phone || '',
|
phone: claimDetails?.dealerPhone || claimDetails?.dealer_phone || claimDetails?.DealerPhone || '',
|
||||||
address: claimDetails.dealerAddress || claimDetails.dealer_address || '',
|
address: claimDetails?.dealerAddress || claimDetails?.dealer_address || claimDetails?.DealerAddress || '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Log warning if dealer info is missing (should always be present for claim management requests)
|
||||||
|
if (!dealerInfo.dealerCode || !dealerInfo.dealerName) {
|
||||||
|
console.warn('[claimDataMapper] Dealer information is missing from claimDetails:', {
|
||||||
|
hasClaimDetails: !!claimDetails,
|
||||||
|
dealerCode: dealerInfo.dealerCode,
|
||||||
|
dealerName: dealerInfo.dealerName,
|
||||||
|
rawClaimDetails: claimDetails,
|
||||||
|
availableKeys: claimDetails ? Object.keys(claimDetails) : [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Map proposal details
|
// Map proposal details
|
||||||
const proposal = proposalDetails ? {
|
const proposal = proposalDetails ? {
|
||||||
proposalDocumentUrl: proposalDetails.proposalDocumentUrl || proposalDetails.proposal_document_url,
|
proposalDocumentUrl: proposalDetails.proposalDocumentUrl || proposalDetails.proposal_document_url,
|
||||||
@ -186,14 +237,23 @@ export function mapToClaimManagementRequest(
|
|||||||
organizedAt: internalOrder.organizedAt || internalOrder.organized_at || '',
|
organizedAt: internalOrder.organizedAt || internalOrder.organized_at || '',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Map DMS details
|
// Map DMS details from new invoice and credit note tables
|
||||||
const dmsDetails = {
|
const dmsDetails = {
|
||||||
eInvoiceNumber: claimDetails.eInvoiceNumber || claimDetails.e_invoice_number,
|
eInvoiceNumber: invoice.invoiceNumber || invoice.invoice_number ||
|
||||||
eInvoiceDate: claimDetails.eInvoiceDate || claimDetails.e_invoice_date,
|
claimDetails.eInvoiceNumber || claimDetails.e_invoice_number,
|
||||||
dmsNumber: claimDetails.dmsNumber || claimDetails.dms_number,
|
eInvoiceDate: invoice.invoiceDate || invoice.invoice_date ||
|
||||||
creditNoteNumber: claimDetails.creditNoteNumber || claimDetails.credit_note_number,
|
claimDetails.eInvoiceDate || claimDetails.e_invoice_date,
|
||||||
creditNoteDate: claimDetails.creditNoteDate || claimDetails.credit_note_date,
|
dmsNumber: invoice.dmsNumber || invoice.dms_number ||
|
||||||
creditNoteAmount: claimDetails.creditNoteAmount || claimDetails.credit_note_amount,
|
claimDetails.dmsNumber || claimDetails.dms_number,
|
||||||
|
creditNoteNumber: creditNote.creditNoteNumber || creditNote.credit_note_number ||
|
||||||
|
claimDetails.creditNoteNumber || claimDetails.credit_note_number,
|
||||||
|
creditNoteDate: creditNote.creditNoteDate || creditNote.credit_note_date ||
|
||||||
|
claimDetails.creditNoteDate || claimDetails.credit_note_date,
|
||||||
|
creditNoteAmount: creditNote.creditNoteAmount ? Number(creditNote.creditNoteAmount) :
|
||||||
|
(creditNote.credit_note_amount ? Number(creditNote.credit_note_amount) :
|
||||||
|
(creditNote.creditNoteAmount ? Number(creditNote.creditNoteAmount) :
|
||||||
|
(claimDetails.creditNoteAmount ? Number(claimDetails.creditNoteAmount) :
|
||||||
|
(claimDetails.credit_note_amount ? Number(claimDetails.credit_note_amount) : undefined)))),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Map claim amounts
|
// Map claim amounts
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user