;
requestTitle?: string;
requestId?: string;
+ // Pre-filled IO data from IO tab
+ preFilledIONumber?: string;
+ preFilledBlockedAmount?: number;
}
export function DeptLeadIOApprovalModal({
@@ -41,13 +45,22 @@ export function DeptLeadIOApprovalModal({
onReject,
requestTitle,
requestId: _requestId,
+ preFilledIONumber,
+ preFilledBlockedAmount,
}: DeptLeadIOApprovalModalProps) {
const [actionType, setActionType] = useState<'approve' | 'reject'>('approve');
- const [ioNumber, setIoNumber] = useState('');
+ const [ioNumber, setIoNumber] = useState(preFilledIONumber || '');
const [ioRemark, setIoRemark] = useState('');
const [comments, setComments] = useState('');
const [submitting, setSubmitting] = useState(false);
+ // Update ioNumber when preFilledIONumber changes (when modal opens with new data)
+ React.useEffect(() => {
+ if (isOpen && preFilledIONumber) {
+ setIoNumber(preFilledIONumber);
+ }
+ }, [isOpen, preFilledIONumber]);
+
const ioRemarkChars = ioRemark.length;
const commentsChars = comments.length;
const maxIoRemarkChars = 300;
@@ -110,7 +123,7 @@ export function DeptLeadIOApprovalModal({
const handleReset = () => {
setActionType('approve');
- setIoNumber('');
+ setIoNumber(preFilledIONumber || '');
setIoRemark('');
setComments('');
};
@@ -211,8 +224,26 @@ export function DeptLeadIOApprovalModal({
onChange={(e) => setIoNumber(e.target.value)}
className="bg-white h-8"
/>
+ {preFilledIONumber && (
+
+ Pre-filled from IO tab
+
+ )}
+ {/* Blocked Amount Display (if available) */}
+ {preFilledBlockedAmount !== undefined && preFilledBlockedAmount > 0 && (
+
+
+ Blocked Amount:
+
+ ₹{preFilledBlockedAmount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
+
+
+
Amount already blocked in SAP from IO tab
+
+ )}
+
{/* IO Remark */}
{/* Title */}
diff --git a/src/pages/MyRequests/components/RequestCard.tsx b/src/pages/MyRequests/components/RequestCard.tsx
index 0f49156..03757be 100644
--- a/src/pages/MyRequests/components/RequestCard.tsx
+++ b/src/pages/MyRequests/components/RequestCard.tsx
@@ -5,7 +5,7 @@
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
-import { ArrowRight, User, TrendingUp, Clock, FileText, Pause } from 'lucide-react';
+import { ArrowRight, User, TrendingUp, Clock, Pause } from 'lucide-react';
import { motion } from 'framer-motion';
import { MyRequest } from '../types/myRequests.types';
import { getPriorityConfig, getStatusConfig } from '../utils/configMappers';
@@ -97,16 +97,32 @@ export function RequestCard({ request, index, onViewRequest }: RequestCardProps)
{request.priority}
- {request.templateType && (
-
-
- Template: {request.templateName}
-
- )}
+ {/* Template Type Badge */}
+ {(() => {
+ const templateType = request?.templateType || (request as any)?.template_type || '';
+ const templateTypeUpper = templateType?.toUpperCase() || '';
+
+ // Direct mapping from templateType
+ let templateLabel = 'Custom';
+ let templateColor = 'bg-purple-100 !text-purple-600 border-purple-200';
+
+ if (templateTypeUpper === 'DEALER CLAIM') {
+ templateLabel = 'Claim Management';
+ templateColor = 'bg-blue-100 !text-blue-700 border-blue-200';
+ } else if (templateTypeUpper === 'TEMPLATE') {
+ templateLabel = 'Template';
+ }
+
+ return (
+
+ {templateLabel}
+
+ );
+ })()}
{stripHtmlTags(request.description || '') || 'No description provided'}
diff --git a/src/pages/MyRequests/types/myRequests.types.ts b/src/pages/MyRequests/types/myRequests.types.ts
index 5a1704a..9e9f06a 100644
--- a/src/pages/MyRequests/types/myRequests.types.ts
+++ b/src/pages/MyRequests/types/myRequests.types.ts
@@ -16,6 +16,7 @@ export interface MyRequest {
currentApprover?: string;
approverLevel?: string;
templateType?: string;
+ workflowType?: string;
templateName?: string;
pauseInfo?: {
isPaused: boolean;
diff --git a/src/pages/MyRequests/utils/requestTransformers.ts b/src/pages/MyRequests/utils/requestTransformers.ts
index 3bcb527..558a878 100644
--- a/src/pages/MyRequests/utils/requestTransformers.ts
+++ b/src/pages/MyRequests/utils/requestTransformers.ts
@@ -29,8 +29,9 @@ export function transformRequest(req: any): MyRequest {
: req.currentStep && req.totalSteps
? `${req.currentStep} of ${req.totalSteps}`
: '—',
- templateType: req.templateType,
- templateName: req.templateName,
+ templateType: req.templateType || req.template_type,
+ workflowType: req.workflowType || req.workflow_type,
+ templateName: req.templateName || req.template_name,
};
}
diff --git a/src/pages/OpenRequests/OpenRequests.tsx b/src/pages/OpenRequests/OpenRequests.tsx
index aee3655..043aca7 100644
--- a/src/pages/OpenRequests/OpenRequests.tsx
+++ b/src/pages/OpenRequests/OpenRequests.tsx
@@ -28,6 +28,7 @@ interface Request {
currentLevelSLA?: any; // Backend-provided SLA for current level
isPaused?: boolean; // Pause status
pauseInfo?: any; // Pause details
+ templateType?: string; // Template type for badge display
}
interface OpenRequestsProps {
@@ -178,6 +179,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
approvalStep: r.currentLevel ? `Step ${r.currentLevel} of ${r.totalLevels || '?'}` : undefined,
department: r.department,
currentLevelSLA: r.currentLevelSLA, // ← Backend-calculated current level SLA
+ templateType: r.templateType || r.template_type, // ← Template type for badge display
};
});
setItems(mapped);
@@ -458,6 +460,32 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
>
{request.priority}
+ {/* Template Type Badge */}
+ {(() => {
+ const templateType = (request as any)?.templateType || (request as any)?.template_type || '';
+ const templateTypeUpper = templateType?.toUpperCase() || '';
+
+ // Direct mapping from templateType
+ let templateLabel = 'Custom';
+ let templateColor = 'bg-purple-100 !text-purple-600 border-purple-200';
+
+ if (templateTypeUpper === 'DEALER CLAIM') {
+ templateLabel = 'Claim Management';
+ templateColor = 'bg-blue-100 !text-blue-700 border-blue-200';
+ } else if (templateTypeUpper === 'TEMPLATE') {
+ templateLabel = 'Template';
+ }
+
+ return (
+
+ {templateLabel}
+
+ );
+ })()}
{/* Title */}
diff --git a/src/pages/RequestDetail/components/QuickActionsSidebar.tsx b/src/pages/RequestDetail/components/QuickActionsSidebar.tsx
index c24cddf..0d26f2b 100644
--- a/src/pages/RequestDetail/components/QuickActionsSidebar.tsx
+++ b/src/pages/RequestDetail/components/QuickActionsSidebar.tsx
@@ -10,7 +10,7 @@ import { UserPlus, Eye, CheckCircle, XCircle, Share2, Pause, Play, AlertCircle }
import { getSharedRecipients, type SharedRecipient } from '@/services/summaryApi';
import { useAuth } from '@/contexts/AuthContext';
import notificationApi, { type Notification } from '@/services/notificationApi';
-import { ProcessDetailsCard } from './claim-cards';
+import { ProcessDetailsCard } from '@/dealer-claim/components/request-detail/claim-cards';
import { isClaimManagementRequest } from '@/utils/claimRequestUtils';
import { determineUserRole, getRoleBasedVisibility, mapToClaimManagementRequest } from '@/utils/claimDataMapper';
diff --git a/src/pages/RequestDetail/components/RequestDetailHeader.tsx b/src/pages/RequestDetail/components/RequestDetailHeader.tsx
index 021cc2b..288f1ea 100644
--- a/src/pages/RequestDetail/components/RequestDetailHeader.tsx
+++ b/src/pages/RequestDetail/components/RequestDetailHeader.tsx
@@ -50,7 +50,7 @@ export function RequestDetailHeader({ request, refreshing, onBack, onRefresh, on
{request.id || 'N/A'}
- {/* Priority and Status Badges */}
+ {/* Priority, Status, and Template Type Badges */}
{statusConfig.label}
+ {/* Template Type Badge */}
+ {(() => {
+ const workflowType = request?.workflowType || request?.workflow_type;
+ const templateType = request?.templateType || request?.template_type;
+ const isClaimManagement = workflowType === 'CLAIM_MANAGEMENT' || templateType === 'claim-management';
+ const templateLabel = isClaimManagement ? 'Claim Management' : 'Custom';
+ const templateColor = isClaimManagement
+ ? 'bg-blue-100 !text-blue-700 border-blue-200'
+ : 'bg-purple-100 !text-purple-600 border-purple-200';
+
+ return (
+
+ {templateLabel}
+
+ );
+ })()}
diff --git a/src/pages/RequestDetail/components/claim-cards/index.ts b/src/pages/RequestDetail/components/claim-cards/index.ts
deleted file mode 100644
index a13bef2..0000000
--- a/src/pages/RequestDetail/components/claim-cards/index.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * Claim Management Card Components
- * Re-export all claim-specific card components for easy imports
- */
-
-export { ActivityInformationCard } from './ActivityInformationCard';
-export { DealerInformationCard } from './DealerInformationCard';
-export { ProposalDetailsCard } from './ProposalDetailsCard';
-export { ProcessDetailsCard } from './ProcessDetailsCard';
-export { RequestInitiatorCard } from './RequestInitiatorCard';
-
-
diff --git a/src/pages/RequestDetail/components/modals/README_CREDIT_NOTE_MODAL.md b/src/pages/RequestDetail/components/modals/README_CREDIT_NOTE_MODAL.md
deleted file mode 100644
index 51dcca4..0000000
--- a/src/pages/RequestDetail/components/modals/README_CREDIT_NOTE_MODAL.md
+++ /dev/null
@@ -1,219 +0,0 @@
-# Credit Note from SAP Modal - Integration Guide
-
-## Overview
-
-The `CreditNoteSAPModal` component is ready for implementation in Step 8 of the dealer claim workflow. This modal allows Finance team to review credit note details (generated from SAP) and send it to the dealer.
-
-## Component Location
-
-**File:** `Re_Figma_Code/src/pages/RequestDetail/components/modals/CreditNoteSAPModal.tsx`
-
-## Features
-
-### Display Sections:
-1. **Credit Note Document Card** (Green gradient)
- - Royal Enfield branding
- - Status badge (Approved/Issued/Sent/Pending)
- - Credit Note Number
- - Issue Date
-
-2. **Credit Note Amount** (Blue box)
- - Large display of credit note amount in ₹
-
-3. **Dealer Information** (Purple box)
- - Dealer Name
- - Dealer Code
- - Activity Name
-
-4. **Reference Details** (Gray box)
- - Request ID
- - Due Date
-
-5. **Available Actions Info** (Blue info box)
- - Explains what Download and Send actions do
-
-### Actions:
-- **Download**: Downloads/saves credit note to Documents tab
-- **Send to Dealer**: Sends email notification to dealer with credit note attachment
-- **Close**: Closes the modal
-
-## Props Interface
-
-```typescript
-interface CreditNoteSAPModalProps {
- isOpen: boolean;
- onClose: () => void;
- onDownload?: () => Promise; // Optional: Custom download handler
- onSendToDealer?: () => Promise; // Optional: Custom send handler
- creditNoteData?: {
- creditNoteNumber?: string;
- creditNoteDate?: string;
- creditNoteAmount?: number;
- status?: 'PENDING' | 'APPROVED' | 'ISSUED' | 'SENT';
- };
- dealerInfo?: {
- dealerName?: string;
- dealerCode?: string;
- dealerEmail?: string;
- };
- activityName?: string;
- requestNumber?: string;
- requestId?: string;
- dueDate?: string;
-}
-```
-
-## Integration Steps
-
-### 1. Import the Modal
-
-In `DealerClaimWorkflowTab.tsx`:
-
-```typescript
-import { CreditNoteSAPModal } from '../modals/CreditNoteSAPModal';
-```
-
-### 2. Add State
-
-```typescript
-const [showCreditNoteModal, setShowCreditNoteModal] = useState(false);
-```
-
-### 3. Add Button for Step 8
-
-In the workflow steps rendering, add button for Step 8 (Finance):
-
-```typescript
-{step.step === 8 && isFinanceUser && (
-
-)}
-```
-
-### 4. Add Modal Component
-
-```typescript
- setShowCreditNoteModal(false)}
- onDownload={handleCreditNoteDownload}
- onSendToDealer={handleSendCreditNoteToDealer}
- creditNoteData={{
- creditNoteNumber: request?.claimDetails?.creditNoteNumber,
- creditNoteDate: request?.claimDetails?.creditNoteDate,
- creditNoteAmount: request?.claimDetails?.creditNoteAmount,
- status: 'APPROVED', // or get from request status
- }}
- dealerInfo={{
- dealerName: request?.claimDetails?.dealerName,
- dealerCode: request?.claimDetails?.dealerCode,
- dealerEmail: request?.claimDetails?.dealerEmail,
- }}
- activityName={request?.claimDetails?.activityName}
- requestNumber={request?.requestNumber}
- requestId={request?.requestId}
- dueDate={/* Calculate due date based on business rules */}
-/>
-```
-
-### 5. Implement Handlers
-
-```typescript
-const handleCreditNoteDownload = async () => {
- try {
- const requestId = request?.id || request?.requestId;
- // TODO: Implement download logic
- // - Generate/download credit note PDF from SAP
- // - Save to Documents tab
- // - Create activity log entry
- toast.success('Credit note downloaded and saved to Documents');
- } catch (error) {
- console.error('Failed to download credit note:', error);
- throw error;
- }
-};
-
-const handleSendCreditNoteToDealer = async () => {
- try {
- const requestId = request?.id || request?.requestId;
- const dealerEmail = request?.claimDetails?.dealerEmail;
-
- // TODO: Implement send logic
- // - Send email to dealer with credit note attachment
- // - Update credit note status to 'SENT'
- // - Create activity log entry
- // - Possibly approve Step 8
-
- toast.success('Credit note sent to dealer successfully');
- } catch (error) {
- console.error('Failed to send credit note:', error);
- throw error;
- }
-};
-```
-
-## API Integration Points
-
-### Backend Endpoints (to be implemented):
-
-1. **Download Credit Note**
- - `GET /api/v1/dealer-claims/:requestId/credit-note/download`
- - Returns credit note PDF/document
-
-2. **Send Credit Note to Dealer**
- - `POST /api/v1/dealer-claims/:requestId/credit-note/send`
- - Sends email notification to dealer
- - Updates credit note status
-
-3. **Get Credit Note Details**
- - Already available via `getClaimDetails()` API
- - Returns `creditNoteNumber`, `creditNoteDate`, `creditNoteAmount` from `claimDetails`
-
-## Data Flow
-
-1. **Credit Note Generation** (Step 7 or Step 8):
- - Credit note is generated from SAP/DMS
- - Stored in `dealer_claim_details` table
- - Fields: `credit_note_number`, `credit_note_date`, `credit_note_amount`
-
-2. **Display in Modal**:
- - Modal reads from `request.claimDetails`
- - Displays credit note information
- - Shows dealer and request details
-
-3. **Actions**:
- - Download: Saves credit note to Documents tab
- - Send: Emails dealer and updates status
-
-## UI Styling
-
-The modal matches the provided HTML structure:
-- ✅ Green gradient card for credit note document
-- ✅ Blue box for amount display
-- ✅ Purple box for dealer information
-- ✅ Gray box for reference details
-- ✅ Blue info box for available actions
-- ✅ Proper icons (Receipt, Hash, Calendar, DollarSign, Building, FileText, Download, Send)
-- ✅ Status badge with checkmark icon
-- ✅ Responsive grid layouts
-
-## Status
-
-✅ **Component Created**: Ready for integration
-⏳ **Integration**: Pending - needs to be added to `DealerClaimWorkflowTab.tsx`
-⏳ **API Handlers**: Pending - download and send handlers need implementation
-⏳ **Backend Endpoints**: Pending - download and send endpoints need to be created
-
-## Next Steps
-
-1. Add modal to `DealerClaimWorkflowTab.tsx` when Step 8 is ready
-2. Implement download handler (integrate with SAP/DMS)
-3. Implement send handler (email notification)
-4. Add Step 8 button visibility logic (Finance team only)
-5. Test credit note flow end-to-end
-
diff --git a/src/pages/RequestDetail/components/tabs/ClaimManagementOverviewTab.tsx b/src/pages/RequestDetail/components/tabs/ClaimManagementOverviewTab.tsx
deleted file mode 100644
index bdb48d8..0000000
--- a/src/pages/RequestDetail/components/tabs/ClaimManagementOverviewTab.tsx
+++ /dev/null
@@ -1,170 +0,0 @@
-/**
- * ClaimManagementOverviewTab Component
- * Specialized overview tab for Claim Management requests
- * Uses modular card components for flexible rendering based on role and request state
- */
-
-import {
- ActivityInformationCard,
- DealerInformationCard,
- ProposalDetailsCard,
- RequestInitiatorCard,
-} from '../claim-cards';
-import { isClaimManagementRequest } from '@/utils/claimRequestUtils';
-import {
- mapToClaimManagementRequest,
- determineUserRole,
- getRoleBasedVisibility,
- type RequestRole,
-} from '@/utils/claimDataMapper';
-
-interface ClaimManagementOverviewTabProps {
- request: any; // Original request object
- apiRequest: any; // API request data
- currentUserId: string;
- isInitiator: boolean;
- onEditClaimAmount?: () => void;
- className?: string;
-}
-
-export function ClaimManagementOverviewTab({
- request: _request,
- apiRequest,
- currentUserId,
- isInitiator: _isInitiator,
- onEditClaimAmount: _onEditClaimAmount,
- className = '',
-}: ClaimManagementOverviewTabProps) {
- // Check if this is a claim management request
- if (!isClaimManagementRequest(apiRequest)) {
- return (
-
-
This is not a claim management request.
-
- );
- }
-
- // Map API data to claim management structure
- const claimRequest = mapToClaimManagementRequest(apiRequest, currentUserId);
-
- if (!claimRequest) {
- console.warn('[ClaimManagementOverviewTab] Failed to map claim data:', {
- apiRequest,
- hasClaimDetails: !!apiRequest?.claimDetails,
- hasProposalDetails: !!apiRequest?.proposalDetails,
- hasCompletionDetails: !!apiRequest?.completionDetails,
- });
- return (
-
-
Unable to load claim management data.
-
Please ensure the request has been properly initialized.
-
- );
- }
-
- // Debug: Log mapped data for troubleshooting
- console.debug('[ClaimManagementOverviewTab] Mapped claim data:', {
- activityInfo: claimRequest.activityInfo,
- dealerInfo: claimRequest.dealerInfo,
- hasProposalDetails: !!claimRequest.proposalDetails,
- closedExpenses: claimRequest.activityInfo?.closedExpenses,
- closedExpensesBreakdown: claimRequest.activityInfo?.closedExpensesBreakdown,
- hasDealerCode: !!claimRequest.dealerInfo?.dealerCode,
- hasDealerName: !!claimRequest.dealerInfo?.dealerName,
- });
-
- // Determine user's role
- const userRole: RequestRole = determineUserRole(apiRequest, currentUserId);
-
- // Get visibility settings based on role
- const visibility = getRoleBasedVisibility(userRole);
-
- console.debug('[ClaimManagementOverviewTab] User role and visibility:', {
- userRole,
- visibility,
- currentUserId,
- showDealerInfo: visibility.showDealerInfo,
- dealerInfoPresent: !!(claimRequest.dealerInfo?.dealerCode || claimRequest.dealerInfo?.dealerName),
- });
-
- // Extract initiator info from request
- // The apiRequest has initiator object with displayName, email, department, phone, etc.
- const initiatorInfo = {
- name: apiRequest.initiator?.name || apiRequest.initiator?.displayName || apiRequest.initiator?.email || 'Unknown',
- role: apiRequest.initiator?.role || apiRequest.initiator?.designation || 'Initiator',
- department: apiRequest.initiator?.department || apiRequest.department || '',
- email: apiRequest.initiator?.email || 'N/A',
- phone: apiRequest.initiator?.phone || apiRequest.initiator?.mobile,
- };
-
- return (
-
- {/* Activity Information - Always visible */}
-
-
- {/* Dealer Information - Always visible */}
-
-
- {/* Proposal Details - Only shown after dealer submits proposal */}
- {visibility.showProposalDetails && claimRequest.proposalDetails && (
-
- )}
-
- {/* Request Initiator */}
-
-
- );
-}
-
-/**
- * Wrapper component that decides whether to show claim management or regular overview
- */
-interface AdaptiveOverviewTabProps {
- request: any;
- apiRequest: any;
- currentUserId: string;
- isInitiator: boolean;
- onEditClaimAmount?: () => void;
- // Props for regular overview tab
- regularOverviewComponent?: React.ComponentType;
- regularOverviewProps?: any;
-}
-
-export function AdaptiveOverviewTab({
- request,
- apiRequest,
- currentUserId,
- isInitiator,
- onEditClaimAmount,
- regularOverviewComponent: RegularOverview,
- regularOverviewProps,
-}: AdaptiveOverviewTabProps) {
- // Determine if this is a claim management request
- const isClaim = isClaimManagementRequest(apiRequest);
-
- if (isClaim) {
- return (
-
- );
- }
-
- // Render regular overview if provided
- if (RegularOverview) {
- return ;
- }
-
- // Fallback
- return (
-
-
No overview available for this request type.
-
- );
-}
-
-
diff --git a/src/pages/RequestDetail/components/tabs/ClaimManagementWorkflowTab.tsx b/src/pages/RequestDetail/components/tabs/ClaimManagementWorkflowTab.tsx
deleted file mode 100644
index c2e468e..0000000
--- a/src/pages/RequestDetail/components/tabs/ClaimManagementWorkflowTab.tsx
+++ /dev/null
@@ -1,311 +0,0 @@
-/**
- * ClaimManagementWorkflowTab Component
- * Displays the 8-step workflow process specific to Claim Management requests
- */
-
-import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
-import { Badge } from '@/components/ui/badge';
-import { Button } from '@/components/ui/button';
-import {
- TrendingUp,
- CircleCheckBig,
- Clock,
- Mail,
- Download,
- Receipt,
- Activity,
- AlertCircle,
-} from 'lucide-react';
-import { format } from 'date-fns';
-
-interface WorkflowStep {
- stepNumber: number;
- stepName: string;
- stepDescription: string;
- assignedTo: string;
- assignedToType: 'dealer' | 'requestor' | 'department_lead' | 'finance' | 'system';
- status: 'pending' | 'in_progress' | 'approved' | 'rejected' | 'skipped';
- tatHours: number;
- elapsedHours?: number;
- remarks?: string;
- approvedAt?: string;
- approvedBy?: string;
- ioDetails?: {
- ioNumber: string;
- ioRemarks: string;
- organisedBy: string;
- organisedAt: string;
- };
- dmsDetails?: {
- dmsNumber: string;
- dmsRemarks: string;
- pushedBy: string;
- pushedAt: string;
- };
- hasEmailNotification?: boolean;
- hasDownload?: boolean;
- downloadUrl?: string;
-}
-
-interface ClaimManagementWorkflowTabProps {
- steps: WorkflowStep[];
- currentStep: number;
- totalSteps?: number;
- onViewEmailTemplate?: (stepNumber: number) => void;
- onDownloadDocument?: (stepNumber: number, url: string) => void;
- className?: string;
-}
-
-export function ClaimManagementWorkflowTab({
- steps,
- currentStep,
- totalSteps = 8,
- onViewEmailTemplate,
- onDownloadDocument,
- className = '',
-}: ClaimManagementWorkflowTabProps) {
-
- const formatDate = (dateString?: string) => {
- if (!dateString) return 'N/A';
- try {
- return format(new Date(dateString), 'MMM d, yyyy, h:mm a');
- } catch {
- return dateString;
- }
- };
-
- const getStepBorderColor = (status: string) => {
- switch (status) {
- case 'approved':
- return 'border-green-500 bg-green-50';
- case 'in_progress':
- return 'border-blue-500 bg-blue-50';
- case 'rejected':
- return 'border-red-500 bg-red-50';
- case 'pending':
- return 'border-gray-300 bg-white';
- case 'skipped':
- return 'border-gray-400 bg-gray-50';
- default:
- return 'border-gray-300 bg-white';
- }
- };
-
- const getStepIconBg = (status: string) => {
- switch (status) {
- case 'approved':
- return 'bg-green-100';
- case 'in_progress':
- return 'bg-blue-100';
- case 'rejected':
- return 'bg-red-100';
- case 'pending':
- return 'bg-gray-100';
- case 'skipped':
- return 'bg-gray-200';
- default:
- return 'bg-gray-100';
- }
- };
-
- const getStepIcon = (status: string) => {
- switch (status) {
- case 'approved':
- return ;
- case 'in_progress':
- return ;
- case 'rejected':
- return ;
- case 'pending':
- return ;
- default:
- return ;
- }
- };
-
- const getStatusBadgeColor = (status: string) => {
- switch (status) {
- case 'approved':
- return 'bg-green-100 text-green-800 border-green-200';
- case 'in_progress':
- return 'bg-blue-100 text-blue-800 border-blue-200';
- case 'rejected':
- return 'bg-red-100 text-red-800 border-red-200';
- case 'pending':
- return 'bg-gray-100 text-gray-600 border-gray-200';
- case 'skipped':
- return 'bg-gray-200 text-gray-700 border-gray-300';
- default:
- return 'bg-gray-100 text-gray-600 border-gray-200';
- }
- };
-
- return (
-
-
-
-
-
-
- Claim Management Workflow
-
-
- 8-Step approval process for dealer claim management
-
-
-
- Step {currentStep} of {totalSteps}
-
-
-
-
-
- {steps.map((step) => (
-
- {/* Step Content */}
-
- {/* Icon */}
-
- {getStepIcon(step.status)}
-
-
- {/* Step Details */}
-
- {/* Header Row */}
-
-
-
-
- Step {step.stepNumber}: {step.stepName}
-
-
- {step.status}
-
-
- {/* Action Buttons */}
- {step.hasEmailNotification && onViewEmailTemplate && (
-
- )}
-
- {step.hasDownload && onDownloadDocument && step.downloadUrl && (
-
- )}
-
-
-
{step.assignedTo}
-
- {step.stepDescription}
-
-
-
- {/* TAT Info */}
-
-
TAT: {step.tatHours}h
- {step.elapsedHours !== undefined && (
-
- Elapsed: {step.elapsedHours}h
-
- )}
-
-
-
- {/* Remarks */}
- {step.remarks && (
-
- )}
-
- {/* IO Details */}
- {step.ioDetails && (
-
-
-
-
- IO Organisation Details
-
-
-
-
- IO Number:
-
- {step.ioDetails.ioNumber}
-
-
-
-
IO Remark:
-
{step.ioDetails.ioRemarks}
-
-
- Organised by {step.ioDetails.organisedBy} on{' '}
- {formatDate(step.ioDetails.organisedAt)}
-
-
-
- )}
-
- {/* DMS Details */}
- {step.dmsDetails && (
-
-
-
-
- DMS Processing Details
-
-
-
-
- DMS Number:
-
- {step.dmsDetails.dmsNumber}
-
-
-
-
DMS Remarks:
-
{step.dmsDetails.dmsRemarks}
-
-
- Pushed by {step.dmsDetails.pushedBy} on{' '}
- {formatDate(step.dmsDetails.pushedAt)}
-
-
-
- )}
-
- {/* Approval Timestamp */}
- {step.approvedAt && (
-
- {step.status === 'approved' ? 'Approved' : 'Updated'} on{' '}
- {formatDate(step.approvedAt)}
-
- )}
-
-
-
- ))}
-
-
-
- );
-}
-
-
diff --git a/src/pages/RequestDetail/components/tabs/DealerClaimWorkflowTab.tsx b/src/pages/RequestDetail/components/tabs/DealerClaimWorkflowTab.tsx
deleted file mode 100644
index 635741b..0000000
--- a/src/pages/RequestDetail/components/tabs/DealerClaimWorkflowTab.tsx
+++ /dev/null
@@ -1,1260 +0,0 @@
-/**
- * Dealer Claim Workflow Tab Component
- *
- * Purpose: Specialized workflow view for dealer claim management
- * Features:
- * - 8-step approval process visualization
- * - Action buttons for document uploads, IO organization, DMS processing
- * - Special sections for IO details and DMS details
- * - Dealer-specific workflow steps
- */
-
-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/DealerProposalSubmissionModal';
-import { InitiatorProposalApprovalModal } from '../modals/InitiatorProposalApprovalModal';
-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';
-import { uploadDocument } from '@/services/documentApi';
-import { createWorkNoteMultipart } from '@/services/workflowApi';
-
-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;
- };
- 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 ;
- case 'pending':
- return ;
- case 'rejected':
- return ;
- default:
- return ;
- }
-};
-
-/**
- * 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 [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([]);
- 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) {
- ioDetails = {
- ioNumber: internalOrder.ioNumber || internalOrder.io_number || '',
- ioRemark: internalOrder.ioRemark || internalOrder.io_remark || 'N/A',
- organizedBy:
- internalOrder.organizer?.displayName ||
- internalOrder.organizer?.name ||
- internalOrder.organizedBy ||
- step.approver ||
- 'N/A',
- organizedAt:
- internalOrder.organizedAt ||
- internalOrder.organized_at ||
- step.approvedAt ||
- request?.updatedAt ||
- '',
- };
-
- // Debug logging for Step 3 IO details
- console.log('[DealerClaimWorkflowTab] Step 3 IO Details Debug:', {
- step: step.step,
- status: step.status,
- internalOrder,
- ioDetails,
- });
- }
- }
-
- // 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,
- });
-
- // Create work note for activity log
- await createWorkNoteMultipart(requestId, {
- message: `Dealer submitted proposal with ${data.costBreakup.length} cost items and estimated budget of ₹${totalBudget.toLocaleString('en-IN')}. ${data.dealerComments ? `Comments: ${data.dealerComments}` : ''}`,
- isPriority: false,
- }, []);
-
- 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);
-
- // Create work note for activity log
- await createWorkNoteMultipart(requestId, {
- message: `Requestor approved the dealer proposal. ${comments ? `Comments: ${comments}` : 'Proceeding to Dept Lead approval.'}`,
- isPriority: false,
- }, []);
-
- 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);
-
- // Create work note for activity log
- await createWorkNoteMultipart(requestId, {
- message: `Requestor rejected the dealer proposal. Request has been cancelled. ${comments ? `Reason: ${comments}` : ''}`,
- isPriority: false,
- }, []);
-
- 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
- // Note: We need to get IO balance from SAP integration, but for now we'll use placeholder values
- // The backend should handle SAP integration
- await updateIODetails(requestId, {
- ioNumber: data.ioNumber,
- ioRemark: data.ioRemark,
- ioAvailableBalance: 0, // Should come from SAP integration
- ioBlockedAmount: 0, // Should come from SAP integration
- ioRemainingBalance: 0, // Should come from SAP integration
- });
-
- // Approve Step 3 using real API
- // IO remark is stored in claimDetails, so we just pass the comments
- await approveLevel(requestId, levelId, data.comments);
-
- // Create work note for activity log
- await createWorkNoteMultipart(requestId, {
- message: `Dept Lead approved request and organized IO ${data.ioNumber}. ${data.ioRemark ? `IO Remark: ${data.ioRemark}. ` : ''}${data.comments ? `Comments: ${data.comments}` : 'Budget will be blocked in SAP.'}`,
- isPriority: false,
- }, []);
-
- 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');
- }
-
- // Create work note with completion description
- await createWorkNoteMultipart(requestId, {
- message: `Dealer submitted completion documents. ${data.completionDescription ? `Description: ${data.completionDescription}` : ''}${data.totalClosedExpenses > 0 ? ` Total closed expenses: ₹${data.totalClosedExpenses.toLocaleString('en-IN')}` : ''}`,
- isPriority: false,
- }, []);
-
- 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 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);
-
- // Create work note for activity log
- await createWorkNoteMultipart(requestId, {
- message: `Dept Lead rejected the request. More clarification required. Request has been cancelled. ${comments ? `Reason: ${comments}` : ''}`,
- isPriority: false,
- }, []);
-
- 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(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 (
- <>
-
-
-
-
-
-
- Claim Management Workflow
-
-
- 8-Step approval process for dealer claim management
-
-
-
- Step {currentStep} of {totalSteps}
-
-
-
-
-
- {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';
-
- // Debug logging for Step 1
- if (step.step === 1) {
- console.log('[DealerClaimWorkflowTab] Step 1 Debug:', {
- step: step.step,
- status: step.status,
- currentStep,
- isActive,
- isDealer,
- isStep1Approver,
- step1ApproverEmail,
- userEmail,
- dealerEmail,
- });
- }
-
- // Debug logging for Step 2
- if (step.step === 2) {
- console.log('[DealerClaimWorkflowTab] Step 2 Debug:', {
- step: step.step,
- status: step.status,
- currentStep,
- isActive,
- isInitiator,
- isStep2Approver,
- showApprovalModal,
- });
- }
-
- // Debug logging for Step 3
- if (step.step === 3) {
- console.log('[DealerClaimWorkflowTab] Step 3 Debug:', {
- step: step.step,
- status: step.status,
- currentStep,
- isActive,
- isCurrentApprover,
- isStep3Approver,
- step3ApproverEmail,
- userEmail,
- approverEmail,
- });
- }
-
- // Debug logging for Step 5
- if (step.step === 5) {
- console.log('[DealerClaimWorkflowTab] Step 5 Debug:', {
- step: step.step,
- status: step.status,
- currentStep,
- isActive,
- isDealer,
- dealerEmail,
- userEmail,
- requestDealerEmail: (request as any)?.dealerEmail,
- requestClaimDetails: (request as any)?.claimDetails,
- claimDetailsDealerEmail: (request as any)?.claimDetails?.dealerEmail,
- });
- }
-
- return (
-
-
- {/* Step Icon */}
-
- {getStepIcon(step.status)}
-
-
- {/* Step Content */}
-
-
-
-
-
- Step {step.step}: {step.title}
-
-
- {step.status.toLowerCase()}
-
- {/* Email Template Button (Step 4) - Show when approved */}
- {step.step === 4 && step.status === 'approved' && (
-
- )}
- {/* E-Invoice Download Button (Step 7) */}
- {step.step === 7 && step.einvoiceUrl && isCompleted && (
-
- )}
-
-
{step.approver}
-
{step.description}
-
-
-
TAT: {step.tatHours}h
- {step.elapsedHours && (
-
- Elapsed: {step.elapsedHours}h
-
- )}
-
-
-
- {/* Comment Section */}
- {step.comment && (
-
- )}
-
- {/* 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 && (
-
-
-
-
- IO Organisation Details
-
-
-
-
- IO Number:
-
- {step.ioDetails.ioNumber}
-
-
-
-
IO Remark:
-
- {step.ioDetails.ioRemark || 'N/A'}
-
-
-
- Organised by {step.ioDetails.organizedBy || step.approver || 'N/A'} on{' '}
- {step.ioDetails.organizedAt
- ? formatDateSafe(step.ioDetails.organizedAt)
- : (step.approvedAt ? formatDateSafe(step.approvedAt) : 'N/A')
- }
-
-
-
- )}
-
- {/* DMS Processing Details (Step 6) */}
- {step.step === 6 && step.dmsDetails && step.dmsDetails.dmsNumber && (
-
-
-
-
- DMS Processing Details
-
-
-
-
- DMS Number:
-
- {step.dmsDetails.dmsNumber}
-
-
- {step.dmsDetails.dmsRemarks && (
-
-
DMS Remarks:
-
{step.dmsDetails.dmsRemarks}
-
- )}
- {step.dmsDetails.pushedAt && (
-
- Pushed by {step.dmsDetails.pushedBy} on{' '}
- {formatDateSafe(step.dmsDetails.pushedAt)}
-
- )}
-
-
- )}
-
- {/* Action Buttons */}
- {isActive && (
-
- {/* Step 1: Submit Proposal Button - Only for dealer or step 1 approver */}
- {step.step === 1 && (isDealer || isStep1Approver) && (
-
- )}
-
- {/* Step 2: Confirm Request - Only for initiator or step 2 approver */}
- {step.step === 2 && (isInitiator || isStep2Approver) && (
-
- )}
-
- {/* 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;
- })() && (
-
- )}
-
- {/* Step 5: Upload Completion Documents - Only for dealer */}
- {step.step === 5 && isDealer && (
-
- )}
-
- {/* 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;
- })()) && (
-
- )}
-
- {/* 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;
- })() && (
-
- )}
-
- )}
-
- {/* Approved Date */}
- {step.approvedAt && (
-
- Approved on {formatDateSafe(step.approvedAt)}
-
- )}
-
-
-
- );
- })}
-
-
-
-
- {/* Dealer Proposal Submission Modal */}
- setShowProposalModal(false)}
- onSubmit={handleProposalSubmit}
- dealerName={dealerName}
- activityName={activityName}
- requestId={request?.id || request?.requestId}
- />
-
- {/* Initiator Proposal Approval Modal */}
- {
- console.log('[DealerClaimWorkflowTab] Closing approval modal');
- setShowApprovalModal(false);
- }}
- onApprove={handleProposalApprove}
- onReject={handleProposalReject}
- proposalData={proposalData}
- dealerName={dealerName}
- activityName={activityName}
- requestId={request?.id || request?.requestId}
- />
-
- {/* Dept Lead IO Approval Modal */}
- setShowIOApprovalModal(false)}
- onApprove={handleIOApproval}
- onReject={handleIORejection}
- requestTitle={request?.title}
- requestId={request?.id || request?.requestId}
- />
-
- {/* Dealer Completion Documents Modal */}
- setShowCompletionModal(false)}
- onSubmit={handleCompletionSubmit}
- dealerName={dealerName}
- activityName={activityName}
- requestId={request?.id || request?.requestId}
- />
-
- {/* Credit Note from SAP Modal (Step 8) */}
- 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 */}
- {
- 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"
- />
- >
- );
-}
-
diff --git a/src/pages/RequestDetail/components/tabs/IOTab.tsx b/src/pages/RequestDetail/components/tabs/IOTab.tsx
deleted file mode 100644
index f1d54e8..0000000
--- a/src/pages/RequestDetail/components/tabs/IOTab.tsx
+++ /dev/null
@@ -1,439 +0,0 @@
-/**
- * IO Tab Component
- *
- * Purpose: Handle IO (Internal Order) budget management for dealer claims
- * Features:
- * - Fetch IO budget from SAP
- * - Block IO amount in SAP
- * - Display blocked IO details
- */
-
-import { useState, useEffect } from 'react';
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
-import { Button } from '@/components/ui/button';
-import { Input } from '@/components/ui/input';
-import { Label } from '@/components/ui/label';
-import { Badge } from '@/components/ui/badge';
-import { DollarSign, Download, CircleCheckBig, Target } from 'lucide-react';
-import { toast } from 'sonner';
-import { validateIO, updateIODetails, getClaimDetails } from '@/services/dealerClaimApi';
-import { useAuth } from '@/contexts/AuthContext';
-
-interface IOTabProps {
- request: any;
- apiRequest?: any;
- onRefresh?: () => void;
-}
-
-interface IOBlockedDetails {
- ioNumber: string;
- blockedAmount: 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
- const internalOrder = apiRequest?.internalOrder || apiRequest?.internal_order || null;
- const existingIONumber = internalOrder?.ioNumber || internalOrder?.io_number || request?.ioNumber || '';
- const existingBlockedAmount = internalOrder?.ioBlockedAmount || internalOrder?.io_blocked_amount || 0;
- const existingAvailableBalance = internalOrder?.ioAvailableBalance || internalOrder?.io_available_balance || 0;
- const existingRemainingBalance = internalOrder?.ioRemainingBalance || internalOrder?.io_remaining_balance || 0;
- const sapDocNumber = internalOrder?.sapDocumentNumber || internalOrder?.sap_document_number || '';
- // 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(null);
- const [amountToBlock, setAmountToBlock] = useState('');
- const [blockedDetails, setBlockedDetails] = useState(null);
- const [blockingBudget, setBlockingBudget] = useState(false);
-
- // Load existing IO block details from apiRequest
- useEffect(() => {
- 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: 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',
- });
- setIoNumber(existingIONumber);
-
- // Set fetched amount if available balance exists
- if (availableBeforeBlock > 0) {
- setFetchedAmount(availableBeforeBlock);
- }
- }
- }, [internalOrder, existingIONumber, existingBlockedAmount, existingAvailableBalance, existingRemainingBalance, sapDocNumber, organizer]);
-
- /**
- * Fetch available budget from SAP
- * 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()) {
- toast.error('Please enter an IO number');
- return;
- }
-
- if (!requestId) {
- toast.error('Request ID not found');
- return;
- }
-
- setFetchingAmount(true);
- try {
- // Call validate IO endpoint - returns dummy data for now, will integrate with SAP later
- const ioData = await validateIO(requestId, ioNumber.trim());
-
- 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('Invalid IO number or no available balance found');
- setFetchedAmount(null);
- setAmountToBlock('');
- }
- } catch (error: any) {
- console.error('Failed to fetch IO budget:', error);
- const errorMessage = error?.response?.data?.message || error?.message || 'Failed to validate IO number or fetch budget from SAP';
- toast.error(errorMessage);
- setFetchedAmount(null);
- } finally {
- setFetchingAmount(false);
- }
- };
-
- /**
- * Block budget in SAP system
- */
- const handleBlockBudget = async () => {
- if (!ioNumber.trim() || fetchedAmount === null) {
- toast.error('Please fetch IO amount first');
- return;
- }
-
- if (!requestId) {
- toast.error('Request ID not found');
- return;
- }
-
- const blockAmount = parseFloat(amountToBlock);
-
- 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 and store in database
- // This will store in internal_orders and claim_budget_tracking tables
- await updateIODetails(requestId, {
- ioNumber: ioNumber.trim(),
- ioAvailableBalance: fetchedAmount,
- ioBlockedAmount: blockAmount,
- ioRemainingBalance: fetchedAmount - blockAmount,
- });
-
- // Fetch updated claim details to get the blocked IO data
- const claimData = await getClaimDetails(requestId);
- 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 || 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
- onRefresh?.();
- } else {
- toast.error('IO blocked but failed to fetch updated details');
- onRefresh?.();
- }
- } catch (error: any) {
- console.error('Failed to block IO budget:', error);
- const errorMessage = error?.response?.data?.message || error?.message || 'Failed to block IO budget in SAP';
- toast.error(errorMessage);
- } finally {
- setBlockingBudget(false);
- }
- };
-
- /**
- * Release blocked budget
- * Note: This functionality may need a separate backend endpoint for releasing IO budget
- * For now, we'll call updateIODetails with blockedAmount=0 to release
- */
- const handleReleaseBudget = async () => {
- if (!blockedDetails || !requestId) {
- toast.error('No blocked budget to release');
- return;
- }
-
- if (!ioNumber.trim()) {
- toast.error('IO number not found');
- return;
- }
-
- try {
- // Release budget by setting blockedAmount to 0
- // Note: Backend may need a dedicated release endpoint for proper SAP integration
- await updateIODetails(requestId, {
- ioNumber: ioNumber.trim(),
- ioAvailableBalance: blockedDetails.availableBalance + blockedDetails.blockedAmount,
- ioBlockedAmount: 0,
- ioRemainingBalance: blockedDetails.availableBalance + blockedDetails.blockedAmount,
- });
-
- // Clear local state
- setBlockedDetails(null);
- setFetchedAmount(null);
- setIoNumber('');
-
- toast.success('IO budget released successfully');
-
- // Refresh request details
- onRefresh?.();
- } catch (error: any) {
- console.error('Failed to release IO budget:', error);
- const errorMessage = error?.response?.data?.message || error?.message || 'Failed to release IO budget';
- toast.error(errorMessage);
- }
- };
-
- return (
-
- {/* IO Budget Management Card */}
-
-
-
-
- IO Budget Management
-
-
- Enter IO number to fetch available budget from SAP
-
-
-
- {/* IO Number Input */}
-
-
-
- setIoNumber(e.target.value)}
- disabled={fetchingAmount || !!blockedDetails}
- className="flex-1"
- />
-
-
-
-
- {/* Fetched Amount Display */}
- {fetchedAmount !== null && !blockedDetails && (
- <>
-
-
-
-
Available Amount
-
- ₹{fetchedAmount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
-
-
-
-
-
-
IO Number: {ioNumber}
-
Fetched from: SAP System
-
-
-
- {/* Amount to Block Input */}
-
-
-
- ₹
- setAmountToBlock(e.target.value)}
- className="pl-8"
- />
-
-
-
- {/* Block Button */}
-
- >
- )}
-
-
-
- {/* IO Blocked Details Card */}
-
-
-
-
- IO Blocked Details
-
-
- Details of IO blocked in SAP system
-
-
-
- {blockedDetails ? (
-
- {/* Success Banner */}
-
-
-
-
-
IO Blocked Successfully
-
Budget has been reserved in SAP system
-
-
-
-
- {/* Blocked Details */}
-
-
-
IO Number
-
{blockedDetails.ioNumber}
-
-
-
SAP Document Number
-
{blockedDetails.sapDocumentNumber || 'N/A'}
-
-
-
Blocked Amount
-
- ₹{blockedDetails.blockedAmount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
-
-
-
-
Available Amount (Before Block)
-
- ₹{blockedDetails.availableBalance.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
-
-
-
-
Remaining Amount (After Block)
-
- ₹{blockedDetails.remainingBalance.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
-
-
-
-
Blocked By
-
{blockedDetails.blockedBy}
-
-
-
Blocked At
-
- {new Date(blockedDetails.blockedDate).toLocaleString('en-IN', {
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- hour: 'numeric',
- minute: 'numeric',
- hour12: true
- })}
-
-
-
-
Status
-
-
- Blocked
-
-
-
-
- ) : (
-
-
-
No IO blocked yet
-
- Enter IO number and fetch amount to block budget
-
-
- )}
-
-
-
- );
-}
-
diff --git a/src/pages/Requests/components/RequestCard.tsx b/src/pages/Requests/components/RequestCard.tsx
index 3b08441..2e8f822 100644
--- a/src/pages/Requests/components/RequestCard.tsx
+++ b/src/pages/Requests/components/RequestCard.tsx
@@ -96,6 +96,32 @@ export function RequestCard({ request, index, onViewRequest }: RequestCardProps)
{request.priority}
+ {/* Template Type Badge */}
+ {(() => {
+ const templateType = request?.templateType || (request as any)?.template_type || '';
+ const templateTypeUpper = templateType?.toUpperCase() || '';
+
+ // Direct mapping from templateType
+ let templateLabel = 'Custom';
+ let templateColor = 'bg-purple-100 !text-purple-600 border-purple-200';
+
+ if (templateTypeUpper === 'DEALER CLAIM') {
+ templateLabel = 'Claim Management';
+ templateColor = 'bg-blue-100 !text-blue-700 border-blue-200';
+ } else if (templateTypeUpper === 'TEMPLATE') {
+ templateLabel = 'Template';
+ }
+
+ return (
+
+ {templateLabel}
+
+ );
+ })()}
{request.department && (