1209 lines
52 KiB
TypeScript
1209 lines
52 KiB
TypeScript
/**
|
|
* Dealer Claim Request Workflow Tab
|
|
*
|
|
* This component is specific to Dealer Claim requests.
|
|
* Located in: src/dealer-claim/components/request-detail/
|
|
*/
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { TrendingUp, Clock, CheckCircle, CircleCheckBig, Upload, Mail, Download, Receipt, Activity } from 'lucide-react';
|
|
import { formatDateTime } from '@/utils/dateFormatter';
|
|
import { DealerProposalSubmissionModal } from './modals';
|
|
import { InitiatorProposalApprovalModal } from './modals';
|
|
import { DeptLeadIOApprovalModal } from './modals';
|
|
import { DealerCompletionDocumentsModal } from './modals';
|
|
import { CreditNoteSAPModal } from './modals';
|
|
import { EmailNotificationTemplateModal } from './modals';
|
|
import { DMSPushModal } from './modals';
|
|
import { toast } from 'sonner';
|
|
import { submitProposal, updateIODetails, submitCompletion, updateEInvoice } from '@/services/dealerClaimApi';
|
|
import { getWorkflowDetails, approveLevel, rejectLevel } from '@/services/workflowApi';
|
|
import { uploadDocument } from '@/services/documentApi';
|
|
|
|
interface DealerClaimWorkflowTabProps {
|
|
request: any;
|
|
user: any;
|
|
isInitiator: boolean;
|
|
onSkipApprover?: (data: any) => void;
|
|
onRefresh?: () => void;
|
|
}
|
|
|
|
interface WorkflowStep {
|
|
step: number;
|
|
title: string;
|
|
approver: string;
|
|
description: string;
|
|
tatHours: number;
|
|
status: 'pending' | 'approved' | 'waiting' | 'rejected' | 'in_progress';
|
|
comment?: string;
|
|
approvedAt?: string;
|
|
elapsedHours?: number;
|
|
// Special fields for dealer claims
|
|
ioDetails?: {
|
|
ioNumber: string;
|
|
ioRemark: string;
|
|
organizedBy: string;
|
|
organizedAt: string;
|
|
blockedAmount?: number;
|
|
availableBalance?: number;
|
|
remainingBalance?: number;
|
|
};
|
|
dmsDetails?: {
|
|
dmsNumber: string;
|
|
dmsRemarks: string;
|
|
pushedBy: string;
|
|
pushedAt: string;
|
|
};
|
|
einvoiceUrl?: string;
|
|
emailTemplateUrl?: string;
|
|
}
|
|
|
|
/**
|
|
* Safe date formatter with fallback
|
|
*/
|
|
const formatDateSafe = (dateString: string | undefined | null): string => {
|
|
if (!dateString) return '';
|
|
try {
|
|
return formatDateTime(dateString);
|
|
} catch (error) {
|
|
// Fallback to simple date format
|
|
try {
|
|
return new Date(dateString).toLocaleString('en-IN', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
hour: 'numeric',
|
|
minute: 'numeric',
|
|
hour12: true,
|
|
});
|
|
} catch {
|
|
return dateString;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get step icon based on status
|
|
*/
|
|
const getStepIcon = (status: string) => {
|
|
switch (status) {
|
|
case 'approved':
|
|
return <CircleCheckBig className="w-5 h-5 text-green-600" />;
|
|
case 'pending':
|
|
return <Clock className="w-5 h-5 text-blue-600" />;
|
|
case 'rejected':
|
|
return <CheckCircle className="w-5 h-5 text-red-600" />;
|
|
default:
|
|
return <Clock className="w-5 h-5 text-gray-400" />;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get step badge variant
|
|
*/
|
|
const getStepBadgeVariant = (status: string) => {
|
|
switch (status) {
|
|
case 'approved':
|
|
return 'bg-green-100 text-green-800 border-green-200';
|
|
case 'pending':
|
|
return 'bg-purple-100 text-purple-800 border-purple-200';
|
|
case 'rejected':
|
|
return 'bg-red-100 text-red-800 border-red-200';
|
|
default:
|
|
return 'bg-gray-100 text-gray-800 border-gray-200';
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get step card styling
|
|
*/
|
|
const getStepCardStyle = (status: string, isActive: boolean) => {
|
|
if (isActive && (status === 'pending' || status === 'in_progress')) {
|
|
return 'border-purple-500 bg-purple-50 shadow-md';
|
|
}
|
|
if (status === 'approved') {
|
|
return 'border-green-500 bg-green-50';
|
|
}
|
|
if (status === 'rejected') {
|
|
return 'border-red-500 bg-red-50';
|
|
}
|
|
return 'border-gray-200 bg-white';
|
|
};
|
|
|
|
/**
|
|
* Get step icon background
|
|
*/
|
|
const getStepIconBg = (status: string) => {
|
|
switch (status) {
|
|
case 'approved':
|
|
return 'bg-green-100';
|
|
case 'pending':
|
|
return 'bg-purple-100';
|
|
case 'rejected':
|
|
return 'bg-red-100';
|
|
default:
|
|
return 'bg-gray-100';
|
|
}
|
|
};
|
|
|
|
export function DealerClaimWorkflowTab({
|
|
request,
|
|
user,
|
|
isInitiator,
|
|
onSkipApprover: _onSkipApprover,
|
|
onRefresh
|
|
}: DealerClaimWorkflowTabProps) {
|
|
const [showProposalModal, setShowProposalModal] = useState(false);
|
|
const [showApprovalModal, setShowApprovalModal] = useState(false);
|
|
const [showIOApprovalModal, setShowIOApprovalModal] = useState(false);
|
|
const [showCompletionModal, setShowCompletionModal] = useState(false);
|
|
const [showDMSPushModal, setShowDMSPushModal] = useState(false);
|
|
const [showCreditNoteModal, setShowCreditNoteModal] = useState(false);
|
|
const [showEmailTemplateModal, setShowEmailTemplateModal] = useState(false);
|
|
const [selectedStepForEmail, setSelectedStepForEmail] = useState<{ stepNumber: number; stepName: string } | null>(null);
|
|
|
|
// Load approval flows from real API
|
|
const [approvalFlow, setApprovalFlow] = useState<any[]>([]);
|
|
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
|
|
|
// Reload approval flows whenever request changes or after refresh
|
|
useEffect(() => {
|
|
const loadApprovalFlows = async () => {
|
|
// First check if request has approvalFlow
|
|
if (request?.approvalFlow && request.approvalFlow.length > 0) {
|
|
setApprovalFlow(request.approvalFlow);
|
|
return;
|
|
}
|
|
|
|
// Load from real API
|
|
if (request?.id || request?.requestId) {
|
|
const requestId = request.id || request.requestId;
|
|
try {
|
|
const details = await getWorkflowDetails(requestId);
|
|
const approvals = details?.approvalLevels || details?.approvals || [];
|
|
if (approvals && approvals.length > 0) {
|
|
// Transform approval levels to match expected format
|
|
const flows = approvals.map((level: any) => ({
|
|
step: level.levelNumber || level.level_number || 0,
|
|
approver: level.approverName || level.approver_name || '',
|
|
approverEmail: (level.approverEmail || level.approver_email || '').toLowerCase(),
|
|
status: level.status?.toLowerCase() || 'waiting',
|
|
tatHours: level.tatHours || level.tat_hours || 24,
|
|
elapsedHours: level.elapsedHours || level.elapsed_hours,
|
|
approvedAt: level.actionDate || level.action_date,
|
|
comment: level.comments || level.comment,
|
|
levelId: level.levelId || level.level_id,
|
|
}));
|
|
setApprovalFlow(flows);
|
|
}
|
|
} catch (error) {
|
|
console.warn('Failed to load approval flows from API:', error);
|
|
}
|
|
}
|
|
};
|
|
|
|
loadApprovalFlows();
|
|
}, [request, refreshTrigger]);
|
|
|
|
// Also reload when request.currentStep changes
|
|
useEffect(() => {
|
|
if (request?.id || request?.requestId) {
|
|
const requestId = request.id || request.requestId;
|
|
const loadApprovalFlows = async () => {
|
|
try {
|
|
const details = await getWorkflowDetails(requestId);
|
|
const approvals = details?.approvalLevels || details?.approvals || [];
|
|
if (approvals && approvals.length > 0) {
|
|
const flows = approvals.map((level: any) => ({
|
|
step: level.levelNumber || level.level_number || 0,
|
|
approver: level.approverName || level.approver_name || '',
|
|
approverEmail: (level.approverEmail || level.approver_email || '').toLowerCase(),
|
|
status: level.status?.toLowerCase() || 'waiting',
|
|
tatHours: level.tatHours || level.tat_hours || 24,
|
|
elapsedHours: level.elapsedHours || level.elapsed_hours,
|
|
approvedAt: level.actionDate || level.action_date,
|
|
comment: level.comments || level.comment,
|
|
levelId: level.levelId || level.level_id,
|
|
}));
|
|
setApprovalFlow(flows);
|
|
}
|
|
} catch (error) {
|
|
console.warn('Failed to load approval flows from API:', error);
|
|
}
|
|
};
|
|
loadApprovalFlows();
|
|
}
|
|
}, [request?.currentStep]);
|
|
|
|
// Enhanced refresh handler that also reloads approval flows
|
|
const handleRefresh = () => {
|
|
setRefreshTrigger(prev => prev + 1);
|
|
onRefresh?.();
|
|
};
|
|
|
|
// Transform approval flow to dealer claim workflow steps
|
|
const workflowSteps: WorkflowStep[] = approvalFlow.map((step: any, index: number) => {
|
|
const stepTitles = [
|
|
'Dealer - Proposal Submission',
|
|
'Requestor Evaluation & Confirmation',
|
|
'Dept Lead Approval',
|
|
'Activity Creation',
|
|
'Dealer - Completion Documents',
|
|
'Requestor - Claim Approval',
|
|
'E-Invoice Generation',
|
|
'Credit Note from SAP',
|
|
];
|
|
|
|
const stepDescriptions = [
|
|
'Dealer submits the proposal for the activity with comments including proposal document with requested details, cost break-up, timeline for closure, and other requests',
|
|
'Requestor evaluates the request and confirms with comments. Decision point: Confirms? (YES → Continue to Dept Lead / NO → Request is cancelled)',
|
|
'Department Lead approval. Decision point: Approved? (YES → Budget is blocked in the respective IO for the activity / NO → More clarification required → Request is cancelled)',
|
|
'Activity is created. Activity confirmation email is auto-triggered to dealer / requestor / Lead. IO confirmation to be made.',
|
|
'Dealer submits the necessary documents upon completion of the activity including document attachments (Zip Folder) and brief description',
|
|
'Requestor approves the claim in full or can modify the amount. If more information is required, can request additional details from dealer.',
|
|
'E-invoice will be generated through DMS.',
|
|
'Got credit note from SAP. Review and send to dealer to complete the claim management process.',
|
|
];
|
|
|
|
// Find approval data for this step
|
|
const approval = request?.approvals?.find((a: any) => a.levelId === step.levelId);
|
|
|
|
// Extract IO details from internalOrder table (Step 3)
|
|
let ioDetails = undefined;
|
|
if (step.step === 3) {
|
|
// Get IO details from dedicated internalOrder table
|
|
const internalOrder = request?.internalOrder || request?.internal_order;
|
|
|
|
if (internalOrder?.ioNumber || internalOrder?.io_number) {
|
|
// Try multiple field name variations for ioRemark
|
|
const ioRemarkValue =
|
|
internalOrder.ioRemark ||
|
|
internalOrder.io_remark ||
|
|
internalOrder.IORemark ||
|
|
internalOrder.IO_Remark ||
|
|
(internalOrder as any)?.ioRemark ||
|
|
(internalOrder as any)?.io_remark ||
|
|
'';
|
|
|
|
ioDetails = {
|
|
ioNumber: internalOrder.ioNumber || internalOrder.io_number || '',
|
|
ioRemark: (ioRemarkValue && typeof ioRemarkValue === 'string' && ioRemarkValue.trim()) ? ioRemarkValue.trim() : 'N/A',
|
|
blockedAmount: internalOrder.ioBlockedAmount || internalOrder.io_blocked_amount || 0,
|
|
availableBalance: internalOrder.ioAvailableBalance || internalOrder.io_available_balance || 0,
|
|
remainingBalance: internalOrder.ioRemainingBalance || internalOrder.io_remaining_balance || 0,
|
|
organizedBy:
|
|
internalOrder.organizer?.displayName ||
|
|
internalOrder.organizer?.name ||
|
|
internalOrder.organizedBy ||
|
|
step.approver ||
|
|
'N/A',
|
|
organizedAt:
|
|
internalOrder.organizedAt ||
|
|
internalOrder.organized_at ||
|
|
step.approvedAt ||
|
|
request?.updatedAt ||
|
|
'',
|
|
};
|
|
}
|
|
}
|
|
|
|
// Extract DMS details from approval data (Step 6)
|
|
let dmsDetails = undefined;
|
|
if (step.step === 6) {
|
|
if (approval?.dmsDetails) {
|
|
dmsDetails = {
|
|
dmsNumber: approval.dmsDetails.dmsNumber || '',
|
|
dmsRemarks: approval.dmsDetails.dmsRemarks || '',
|
|
pushedBy: approval.dmsDetails.pushedBy || step.approver,
|
|
pushedAt: approval.dmsDetails.pushedAt || step.approvedAt || '',
|
|
};
|
|
} else if (request?.dmsNumber) {
|
|
// Fallback to request-level DMS data
|
|
dmsDetails = {
|
|
dmsNumber: request.dmsNumber || '',
|
|
dmsRemarks: request.dmsRemarks || request.dmsDetails?.dmsRemarks || '',
|
|
pushedBy: step.approver,
|
|
pushedAt: step.approvedAt || request.updatedAt || '',
|
|
};
|
|
}
|
|
}
|
|
|
|
// Normalize status - handle "in-review" and other variations
|
|
let normalizedStatus = (step.status || 'waiting').toLowerCase();
|
|
if (normalizedStatus === 'in-review' || normalizedStatus === 'in_review' || normalizedStatus === 'in review') {
|
|
normalizedStatus = 'in_progress';
|
|
}
|
|
|
|
return {
|
|
step: step.step || index + 1,
|
|
title: stepTitles[index] || `Step ${step.step || index + 1}`,
|
|
approver: step.approver || 'Unknown',
|
|
description: stepDescriptions[index] || step.description || '',
|
|
tatHours: step.tatHours || 24,
|
|
status: normalizedStatus as any,
|
|
comment: step.comment || approval?.comment,
|
|
approvedAt: step.approvedAt || approval?.timestamp,
|
|
elapsedHours: step.elapsedHours,
|
|
ioDetails,
|
|
dmsDetails,
|
|
einvoiceUrl: step.step === 7 ? (approval as any)?.einvoiceUrl : undefined,
|
|
emailTemplateUrl: step.step === 4 ? (approval as any)?.emailTemplateUrl : undefined,
|
|
};
|
|
});
|
|
|
|
const totalSteps = request?.totalSteps || 8;
|
|
|
|
// Calculate currentStep from approval flow - find the first pending or in_progress step
|
|
// If no pending/in_progress step, use the request's currentStep
|
|
// Note: Status normalization already handled in workflowSteps mapping above
|
|
const activeStep = workflowSteps.find(s => {
|
|
const status = s.status?.toLowerCase() || '';
|
|
return status === 'pending' || status === 'in_progress' || status === 'in-review' || status === 'in_review';
|
|
});
|
|
const currentStep = activeStep ? activeStep.step : (request?.currentStep || 1);
|
|
|
|
// Check if current user is the dealer (for steps 1 and 5)
|
|
const userEmail = (user as any)?.email?.toLowerCase() || '';
|
|
const dealerEmail = (
|
|
(request as any)?.dealerEmail?.toLowerCase() ||
|
|
(request as any)?.dealer?.email?.toLowerCase() ||
|
|
(request as any)?.claimDetails?.dealerEmail?.toLowerCase() ||
|
|
(request as any)?.claimDetails?.dealer_email?.toLowerCase() ||
|
|
''
|
|
);
|
|
const isDealer = dealerEmail && userEmail === dealerEmail;
|
|
|
|
// Check if current user is the approver for the current step
|
|
const currentApprovalLevel = approvalFlow.find((level: any) =>
|
|
(level.step || level.levelNumber || level.level_number) === currentStep
|
|
);
|
|
const approverEmail = (currentApprovalLevel?.approverEmail || '').toLowerCase();
|
|
const isCurrentApprover = approverEmail && userEmail === approverEmail;
|
|
|
|
// Check if user is approver for step 2 (requestor evaluation) - match by email
|
|
const step2Level = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === 2);
|
|
const step2ApproverEmail = (step2Level?.approverEmail || '').toLowerCase();
|
|
const isStep2Approver = step2ApproverEmail && userEmail === step2ApproverEmail;
|
|
|
|
// Check if user is approver for step 1 (dealer proposal submission) - match by email
|
|
const step1Level = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === 1);
|
|
const step1ApproverEmail = (step1Level?.approverEmail || '').toLowerCase();
|
|
const isStep1Approver = step1ApproverEmail && userEmail === step1ApproverEmail;
|
|
|
|
// Check if user is approver for step 3 (department lead approval) - match by email
|
|
const step3Level = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === 3);
|
|
const step3ApproverEmail = (step3Level?.approverEmail || '').toLowerCase();
|
|
const isStep3Approver = step3ApproverEmail && userEmail === step3ApproverEmail;
|
|
|
|
// Handle proposal submission
|
|
const handleProposalSubmit = async (data: {
|
|
proposalDocument: File | null;
|
|
costBreakup: Array<{ id: string; description: string; amount: number }>;
|
|
expectedCompletionDate: string;
|
|
otherDocuments: File[];
|
|
dealerComments: string;
|
|
}) => {
|
|
try {
|
|
if (!request?.id && !request?.requestId) {
|
|
throw new Error('Request ID not found');
|
|
}
|
|
|
|
const requestId = request.id || request.requestId;
|
|
|
|
// Upload proposal document if provided
|
|
if (data.proposalDocument) {
|
|
await uploadDocument(data.proposalDocument, requestId, 'APPROVAL');
|
|
}
|
|
|
|
// Upload other supporting documents
|
|
for (const file of data.otherDocuments) {
|
|
await uploadDocument(file, requestId, 'SUPPORTING');
|
|
}
|
|
|
|
// Submit proposal using dealer claim API
|
|
const totalBudget = data.costBreakup.reduce((sum, item) => sum + item.amount, 0);
|
|
await submitProposal(requestId, {
|
|
proposalDocument: data.proposalDocument || undefined,
|
|
costBreakup: data.costBreakup.map(item => ({
|
|
description: item.description,
|
|
amount: item.amount,
|
|
})),
|
|
totalEstimatedBudget: totalBudget,
|
|
expectedCompletionDate: data.expectedCompletionDate,
|
|
dealerComments: data.dealerComments,
|
|
});
|
|
|
|
// Activity is logged by backend service - no need to create work note
|
|
toast.success('Proposal submitted successfully');
|
|
handleRefresh();
|
|
} catch (error: any) {
|
|
console.error('Failed to submit proposal:', error);
|
|
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to submit proposal. Please try again.';
|
|
toast.error(errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// Handle proposal approval
|
|
const handleProposalApprove = async (comments: string) => {
|
|
try {
|
|
if (!request?.id && !request?.requestId) {
|
|
throw new Error('Request ID not found');
|
|
}
|
|
|
|
const requestId = request.id || request.requestId;
|
|
|
|
// Get approval levels to find Step 2 levelId
|
|
const details = await getWorkflowDetails(requestId);
|
|
const approvals = details?.approvalLevels || details?.approvals || [];
|
|
const step2Level = approvals.find((level: any) =>
|
|
(level.levelNumber || level.level_number) === 2
|
|
);
|
|
|
|
if (!step2Level?.levelId && !step2Level?.level_id) {
|
|
throw new Error('Step 2 approval level not found');
|
|
}
|
|
|
|
const levelId = step2Level.levelId || step2Level.level_id;
|
|
|
|
// Approve Step 2 using real API
|
|
await approveLevel(requestId, levelId, comments);
|
|
|
|
// Activity is logged by backend approval service - no need to create work note
|
|
toast.success('Proposal approved successfully');
|
|
handleRefresh();
|
|
} catch (error: any) {
|
|
console.error('Failed to approve proposal:', error);
|
|
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to approve proposal. Please try again.';
|
|
toast.error(errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// Handle proposal rejection
|
|
const handleProposalReject = async (comments: string) => {
|
|
try {
|
|
if (!request?.id && !request?.requestId) {
|
|
throw new Error('Request ID not found');
|
|
}
|
|
|
|
const requestId = request.id || request.requestId;
|
|
|
|
// Get approval levels to find Step 2 levelId
|
|
const details = await getWorkflowDetails(requestId);
|
|
const approvals = details?.approvalLevels || details?.approvals || [];
|
|
const step2Level = approvals.find((level: any) =>
|
|
(level.levelNumber || level.level_number) === 2
|
|
);
|
|
|
|
if (!step2Level?.levelId && !step2Level?.level_id) {
|
|
throw new Error('Step 2 approval level not found');
|
|
}
|
|
|
|
const levelId = step2Level.levelId || step2Level.level_id;
|
|
|
|
// Reject Step 2 using real API
|
|
await rejectLevel(requestId, levelId, 'Proposal rejected by requestor', comments);
|
|
|
|
// Activity is logged by backend approval service - no need to create work note
|
|
toast.success('Proposal rejected. Request has been cancelled.');
|
|
handleRefresh();
|
|
} catch (error: any) {
|
|
console.error('Failed to reject proposal:', error);
|
|
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to reject proposal. Please try again.';
|
|
toast.error(errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// Handle IO approval (Step 3)
|
|
const handleIOApproval = async (data: {
|
|
ioNumber: string;
|
|
ioRemark: string;
|
|
comments: string;
|
|
}) => {
|
|
try {
|
|
if (!request?.id && !request?.requestId) {
|
|
throw new Error('Request ID not found');
|
|
}
|
|
|
|
const requestId = request.id || request.requestId;
|
|
|
|
// Get approval levels to find Step 3 levelId
|
|
const details = await getWorkflowDetails(requestId);
|
|
const approvals = details?.approvalLevels || details?.approvals || [];
|
|
const step3Level = approvals.find((level: any) =>
|
|
(level.levelNumber || level.level_number) === 3
|
|
);
|
|
|
|
if (!step3Level?.levelId && !step3Level?.level_id) {
|
|
throw new Error('Step 3 approval level not found');
|
|
}
|
|
|
|
const levelId = step3Level.levelId || step3Level.level_id;
|
|
|
|
// First, update IO details using dealer claim API
|
|
// Only pass ioNumber and ioRemark - don't override existing balance values
|
|
// Balance values should already be stored when amount was blocked earlier
|
|
await updateIODetails(requestId, {
|
|
ioNumber: data.ioNumber,
|
|
ioRemark: data.ioRemark,
|
|
// Don't pass balance fields - let backend preserve existing values
|
|
});
|
|
|
|
// Approve Step 3 using real API
|
|
// IO remark is stored in claimDetails, so we just pass the comments
|
|
await approveLevel(requestId, levelId, data.comments);
|
|
|
|
// Activity is logged by backend approval service - no need to create work note
|
|
toast.success('Request approved and IO organized successfully');
|
|
handleRefresh();
|
|
} catch (error: any) {
|
|
console.error('Failed to approve and organize IO:', error);
|
|
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to approve request. Please try again.';
|
|
toast.error(errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// Handle completion documents submission (Step 5)
|
|
const handleCompletionSubmit = async (data: {
|
|
activityCompletionDate: string;
|
|
numberOfParticipants?: number;
|
|
closedExpenses: Array<{ id: string; description: string; amount: number }>;
|
|
totalClosedExpenses: number;
|
|
completionDocuments: File[];
|
|
activityPhotos: File[];
|
|
invoicesReceipts?: File[];
|
|
attendanceSheet?: File;
|
|
completionDescription: string;
|
|
}) => {
|
|
try {
|
|
if (!request?.id && !request?.requestId) {
|
|
throw new Error('Request ID not found');
|
|
}
|
|
|
|
const requestId = request.id || request.requestId;
|
|
|
|
// Transform expense items to match API format
|
|
const closedExpenses = data.closedExpenses.map(item => ({
|
|
description: item.description,
|
|
amount: item.amount,
|
|
}));
|
|
|
|
// Submit completion documents using dealer claim API
|
|
await submitCompletion(requestId, {
|
|
activityCompletionDate: data.activityCompletionDate,
|
|
numberOfParticipants: data.numberOfParticipants,
|
|
closedExpenses,
|
|
totalClosedExpenses: data.totalClosedExpenses,
|
|
completionDocuments: data.completionDocuments,
|
|
activityPhotos: data.activityPhotos,
|
|
});
|
|
|
|
// Upload supporting documents if provided
|
|
if (data.invoicesReceipts && data.invoicesReceipts.length > 0) {
|
|
for (const file of data.invoicesReceipts) {
|
|
await uploadDocument(file, requestId, 'SUPPORTING');
|
|
}
|
|
}
|
|
|
|
if (data.attendanceSheet) {
|
|
await uploadDocument(data.attendanceSheet, requestId, 'SUPPORTING');
|
|
}
|
|
|
|
// Activity is logged by backend service - no need to create work note
|
|
toast.success('Completion documents submitted successfully');
|
|
handleRefresh();
|
|
} catch (error: any) {
|
|
console.error('Failed to submit completion documents:', error);
|
|
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to submit completion documents. Please try again.';
|
|
toast.error(errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// Handle DMS push (Step 6)
|
|
const handleDMSPush = async (_comments: string) => {
|
|
try {
|
|
if (!request?.id && !request?.requestId) {
|
|
throw new Error('Request ID not found');
|
|
}
|
|
|
|
const requestId = request.id || request.requestId;
|
|
|
|
// 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().slice(0, 10);
|
|
await updateEInvoice(requestId as string, {
|
|
eInvoiceDate: today,
|
|
});
|
|
|
|
// Activity is logged by backend service - no need to create work note
|
|
toast.success('Pushed to DMS successfully. E-invoice will be generated automatically.');
|
|
handleRefresh();
|
|
} catch (error: any) {
|
|
console.error('[DealerClaimWorkflowTab] Error pushing to DMS:', error);
|
|
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to push to DMS. Please try again.';
|
|
toast.error(errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// Handle IO rejection (Step 3)
|
|
const handleIORejection = async (comments: string) => {
|
|
try {
|
|
if (!request?.id && !request?.requestId) {
|
|
throw new Error('Request ID not found');
|
|
}
|
|
|
|
const requestId = request.id || request.requestId;
|
|
|
|
// Get approval levels to find Step 3 levelId
|
|
const details = await getWorkflowDetails(requestId);
|
|
const approvals = details?.approvalLevels || details?.approvals || [];
|
|
const step3Level = approvals.find((level: any) =>
|
|
(level.levelNumber || level.level_number) === 3
|
|
);
|
|
|
|
if (!step3Level?.levelId && !step3Level?.level_id) {
|
|
throw new Error('Step 3 approval level not found');
|
|
}
|
|
|
|
const levelId = step3Level.levelId || step3Level.level_id;
|
|
|
|
// Reject Step 3 using real API
|
|
await rejectLevel(requestId, levelId, 'Dept Lead rejected - More clarification required', comments);
|
|
|
|
// Activity is logged by backend approval service - no need to create work note
|
|
toast.success('Request rejected. Request has been cancelled.');
|
|
handleRefresh();
|
|
} catch (error: any) {
|
|
console.error('Failed to reject request:', error);
|
|
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to reject request. Please try again.';
|
|
toast.error(errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// Extract proposal data from request
|
|
const [proposalData, setProposalData] = useState<any | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!request) {
|
|
setProposalData(null);
|
|
return;
|
|
}
|
|
|
|
const loadProposalData = async () => {
|
|
try {
|
|
const requestId = request.id || request.requestId;
|
|
if (!requestId) {
|
|
setProposalData(null);
|
|
return;
|
|
}
|
|
|
|
// Get workflow details which includes documents and proposal details
|
|
const details = await getWorkflowDetails(requestId);
|
|
const documents = details?.documents || [];
|
|
const proposalDetails = request.proposalDetails || details?.proposalDetails || {};
|
|
|
|
// Find proposal document (category APPROVAL or type proposal)
|
|
const proposalDoc = documents.find((d: any) =>
|
|
d.category === 'APPROVAL' || d.type === 'proposal' || d.documentCategory === 'APPROVAL'
|
|
);
|
|
|
|
// Find supporting documents
|
|
const otherDocs = documents.filter((d: any) =>
|
|
d.category === 'SUPPORTING' || d.type === 'supporting' || d.documentCategory === 'SUPPORTING'
|
|
);
|
|
|
|
// Ensure costBreakup is an array
|
|
let costBreakup = proposalDetails.costBreakup || [];
|
|
if (typeof costBreakup === 'string') {
|
|
try {
|
|
costBreakup = JSON.parse(costBreakup);
|
|
} catch (e) {
|
|
console.warn('Failed to parse costBreakup JSON:', e);
|
|
costBreakup = [];
|
|
}
|
|
}
|
|
if (!Array.isArray(costBreakup)) {
|
|
costBreakup = [];
|
|
}
|
|
|
|
setProposalData({
|
|
proposalDocument: proposalDoc ? {
|
|
name: proposalDoc.fileName || proposalDoc.file_name || proposalDoc.name,
|
|
id: proposalDoc.documentId || proposalDoc.document_id || proposalDoc.id,
|
|
} : undefined,
|
|
costBreakup: costBreakup,
|
|
expectedCompletionDate: proposalDetails.expectedCompletionDate || '',
|
|
otherDocuments: otherDocs.map((d: any) => ({
|
|
name: d.fileName || d.file_name || d.name,
|
|
id: d.documentId || d.document_id || d.id,
|
|
})),
|
|
dealerComments: proposalDetails.dealerComments || '',
|
|
submittedAt: proposalDetails.submittedAt,
|
|
});
|
|
} catch (error) {
|
|
console.warn('Failed to load proposal data:', error);
|
|
// Fallback to request data only
|
|
const proposalDetails = request.proposalDetails || {};
|
|
|
|
// Ensure costBreakup is an array
|
|
let costBreakup = proposalDetails.costBreakup || [];
|
|
if (typeof costBreakup === 'string') {
|
|
try {
|
|
costBreakup = JSON.parse(costBreakup);
|
|
} catch (e) {
|
|
console.warn('Failed to parse costBreakup JSON:', e);
|
|
costBreakup = [];
|
|
}
|
|
}
|
|
if (!Array.isArray(costBreakup)) {
|
|
costBreakup = [];
|
|
}
|
|
|
|
setProposalData({
|
|
proposalDocument: undefined,
|
|
costBreakup: costBreakup,
|
|
expectedCompletionDate: proposalDetails.expectedCompletionDate || '',
|
|
otherDocuments: [],
|
|
dealerComments: proposalDetails.dealerComments || '',
|
|
submittedAt: proposalDetails.submittedAt,
|
|
});
|
|
}
|
|
};
|
|
|
|
loadProposalData();
|
|
}, [request]);
|
|
|
|
// Get dealer and activity info
|
|
const dealerName = request?.claimDetails?.dealerName ||
|
|
request?.dealerInfo?.name ||
|
|
'Dealer';
|
|
const activityName = request?.claimDetails?.activityName ||
|
|
request?.activityInfo?.activityName ||
|
|
request?.title ||
|
|
'Activity';
|
|
|
|
return (
|
|
<>
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<TrendingUp className="w-5 h-5 text-purple-600" />
|
|
Claim Management Workflow
|
|
</CardTitle>
|
|
<CardDescription className="mt-2">
|
|
8-Step approval process for dealer claim management
|
|
</CardDescription>
|
|
</div>
|
|
<Badge variant="outline" className="font-medium">
|
|
Step {currentStep} of {totalSteps}
|
|
</Badge>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{workflowSteps.map((step, index) => {
|
|
// Step is active if it's pending or in_progress and matches currentStep
|
|
const isActive = (step.status === 'pending' || step.status === 'in_progress') && step.step === currentStep;
|
|
const isCompleted = step.status === 'approved';
|
|
|
|
return (
|
|
<div
|
|
key={index}
|
|
className={`relative p-5 rounded-lg border-2 transition-all ${getStepCardStyle(step.status, isActive)}`}
|
|
>
|
|
<div className="flex items-start gap-4">
|
|
{/* Step Icon */}
|
|
<div className={`p-3 rounded-xl ${getStepIconBg(step.status)}`}>
|
|
{getStepIcon(step.status)}
|
|
</div>
|
|
|
|
{/* Step Content */}
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-start justify-between gap-4 mb-2">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<h4 className="font-semibold text-gray-900">
|
|
Step {step.step}: {step.title}
|
|
</h4>
|
|
<Badge className={getStepBadgeVariant(step.status)}>
|
|
{step.status.toLowerCase()}
|
|
</Badge>
|
|
{/* Email Template Button (Step 4) - Show when approved */}
|
|
{step.step === 4 && step.status === 'approved' && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 w-6 p-0 hover:bg-blue-100"
|
|
title="View email template"
|
|
onClick={() => {
|
|
setSelectedStepForEmail({ stepNumber: step.step, stepName: step.title });
|
|
setShowEmailTemplateModal(true);
|
|
}}
|
|
>
|
|
<Mail className="w-3.5 h-3.5 text-blue-600" />
|
|
</Button>
|
|
)}
|
|
{/* E-Invoice Download Button (Step 7) */}
|
|
{step.step === 7 && step.einvoiceUrl && isCompleted && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 w-6 p-0 hover:bg-green-100"
|
|
title="Download E-Invoice"
|
|
onClick={() => window.open(step.einvoiceUrl, '_blank')}
|
|
>
|
|
<Download className="w-3.5 h-3.5 text-green-600" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
<p className="text-sm text-gray-600">{step.approver}</p>
|
|
<p className="text-sm text-gray-500 mt-2 italic">{step.description}</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="text-xs text-gray-500">TAT: {step.tatHours}h</p>
|
|
{step.elapsedHours && (
|
|
<p className="text-xs text-gray-600 font-medium">
|
|
Elapsed: {step.elapsedHours}h
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Comment Section */}
|
|
{step.comment && (
|
|
<div className="mt-3 p-3 bg-white rounded-lg border border-gray-200">
|
|
<p className="text-sm text-gray-700">{step.comment}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* IO Organization Details (Step 3) - Show when step is approved and has IO details */}
|
|
{step.step === 3 && step.status === 'approved' && step.ioDetails && step.ioDetails.ioNumber && (
|
|
<div className="mt-3 p-3 bg-blue-50 rounded-lg border border-blue-200">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Receipt className="w-4 h-4 text-blue-600" />
|
|
<p className="text-xs font-semibold text-blue-900 uppercase tracking-wide">
|
|
IO Organisation Details
|
|
</p>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs text-gray-600">IO Number:</span>
|
|
<span className="text-sm font-semibold text-gray-900">
|
|
{step.ioDetails.ioNumber}
|
|
</span>
|
|
</div>
|
|
{step.ioDetails.blockedAmount !== undefined && step.ioDetails.blockedAmount > 0 && (
|
|
<div className="flex items-center justify-between pt-1.5 border-t border-blue-100">
|
|
<span className="text-xs text-gray-600">Blocked Amount:</span>
|
|
<span className="text-sm font-bold text-green-700">
|
|
₹{step.ioDetails.blockedAmount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
|
</span>
|
|
</div>
|
|
)}
|
|
<div className="pt-1.5 border-t border-blue-100">
|
|
<p className="text-xs text-gray-600 mb-1">IO Remark:</p>
|
|
<p className="text-sm text-gray-900">
|
|
{step.ioDetails.ioRemark || 'N/A'}
|
|
</p>
|
|
</div>
|
|
<div className="pt-1.5 border-t border-blue-100 text-xs text-gray-500">
|
|
Organised by {step.ioDetails.organizedBy || step.approver || 'N/A'} on{' '}
|
|
{step.ioDetails.organizedAt
|
|
? formatDateSafe(step.ioDetails.organizedAt)
|
|
: (step.approvedAt ? formatDateSafe(step.approvedAt) : 'N/A')
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* DMS Processing Details (Step 6) */}
|
|
{step.step === 6 && step.dmsDetails && step.dmsDetails.dmsNumber && (
|
|
<div className="mt-3 p-3 bg-purple-50 rounded-lg border border-purple-200">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Activity className="w-4 h-4 text-purple-600" />
|
|
<p className="text-xs font-semibold text-purple-900 uppercase tracking-wide">
|
|
DMS Processing Details
|
|
</p>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs text-gray-600">DMS Number:</span>
|
|
<span className="text-sm font-semibold text-gray-900">
|
|
{step.dmsDetails.dmsNumber}
|
|
</span>
|
|
</div>
|
|
{step.dmsDetails.dmsRemarks && (
|
|
<div className="pt-1.5 border-t border-purple-100">
|
|
<p className="text-xs text-gray-600 mb-1">DMS Remarks:</p>
|
|
<p className="text-sm text-gray-900">{step.dmsDetails.dmsRemarks}</p>
|
|
</div>
|
|
)}
|
|
{step.dmsDetails.pushedAt && (
|
|
<div className="pt-1.5 border-t border-purple-100 text-xs text-gray-500">
|
|
Pushed by {step.dmsDetails.pushedBy} on{' '}
|
|
{formatDateSafe(step.dmsDetails.pushedAt)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Action Buttons */}
|
|
{isActive && (
|
|
<div className="mt-4 flex gap-2">
|
|
{/* Step 1: Submit Proposal Button - Only for dealer or step 1 approver */}
|
|
{step.step === 1 && (isDealer || isStep1Approver) && (
|
|
<Button
|
|
className="bg-purple-600 hover:bg-purple-700"
|
|
onClick={() => {
|
|
setShowProposalModal(true);
|
|
}}
|
|
>
|
|
<Upload className="w-4 h-4 mr-2" />
|
|
Submit Proposal
|
|
</Button>
|
|
)}
|
|
|
|
{/* Step 2: Confirm Request - Only for initiator or step 2 approver */}
|
|
{step.step === 2 && (isInitiator || isStep2Approver) && (
|
|
<Button
|
|
className="bg-blue-600 hover:bg-blue-700"
|
|
onClick={() => {
|
|
setShowApprovalModal(true);
|
|
}}
|
|
>
|
|
<CheckCircle className="w-4 h-4 mr-2" />
|
|
Confirm Request
|
|
</Button>
|
|
)}
|
|
|
|
{/* Step 3: Approve and Organise IO - Only for department lead (step 3 approver) */}
|
|
{step.step === 3 && (() => {
|
|
// Find step 3 from approvalFlow to get approverEmail
|
|
const step3Level = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === 3);
|
|
const step3ApproverEmail = (step3Level?.approverEmail || '').toLowerCase();
|
|
const isStep3ApproverByEmail = step3ApproverEmail && userEmail === step3ApproverEmail;
|
|
return isStep3ApproverByEmail || isStep3Approver || isCurrentApprover;
|
|
})() && (
|
|
<Button
|
|
className="bg-green-600 hover:bg-green-700"
|
|
onClick={() => {
|
|
setShowIOApprovalModal(true);
|
|
}}
|
|
>
|
|
<CheckCircle className="w-4 h-4 mr-2" />
|
|
Approve and Organise IO
|
|
</Button>
|
|
)}
|
|
|
|
{/* Step 5: Upload Completion Documents - Only for dealer */}
|
|
{step.step === 5 && isDealer && (
|
|
<Button
|
|
className="bg-purple-600 hover:bg-purple-700"
|
|
onClick={() => {
|
|
setShowCompletionModal(true);
|
|
}}
|
|
>
|
|
<Upload className="w-4 h-4 mr-2" />
|
|
Upload Completion Docs
|
|
</Button>
|
|
)}
|
|
|
|
{/* Step 6: Push to DMS - Only for initiator or step 6 approver */}
|
|
{step.step === 6 && (isInitiator || (() => {
|
|
const step6Level = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === 6);
|
|
const step6ApproverEmail = (step6Level?.approverEmail || '').toLowerCase();
|
|
return step6ApproverEmail && userEmail === step6ApproverEmail;
|
|
})()) && (
|
|
<Button
|
|
className="bg-indigo-600 hover:bg-indigo-700"
|
|
onClick={() => {
|
|
setShowDMSPushModal(true);
|
|
}}
|
|
>
|
|
<Activity className="w-4 h-4 mr-2" />
|
|
Push to DMS
|
|
</Button>
|
|
)}
|
|
|
|
{/* Step 8: View & Send Credit Note - Only for finance approver or step 8 approver */}
|
|
{step.step === 8 && (() => {
|
|
const step8Level = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === 8);
|
|
const step8ApproverEmail = (step8Level?.approverEmail || '').toLowerCase();
|
|
const isStep8Approver = step8ApproverEmail && userEmail === step8ApproverEmail;
|
|
// Also check if user has finance role
|
|
const userRole = (user as any)?.role?.toUpperCase() || '';
|
|
const isFinanceUser = userRole === 'FINANCE' || userRole === 'ADMIN';
|
|
return isStep8Approver || isFinanceUser;
|
|
})() && (
|
|
<Button
|
|
className="bg-green-600 hover:bg-green-700"
|
|
onClick={() => {
|
|
setShowCreditNoteModal(true);
|
|
}}
|
|
>
|
|
<Receipt className="w-4 h-4 mr-2" />
|
|
View & Send Credit Note
|
|
</Button>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Approved Date */}
|
|
{step.approvedAt && (
|
|
<p className="text-xs text-gray-500 mt-2">
|
|
Approved on {formatDateSafe(step.approvedAt)}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Dealer Proposal Submission Modal */}
|
|
<DealerProposalSubmissionModal
|
|
isOpen={showProposalModal}
|
|
onClose={() => setShowProposalModal(false)}
|
|
onSubmit={handleProposalSubmit}
|
|
dealerName={dealerName}
|
|
activityName={activityName}
|
|
requestId={request?.id || request?.requestId}
|
|
/>
|
|
|
|
{/* Initiator Proposal Approval Modal */}
|
|
<InitiatorProposalApprovalModal
|
|
isOpen={showApprovalModal}
|
|
onClose={() => {
|
|
setShowApprovalModal(false);
|
|
}}
|
|
onApprove={handleProposalApprove}
|
|
onReject={handleProposalReject}
|
|
proposalData={proposalData}
|
|
dealerName={dealerName}
|
|
activityName={activityName}
|
|
requestId={request?.id || request?.requestId}
|
|
/>
|
|
|
|
{/* Dept Lead IO Approval Modal */}
|
|
<DeptLeadIOApprovalModal
|
|
isOpen={showIOApprovalModal}
|
|
onClose={() => setShowIOApprovalModal(false)}
|
|
onApprove={handleIOApproval}
|
|
onReject={handleIORejection}
|
|
requestTitle={request?.title}
|
|
requestId={request?.id || request?.requestId}
|
|
preFilledIONumber={request?.internalOrder?.ioNumber || request?.internalOrder?.io_number || request?.internal_order?.ioNumber || request?.internal_order?.io_number || undefined}
|
|
preFilledBlockedAmount={request?.internalOrder?.ioBlockedAmount || request?.internalOrder?.io_blocked_amount || request?.internal_order?.ioBlockedAmount || request?.internal_order?.io_blocked_amount || undefined}
|
|
preFilledRemainingBalance={request?.internalOrder?.ioRemainingBalance || request?.internalOrder?.io_remaining_balance || request?.internal_order?.ioRemainingBalance || request?.internal_order?.io_remaining_balance || undefined}
|
|
/>
|
|
|
|
{/* Dealer Completion Documents Modal */}
|
|
<DealerCompletionDocumentsModal
|
|
isOpen={showCompletionModal}
|
|
onClose={() => setShowCompletionModal(false)}
|
|
onSubmit={handleCompletionSubmit}
|
|
dealerName={dealerName}
|
|
activityName={activityName}
|
|
requestId={request?.id || request?.requestId}
|
|
/>
|
|
|
|
{/* DMS Push Modal */}
|
|
<DMSPushModal
|
|
isOpen={showDMSPushModal}
|
|
onClose={() => setShowDMSPushModal(false)}
|
|
onPush={handleDMSPush}
|
|
completionDetails={{
|
|
activityCompletionDate: request?.completionDetails?.activityCompletionDate || request?.completionDetails?.activity_completion_date,
|
|
numberOfParticipants: request?.completionDetails?.numberOfParticipants || request?.completionDetails?.number_of_participants,
|
|
closedExpenses: request?.completionExpenses || request?.completion_expenses || request?.completionDetails?.closedExpenses || request?.completionDetails?.closed_expenses,
|
|
totalClosedExpenses: request?.budgetTracking?.closedExpenses || request?.budgetTracking?.closed_expenses || request?.completionDetails?.totalClosedExpenses || request?.completionDetails?.total_closed_expenses,
|
|
completionDescription: request?.completionDetails?.completionDescription || request?.completionDetails?.completion_description,
|
|
}}
|
|
ioDetails={{
|
|
ioNumber: request?.internalOrder?.ioNumber || request?.internalOrder?.io_number || request?.internal_order?.ioNumber || request?.internal_order?.io_number,
|
|
blockedAmount: request?.internalOrder?.ioBlockedAmount || request?.internalOrder?.io_blocked_amount || request?.internal_order?.ioBlockedAmount || request?.internal_order?.io_blocked_amount,
|
|
availableBalance: request?.internalOrder?.ioAvailableBalance || request?.internalOrder?.io_available_balance || request?.internal_order?.ioAvailableBalance || request?.internal_order?.io_available_balance,
|
|
remainingBalance: request?.internalOrder?.ioRemainingBalance || request?.internalOrder?.io_remaining_balance || request?.internal_order?.ioRemainingBalance || request?.internal_order?.io_remaining_balance,
|
|
}}
|
|
requestTitle={request?.title}
|
|
requestNumber={request?.requestNumber || request?.request_number || request?.id}
|
|
/>
|
|
|
|
{/* Credit Note from SAP Modal (Step 8) */}
|
|
<CreditNoteSAPModal
|
|
isOpen={showCreditNoteModal}
|
|
onClose={() => setShowCreditNoteModal(false)}
|
|
onDownload={async () => {
|
|
// TODO: Implement download functionality
|
|
toast.info('Download functionality will be implemented');
|
|
}}
|
|
onSendToDealer={async () => {
|
|
// TODO: Implement send to dealer functionality
|
|
toast.info('Send to dealer functionality will be implemented');
|
|
}}
|
|
creditNoteData={{
|
|
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={{
|
|
dealerName: (request as any)?.claimDetails?.dealerName || (request as any)?.claimDetails?.dealer_name,
|
|
dealerCode: (request as any)?.claimDetails?.dealerCode || (request as any)?.claimDetails?.dealer_code,
|
|
dealerEmail: (request as any)?.claimDetails?.dealerEmail || (request as any)?.claimDetails?.dealer_email,
|
|
}}
|
|
activityName={(request as any)?.claimDetails?.activityName || (request as any)?.claimDetails?.activity_name}
|
|
requestNumber={request?.requestNumber || request?.id}
|
|
requestId={request?.requestId || request?.id}
|
|
dueDate={request?.dueDate}
|
|
/>
|
|
|
|
{/* Email Notification Template Modal */}
|
|
<EmailNotificationTemplateModal
|
|
isOpen={showEmailTemplateModal}
|
|
onClose={() => {
|
|
setShowEmailTemplateModal(false);
|
|
setSelectedStepForEmail(null);
|
|
}}
|
|
stepNumber={selectedStepForEmail?.stepNumber || 4}
|
|
stepName={selectedStepForEmail?.stepName || 'Activity Creation'}
|
|
requestNumber={request?.requestNumber || request?.id || request?.request_number}
|
|
recipientEmail="system@royalenfield.com"
|
|
/>
|
|
</>
|
|
);
|
|
}
|