1630 lines
76 KiB
TypeScript
1630 lines
76 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 { Progress } from '@/components/ui/progress';
|
|
import { TrendingUp, Clock, CheckCircle, CircleCheckBig, Upload, Mail, Download, Receipt, Activity, AlertTriangle, AlertOctagon } from 'lucide-react';
|
|
import { formatDateTime, formatDateDDMMYYYY } from '@/utils/dateFormatter';
|
|
import { formatHoursMinutes } from '@/utils/slaTracker';
|
|
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, sendCreditNoteToDealer } 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
|
|
// Always fetch from API to ensure fresh data (don't rely on cached request.approvalFlow)
|
|
// Also watch for changes in totalLevels to detect when approvers are added
|
|
useEffect(() => {
|
|
const loadApprovalFlows = async () => {
|
|
// Always load from real API to get the latest data
|
|
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
|
|
// Include levelName and levelNumber for proper mapping
|
|
const flows = approvals
|
|
.map((level: any) => ({
|
|
step: level.levelNumber || level.level_number || 0,
|
|
levelNumber: level.levelNumber || level.level_number || 0,
|
|
levelName: level.levelName || level.level_name,
|
|
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,
|
|
}))
|
|
// Sort by levelNumber to ensure correct order (critical for proper display)
|
|
.sort((a: any, b: any) => (a.levelNumber || 0) - (b.levelNumber || 0));
|
|
|
|
// Only update if the data actually changed (avoid unnecessary re-renders)
|
|
setApprovalFlow(prevFlows => {
|
|
// Check if flows are different
|
|
if (prevFlows.length !== flows.length) {
|
|
return flows;
|
|
}
|
|
// Check if any levelNumber or levelName changed
|
|
const hasChanges = prevFlows.some((prev: any, idx: number) => {
|
|
const curr = flows[idx];
|
|
return !curr ||
|
|
prev.levelNumber !== curr.levelNumber ||
|
|
prev.levelName !== curr.levelName ||
|
|
prev.approverEmail !== curr.approverEmail;
|
|
});
|
|
return hasChanges ? flows : prevFlows;
|
|
});
|
|
} else {
|
|
// If no approvals found, clear the flow
|
|
setApprovalFlow([]);
|
|
}
|
|
} catch (error) {
|
|
console.warn('Failed to load approval flows from API:', error);
|
|
// On error, try to use request.approvalFlow as fallback
|
|
if (request?.approvalFlow && request.approvalFlow.length > 0) {
|
|
setApprovalFlow(request.approvalFlow);
|
|
}
|
|
}
|
|
} else if (request?.approvalFlow && request.approvalFlow.length > 0) {
|
|
// Fallback: use request.approvalFlow only if no requestId available
|
|
setApprovalFlow(request.approvalFlow);
|
|
}
|
|
};
|
|
|
|
loadApprovalFlows();
|
|
}, [request?.id, request?.requestId, request?.totalLevels, refreshTrigger]);
|
|
|
|
// Also reload when request.currentStep or totalLevels changes (to catch step transitions and new approvers)
|
|
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,
|
|
levelNumber: level.levelNumber || level.level_number || 0,
|
|
levelName: level.levelName || level.level_name,
|
|
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,
|
|
}))
|
|
// Sort by levelNumber to ensure correct order
|
|
.sort((a: any, b: any) => (a.levelNumber || 0) - (b.levelNumber || 0));
|
|
|
|
// Update state with new flows
|
|
setApprovalFlow(flows);
|
|
}
|
|
} catch (error) {
|
|
console.warn('Failed to load approval flows from API:', error);
|
|
}
|
|
};
|
|
loadApprovalFlows();
|
|
}
|
|
}, [request?.currentStep, request?.totalLevels]);
|
|
|
|
// Enhanced refresh handler that also reloads approval flows
|
|
const handleRefresh = () => {
|
|
setRefreshTrigger(prev => prev + 1);
|
|
onRefresh?.();
|
|
};
|
|
|
|
// Step title and description mapping based on actual step number (not array index)
|
|
// This handles cases where approvers are added between steps
|
|
const getStepTitle = (stepNumber: number, levelName?: string, approverName?: string): string => {
|
|
// Use levelName from backend if available (most accurate)
|
|
// Check if it's an "Additional Approver" - this indicates a dynamically added approver
|
|
if (levelName && levelName.trim()) {
|
|
// If it starts with "Additional Approver", use it as-is (it's already formatted)
|
|
if (levelName.toLowerCase().includes('additional approver')) {
|
|
return levelName;
|
|
}
|
|
// Otherwise use the levelName from backend (preserved from original step)
|
|
return levelName;
|
|
}
|
|
|
|
// Fallback to mapping based on step number
|
|
const stepTitleMap: Record<number, string> = {
|
|
1: 'Dealer - Proposal Submission',
|
|
2: 'Requestor Evaluation & Confirmation',
|
|
3: 'Department Lead Approval',
|
|
4: 'Activity Creation',
|
|
5: 'Dealer - Completion Documents',
|
|
6: 'Requestor - Claim Approval',
|
|
7: 'E-Invoice Generation',
|
|
8: 'Credit Note from SAP',
|
|
};
|
|
|
|
// If step number exists in map, use it
|
|
if (stepTitleMap[stepNumber]) {
|
|
return stepTitleMap[stepNumber];
|
|
}
|
|
|
|
// For dynamically added steps, create a title from approver name or generic
|
|
if (approverName && approverName !== 'Unknown' && approverName !== 'System') {
|
|
return `Additional Approver - ${approverName}`;
|
|
}
|
|
|
|
return `Additional Approver - Step ${stepNumber}`;
|
|
};
|
|
|
|
const getStepDescription = (stepNumber: number, levelName?: string, approverName?: string): string => {
|
|
// Check if this is an "Additional Approver" (dynamically added)
|
|
const isAdditionalApprover = levelName && levelName.toLowerCase().includes('additional approver');
|
|
|
|
// If this is an additional approver, use generic description
|
|
if (isAdditionalApprover) {
|
|
if (approverName && approverName !== 'Unknown' && approverName !== 'System') {
|
|
return `${approverName} will review and approve this request as an additional approver.`;
|
|
}
|
|
return `Additional approver will review and approve this request.`;
|
|
}
|
|
|
|
// Use levelName to determine description (handles shifted steps correctly)
|
|
// This ensures descriptions shift with their steps when approvers are added
|
|
if (levelName && levelName.trim()) {
|
|
const levelNameLower = levelName.toLowerCase();
|
|
|
|
// Map level names to descriptions (works even after shifting)
|
|
if (levelNameLower.includes('dealer') && levelNameLower.includes('proposal')) {
|
|
return 'Dealer submits the proposal for the activity with comments including proposal document with requested details, cost break-up, timeline for closure, and other requests';
|
|
}
|
|
if (levelNameLower.includes('requestor') && (levelNameLower.includes('evaluation') || levelNameLower.includes('confirmation'))) {
|
|
return 'Requestor evaluates the request and confirms with comments. Decision point: Confirms? (YES → Continue to Dept Lead / NO → Request is cancelled)';
|
|
}
|
|
if (levelNameLower.includes('department lead')) {
|
|
return 'Department Lead approval. Decision point: Approved? (YES → Budget is blocked in the respective IO for the activity / NO → More clarification required → Request is cancelled)';
|
|
}
|
|
if (levelNameLower.includes('activity creation')) {
|
|
return 'Activity is created. Activity confirmation email is auto-triggered to dealer / requestor / Lead. IO confirmation to be made.';
|
|
}
|
|
if (levelNameLower.includes('dealer') && (levelNameLower.includes('completion') || levelNameLower.includes('documents'))) {
|
|
return 'Dealer submits the necessary documents upon completion of the activity including document attachments (Zip Folder) and brief description';
|
|
}
|
|
if (levelNameLower.includes('requestor') && (levelNameLower.includes('claim') || levelNameLower.includes('approval'))) {
|
|
return 'Requestor approves the claim in full or can modify the amount. If more information is required, can request additional details from dealer.';
|
|
}
|
|
if (levelNameLower.includes('e-invoice') || levelNameLower.includes('invoice generation')) {
|
|
return 'E-invoice will be generated through DMS.';
|
|
}
|
|
if (levelNameLower.includes('credit note') || levelNameLower.includes('sap')) {
|
|
return 'Got credit note from SAP. Review and send to dealer to complete the claim management process.';
|
|
}
|
|
}
|
|
|
|
// Fallback to step number mapping (for backwards compatibility)
|
|
const stepDescriptionMap: Record<number, string> = {
|
|
1: 'Dealer submits the proposal for the activity with comments including proposal document with requested details, cost break-up, timeline for closure, and other requests',
|
|
2: 'Requestor evaluates the request and confirms with comments. Decision point: Confirms? (YES → Continue to Dept Lead / NO → Request is cancelled)',
|
|
3: 'Department Lead approval. Decision point: Approved? (YES → Budget is blocked in the respective IO for the activity / NO → More clarification required → Request is cancelled)',
|
|
4: 'Activity is created. Activity confirmation email is auto-triggered to dealer / requestor / Lead. IO confirmation to be made.',
|
|
5: 'Dealer submits the necessary documents upon completion of the activity including document attachments (Zip Folder) and brief description',
|
|
6: 'Requestor approves the claim in full or can modify the amount. If more information is required, can request additional details from dealer.',
|
|
7: 'E-invoice will be generated through DMS.',
|
|
8: 'Got credit note from SAP. Review and send to dealer to complete the claim management process.',
|
|
};
|
|
|
|
if (stepDescriptionMap[stepNumber]) {
|
|
return stepDescriptionMap[stepNumber];
|
|
}
|
|
|
|
// Final fallback
|
|
if (approverName && approverName !== 'Unknown' && approverName !== 'System') {
|
|
return `${approverName} will review and approve this request.`;
|
|
}
|
|
|
|
return `Step ${stepNumber} approval required.`;
|
|
};
|
|
|
|
// Transform approval flow to dealer claim workflow steps
|
|
const workflowSteps: WorkflowStep[] = approvalFlow.map((step: any, index: number) => {
|
|
// Get actual step number from levelNumber or step field
|
|
const actualStepNumber = step.levelNumber || step.level_number || step.step || index + 1;
|
|
|
|
// Get levelName from the approval level if available
|
|
const levelName = step.levelName || step.level_name;
|
|
|
|
// Find approval data for this step
|
|
const approval = request?.approvals?.find((a: any) => a.levelId === step.levelId);
|
|
|
|
// Extract IO details from internalOrder table (Department Lead step - check by levelName)
|
|
let ioDetails = undefined;
|
|
const isDeptLeadStep = levelName && levelName.toLowerCase().includes('department lead');
|
|
if (isDeptLeadStep || actualStepNumber === 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 (actualStepNumber === 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';
|
|
}
|
|
|
|
// Business logic: Only show elapsed time for active or completed steps
|
|
// Waiting steps (future steps) should have elapsedHours = 0
|
|
// This ensures that when in step 1, only step 1 shows elapsed time, others show 0
|
|
const isWaiting = normalizedStatus === 'waiting';
|
|
|
|
// Only calculate/show elapsed hours for active or completed steps
|
|
// For waiting steps, elapsedHours should be 0 (they haven't started yet)
|
|
const elapsedHours = isWaiting ? 0 : (step.elapsedHours || 0);
|
|
|
|
const approverName = step.approver || step.approverName || 'Unknown';
|
|
|
|
return {
|
|
step: actualStepNumber,
|
|
title: getStepTitle(actualStepNumber, levelName, approverName),
|
|
approver: approverName,
|
|
description: getStepDescription(actualStepNumber, levelName, approverName) || step.description || '',
|
|
tatHours: step.tatHours || 24,
|
|
status: normalizedStatus as any,
|
|
comment: step.comment || approval?.comment,
|
|
approvedAt: step.approvedAt || approval?.timestamp,
|
|
elapsedHours, // Only non-zero for active/completed steps
|
|
ioDetails,
|
|
dmsDetails,
|
|
einvoiceUrl: actualStepNumber === 7 ? (approval as any)?.einvoiceUrl : undefined,
|
|
emailTemplateUrl: (approval as any)?.emailTemplateUrl || undefined,
|
|
};
|
|
});
|
|
|
|
const totalSteps = request?.totalSteps || 8;
|
|
|
|
// Calculate currentStep from approval flow - find the first pending or in_progress step
|
|
// IMPORTANT: Use the workflow's currentLevel from backend (most accurate)
|
|
// Fallback to finding first pending step if currentLevel not available
|
|
// Note: Status normalization already handled in workflowSteps mapping above
|
|
const backendCurrentLevel = request?.currentLevel || request?.current_level || request?.currentStep;
|
|
|
|
// Find the step that matches backend's currentLevel
|
|
const activeStepFromBackend = workflowSteps.find(s => s.step === backendCurrentLevel);
|
|
|
|
// If backend currentLevel exists and step is pending/in_progress, use it
|
|
// Otherwise, find first pending/in_progress step
|
|
const activeStep = activeStepFromBackend &&
|
|
(activeStepFromBackend.status === 'pending' || activeStepFromBackend.status === 'in_progress')
|
|
? activeStepFromBackend
|
|
: 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 : (backendCurrentLevel || 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;
|
|
|
|
// Find the initiator's step dynamically (Requestor Evaluation step)
|
|
// This handles cases where approvers are added between steps, causing step numbers to shift
|
|
const initiatorEmail = (
|
|
(request as any)?.initiator?.email?.toLowerCase() ||
|
|
(request as any)?.initiatorEmail?.toLowerCase() ||
|
|
''
|
|
);
|
|
|
|
// Find the step where the initiator is the approver
|
|
// Check by: 1) approverEmail matches initiatorEmail, OR 2) levelName contains "Requestor Evaluation"
|
|
const initiatorStepLevel = approvalFlow.find((l: any) => {
|
|
const levelApproverEmail = (l.approverEmail || '').toLowerCase();
|
|
const levelName = (l.levelName || '').toLowerCase();
|
|
return (initiatorEmail && levelApproverEmail === initiatorEmail) ||
|
|
levelName.includes('requestor evaluation') ||
|
|
levelName.includes('requestor') && levelName.includes('confirmation');
|
|
});
|
|
|
|
const initiatorStepNumber = initiatorStepLevel
|
|
? (initiatorStepLevel.step || initiatorStepLevel.levelNumber || initiatorStepLevel.level_number || 2)
|
|
: 2; // Fallback to 2 if not found
|
|
|
|
// Check if user is approver for the initiator's step (requestor evaluation)
|
|
const step2Level = initiatorStepLevel || 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;
|
|
|
|
// Find Department Lead step dynamically (handles step shifts)
|
|
const deptLeadStepLevel = approvalFlow.find((l: any) => {
|
|
const levelName = (l.levelName || '').toLowerCase();
|
|
return levelName.includes('department lead');
|
|
});
|
|
const step3ApproverEmail = (deptLeadStepLevel?.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 the initiator's step levelId dynamically
|
|
const details = await getWorkflowDetails(requestId);
|
|
const approvals = details?.approvalLevels || details?.approvals || [];
|
|
|
|
// Find the initiator's step by checking approverEmail or levelName
|
|
const initiatorEmail = (
|
|
(request as any)?.initiator?.email?.toLowerCase() ||
|
|
(request as any)?.initiatorEmail?.toLowerCase() ||
|
|
''
|
|
);
|
|
|
|
const step2Level = approvals.find((level: any) => {
|
|
const levelApproverEmail = (level.approverEmail || level.approver_email || '').toLowerCase();
|
|
const levelName = (level.levelName || level.level_name || '').toLowerCase();
|
|
const levelNumber = level.levelNumber || level.level_number;
|
|
|
|
// Check if this is the initiator's step
|
|
return (initiatorEmail && levelApproverEmail === initiatorEmail) ||
|
|
levelName.includes('requestor evaluation') ||
|
|
(levelName.includes('requestor') && levelName.includes('confirmation')) ||
|
|
// Fallback: if initiatorStepNumber was found earlier, use it
|
|
(levelNumber === initiatorStepNumber);
|
|
}) || approvals.find((level: any) =>
|
|
(level.levelNumber || level.level_number) === 2
|
|
); // Final fallback to level 2
|
|
|
|
if (!step2Level?.levelId && !step2Level?.level_id) {
|
|
throw new Error('Initiator approval level not found');
|
|
}
|
|
|
|
const levelId = step2Level.levelId || step2Level.level_id;
|
|
|
|
// Approve the initiator's step 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 the initiator's step levelId dynamically
|
|
const details = await getWorkflowDetails(requestId);
|
|
const approvals = details?.approvalLevels || details?.approvals || [];
|
|
|
|
// Find the initiator's step by checking approverEmail or levelName
|
|
const initiatorEmail = (
|
|
(request as any)?.initiator?.email?.toLowerCase() ||
|
|
(request as any)?.initiatorEmail?.toLowerCase() ||
|
|
''
|
|
);
|
|
|
|
const step2Level = approvals.find((level: any) => {
|
|
const levelApproverEmail = (level.approverEmail || level.approver_email || '').toLowerCase();
|
|
const levelName = (level.levelName || level.level_name || '').toLowerCase();
|
|
const levelNumber = level.levelNumber || level.level_number;
|
|
|
|
// Check if this is the initiator's step
|
|
return (initiatorEmail && levelApproverEmail === initiatorEmail) ||
|
|
levelName.includes('requestor evaluation') ||
|
|
(levelName.includes('requestor') && levelName.includes('confirmation')) ||
|
|
// Fallback: if initiatorStepNumber was found earlier, use it
|
|
(levelNumber === initiatorStepNumber);
|
|
}) || approvals.find((level: any) =>
|
|
(level.levelNumber || level.level_number) === 2
|
|
); // Final fallback to level 2
|
|
|
|
if (!step2Level?.levelId && !step2Level?.level_id) {
|
|
throw new Error('Initiator approval level not found');
|
|
}
|
|
|
|
const levelId = step2Level.levelId || step2Level.level_id;
|
|
|
|
// Reject the initiator's step 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 (Department Lead step - found dynamically)
|
|
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 Department Lead step levelId dynamically
|
|
const details = await getWorkflowDetails(requestId);
|
|
const approvals = details?.approvalLevels || details?.approvals || [];
|
|
|
|
// Find Department Lead step by levelName (handles step shifts)
|
|
const step3Level = approvals.find((level: any) => {
|
|
const levelName = (level.levelName || level.level_name || '').toLowerCase();
|
|
return levelName.includes('department lead');
|
|
}) || approvals.find((level: any) =>
|
|
(level.levelNumber || level.level_number) === 3
|
|
); // Fallback to level 3
|
|
|
|
if (!step3Level?.levelId && !step3Level?.level_id) {
|
|
throw new Error('Department Lead 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 Department Lead step levelId dynamically
|
|
const details = await getWorkflowDetails(requestId);
|
|
const approvals = details?.approvalLevels || details?.approvals || [];
|
|
|
|
// Find Department Lead step by levelName (handles step shifts)
|
|
const step3Level = approvals.find((level: any) => {
|
|
const levelName = (level.levelName || level.level_name || '').toLowerCase();
|
|
return levelName.includes('department lead');
|
|
}) || approvals.find((level: any) =>
|
|
(level.levelNumber || level.level_number) === 3
|
|
); // Fallback to level 3
|
|
|
|
if (!step3Level?.levelId && !step3Level?.level_id) {
|
|
throw new Error('Department Lead approval level not found');
|
|
}
|
|
|
|
const levelId = step3Level.levelId || step3Level.level_id;
|
|
|
|
// Reject Department Lead step 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:
|
|
// 1. It's pending or in_progress
|
|
// 2. AND it matches currentStep (from backend or calculated)
|
|
// 3. AND it's the actual current step (not a future step that happens to be pending)
|
|
const stepStatus = step.status?.toLowerCase() || '';
|
|
const isPendingOrInProgress = stepStatus === 'pending' || stepStatus === 'in_progress';
|
|
const matchesCurrentStep = step.step === currentStep;
|
|
|
|
// Step is active only if it matches the current step AND is pending/in_progress
|
|
const isActive = isPendingOrInProgress && matchesCurrentStep;
|
|
const isCompleted = step.status === 'approved';
|
|
|
|
// Find approval data for this step to get SLA information
|
|
// First find the corresponding level in approvalFlow to get levelId
|
|
const stepLevel = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === step.step);
|
|
const approval = stepLevel?.levelId
|
|
? request?.approvals?.find((a: any) => a.levelId === stepLevel.levelId || a.level_id === stepLevel.levelId)
|
|
: null;
|
|
|
|
// Check if step is paused
|
|
const isPaused = approval?.status === 'PAUSED' ||
|
|
(request?.pauseInfo?.isPaused &&
|
|
(request?.pauseInfo?.levelId === approval?.levelId ||
|
|
request?.pauseInfo?.level_id === approval?.levelId));
|
|
|
|
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 - Show when step has emailTemplateUrl and is approved */}
|
|
{step.emailTemplateUrl && 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: {formatHoursMinutes(step.tatHours)}</p>
|
|
{/* Only show elapsed time for active or completed steps, not for waiting steps */}
|
|
{step.elapsedHours && (isActive || isCompleted) && (
|
|
<p className="text-xs text-gray-600 font-medium">
|
|
Elapsed: {formatHoursMinutes(step.elapsedHours)}
|
|
</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>
|
|
)}
|
|
|
|
{/* Active Approver - SLA Time Tracking (Only show for current active step) */}
|
|
{isActive && approval?.sla && (
|
|
<div className="mt-3 space-y-3">
|
|
<div className="flex items-center justify-between text-xs">
|
|
<span className="text-gray-600">Due by:</span>
|
|
<span className="font-medium text-gray-900">
|
|
{approval.sla.deadline ? formatDateDDMMYYYY(approval.sla.deadline, true) : 'Not set'}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Current Approver - Time Tracking */}
|
|
<div className={`border rounded-lg p-3 ${
|
|
isPaused ? 'bg-gray-100 border-gray-300' :
|
|
(approval.sla.percentageUsed || 0) >= 100 ? 'bg-red-50 border-red-200' :
|
|
(approval.sla.percentageUsed || 0) >= 75 ? 'bg-orange-50 border-orange-200' :
|
|
(approval.sla.percentageUsed || 0) >= 50 ? 'bg-amber-50 border-amber-200' :
|
|
'bg-green-50 border-green-200'
|
|
}`}>
|
|
<p className="text-xs font-semibold text-gray-900 mb-3 flex items-center gap-2">
|
|
<Clock className="w-4 h-4" />
|
|
Current Approver - Time Tracking {isPaused && '(Paused)'}
|
|
</p>
|
|
|
|
<div className="space-y-2 text-xs mb-3">
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">Time elapsed since assigned:</span>
|
|
<span className="font-medium text-gray-900">{approval.sla.elapsedText || '0 hours'}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">Time used:</span>
|
|
<span className="font-medium text-gray-900">
|
|
{approval.sla.elapsedText || '0 hours'} / {formatHoursMinutes(step.tatHours)} allocated
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Progress Bar */}
|
|
<div className="space-y-2">
|
|
{(() => {
|
|
const percentUsed = approval.sla.percentageUsed || 0;
|
|
const getActiveIndicatorColor = () => {
|
|
if (isPaused) return 'bg-gray-500';
|
|
if (percentUsed >= 100) return 'bg-red-600';
|
|
if (percentUsed >= 75) return 'bg-orange-500';
|
|
if (percentUsed >= 50) return 'bg-amber-500';
|
|
return 'bg-green-600';
|
|
};
|
|
const getActiveTextColor = () => {
|
|
if (isPaused) return 'text-gray-600';
|
|
if (percentUsed >= 100) return 'text-red-600';
|
|
if (percentUsed >= 75) return 'text-orange-600';
|
|
if (percentUsed >= 50) return 'text-amber-600';
|
|
return 'text-green-600';
|
|
};
|
|
return (
|
|
<>
|
|
<Progress
|
|
value={percentUsed}
|
|
className="h-3"
|
|
indicatorClassName={getActiveIndicatorColor()}
|
|
/>
|
|
<div className="flex items-center justify-between">
|
|
<span className={`text-xs font-semibold ${getActiveTextColor()}`}>
|
|
Progress: {Math.min(100, percentUsed)}% of TAT used
|
|
</span>
|
|
<span className="text-xs font-medium text-gray-700">
|
|
{approval.sla.remainingText || '0 hours'} remaining
|
|
</span>
|
|
</div>
|
|
</>
|
|
);
|
|
})()}
|
|
|
|
{approval.sla.status === 'breached' && (
|
|
<p className="text-xs font-semibold text-center text-red-600 flex items-center justify-center gap-1.5">
|
|
<AlertOctagon className="w-4 h-4" />
|
|
Deadline Breached
|
|
</p>
|
|
)}
|
|
{approval.sla.status === 'critical' && (
|
|
<p className="text-xs font-semibold text-center text-orange-600 flex items-center justify-center gap-1.5">
|
|
<AlertTriangle className="w-4 h-4" />
|
|
Approaching Deadline
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</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 */}
|
|
{/* Only show action buttons if:
|
|
1. Step is active (pending/in_progress and matches currentStep)
|
|
2. AND current user is the approver for this step (or is dealer for dealer steps) */}
|
|
{(() => {
|
|
// Find the step level from approvalFlow to verify user is the approver
|
|
const stepLevel = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === step.step);
|
|
const stepApproverEmail = (stepLevel?.approverEmail || '').toLowerCase();
|
|
const isUserApproverForThisStep = stepApproverEmail && userEmail === stepApproverEmail;
|
|
|
|
// For dealer steps (1 and 5), also check if user is dealer
|
|
const isDealerStep = step.step === 1 ||
|
|
(stepLevel?.levelName && stepLevel.levelName.toLowerCase().includes('dealer'));
|
|
const isUserAuthorized = isUserApproverForThisStep || (isDealerStep && isDealer);
|
|
|
|
// Step must be active AND user must be authorized
|
|
return isActive && isUserAuthorized;
|
|
})() && (
|
|
<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 (or shifted step): Review Request - Only for initiator or step approver */}
|
|
{/* Use initiatorStepNumber to handle cases where approvers are added between steps */}
|
|
{step.step === initiatorStepNumber && (isInitiator || isStep2Approver) && (
|
|
<Button
|
|
className="bg-blue-600 hover:bg-blue-700"
|
|
onClick={() => {
|
|
setShowApprovalModal(true);
|
|
}}
|
|
>
|
|
<CheckCircle className="w-4 h-4 mr-2" />
|
|
Review Request
|
|
</Button>
|
|
)}
|
|
|
|
{/* Department Lead Step: Approve and Organise IO - Find dynamically by levelName */}
|
|
{(() => {
|
|
// Find Department Lead step dynamically (handles step shifts)
|
|
const deptLeadStepLevel = approvalFlow.find((l: any) => {
|
|
const levelName = (l.levelName || '').toLowerCase();
|
|
return levelName.includes('department lead');
|
|
});
|
|
|
|
// Check if this is the Department Lead step
|
|
const isDeptLeadStep = deptLeadStepLevel &&
|
|
(step.step === (deptLeadStepLevel.step || deptLeadStepLevel.levelNumber || deptLeadStepLevel.level_number));
|
|
|
|
if (!isDeptLeadStep) return null;
|
|
|
|
// Check if user is the Department Lead approver
|
|
const deptLeadApproverEmail = (deptLeadStepLevel?.approverEmail || '').toLowerCase();
|
|
const isDeptLeadApprover = deptLeadApproverEmail && userEmail === deptLeadApproverEmail;
|
|
|
|
if (!(isDeptLeadApprover || isStep3Approver || isCurrentApprover)) return null;
|
|
|
|
// Check if IO number is available (same way as IO tab and modal)
|
|
const internalOrder = request?.internalOrder || request?.internal_order;
|
|
const ioNumber = internalOrder?.ioNumber || internalOrder?.io_number || request?.ioNumber || '';
|
|
const hasIONumber = ioNumber && ioNumber.trim() !== '';
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
{!hasIONumber && (
|
|
<div className="flex items-start gap-2 p-3 bg-amber-50 border border-amber-200 rounded-lg">
|
|
<AlertTriangle className="w-4 h-4 text-amber-600 flex-shrink-0 mt-0.5" />
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium text-amber-800">IO Number Not Available</p>
|
|
<p className="text-xs text-amber-700 mt-1">
|
|
Please add an IO number in the IO tab before approving this step.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<Button
|
|
className="bg-green-600 hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
onClick={() => {
|
|
setShowIOApprovalModal(true);
|
|
}}
|
|
disabled={!hasIONumber}
|
|
>
|
|
<CheckCircle className="w-4 h-4 mr-2" />
|
|
Approve and Organise IO
|
|
</Button>
|
|
</div>
|
|
);
|
|
})()}
|
|
|
|
{/* Step 5 (or shifted step): Upload Completion Documents - Only for dealer */}
|
|
{/* Check if dealer is the approver for this step (handles step shifts) */}
|
|
{(() => {
|
|
// Find the step level from approvalFlow to verify dealer is the approver
|
|
const stepLevel = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === step.step);
|
|
const stepApproverEmail = (stepLevel?.approverEmail || '').toLowerCase();
|
|
// Check if dealer is the approver for this step
|
|
const isDealerForThisStep = isDealer && stepApproverEmail === dealerEmail;
|
|
// Check if this is the Dealer Completion Documents step
|
|
// by checking if the levelName contains "Dealer Completion" or "Completion Documents"
|
|
const levelName = (stepLevel?.levelName || step.title || '').toLowerCase();
|
|
const isDealerCompletionStep = levelName.includes('dealer completion') ||
|
|
levelName.includes('completion documents');
|
|
return isDealerForThisStep && isDealerCompletionStep;
|
|
})() && (
|
|
<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}
|
|
preFilledIORemark={request?.internalOrder?.ioRemark || request?.internalOrder?.io_remark || request?.internal_order?.ioRemark || request?.internal_order?.io_remark || 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 () => {
|
|
try {
|
|
const requestId = request?.requestId || request?.id;
|
|
if (!requestId) {
|
|
toast.error('Request ID not found');
|
|
return;
|
|
}
|
|
|
|
await sendCreditNoteToDealer(requestId);
|
|
|
|
toast.success('Credit note sent to dealer successfully. Step 8 has been approved.');
|
|
|
|
// Refresh the request details to show updated status
|
|
if (onRefresh) {
|
|
onRefresh();
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Failed to send credit note to dealer:', error);
|
|
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to send credit note to dealer';
|
|
toast.error(errorMessage);
|
|
}
|
|
}}
|
|
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: (request as any)?.creditNote?.status ||
|
|
(request as any)?.claimDetails?.creditNote?.status ||
|
|
((request as any)?.creditNote?.creditNoteNumber ? 'CONFIRMED' : 'PENDING'),
|
|
}}
|
|
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"
|
|
/>
|
|
</>
|
|
);
|
|
}
|