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;
|
||||
completionDetails = claimData.completionDetails || claimData.completion_details;
|
||||
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:', {
|
||||
claimDetails: claimDetails ? {
|
||||
@ -278,6 +291,10 @@ export function useRequestDetails(
|
||||
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');
|
||||
@ -337,6 +354,11 @@ export function useRequestDetails(
|
||||
proposalDetails: proposalDetails || null,
|
||||
completionDetails: completionDetails || 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);
|
||||
|
||||
@ -26,6 +26,7 @@ import {
|
||||
ShieldX,
|
||||
RefreshCw,
|
||||
ArrowLeft,
|
||||
DollarSign,
|
||||
} from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
@ -51,7 +52,9 @@ import { DocumentsTab } from './components/tabs/DocumentsTab';
|
||||
import { ActivityTab } from './components/tabs/ActivityTab';
|
||||
import { WorkNotesTab } from './components/tabs/WorkNotesTab';
|
||||
import { SummaryTab } from './components/tabs/SummaryTab';
|
||||
import { IOTab } from './components/tabs/IOTab';
|
||||
import { isClaimManagementRequest } from '@/utils/claimRequestUtils';
|
||||
import { determineUserRole } from '@/utils/claimDataMapper';
|
||||
import { QuickActionsSidebar } from './components/QuickActionsSidebar';
|
||||
import { RequestDetailModals } from './components/RequestDetailModals';
|
||||
import { RequestDetailProps } from './types/requestDetail.types';
|
||||
@ -133,6 +136,42 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
|
||||
accessDenied,
|
||||
} = 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 {
|
||||
mergedMessages,
|
||||
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" />
|
||||
<span className="truncate">Workflow</span>
|
||||
</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
|
||||
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"
|
||||
@ -547,6 +596,16 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{showIOTab && (
|
||||
<TabsContent value="io" className="mt-0">
|
||||
<IOTab
|
||||
request={request}
|
||||
apiRequest={apiRequest}
|
||||
onRefresh={refreshDetails}
|
||||
/>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
<TabsContent value="documents" className="mt-0">
|
||||
<DocumentsTab
|
||||
request={request}
|
||||
@ -595,6 +654,7 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
|
||||
refreshTrigger={sharedRecipientsRefreshTrigger}
|
||||
pausedByUserId={request?.pauseInfo?.pausedBy?.userId}
|
||||
currentUserId={(user as any)?.userId}
|
||||
apiRequest={apiRequest}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
* 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 { Button } from '@/components/ui/button';
|
||||
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 { useAuth } from '@/contexts/AuthContext';
|
||||
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 {
|
||||
request: any;
|
||||
@ -27,6 +30,8 @@ interface QuickActionsSidebarProps {
|
||||
refreshTrigger?: number; // Trigger to refresh shared recipients list
|
||||
pausedByUserId?: string; // User ID of the approver who paused (kept for backwards compatibility)
|
||||
currentUserId?: string; // Current user's ID (kept for backwards compatibility)
|
||||
apiRequest?: any;
|
||||
onEditClaimAmount?: () => void;
|
||||
}
|
||||
|
||||
export function QuickActionsSidebar({
|
||||
@ -43,6 +48,10 @@ export function QuickActionsSidebar({
|
||||
onRetrigger,
|
||||
summaryId,
|
||||
refreshTrigger,
|
||||
pausedByUserId: pausedByUserIdProp,
|
||||
currentUserId: currentUserIdProp,
|
||||
apiRequest,
|
||||
onEditClaimAmount,
|
||||
}: QuickActionsSidebarProps) {
|
||||
const { user } = useAuth();
|
||||
const [sharedRecipients, setSharedRecipients] = useState<SharedRecipient[]>([]);
|
||||
@ -50,8 +59,8 @@ export function QuickActionsSidebar({
|
||||
const [hasRetriggerNotification, setHasRetriggerNotification] = useState(false);
|
||||
const isClosed = request?.status === 'closed';
|
||||
const isPaused = request?.pauseInfo?.isPaused || false;
|
||||
const pausedByUserId = request?.pauseInfo?.pausedBy?.userId;
|
||||
const currentUserId = (user as any)?.userId || '';
|
||||
const pausedByUserId = pausedByUserIdProp || request?.pauseInfo?.pausedBy?.userId;
|
||||
const currentUserId = currentUserIdProp || (user as any)?.userId || '';
|
||||
|
||||
// Both approver AND initiator can pause (when not already paused and not closed)
|
||||
const canPause = !isPaused && !isClosed && (currentApprovalLevel || isInitiator);
|
||||
@ -117,6 +126,16 @@ export function QuickActionsSidebar({
|
||||
fetchSharedRecipients();
|
||||
}, [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 (
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
{/* Quick Actions Card - Hide entire card for spectators and closed requests */}
|
||||
@ -339,6 +358,19 @@ export function QuickActionsSidebar({
|
||||
</CardContent>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -105,8 +105,8 @@ export function ActivityInformationCard({ activityInfo, className }: ActivityInf
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Closed Expenses */}
|
||||
{activityInfo.closedExpenses !== undefined && (
|
||||
{/* Closed Expenses - Show if value exists (including 0) */}
|
||||
{activityInfo.closedExpenses !== undefined && activityInfo.closedExpenses !== null && (
|
||||
<div>
|
||||
<label className="text-xs font-medium text-gray-500 uppercase tracking-wide">
|
||||
Closed Expenses
|
||||
|
||||
@ -8,15 +8,44 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Activity, Receipt, DollarSign, Pen } from 'lucide-react';
|
||||
import {
|
||||
IODetails,
|
||||
DMSDetails,
|
||||
ClaimAmountDetails,
|
||||
CostBreakdownItem,
|
||||
RoleBasedVisibility,
|
||||
} from '../../types/claimManagement.types';
|
||||
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 {
|
||||
ioDetails?: IODetails;
|
||||
dmsDetails?: DMSDetails;
|
||||
@ -38,29 +67,34 @@ export function ProcessDetailsCard({
|
||||
onEditClaimAmount,
|
||||
className,
|
||||
}: 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 })}`;
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const formatDate = (dateString?: string | null) => {
|
||||
if (!dateString) return '';
|
||||
try {
|
||||
return format(new Date(dateString), 'MMM d, yyyy, h:mm a');
|
||||
} catch {
|
||||
return dateString;
|
||||
return dateString || '';
|
||||
}
|
||||
};
|
||||
|
||||
const calculateTotal = (items: CostBreakdownItem[]) => {
|
||||
return items.reduce((sum, item) => sum + item.amount, 0);
|
||||
const calculateTotal = (items?: CostBreakdownItem[]) => {
|
||||
if (!items || items.length === 0) return 0;
|
||||
return items.reduce((sum, item) => sum + (item.amount ?? 0), 0);
|
||||
};
|
||||
|
||||
// Don't render if nothing to show
|
||||
const hasContent =
|
||||
(visibility.showIODetails && ioDetails) ||
|
||||
(visibility.showDMSDetails && dmsDetails) ||
|
||||
(visibility.showClaimAmount && claimAmount) ||
|
||||
estimatedBudgetBreakdown ||
|
||||
closedExpensesBreakdown;
|
||||
(visibility.showClaimAmount && claimAmount && claimAmount.amount !== undefined && claimAmount.amount !== null) ||
|
||||
(estimatedBudgetBreakdown && estimatedBudgetBreakdown.length > 0) ||
|
||||
(closedExpensesBreakdown && closedExpensesBreakdown.length > 0);
|
||||
|
||||
if (!hasContent) {
|
||||
return null;
|
||||
|
||||
@ -5,32 +5,50 @@
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Receipt, Calendar } from 'lucide-react';
|
||||
import { ProposalDetails } from '../../types/claimManagement.types';
|
||||
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 {
|
||||
proposalDetails: ProposalDetails;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
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 })}`;
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const formatDate = (dateString?: string | null) => {
|
||||
if (!dateString) return '';
|
||||
try {
|
||||
return format(new Date(dateString), 'MMM d, yyyy, h:mm a');
|
||||
} catch {
|
||||
return dateString;
|
||||
return dateString || '';
|
||||
}
|
||||
};
|
||||
|
||||
const formatTimelineDate = (dateString: string) => {
|
||||
const formatTimelineDate = (dateString?: string | null) => {
|
||||
if (!dateString) return '-';
|
||||
try {
|
||||
return format(new Date(dateString), 'MMM d, yyyy');
|
||||
} catch {
|
||||
return dateString;
|
||||
return dateString || '-';
|
||||
}
|
||||
};
|
||||
|
||||
@ -66,7 +84,7 @@ export function ProposalDetailsCard({ proposalDetails, className }: ProposalDeta
|
||||
</tr>
|
||||
</thead>
|
||||
<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">
|
||||
<td className="px-4 py-3 text-sm text-gray-900">
|
||||
{item.description}
|
||||
|
||||
@ -9,7 +9,6 @@ import {
|
||||
ActivityInformationCard,
|
||||
DealerInformationCard,
|
||||
ProposalDetailsCard,
|
||||
ProcessDetailsCard,
|
||||
RequestInitiatorCard,
|
||||
} from '../claim-cards';
|
||||
import { isClaimManagementRequest } from '@/utils/claimRequestUtils';
|
||||
@ -70,6 +69,10 @@ export function ClaimManagementOverviewTab({
|
||||
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
|
||||
@ -82,50 +85,35 @@ export function ClaimManagementOverviewTab({
|
||||
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.
|
||||
const initiatorInfo = {
|
||||
name: apiRequest.requestedBy?.name || apiRequest.createdByName || 'Unknown',
|
||||
role: 'initiator',
|
||||
department: apiRequest.requestedBy?.department || apiRequest.department || '',
|
||||
email: apiRequest.requestedBy?.email || 'N/A',
|
||||
phone: apiRequest.requestedBy?.phone || apiRequest.requestedBy?.mobile,
|
||||
name: apiRequest.initiator?.name || apiRequest.initiator?.displayName || apiRequest.initiator?.email || 'Unknown',
|
||||
role: apiRequest.initiator?.role || apiRequest.initiator?.designation || 'Initiator',
|
||||
department: apiRequest.initiator?.department || apiRequest.department || '',
|
||||
email: apiRequest.initiator?.email || 'N/A',
|
||||
phone: apiRequest.initiator?.phone || apiRequest.initiator?.mobile,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`grid grid-cols-1 lg:grid-cols-3 gap-6 ${className}`}>
|
||||
{/* Left Column: Main Information Cards */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Activity Information - Always visible */}
|
||||
<ActivityInformationCard activityInfo={claimRequest.activityInfo} />
|
||||
<div className={`space-y-6 ${className}`}>
|
||||
{/* Activity Information - Always visible */}
|
||||
<ActivityInformationCard activityInfo={claimRequest.activityInfo} />
|
||||
|
||||
{/* Dealer Information - Visible based on role */}
|
||||
{visibility.showDealerInfo && (
|
||||
<DealerInformationCard dealerInfo={claimRequest.dealerInfo} />
|
||||
)}
|
||||
{/* Dealer Information - Always visible */}
|
||||
<DealerInformationCard dealerInfo={claimRequest.dealerInfo} />
|
||||
|
||||
{/* Proposal Details - Only shown after dealer submits proposal */}
|
||||
{visibility.showProposalDetails && claimRequest.proposalDetails && (
|
||||
<ProposalDetailsCard proposalDetails={claimRequest.proposalDetails} />
|
||||
)}
|
||||
{/* Proposal Details - Only shown after dealer submits proposal */}
|
||||
{visibility.showProposalDetails && claimRequest.proposalDetails && (
|
||||
<ProposalDetailsCard proposalDetails={claimRequest.proposalDetails} />
|
||||
)}
|
||||
|
||||
{/* Request Initiator */}
|
||||
<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>
|
||||
{/* Request Initiator */}
|
||||
<RequestInitiatorCard initiatorInfo={initiatorInfo} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1099,7 +1099,8 @@ export function DealerClaimWorkflowTab({
|
||||
}
|
||||
// Call API to push to DMS (this will auto-generate e-invoice)
|
||||
// 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, {
|
||||
eInvoiceDate: today,
|
||||
});
|
||||
@ -1213,9 +1214,26 @@ export function DealerClaimWorkflowTab({
|
||||
toast.info('Send to dealer functionality will be implemented');
|
||||
}}
|
||||
creditNoteData={{
|
||||
creditNoteNumber: (request as any)?.claimDetails?.creditNoteNumber || (request as any)?.claimDetails?.credit_note_number,
|
||||
creditNoteDate: (request as any)?.claimDetails?.creditNoteDate || (request as any)?.claimDetails?.credit_note_date,
|
||||
creditNoteAmount: (request as any)?.claimDetails?.creditNoteAmount || (request as any)?.claimDetails?.credit_note_amount,
|
||||
creditNoteNumber: (request as any)?.creditNote?.creditNoteNumber ||
|
||||
(request as any)?.creditNote?.credit_note_number ||
|
||||
(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',
|
||||
}}
|
||||
dealerInfo={{
|
||||
|
||||
@ -15,16 +15,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { DollarSign, Download, CircleCheckBig, AlertCircle } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { mockApi } from '@/services/mockApi';
|
||||
|
||||
// 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;
|
||||
};
|
||||
import { updateIODetails, getClaimDetails } from '@/services/dealerClaimApi';
|
||||
|
||||
interface IOTabProps {
|
||||
request: any;
|
||||
@ -43,41 +34,46 @@ interface IOBlockedDetails {
|
||||
|
||||
export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
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 [fetchedAmount, setFetchedAmount] = useState<number | null>(null);
|
||||
const [blockedDetails, setBlockedDetails] = useState<IOBlockedDetails | null>(null);
|
||||
const [blockingBudget, setBlockingBudget] = useState(false);
|
||||
|
||||
// Load existing IO block from mock API
|
||||
// Load existing IO block details from apiRequest
|
||||
useEffect(() => {
|
||||
if (requestId) {
|
||||
mockApi.getIOBlock(requestId).then(response => {
|
||||
try {
|
||||
const ioBlock = handleApiResponse<any>(response);
|
||||
if (ioBlock) {
|
||||
setBlockedDetails({
|
||||
ioNumber: ioBlock.ioNumber,
|
||||
blockedAmount: ioBlock.blockedAmount,
|
||||
availableBalance: ioBlock.availableBalance,
|
||||
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);
|
||||
if (internalOrder && existingIONumber) {
|
||||
setBlockedDetails({
|
||||
ioNumber: existingIONumber,
|
||||
blockedAmount: Number(existingBlockedAmount) || 0,
|
||||
availableBalance: Number(existingAvailableBalance) || 0,
|
||||
blockedDate: internalOrder.organizedAt || internalOrder.organized_at || new Date().toISOString(),
|
||||
sapDocumentNumber: sapDocNumber,
|
||||
status: (internalOrder.status === 'BLOCKED' ? 'blocked' :
|
||||
internalOrder.status === 'RELEASED' ? 'released' : 'blocked') as 'blocked' | 'released' | 'failed',
|
||||
});
|
||||
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
|
||||
* Validates IO number and gets available balance
|
||||
* Calls updateIODetails with blockedAmount=0 to validate without blocking
|
||||
*/
|
||||
const handleFetchAmount = async () => {
|
||||
if (!ioNumber.trim()) {
|
||||
@ -85,23 +81,44 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!requestId) {
|
||||
toast.error('Request ID not found');
|
||||
return;
|
||||
}
|
||||
|
||||
setFetchingAmount(true);
|
||||
try {
|
||||
// TODO: Replace with actual SAP API integration
|
||||
// const response = await fetch(`/api/sap/io/${ioNumber}/budget`);
|
||||
// const data = await response.json();
|
||||
// Validate IO number by calling updateIODetails with blockedAmount=0
|
||||
// This validates the IO with SAP and returns available balance without blocking
|
||||
// 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
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
// Mock response
|
||||
const mockAvailableAmount = 50000; // ₹50,000
|
||||
|
||||
setFetchedAmount(mockAvailableAmount);
|
||||
toast.success('IO budget fetched successfully from SAP');
|
||||
if (updatedInternalOrder) {
|
||||
const availableBalance = Number(updatedInternalOrder.ioAvailableBalance || updatedInternalOrder.io_available_balance || 0);
|
||||
if (availableBalance > 0) {
|
||||
setFetchedAmount(availableBalance);
|
||||
toast.success(`IO validated successfully. Available balance: ₹${availableBalance.toLocaleString('en-IN')}`);
|
||||
} else {
|
||||
toast.error('No available balance found for this IO number');
|
||||
setFetchedAmount(null);
|
||||
}
|
||||
} else {
|
||||
toast.error('Failed to fetch IO details after validation');
|
||||
setFetchedAmount(null);
|
||||
}
|
||||
} catch (error: any) {
|
||||
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);
|
||||
} finally {
|
||||
setFetchingAmount(false);
|
||||
@ -112,12 +129,23 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
* Block budget in SAP system
|
||||
*/
|
||||
const handleBlockBudget = async () => {
|
||||
if (!ioNumber.trim() || !fetchedAmount) {
|
||||
if (!ioNumber.trim() || fetchedAmount === null) {
|
||||
toast.error('Please fetch IO amount first');
|
||||
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) {
|
||||
toast.error('Claim amount exceeds available IO budget');
|
||||
@ -126,76 +154,41 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
|
||||
setBlockingBudget(true);
|
||||
try {
|
||||
// TODO: Replace with actual SAP API integration
|
||||
// const response = await fetch(`/api/sap/io/block`, {
|
||||
// method: 'POST',
|
||||
// headers: { 'Content-Type': 'application/json' },
|
||||
// body: JSON.stringify({
|
||||
// ioNumber,
|
||||
// amount: claimAmount,
|
||||
// requestId: apiRequest?.requestId,
|
||||
// }),
|
||||
// });
|
||||
// const data = await response.json();
|
||||
// Call updateIODetails with blockedAmount to block budget in SAP
|
||||
await updateIODetails(requestId, {
|
||||
ioNumber: ioNumber.trim(),
|
||||
ioAvailableBalance: fetchedAmount,
|
||||
ioBlockedAmount: claimAmount,
|
||||
ioRemainingBalance: fetchedAmount - claimAmount,
|
||||
});
|
||||
|
||||
// Mock API call
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
// Fetch updated claim details to get the blocked IO data
|
||||
const claimData = await getClaimDetails(requestId);
|
||||
const updatedInternalOrder = claimData?.internalOrder || claimData?.internal_order;
|
||||
|
||||
// Mock blocked details
|
||||
const blocked: IOBlockedDetails = {
|
||||
ioNumber,
|
||||
blockedAmount: claimAmount,
|
||||
availableBalance: fetchedAmount - claimAmount,
|
||||
blockedDate: new Date().toISOString(),
|
||||
sapDocumentNumber: `SAP-${Date.now()}`,
|
||||
status: 'blocked',
|
||||
};
|
||||
|
||||
// Save to mock API
|
||||
if (requestId) {
|
||||
try {
|
||||
const ioBlockResponse = await mockApi.createIOBlock(requestId, {
|
||||
id: `io-${Date.now()}`,
|
||||
ioNumber: blocked.ioNumber,
|
||||
blockedAmount: blocked.blockedAmount,
|
||||
availableBalance: blocked.availableBalance,
|
||||
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);
|
||||
}
|
||||
if (updatedInternalOrder) {
|
||||
const blocked: IOBlockedDetails = {
|
||||
ioNumber: updatedInternalOrder.ioNumber || updatedInternalOrder.io_number || ioNumber,
|
||||
blockedAmount: Number(updatedInternalOrder.ioBlockedAmount || updatedInternalOrder.io_blocked_amount || claimAmount),
|
||||
availableBalance: Number(updatedInternalOrder.ioAvailableBalance || updatedInternalOrder.io_available_balance || fetchedAmount),
|
||||
blockedDate: updatedInternalOrder.organizedAt || updatedInternalOrder.organized_at || new Date().toISOString(),
|
||||
sapDocumentNumber: updatedInternalOrder.sapDocumentNumber || updatedInternalOrder.sap_document_number || '',
|
||||
status: 'blocked',
|
||||
};
|
||||
|
||||
setBlockedDetails(blocked);
|
||||
toast.success('IO budget blocked successfully in SAP');
|
||||
|
||||
// Refresh request details
|
||||
onRefresh?.();
|
||||
} else {
|
||||
toast.error('IO blocked but failed to fetch updated details');
|
||||
onRefresh?.();
|
||||
}
|
||||
|
||||
setBlockedDetails(blocked);
|
||||
toast.success('IO budget blocked successfully in SAP');
|
||||
|
||||
// Refresh request details
|
||||
onRefresh?.();
|
||||
} catch (error: any) {
|
||||
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 {
|
||||
setBlockingBudget(false);
|
||||
}
|
||||
@ -203,60 +196,53 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
|
||||
/**
|
||||
* 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 () => {
|
||||
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 {
|
||||
// TODO: Replace with actual SAP API integration
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
// Update IO block in mock API
|
||||
try {
|
||||
const ioBlockResponse = await mockApi.getIOBlock(requestId);
|
||||
const ioBlock = handleApiResponse<any>(ioBlockResponse);
|
||||
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);
|
||||
}
|
||||
// Release budget by setting blockedAmount to 0
|
||||
// Note: Backend may need a dedicated release endpoint for proper SAP integration
|
||||
await updateIODetails(requestId, {
|
||||
ioNumber: ioNumber.trim(),
|
||||
ioAvailableBalance: blockedDetails.availableBalance + blockedDetails.blockedAmount,
|
||||
ioBlockedAmount: 0,
|
||||
ioRemainingBalance: blockedDetails.availableBalance + blockedDetails.blockedAmount,
|
||||
});
|
||||
|
||||
// Clear local state
|
||||
setBlockedDetails(null);
|
||||
setFetchedAmount(null);
|
||||
setIoNumber('');
|
||||
|
||||
toast.success('IO budget released successfully');
|
||||
|
||||
// Refresh request details
|
||||
onRefresh?.();
|
||||
} catch (error: any) {
|
||||
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 (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
|
||||
@ -108,6 +108,12 @@ export function mapToClaimManagementRequest(
|
||||
const proposalDetails = apiRequest.proposalDetails || {};
|
||||
const completionDetails = apiRequest.completionDetails || {};
|
||||
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
|
||||
console.debug('[claimDataMapper] Raw claimDetails:', claimDetails);
|
||||
@ -115,6 +121,10 @@ export function mapToClaimManagementRequest(
|
||||
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,
|
||||
});
|
||||
|
||||
@ -136,6 +146,37 @@ export function mapToClaimManagementRequest(
|
||||
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 = {
|
||||
activityName,
|
||||
activityType,
|
||||
@ -145,24 +186,34 @@ export function mapToClaimManagementRequest(
|
||||
startDate: periodStartDate,
|
||||
endDate: periodEndDate,
|
||||
} : undefined,
|
||||
estimatedBudget: claimDetails.estimatedBudget || claimDetails.estimated_budget,
|
||||
closedExpenses: claimDetails.closedExpenses || claimDetails.closed_expenses,
|
||||
closedExpensesBreakdown: completionDetails.closedExpenses ||
|
||||
completionDetails.closed_expenses ||
|
||||
completionDetails.closedExpensesBreakdown ||
|
||||
[],
|
||||
estimatedBudget,
|
||||
closedExpenses,
|
||||
closedExpensesBreakdown,
|
||||
description: apiRequest.description || '', // Get description from workflow request
|
||||
};
|
||||
|
||||
// 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 = {
|
||||
dealerCode: claimDetails.dealerCode || claimDetails.dealer_code || '',
|
||||
dealerName: claimDetails.dealerName || claimDetails.dealer_name || '',
|
||||
email: claimDetails.dealerEmail || claimDetails.dealer_email || '',
|
||||
phone: claimDetails.dealerPhone || claimDetails.dealer_phone || '',
|
||||
address: claimDetails.dealerAddress || claimDetails.dealer_address || '',
|
||||
dealerCode: claimDetails?.dealerCode || claimDetails?.dealer_code || claimDetails?.DealerCode || '',
|
||||
dealerName: claimDetails?.dealerName || claimDetails?.dealer_name || claimDetails?.DealerName || '',
|
||||
email: claimDetails?.dealerEmail || claimDetails?.dealer_email || claimDetails?.DealerEmail || '',
|
||||
phone: claimDetails?.dealerPhone || claimDetails?.dealer_phone || claimDetails?.DealerPhone || '',
|
||||
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
|
||||
const proposal = proposalDetails ? {
|
||||
proposalDocumentUrl: proposalDetails.proposalDocumentUrl || proposalDetails.proposal_document_url,
|
||||
@ -186,14 +237,23 @@ export function mapToClaimManagementRequest(
|
||||
organizedAt: internalOrder.organizedAt || internalOrder.organized_at || '',
|
||||
};
|
||||
|
||||
// Map DMS details
|
||||
// Map DMS details from new invoice and credit note tables
|
||||
const dmsDetails = {
|
||||
eInvoiceNumber: claimDetails.eInvoiceNumber || claimDetails.e_invoice_number,
|
||||
eInvoiceDate: claimDetails.eInvoiceDate || claimDetails.e_invoice_date,
|
||||
dmsNumber: claimDetails.dmsNumber || claimDetails.dms_number,
|
||||
creditNoteNumber: claimDetails.creditNoteNumber || claimDetails.credit_note_number,
|
||||
creditNoteDate: claimDetails.creditNoteDate || claimDetails.credit_note_date,
|
||||
creditNoteAmount: claimDetails.creditNoteAmount || claimDetails.credit_note_amount,
|
||||
eInvoiceNumber: invoice.invoiceNumber || invoice.invoice_number ||
|
||||
claimDetails.eInvoiceNumber || claimDetails.e_invoice_number,
|
||||
eInvoiceDate: invoice.invoiceDate || invoice.invoice_date ||
|
||||
claimDetails.eInvoiceDate || claimDetails.e_invoice_date,
|
||||
dmsNumber: invoice.dmsNumber || invoice.dms_number ||
|
||||
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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user