manager is pickd from the user tam and search user by dispaly name strategy
This commit is contained in:
parent
636dc4a1c5
commit
001d636e6c
81
src/App.tsx
81
src/App.tsx
@ -24,16 +24,9 @@ import { ApprovalActionModal } from '@/components/modals/ApprovalActionModal';
|
||||
import { Toaster } from '@/components/ui/sonner';
|
||||
import { toast } from 'sonner';
|
||||
import { CUSTOM_REQUEST_DATABASE } from '@/utils/customRequestDatabase';
|
||||
import { CLAIM_MANAGEMENT_DATABASE } from '@/utils/claimManagementDatabase';
|
||||
import { AuthCallback } from '@/pages/Auth/AuthCallback';
|
||||
import { createClaimRequest } from '@/services/dealerClaimApi';
|
||||
|
||||
// Combined Request Database for backward compatibility
|
||||
// This combines both custom and claim management requests
|
||||
export const REQUEST_DATABASE: any = {
|
||||
...CUSTOM_REQUEST_DATABASE,
|
||||
...CLAIM_MANAGEMENT_DATABASE
|
||||
};
|
||||
import { ManagerSelectionModal } from '@/components/modals/ManagerSelectionModal';
|
||||
|
||||
interface AppProps {
|
||||
onLogout?: () => void;
|
||||
@ -62,6 +55,20 @@ function AppRoutes({ onLogout }: AppProps) {
|
||||
const [dynamicRequests, setDynamicRequests] = useState<any[]>([]);
|
||||
const [selectedRequestId, setSelectedRequestId] = useState<string>('');
|
||||
const [selectedRequestTitle, setSelectedRequestTitle] = useState<string>('');
|
||||
const [managerModalOpen, setManagerModalOpen] = useState(false);
|
||||
const [managerModalData, setManagerModalData] = useState<{
|
||||
errorType: 'NO_MANAGER_FOUND' | 'MULTIPLE_MANAGERS_FOUND';
|
||||
managers?: Array<{
|
||||
userId: string;
|
||||
email: string;
|
||||
displayName: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
department?: string;
|
||||
}>;
|
||||
message?: string;
|
||||
pendingClaimData?: any;
|
||||
} | null>(null);
|
||||
|
||||
// Retrieve dynamic requests from localStorage on mount
|
||||
useEffect(() => {
|
||||
@ -266,7 +273,7 @@ function AppRoutes({ onLogout }: AppProps) {
|
||||
setApprovalAction(null);
|
||||
};
|
||||
|
||||
const handleClaimManagementSubmit = async (claimData: any) => {
|
||||
const handleClaimManagementSubmit = async (claimData: any, selectedManagerEmail?: string) => {
|
||||
try {
|
||||
// Prepare payload for API
|
||||
const payload = {
|
||||
@ -283,12 +290,17 @@ function AppRoutes({ onLogout }: AppProps) {
|
||||
periodStartDate: claimData.periodStartDate ? new Date(claimData.periodStartDate).toISOString() : undefined,
|
||||
periodEndDate: claimData.periodEndDate ? new Date(claimData.periodEndDate).toISOString() : undefined,
|
||||
estimatedBudget: claimData.estimatedBudget || undefined,
|
||||
selectedManagerEmail: selectedManagerEmail || undefined,
|
||||
};
|
||||
|
||||
// Call API to create claim request
|
||||
const response = await createClaimRequest(payload);
|
||||
const createdRequest = response.request;
|
||||
|
||||
// Close manager modal if open
|
||||
setManagerModalOpen(false);
|
||||
setManagerModalData(null);
|
||||
|
||||
toast.success('Claim Request Submitted', {
|
||||
description: 'Your claim management request has been created successfully.',
|
||||
});
|
||||
@ -301,6 +313,36 @@ function AppRoutes({ onLogout }: AppProps) {
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('[App] Error creating claim request:', error);
|
||||
|
||||
// Check for manager-related errors
|
||||
const errorData = error?.response?.data;
|
||||
const errorCode = errorData?.code || errorData?.error?.code;
|
||||
|
||||
if (errorCode === 'NO_MANAGER_FOUND') {
|
||||
// Show modal for no manager found
|
||||
setManagerModalData({
|
||||
errorType: 'NO_MANAGER_FOUND',
|
||||
message: errorData?.message || errorData?.error?.message || 'No reporting manager found. Please ensure your manager is correctly configured in the system.',
|
||||
pendingClaimData: claimData,
|
||||
});
|
||||
setManagerModalOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (errorCode === 'MULTIPLE_MANAGERS_FOUND') {
|
||||
// Show modal with manager list for selection
|
||||
const managers = errorData?.managers || errorData?.error?.managers || [];
|
||||
setManagerModalData({
|
||||
errorType: 'MULTIPLE_MANAGERS_FOUND',
|
||||
managers: managers,
|
||||
message: errorData?.message || errorData?.error?.message || 'Multiple managers found. Please select one.',
|
||||
pendingClaimData: claimData,
|
||||
});
|
||||
setManagerModalOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Other errors - show toast
|
||||
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to create claim request';
|
||||
toast.error('Failed to Submit Claim Request', {
|
||||
description: errorMessage,
|
||||
@ -719,6 +761,27 @@ function AppRoutes({ onLogout }: AppProps) {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Manager Selection Modal */}
|
||||
<ManagerSelectionModal
|
||||
open={managerModalOpen}
|
||||
onClose={() => {
|
||||
setManagerModalOpen(false);
|
||||
setManagerModalData(null);
|
||||
}}
|
||||
onSelect={async (managerEmail: string) => {
|
||||
if (managerModalData?.pendingClaimData) {
|
||||
// Retry creating claim request with selected manager
|
||||
// The pendingClaimData contains all the form data from the wizard
|
||||
// This preserves the entire submission state while waiting for manager selection
|
||||
await handleClaimManagementSubmit(managerModalData.pendingClaimData, managerEmail);
|
||||
}
|
||||
}}
|
||||
managers={managerModalData?.managers}
|
||||
errorType={managerModalData?.errorType || 'NO_MANAGER_FOUND'}
|
||||
message={managerModalData?.message}
|
||||
isLoading={false} // Will be set to true during retry if needed
|
||||
/>
|
||||
|
||||
{/* Approval Action Modal */}
|
||||
{approvalAction && (
|
||||
<ApprovalActionModal
|
||||
|
||||
164
src/components/modals/ManagerSelectionModal.tsx
Normal file
164
src/components/modals/ManagerSelectionModal.tsx
Normal file
@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Manager Selection Modal
|
||||
* Shows when multiple managers are found or no manager is found
|
||||
* Allows user to select a manager from the list
|
||||
*/
|
||||
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { AlertCircle, CheckCircle2, User, Mail, Building2 } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
interface Manager {
|
||||
userId: string;
|
||||
email: string;
|
||||
displayName: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
department?: string;
|
||||
}
|
||||
|
||||
interface ManagerSelectionModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSelect: (managerEmail: string) => void;
|
||||
managers?: Manager[];
|
||||
errorType: 'NO_MANAGER_FOUND' | 'MULTIPLE_MANAGERS_FOUND';
|
||||
message?: string;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function ManagerSelectionModal({
|
||||
open,
|
||||
onClose,
|
||||
onSelect,
|
||||
managers = [],
|
||||
errorType,
|
||||
message,
|
||||
isLoading = false,
|
||||
}: ManagerSelectionModalProps) {
|
||||
const handleSelect = (managerEmail: string) => {
|
||||
onSelect(managerEmail);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
{errorType === 'NO_MANAGER_FOUND' ? (
|
||||
<>
|
||||
<AlertCircle className="w-5 h-5 text-amber-500" />
|
||||
Manager Not Found
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle2 className="w-5 h-5 text-blue-500" />
|
||||
Select Your Manager
|
||||
</>
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{errorType === 'NO_MANAGER_FOUND' ? (
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-600">
|
||||
{message || 'No reporting manager found in the system. Please ensure your manager is correctly configured in your profile.'}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 mt-2">
|
||||
Please contact your administrator to update your manager information, or try again later.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-600">
|
||||
{message || 'Multiple managers were found with the same name. Please select the correct manager from the list below.'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="mt-4">
|
||||
{errorType === 'NO_MANAGER_FOUND' ? (
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-amber-600 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-amber-900">
|
||||
Unable to Proceed
|
||||
</p>
|
||||
<p className="text-sm text-amber-700 mt-1">
|
||||
We couldn't find your reporting manager in the system. The claim request cannot be created without a valid manager assigned.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 max-h-[400px] overflow-y-auto">
|
||||
{managers.map((manager) => (
|
||||
<div
|
||||
key={manager.userId}
|
||||
className="border rounded-lg p-4 hover:bg-gray-50 transition-colors cursor-pointer"
|
||||
onClick={() => !isLoading && handleSelect(manager.email)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-3 flex-1">
|
||||
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0">
|
||||
<User className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<p className="font-medium text-gray-900">
|
||||
{manager.displayName || `${manager.firstName || ''} ${manager.lastName || ''}`.trim() || 'Unknown'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<Mail className="w-4 h-4" />
|
||||
<span className="truncate">{manager.email}</span>
|
||||
</div>
|
||||
{manager.department && (
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<Building2 className="w-4 h-4" />
|
||||
<span>{manager.department}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSelect(manager.email);
|
||||
}}
|
||||
disabled={isLoading}
|
||||
className="flex-shrink-0"
|
||||
size="sm"
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 mt-6">
|
||||
{errorType === 'NO_MANAGER_FOUND' ? (
|
||||
<Button onClick={onClose} variant="outline">
|
||||
Close
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button onClick={onClose} variant="outline" disabled={isLoading}>
|
||||
Cancel
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@ -151,23 +151,55 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
|
||||
(level.levelNumber || level.level_number) === 3
|
||||
);
|
||||
const deptLeadUserId = step3Level?.approverId || step3Level?.approver?.userId;
|
||||
const isDeptLead = deptLeadUserId && deptLeadUserId === currentUserId;
|
||||
const deptLeadEmail = (step3Level?.approverEmail || step3Level?.approver?.email || step3Level?.approverEmail || '').toLowerCase().trim();
|
||||
|
||||
// Check if user is department lead by userId or email (case-insensitive)
|
||||
const isDeptLead = (deptLeadUserId && deptLeadUserId === currentUserId) ||
|
||||
(deptLeadEmail && currentUserEmail && deptLeadEmail === currentUserEmail);
|
||||
|
||||
// Get step 3 status (case-insensitive check)
|
||||
const step3Status = step3Level?.status ? String(step3Level.status).toUpperCase() : '';
|
||||
const isStep3PendingOrInProgress = step3Status === 'PENDING' ||
|
||||
step3Status === 'IN_PROGRESS';
|
||||
|
||||
// Check if user is current approver for step 3 (can access IO tab when step is pending/in-progress)
|
||||
// Also check if currentLevel is 3 (workflow is at step 3)
|
||||
const currentLevel = apiRequest?.currentLevel || apiRequest?.current_level || 0;
|
||||
const isStep3CurrentLevel = currentLevel === 3;
|
||||
|
||||
const isStep3CurrentApprover = step3Level && isStep3PendingOrInProgress && isStep3CurrentLevel && (
|
||||
(deptLeadUserId && deptLeadUserId === currentUserId) ||
|
||||
(deptLeadEmail && currentUserEmail && deptLeadEmail === currentUserEmail)
|
||||
);
|
||||
|
||||
// Check if IO tab should be visible (for initiator and department lead in claim management requests)
|
||||
// Department lead can access IO tab when they are the current approver for step 3 (to fetch and block IO)
|
||||
const showIOTab = isClaimManagementRequest(apiRequest) &&
|
||||
(isUserInitiator || isDeptLead);
|
||||
(isUserInitiator || isDeptLead || isStep3CurrentApprover);
|
||||
|
||||
// Debug logging for troubleshooting
|
||||
console.debug('[RequestDetail] IO Tab visibility:', {
|
||||
isClaimManagement: isClaimManagementRequest(apiRequest),
|
||||
isUserInitiator,
|
||||
isDeptLead,
|
||||
isStep3CurrentApprover,
|
||||
currentUserId,
|
||||
currentUserEmail,
|
||||
initiatorUserId,
|
||||
initiatorEmail,
|
||||
step3Level: step3Level ? { levelNumber: step3Level.levelNumber || step3Level.level_number, approverId: step3Level.approverId || step3Level.approver?.userId } : null,
|
||||
currentLevel,
|
||||
isStep3CurrentLevel,
|
||||
step3Level: step3Level ? {
|
||||
levelNumber: step3Level.levelNumber || step3Level.level_number,
|
||||
approverId: step3Level.approverId || step3Level.approver?.userId,
|
||||
approverEmail: step3Level.approverEmail || step3Level.approver?.email,
|
||||
status: step3Level.status,
|
||||
statusUpper: step3Status,
|
||||
isPendingOrInProgress: isStep3PendingOrInProgress
|
||||
} : null,
|
||||
deptLeadUserId,
|
||||
deptLeadEmail,
|
||||
emailMatch: deptLeadEmail && currentUserEmail ? deptLeadEmail === currentUserEmail : false,
|
||||
showIOTab,
|
||||
});
|
||||
|
||||
|
||||
@ -27,6 +27,26 @@ interface ProposalDetailsCardProps {
|
||||
}
|
||||
|
||||
export function ProposalDetailsCard({ proposalDetails, className }: ProposalDetailsCardProps) {
|
||||
// Calculate estimated total from costBreakup if not provided
|
||||
const calculateEstimatedTotal = () => {
|
||||
if (proposalDetails.estimatedBudgetTotal !== undefined && proposalDetails.estimatedBudgetTotal !== null) {
|
||||
return proposalDetails.estimatedBudgetTotal;
|
||||
}
|
||||
|
||||
// Calculate sum from costBreakup items
|
||||
if (proposalDetails.costBreakup && proposalDetails.costBreakup.length > 0) {
|
||||
const total = proposalDetails.costBreakup.reduce((sum, item) => {
|
||||
const amount = item.amount || 0;
|
||||
return sum + (Number.isNaN(amount) ? 0 : amount);
|
||||
}, 0);
|
||||
return total;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
const estimatedTotal = calculateEstimatedTotal();
|
||||
|
||||
const formatCurrency = (amount?: number | null) => {
|
||||
if (amount === undefined || amount === null || Number.isNaN(amount)) {
|
||||
return '₹0.00';
|
||||
@ -99,7 +119,7 @@ export function ProposalDetailsCard({ proposalDetails, className }: ProposalDeta
|
||||
Estimated Budget (Total)
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-green-700 text-right">
|
||||
{formatCurrency(proposalDetails.estimatedBudgetTotal)}
|
||||
{formatCurrency(estimatedTotal)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@ -0,0 +1,148 @@
|
||||
/**
|
||||
* EmailNotificationTemplateModal Component
|
||||
* Modal for displaying email notification templates for automated workflow steps
|
||||
* Used for Step 4: Activity Creation and other auto-triggered steps
|
||||
*/
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Mail, User, Building, Calendar, X } from 'lucide-react';
|
||||
|
||||
interface EmailNotificationTemplateModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
stepNumber: number;
|
||||
stepName: string;
|
||||
requestNumber?: string;
|
||||
recipientEmail?: string;
|
||||
subject?: string;
|
||||
emailBody?: string;
|
||||
}
|
||||
|
||||
export function EmailNotificationTemplateModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
stepNumber,
|
||||
stepName,
|
||||
requestNumber = 'RE-REQ-2024-CM-101',
|
||||
recipientEmail = 'system@royalenfield.com',
|
||||
subject,
|
||||
emailBody,
|
||||
}: EmailNotificationTemplateModalProps) {
|
||||
// Default subject if not provided
|
||||
const defaultSubject = `System Notification: Activity Created - ${requestNumber}`;
|
||||
const finalSubject = subject || defaultSubject;
|
||||
|
||||
// Default email body if not provided
|
||||
const defaultEmailBody = `System Notification
|
||||
|
||||
Activity has been automatically created for claim ${requestNumber}.
|
||||
|
||||
All stakeholders have been notified.
|
||||
|
||||
This is an automated message.`;
|
||||
|
||||
const finalEmailBody = emailBody || defaultEmailBody;
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="sm:max-w-2xl max-w-2xl">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center">
|
||||
<Mail className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<DialogTitle className="text-lg leading-none font-semibold">
|
||||
Email Notification Template
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-sm">
|
||||
Step {stepNumber}: {stepName}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Email Header Section */}
|
||||
<div className="bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg p-4 border border-blue-200">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<User className="w-4 h-4 text-gray-600 mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-xs text-gray-600">To:</p>
|
||||
<p className="text-sm font-medium text-gray-900">{recipientEmail}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<Mail className="w-4 h-4 text-gray-600 mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-xs text-gray-600">Subject:</p>
|
||||
<p className="text-sm font-semibold text-gray-900">{finalSubject}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email Body Section */}
|
||||
<div className="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<div className="space-y-4">
|
||||
{/* Company Header */}
|
||||
<div className="flex items-center gap-2 pb-3 border-b border-gray-200">
|
||||
<Building className="w-5 h-5 text-purple-600" />
|
||||
<span className="font-semibold text-gray-900">Royal Enfield</span>
|
||||
</div>
|
||||
|
||||
{/* Email Content */}
|
||||
<div className="prose prose-sm max-w-none">
|
||||
<pre className="whitespace-pre-wrap font-sans text-sm text-gray-700 leading-relaxed bg-transparent p-0 border-0">
|
||||
{finalEmailBody}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="pt-3 border-t border-gray-200">
|
||||
<div className="flex items-center gap-2 text-xs text-gray-500">
|
||||
<Calendar className="w-3 h-3" />
|
||||
<span>Automated email • Royal Enfield Claims Portal</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Badges */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className="bg-blue-50 text-blue-700 border-blue-200">
|
||||
Step {stepNumber}
|
||||
</Badge>
|
||||
<Badge className="bg-purple-50 text-purple-700 border-purple-200">
|
||||
Auto-triggered
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
className="h-9"
|
||||
>
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ import { InitiatorProposalApprovalModal } from '../modals/InitiatorProposalAppro
|
||||
import { DeptLeadIOApprovalModal } from '../modals/DeptLeadIOApprovalModal';
|
||||
import { DealerCompletionDocumentsModal } from '../modals/DealerCompletionDocumentsModal';
|
||||
import { CreditNoteSAPModal } from '../modals/CreditNoteSAPModal';
|
||||
import { EmailNotificationTemplateModal } from '../modals/EmailNotificationTemplateModal';
|
||||
import { toast } from 'sonner';
|
||||
import { submitProposal, updateIODetails, submitCompletion, updateEInvoice } from '@/services/dealerClaimApi';
|
||||
import { getWorkflowDetails, approveLevel, rejectLevel } from '@/services/workflowApi';
|
||||
@ -161,6 +162,8 @@ export function DealerClaimWorkflowTab({
|
||||
const [showIOApprovalModal, setShowIOApprovalModal] = useState(false);
|
||||
const [showCompletionModal, setShowCompletionModal] = 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[]>([]);
|
||||
@ -895,14 +898,17 @@ export function DealerClaimWorkflowTab({
|
||||
<Badge className={getStepBadgeVariant(step.status)}>
|
||||
{step.status.toLowerCase()}
|
||||
</Badge>
|
||||
{/* Email Template Button (Step 4) */}
|
||||
{step.step === 4 && step.emailTemplateUrl && (
|
||||
{/* 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={() => window.open(step.emailTemplateUrl, '_blank')}
|
||||
onClick={() => {
|
||||
setSelectedStepForEmail({ stepNumber: step.step, stepName: step.title });
|
||||
setShowEmailTemplateModal(true);
|
||||
}}
|
||||
>
|
||||
<Mail className="w-3.5 h-3.5 text-blue-600" />
|
||||
</Button>
|
||||
@ -1235,6 +1241,19 @@ export function DealerClaimWorkflowTab({
|
||||
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"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -13,9 +13,11 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { DollarSign, Download, CircleCheckBig, AlertCircle } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { DollarSign, Download, CircleCheckBig, Target } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { updateIODetails, getClaimDetails } from '@/services/dealerClaimApi';
|
||||
import { validateIO, updateIODetails, getClaimDetails } from '@/services/dealerClaimApi';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
|
||||
interface IOTabProps {
|
||||
request: any;
|
||||
@ -26,13 +28,16 @@ interface IOTabProps {
|
||||
interface IOBlockedDetails {
|
||||
ioNumber: string;
|
||||
blockedAmount: number;
|
||||
availableBalance: number;
|
||||
availableBalance: number; // Available amount before block
|
||||
remainingBalance: number; // Remaining amount after block
|
||||
blockedDate: string;
|
||||
blockedBy: string; // User who blocked
|
||||
sapDocumentNumber: string;
|
||||
status: 'blocked' | 'released' | 'failed';
|
||||
}
|
||||
|
||||
export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
const { user } = useAuth();
|
||||
const requestId = apiRequest?.requestId || request?.requestId;
|
||||
|
||||
// Load existing IO data from apiRequest or request
|
||||
@ -42,21 +47,36 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
const existingAvailableBalance = internalOrder?.ioAvailableBalance || internalOrder?.io_available_balance || 0;
|
||||
const existingRemainingBalance = internalOrder?.ioRemainingBalance || internalOrder?.io_remaining_balance || 0;
|
||||
const sapDocNumber = internalOrder?.sapDocumentNumber || internalOrder?.sap_document_number || '';
|
||||
// Get organizer user object from association (organizer) or fallback to organizedBy UUID
|
||||
const organizer = internalOrder?.organizer || null;
|
||||
|
||||
const [ioNumber, setIoNumber] = useState(existingIONumber);
|
||||
const [fetchingAmount, setFetchingAmount] = useState(false);
|
||||
const [fetchedAmount, setFetchedAmount] = useState<number | null>(null);
|
||||
const [amountToBlock, setAmountToBlock] = useState<string>('');
|
||||
const [blockedDetails, setBlockedDetails] = useState<IOBlockedDetails | null>(null);
|
||||
const [blockingBudget, setBlockingBudget] = useState(false);
|
||||
|
||||
// Load existing IO block details from apiRequest
|
||||
useEffect(() => {
|
||||
if (internalOrder && existingIONumber) {
|
||||
if (internalOrder && existingIONumber && existingBlockedAmount > 0) {
|
||||
const availableBeforeBlock = Number(existingAvailableBalance) + Number(existingBlockedAmount) || Number(existingAvailableBalance);
|
||||
// Get blocked by user name from organizer association (who blocked the amount)
|
||||
// When amount is blocked, organizedBy stores the user who blocked it
|
||||
const blockedByName = organizer?.displayName ||
|
||||
organizer?.display_name ||
|
||||
organizer?.name ||
|
||||
(organizer?.firstName && organizer?.lastName ? `${organizer.firstName} ${organizer.lastName}`.trim() : null) ||
|
||||
organizer?.email ||
|
||||
'Unknown User';
|
||||
|
||||
setBlockedDetails({
|
||||
ioNumber: existingIONumber,
|
||||
blockedAmount: Number(existingBlockedAmount) || 0,
|
||||
availableBalance: Number(existingAvailableBalance) || 0,
|
||||
availableBalance: availableBeforeBlock, // Available amount before block
|
||||
remainingBalance: Number(existingRemainingBalance) || Number(existingAvailableBalance),
|
||||
blockedDate: internalOrder.organizedAt || internalOrder.organized_at || new Date().toISOString(),
|
||||
blockedBy: blockedByName,
|
||||
sapDocumentNumber: sapDocNumber,
|
||||
status: (internalOrder.status === 'BLOCKED' ? 'blocked' :
|
||||
internalOrder.status === 'RELEASED' ? 'released' : 'blocked') as 'blocked' | 'released' | 'failed',
|
||||
@ -64,16 +84,16 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
setIoNumber(existingIONumber);
|
||||
|
||||
// Set fetched amount if available balance exists
|
||||
if (existingAvailableBalance > 0) {
|
||||
setFetchedAmount(Number(existingAvailableBalance));
|
||||
if (availableBeforeBlock > 0) {
|
||||
setFetchedAmount(availableBeforeBlock);
|
||||
}
|
||||
}
|
||||
}, [internalOrder, existingIONumber, existingBlockedAmount, existingAvailableBalance, existingRemainingBalance, sapDocNumber]);
|
||||
}, [internalOrder, existingIONumber, existingBlockedAmount, existingAvailableBalance, existingRemainingBalance, sapDocNumber, organizer]);
|
||||
|
||||
/**
|
||||
* Fetch available budget from SAP
|
||||
* Validates IO number and gets available balance
|
||||
* Calls updateIODetails with blockedAmount=0 to validate without blocking
|
||||
* Validates IO number and gets available balance (returns dummy data for now)
|
||||
* Does not store anything in database - only validates
|
||||
*/
|
||||
const handleFetchAmount = async () => {
|
||||
if (!ioNumber.trim()) {
|
||||
@ -88,32 +108,18 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
|
||||
setFetchingAmount(true);
|
||||
try {
|
||||
// Validate IO number by calling updateIODetails with blockedAmount=0
|
||||
// This validates the IO with SAP and returns available balance without blocking
|
||||
// The backend will validate the IO number and return the availableBalance from SAP
|
||||
await updateIODetails(requestId, {
|
||||
ioNumber: ioNumber.trim(),
|
||||
ioAvailableBalance: 0, // Will be fetched from SAP validation by backend
|
||||
ioBlockedAmount: 0, // No blocking, just validation
|
||||
ioRemainingBalance: 0, // Will be calculated by backend
|
||||
});
|
||||
// Call validate IO endpoint - returns dummy data for now, will integrate with SAP later
|
||||
const ioData = await validateIO(requestId, ioNumber.trim());
|
||||
|
||||
// Fetch updated claim details to get the validated IO data
|
||||
const claimData = await getClaimDetails(requestId);
|
||||
const updatedInternalOrder = claimData?.internalOrder || claimData?.internal_order;
|
||||
|
||||
if (updatedInternalOrder) {
|
||||
const availableBalance = Number(updatedInternalOrder.ioAvailableBalance || updatedInternalOrder.io_available_balance || 0);
|
||||
if (availableBalance > 0) {
|
||||
setFetchedAmount(availableBalance);
|
||||
toast.success(`IO validated successfully. Available balance: ₹${availableBalance.toLocaleString('en-IN')}`);
|
||||
if (ioData.isValid && ioData.availableBalance > 0) {
|
||||
setFetchedAmount(ioData.availableBalance);
|
||||
// Pre-fill amount to block with available balance
|
||||
setAmountToBlock(String(ioData.availableBalance));
|
||||
toast.success(`IO fetched from SAP. Available balance: ₹${ioData.availableBalance.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`);
|
||||
} else {
|
||||
toast.error('No available balance found for this IO number');
|
||||
setFetchedAmount(null);
|
||||
}
|
||||
} else {
|
||||
toast.error('Failed to fetch IO details after validation');
|
||||
toast.error('Invalid IO number or no available balance found');
|
||||
setFetchedAmount(null);
|
||||
setAmountToBlock('');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Failed to fetch IO budget:', error);
|
||||
@ -139,27 +145,27 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get claim amount from budget tracking or proposal details
|
||||
const claimAmount = apiRequest?.budgetTracking?.proposalEstimatedBudget ||
|
||||
apiRequest?.proposalDetails?.totalEstimatedBudget ||
|
||||
apiRequest?.claimDetails?.estimatedBudget ||
|
||||
request?.claimAmount ||
|
||||
request?.amount ||
|
||||
0;
|
||||
const blockAmount = parseFloat(amountToBlock);
|
||||
|
||||
if (claimAmount > fetchedAmount) {
|
||||
toast.error('Claim amount exceeds available IO budget');
|
||||
if (!amountToBlock || isNaN(blockAmount) || blockAmount <= 0) {
|
||||
toast.error('Please enter a valid amount to block');
|
||||
return;
|
||||
}
|
||||
|
||||
if (blockAmount > fetchedAmount) {
|
||||
toast.error('Amount to block exceeds available IO budget');
|
||||
return;
|
||||
}
|
||||
|
||||
setBlockingBudget(true);
|
||||
try {
|
||||
// Call updateIODetails with blockedAmount to block budget in SAP
|
||||
// Call updateIODetails with blockedAmount to block budget in SAP and store in database
|
||||
// This will store in internal_orders and claim_budget_tracking tables
|
||||
await updateIODetails(requestId, {
|
||||
ioNumber: ioNumber.trim(),
|
||||
ioAvailableBalance: fetchedAmount,
|
||||
ioBlockedAmount: claimAmount,
|
||||
ioRemainingBalance: fetchedAmount - claimAmount,
|
||||
ioBlockedAmount: blockAmount,
|
||||
ioRemainingBalance: fetchedAmount - blockAmount,
|
||||
});
|
||||
|
||||
// Fetch updated claim details to get the blocked IO data
|
||||
@ -167,16 +173,29 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
const updatedInternalOrder = claimData?.internalOrder || claimData?.internal_order;
|
||||
|
||||
if (updatedInternalOrder) {
|
||||
const currentUser = user as any;
|
||||
// When blocking, always use the current user who is performing the block action
|
||||
// The organizer association may be from initial IO organization, but we want who blocked the amount
|
||||
const blockedByName = currentUser?.displayName ||
|
||||
currentUser?.display_name ||
|
||||
currentUser?.name ||
|
||||
(currentUser?.firstName && currentUser?.lastName ? `${currentUser.firstName} ${currentUser.lastName}`.trim() : null) ||
|
||||
currentUser?.email ||
|
||||
'Current User';
|
||||
|
||||
const blocked: IOBlockedDetails = {
|
||||
ioNumber: updatedInternalOrder.ioNumber || updatedInternalOrder.io_number || ioNumber,
|
||||
blockedAmount: Number(updatedInternalOrder.ioBlockedAmount || updatedInternalOrder.io_blocked_amount || claimAmount),
|
||||
availableBalance: Number(updatedInternalOrder.ioAvailableBalance || updatedInternalOrder.io_available_balance || fetchedAmount),
|
||||
blockedAmount: Number(updatedInternalOrder.ioBlockedAmount || updatedInternalOrder.io_blocked_amount || blockAmount),
|
||||
availableBalance: fetchedAmount, // Available amount before block
|
||||
remainingBalance: Number(updatedInternalOrder.ioRemainingBalance || updatedInternalOrder.io_remaining_balance || (fetchedAmount - blockAmount)),
|
||||
blockedDate: updatedInternalOrder.organizedAt || updatedInternalOrder.organized_at || new Date().toISOString(),
|
||||
blockedBy: blockedByName,
|
||||
sapDocumentNumber: updatedInternalOrder.sapDocumentNumber || updatedInternalOrder.sap_document_number || '',
|
||||
status: 'blocked',
|
||||
};
|
||||
|
||||
setBlockedDetails(blocked);
|
||||
setAmountToBlock(''); // Clear the input
|
||||
toast.success('IO budget blocked successfully in SAP');
|
||||
|
||||
// Refresh request details
|
||||
@ -236,14 +255,6 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
}
|
||||
};
|
||||
|
||||
// Get claim amount for display purposes (from budget tracking or proposal details)
|
||||
const claimAmount = apiRequest?.budgetTracking?.proposalEstimatedBudget ||
|
||||
apiRequest?.proposalDetails?.totalEstimatedBudget ||
|
||||
apiRequest?.claimDetails?.estimatedBudget ||
|
||||
request?.claimAmount ||
|
||||
request?.amount ||
|
||||
0;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* IO Budget Management Card */}
|
||||
@ -283,49 +294,51 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
|
||||
{/* Fetched Amount Display */}
|
||||
{fetchedAmount !== null && !blockedDetails && (
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<>
|
||||
<div className="bg-green-50 border-2 border-green-200 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-green-600 font-medium">Available Budget</p>
|
||||
<p className="text-2xl font-bold text-green-900">
|
||||
₹{fetchedAmount.toLocaleString('en-IN')}
|
||||
<p className="text-xs text-gray-600 uppercase tracking-wide mb-1">Available Amount</p>
|
||||
<p className="text-2xl font-bold text-green-700">
|
||||
₹{fetchedAmount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</p>
|
||||
</div>
|
||||
<CircleCheckBig className="w-8 h-8 text-green-600" />
|
||||
</div>
|
||||
|
||||
<div className="border-t border-green-200 pt-3 space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-green-700">Claim Amount:</span>
|
||||
<span className="font-semibold text-green-900">
|
||||
₹{claimAmount.toLocaleString('en-IN')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-green-700">Balance After Block:</span>
|
||||
<span className="font-semibold text-green-900">
|
||||
₹{(fetchedAmount - claimAmount).toLocaleString('en-IN')}
|
||||
</span>
|
||||
<div className="mt-3 pt-3 border-t border-green-200">
|
||||
<p className="text-xs text-gray-600"><strong>IO Number:</strong> {ioNumber}</p>
|
||||
<p className="text-xs text-gray-600 mt-1"><strong>Fetched from:</strong> SAP System</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{claimAmount > fetchedAmount ? (
|
||||
<div className="flex items-center gap-2 bg-red-100 text-red-700 p-3 rounded-md">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
<p className="text-xs font-medium">
|
||||
Insufficient budget! Claim amount exceeds available balance.
|
||||
</p>
|
||||
{/* Amount to Block Input */}
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="blockAmount">Amount to Block *</Label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">₹</span>
|
||||
<Input
|
||||
type="number"
|
||||
id="blockAmount"
|
||||
placeholder="Enter amount to block"
|
||||
min="0"
|
||||
step="0.01"
|
||||
value={amountToBlock}
|
||||
onChange={(e) => setAmountToBlock(e.target.value)}
|
||||
className="pl-8"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
</div>
|
||||
|
||||
{/* Block Button */}
|
||||
<Button
|
||||
onClick={handleBlockBudget}
|
||||
disabled={blockingBudget}
|
||||
disabled={blockingBudget || !amountToBlock || parseFloat(amountToBlock) <= 0 || parseFloat(amountToBlock) > fetchedAmount}
|
||||
className="w-full bg-[#2d4a3e] hover:bg-[#1f3329]"
|
||||
>
|
||||
{blockingBudget ? 'Blocking in SAP...' : 'Block Budget in SAP'}
|
||||
<Target className="w-4 h-4 mr-2" />
|
||||
{blockingBudget ? 'Blocking in SAP...' : 'Block IO in SAP'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -345,64 +358,69 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
||||
{blockedDetails ? (
|
||||
<div className="space-y-4">
|
||||
{/* Success Banner */}
|
||||
<div className="bg-gradient-to-r from-emerald-50 to-green-50 rounded-lg border-2 border-emerald-300 p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-10 h-10 rounded-full bg-emerald-500 flex items-center justify-center">
|
||||
<CircleCheckBig className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div className="bg-green-50 border-2 border-green-500 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<CircleCheckBig className="w-6 h-6 text-green-600 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-semibold text-emerald-900">Budget Blocked Successfully!</p>
|
||||
<p className="text-xs text-emerald-700">SAP integration completed</p>
|
||||
<p className="font-semibold text-green-900">IO Blocked Successfully</p>
|
||||
<p className="text-sm text-green-700 mt-1">Budget has been reserved in SAP system</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Blocked Details */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between py-2 border-b">
|
||||
<span className="text-sm text-gray-600">IO Number:</span>
|
||||
<span className="text-sm font-semibold text-gray-900">{blockedDetails.ioNumber}</span>
|
||||
<div className="border rounded-lg divide-y">
|
||||
<div className="p-4">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">IO Number</p>
|
||||
<p className="text-sm font-semibold text-gray-900">{blockedDetails.ioNumber}</p>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b">
|
||||
<span className="text-sm text-gray-600">Blocked Amount:</span>
|
||||
<span className="text-sm font-semibold text-green-700">
|
||||
₹{blockedDetails.blockedAmount.toLocaleString('en-IN')}
|
||||
</span>
|
||||
<div className="p-4">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">SAP Document Number</p>
|
||||
<p className="text-sm font-semibold text-gray-900">{blockedDetails.sapDocumentNumber || 'N/A'}</p>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b">
|
||||
<span className="text-sm text-gray-600">Available Balance:</span>
|
||||
<span className="text-sm font-semibold text-gray-900">
|
||||
₹{blockedDetails.availableBalance.toLocaleString('en-IN')}
|
||||
</span>
|
||||
<div className="p-4 bg-green-50">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">Blocked Amount</p>
|
||||
<p className="text-xl font-bold text-green-700">
|
||||
₹{blockedDetails.blockedAmount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b">
|
||||
<span className="text-sm text-gray-600">Blocked Date:</span>
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{new Date(blockedDetails.blockedDate).toLocaleString('en-IN')}
|
||||
</span>
|
||||
<div className="p-4">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">Available Amount (Before Block)</p>
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
₹{blockedDetails.availableBalance.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b">
|
||||
<span className="text-sm text-gray-600">SAP Document No:</span>
|
||||
<span className="text-sm font-mono font-medium text-blue-700">
|
||||
{blockedDetails.sapDocumentNumber}
|
||||
</span>
|
||||
<div className="p-4 bg-blue-50">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">Remaining Amount (After Block)</p>
|
||||
<p className="text-sm font-bold text-blue-700">
|
||||
₹{blockedDetails.remainingBalance.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-between py-2">
|
||||
<span className="text-sm text-gray-600">Status:</span>
|
||||
<span className="text-xs font-semibold px-2 py-1 bg-green-100 text-green-800 rounded-full">
|
||||
{blockedDetails.status.toUpperCase()}
|
||||
</span>
|
||||
<div className="p-4">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">Blocked By</p>
|
||||
<p className="text-sm font-medium text-gray-900">{blockedDetails.blockedBy}</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">Blocked At</p>
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{new Date(blockedDetails.blockedDate).toLocaleString('en-IN', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
hour12: true
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 bg-gray-50">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">Status</p>
|
||||
<Badge className="bg-green-100 text-green-800 border-green-200">
|
||||
<CircleCheckBig className="w-3 h-3 mr-1" />
|
||||
Blocked
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Release Button */}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleReleaseBudget}
|
||||
className="w-full border-red-300 text-red-700 hover:bg-red-50"
|
||||
>
|
||||
Release Budget
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
|
||||
@ -19,6 +19,7 @@ export interface CreateClaimRequestPayload {
|
||||
periodStartDate?: string; // ISO date string
|
||||
periodEndDate?: string; // ISO date string
|
||||
estimatedBudget?: string | number;
|
||||
selectedManagerEmail?: string; // Optional: When multiple managers found, user selects one
|
||||
}
|
||||
|
||||
export interface ClaimRequestResponse {
|
||||
@ -180,8 +181,34 @@ export async function submitCompletion(
|
||||
}
|
||||
|
||||
/**
|
||||
* Update IO details (Step 3)
|
||||
* Validate/Fetch IO details from SAP (returns dummy data for now)
|
||||
* GET /api/v1/dealer-claims/:requestId/io/validate?ioNumber=IO1234
|
||||
* This only validates and returns IO details, does not store anything
|
||||
*/
|
||||
export async function validateIO(
|
||||
requestId: string,
|
||||
ioNumber: string
|
||||
): Promise<{
|
||||
ioNumber: string;
|
||||
availableBalance: number;
|
||||
currency: string;
|
||||
isValid: boolean;
|
||||
}> {
|
||||
try {
|
||||
const response = await apiClient.get(`/dealer-claims/${requestId}/io/validate`, {
|
||||
params: { ioNumber }
|
||||
});
|
||||
return response.data?.data || response.data;
|
||||
} catch (error: any) {
|
||||
console.error('[DealerClaimAPI] Error validating IO:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update IO details and block amount (Step 3)
|
||||
* PUT /api/v1/dealer-claims/:requestId/io
|
||||
* Only stores data when blocking amount > 0
|
||||
*/
|
||||
export async function updateIODetails(
|
||||
requestId: string,
|
||||
|
||||
@ -215,15 +215,17 @@ export function mapToClaimManagementRequest(
|
||||
}
|
||||
|
||||
// Map proposal details
|
||||
const expectedCompletionDate = proposalDetails?.expectedCompletionDate || proposalDetails?.expected_completion_date;
|
||||
const proposal = proposalDetails ? {
|
||||
proposalDocumentUrl: proposalDetails.proposalDocumentUrl || proposalDetails.proposal_document_url,
|
||||
costBreakup: proposalDetails.costBreakup || proposalDetails.cost_breakup || [],
|
||||
totalEstimatedBudget: proposalDetails.totalEstimatedBudget || proposalDetails.total_estimated_budget || 0,
|
||||
timelineMode: proposalDetails.timelineMode || proposalDetails.timeline_mode,
|
||||
expectedCompletionDate: proposalDetails.expectedCompletionDate || proposalDetails.expected_completion_date,
|
||||
expectedCompletionDate: expectedCompletionDate,
|
||||
expectedCompletionDays: proposalDetails.expectedCompletionDays || proposalDetails.expected_completion_days,
|
||||
timelineForClosure: expectedCompletionDate, // Map expectedCompletionDate to timelineForClosure for ProposalDetailsCard
|
||||
dealerComments: proposalDetails.dealerComments || proposalDetails.dealer_comments,
|
||||
submittedAt: proposalDetails.submittedAt || proposalDetails.submitted_at,
|
||||
submittedOn: proposalDetails.submittedAt || proposalDetails.submitted_at || proposalDetails.submittedOn,
|
||||
} : undefined;
|
||||
|
||||
// Map IO details from dedicated internal_orders table
|
||||
|
||||
Loading…
Reference in New Issue
Block a user