Re_Figma_Code/src/dealer-claim/components/request-detail/WorkflowTab.tsx

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"
/>
</>
);
}