files moved for plug and play and activity capture enhanced
This commit is contained in:
parent
9b3194d9ca
commit
22223fa00c
29
src/App.tsx
29
src/App.tsx
@ -9,7 +9,7 @@ import { SharedSummaries } from '@/pages/SharedSummaries/SharedSummaries';
|
|||||||
import { SharedSummaryDetail } from '@/pages/SharedSummaries/SharedSummaryDetail';
|
import { SharedSummaryDetail } from '@/pages/SharedSummaries/SharedSummaryDetail';
|
||||||
import { WorkNotes } from '@/pages/WorkNotes';
|
import { WorkNotes } from '@/pages/WorkNotes';
|
||||||
import { CreateRequest } from '@/pages/CreateRequest';
|
import { CreateRequest } from '@/pages/CreateRequest';
|
||||||
import { ClaimManagementWizard } from '@/components/workflow/ClaimManagementWizard';
|
import { ClaimManagementWizard } from '@/dealer-claim/components/request-creation/ClaimManagementWizard';
|
||||||
import { MyRequests } from '@/pages/MyRequests';
|
import { MyRequests } from '@/pages/MyRequests';
|
||||||
import { Requests } from '@/pages/Requests/Requests';
|
import { Requests } from '@/pages/Requests/Requests';
|
||||||
import { UserAllRequests } from '@/pages/Requests/UserAllRequests';
|
import { UserAllRequests } from '@/pages/Requests/UserAllRequests';
|
||||||
@ -297,25 +297,36 @@ function AppRoutes({ onLogout }: AppProps) {
|
|||||||
|
|
||||||
// Call API to create claim request
|
// Call API to create claim request
|
||||||
const response = await createClaimRequest(payload);
|
const response = await createClaimRequest(payload);
|
||||||
|
|
||||||
|
// Validate response - ensure request was actually created successfully
|
||||||
|
if (!response || !response.request) {
|
||||||
|
throw new Error('Invalid response from server: Request object not found');
|
||||||
|
}
|
||||||
|
|
||||||
const createdRequest = response.request;
|
const createdRequest = response.request;
|
||||||
|
|
||||||
|
// Validate that we have at least one identifier (requestNumber or requestId)
|
||||||
|
if (!createdRequest.requestNumber && !createdRequest.requestId) {
|
||||||
|
throw new Error('Invalid response from server: Request identifier not found');
|
||||||
|
}
|
||||||
|
|
||||||
// Close manager modal if open
|
// Close manager modal if open
|
||||||
setManagerModalOpen(false);
|
setManagerModalOpen(false);
|
||||||
setManagerModalData(null);
|
setManagerModalData(null);
|
||||||
|
|
||||||
|
// Only show success toast if request was actually created successfully
|
||||||
toast.success('Claim Request Submitted', {
|
toast.success('Claim Request Submitted', {
|
||||||
description: 'Your claim management request has been created successfully.',
|
description: 'Your claim management request has been created successfully.',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Navigate to the created request detail page
|
// Navigate to the created request detail page using requestNumber
|
||||||
if (createdRequest?.requestId) {
|
if (createdRequest.requestNumber) {
|
||||||
const { navigateToRequest } = await import('@/utils/requestNavigation');
|
navigate(`/request/${createdRequest.requestNumber}`);
|
||||||
navigateToRequest({
|
} else if (createdRequest.requestId) {
|
||||||
requestId: createdRequest.requestId,
|
// Fallback to requestId if requestNumber is not available
|
||||||
request: createdRequest,
|
navigate(`/request/${createdRequest.requestId}`);
|
||||||
navigate,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
|
// This should not happen due to validation above, but just in case
|
||||||
navigate('/my-requests');
|
navigate('/my-requests');
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@ -7,7 +7,6 @@
|
|||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { AlertCircle, CheckCircle2, User, Mail, Building2 } from 'lucide-react';
|
import { AlertCircle, CheckCircle2, User, Mail, Building2 } from 'lucide-react';
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
|
|
||||||
interface Manager {
|
interface Manager {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
|||||||
@ -1,794 +0,0 @@
|
|||||||
import { useState, useMemo } 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 { toast } from 'sonner';
|
|
||||||
import { DealerDocumentModal } from '@/components/modals/DealerDocumentModal';
|
|
||||||
import { InitiatorVerificationModal } from '@/components/modals/InitiatorVerificationModal';
|
|
||||||
import { Progress } from '@/components/ui/progress';
|
|
||||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
||||||
import { CLAIM_MANAGEMENT_DATABASE } from '@/utils/claimManagementDatabase';
|
|
||||||
import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
|
|
||||||
import {
|
|
||||||
ArrowLeft,
|
|
||||||
Clock,
|
|
||||||
FileText,
|
|
||||||
MessageSquare,
|
|
||||||
CheckCircle,
|
|
||||||
XCircle,
|
|
||||||
Download,
|
|
||||||
Eye,
|
|
||||||
Flame,
|
|
||||||
Target,
|
|
||||||
TrendingUp,
|
|
||||||
RefreshCw,
|
|
||||||
Activity,
|
|
||||||
MapPin,
|
|
||||||
Mail,
|
|
||||||
Phone,
|
|
||||||
Building,
|
|
||||||
Receipt,
|
|
||||||
Upload,
|
|
||||||
UserPlus,
|
|
||||||
ClipboardList,
|
|
||||||
DollarSign,
|
|
||||||
Calendar
|
|
||||||
} from 'lucide-react';
|
|
||||||
|
|
||||||
interface ClaimManagementDetailProps {
|
|
||||||
requestId: string;
|
|
||||||
onBack?: () => void;
|
|
||||||
onOpenModal?: (modal: string) => void;
|
|
||||||
dynamicRequests?: any[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility functions
|
|
||||||
const getPriorityConfig = (priority: string) => {
|
|
||||||
switch (priority) {
|
|
||||||
case 'express':
|
|
||||||
case 'urgent':
|
|
||||||
return {
|
|
||||||
color: 'bg-red-100 text-red-800 border-red-200',
|
|
||||||
icon: Flame
|
|
||||||
};
|
|
||||||
case 'standard':
|
|
||||||
return {
|
|
||||||
color: 'bg-blue-100 text-blue-800 border-blue-200',
|
|
||||||
icon: Target
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
color: 'bg-gray-100 text-gray-800 border-gray-200',
|
|
||||||
icon: Target
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusConfig = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'pending':
|
|
||||||
return {
|
|
||||||
color: 'bg-yellow-100 text-yellow-800 border-yellow-200',
|
|
||||||
icon: Clock
|
|
||||||
};
|
|
||||||
case 'in-review':
|
|
||||||
return {
|
|
||||||
color: 'bg-blue-100 text-blue-800 border-blue-200',
|
|
||||||
icon: Eye
|
|
||||||
};
|
|
||||||
case 'approved':
|
|
||||||
return {
|
|
||||||
color: 'bg-green-100 text-green-800 border-green-200',
|
|
||||||
icon: CheckCircle
|
|
||||||
};
|
|
||||||
case 'rejected':
|
|
||||||
return {
|
|
||||||
color: 'bg-red-100 text-red-800 border-red-200',
|
|
||||||
icon: XCircle
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
color: 'bg-gray-100 text-gray-800 border-gray-200',
|
|
||||||
icon: Clock
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSLAConfig = (progress: number) => {
|
|
||||||
if (progress >= 80) {
|
|
||||||
return {
|
|
||||||
bg: 'bg-red-50',
|
|
||||||
color: 'bg-red-500',
|
|
||||||
textColor: 'text-red-700'
|
|
||||||
};
|
|
||||||
} else if (progress >= 60) {
|
|
||||||
return {
|
|
||||||
bg: 'bg-orange-50',
|
|
||||||
color: 'bg-orange-500',
|
|
||||||
textColor: 'text-orange-700'
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
bg: 'bg-green-50',
|
|
||||||
color: 'bg-green-500',
|
|
||||||
textColor: 'text-green-700'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStepIcon = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'approved':
|
|
||||||
return <CheckCircle className="w-5 h-5 text-green-600" />;
|
|
||||||
case 'rejected':
|
|
||||||
return <XCircle className="w-5 h-5 text-red-600" />;
|
|
||||||
case 'pending':
|
|
||||||
case 'in-review':
|
|
||||||
return <Clock className="w-5 h-5 text-blue-600" />;
|
|
||||||
default:
|
|
||||||
return <Clock className="w-5 h-5 text-gray-400" />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getActionTypeIcon = (type: string) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'approval':
|
|
||||||
case 'approved':
|
|
||||||
return <CheckCircle className="w-5 h-5 text-green-600" />;
|
|
||||||
case 'rejection':
|
|
||||||
case 'rejected':
|
|
||||||
return <XCircle className="w-5 h-5 text-red-600" />;
|
|
||||||
case 'comment':
|
|
||||||
return <MessageSquare className="w-5 h-5 text-blue-600" />;
|
|
||||||
case 'status_change':
|
|
||||||
return <RefreshCw className="w-5 h-5 text-orange-600" />;
|
|
||||||
case 'assignment':
|
|
||||||
return <UserPlus className="w-5 h-5 text-purple-600" />;
|
|
||||||
case 'created':
|
|
||||||
return <FileText className="w-5 h-5 text-blue-600" />;
|
|
||||||
default:
|
|
||||||
return <Activity className="w-5 h-5 text-gray-600" />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ClaimManagementDetail({
|
|
||||||
requestId,
|
|
||||||
onBack,
|
|
||||||
onOpenModal,
|
|
||||||
dynamicRequests = []
|
|
||||||
}: ClaimManagementDetailProps) {
|
|
||||||
const [activeTab, setActiveTab] = useState('overview');
|
|
||||||
const [dealerDocModal, setDealerDocModal] = useState(false);
|
|
||||||
const [initiatorVerificationModal, setInitiatorVerificationModal] = useState(false);
|
|
||||||
|
|
||||||
// Get claim from database or dynamic requests
|
|
||||||
const claim = useMemo(() => {
|
|
||||||
// First check static database
|
|
||||||
const staticClaim = CLAIM_MANAGEMENT_DATABASE[requestId];
|
|
||||||
if (staticClaim) return staticClaim;
|
|
||||||
|
|
||||||
// Then check dynamic requests
|
|
||||||
const dynamicClaim = dynamicRequests.find((req: any) => req.id === requestId);
|
|
||||||
if (dynamicClaim) return dynamicClaim;
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}, [requestId, dynamicRequests]);
|
|
||||||
|
|
||||||
if (!claim) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center h-screen">
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Claim Not Found</h2>
|
|
||||||
<p className="text-gray-600 mb-4">The claim request you're looking for doesn't exist.</p>
|
|
||||||
<Button onClick={onBack}>
|
|
||||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
||||||
Go Back
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const priorityConfig = getPriorityConfig(claim.priority);
|
|
||||||
const statusConfig = getStatusConfig(claim.status);
|
|
||||||
const slaConfig = getSLAConfig(claim.slaProgress);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50">
|
|
||||||
<div className="max-w-7xl mx-auto p-6">
|
|
||||||
{/* Header Section */}
|
|
||||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-6">
|
|
||||||
<div className="flex items-start justify-between mb-4">
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={onBack}
|
|
||||||
className="mt-1"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div className="w-12 h-12 rounded-full bg-purple-100 flex items-center justify-center">
|
|
||||||
<Receipt className="w-6 h-6 text-purple-600" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-3 mb-2">
|
|
||||||
<h1 className="text-xl font-bold text-gray-900">{claim.id}</h1>
|
|
||||||
<Badge className={`${priorityConfig.color}`} variant="outline">
|
|
||||||
{claim.priority} priority
|
|
||||||
</Badge>
|
|
||||||
<Badge className={`${statusConfig.color}`} variant="outline">
|
|
||||||
<statusConfig.icon className="w-3 h-3 mr-1" />
|
|
||||||
{claim.status}
|
|
||||||
</Badge>
|
|
||||||
<Badge className="bg-purple-100 text-purple-800 border-purple-200" variant="outline">
|
|
||||||
<Receipt className="w-3 h-3 mr-1" />
|
|
||||||
Claim Management
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<h2 className="text-lg font-semibold text-gray-900">{claim.title}</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
{claim.amount && claim.amount !== 'TBD' && (
|
|
||||||
<div className="text-right">
|
|
||||||
<p className="text-sm text-gray-500">Claim Amount</p>
|
|
||||||
<p className="text-xl font-bold text-gray-900">{claim.amount}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
<RefreshCw className="w-4 h-4 mr-2" />
|
|
||||||
Refresh
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* SLA Progress */}
|
|
||||||
<div className={`${slaConfig.bg} rounded-lg border p-4 mt-4`}>
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Clock className={`h-4 w-4 ${slaConfig.textColor}`} />
|
|
||||||
<span className="text-sm font-medium text-gray-900">SLA Progress</span>
|
|
||||||
</div>
|
|
||||||
<span className={`text-sm font-semibold ${slaConfig.textColor}`}>
|
|
||||||
{claim.slaRemaining}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Progress value={claim.slaProgress} className="h-2 mb-2" />
|
|
||||||
<p className="text-xs text-gray-600">
|
|
||||||
Due: {formatDateDDMMYYYY(claim.slaEndDate, true)} • {claim.slaProgress}% elapsed
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tabs */}
|
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
|
||||||
<TabsList className="grid w-full grid-cols-4 bg-gray-100 h-10 mb-6">
|
|
||||||
<TabsTrigger value="overview" className="flex items-center gap-2 text-xs sm:text-sm px-2">
|
|
||||||
<ClipboardList className="w-3 h-3 sm:w-4 sm:h-4" />
|
|
||||||
Overview
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="workflow" className="flex items-center gap-2 text-xs sm:text-sm px-2">
|
|
||||||
<TrendingUp className="w-3 h-3 sm:w-4 sm:h-4" />
|
|
||||||
Workflow (8-Steps)
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="documents" className="flex items-center gap-2 text-xs sm:text-sm px-2">
|
|
||||||
<FileText className="w-3 h-3 sm:w-4 sm:h-4" />
|
|
||||||
Documents
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="activity" className="flex items-center gap-2 text-xs sm:text-sm px-2">
|
|
||||||
<Activity className="w-3 h-3 sm:w-4 sm:h-4" />
|
|
||||||
Activity
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
{/* Overview Tab */}
|
|
||||||
<TabsContent value="overview">
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
||||||
{/* Left Column - Main Content (2/3 width) */}
|
|
||||||
<div className="lg:col-span-2 space-y-6">
|
|
||||||
{/* Activity Information */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<CardTitle className="flex items-center gap-2 text-base">
|
|
||||||
<Calendar className="w-5 h-5 text-blue-600" />
|
|
||||||
Activity Information
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="text-xs font-medium text-gray-500 uppercase tracking-wide">Activity Name</label>
|
|
||||||
<p className="text-sm text-gray-900 font-medium mt-1">{claim.claimDetails?.activityName || 'N/A'}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-xs font-medium text-gray-500 uppercase tracking-wide">Activity Type</label>
|
|
||||||
<p className="text-sm text-gray-900 font-medium mt-1">{claim.claimDetails?.activityType || 'N/A'}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-xs font-medium text-gray-500 uppercase tracking-wide">Location</label>
|
|
||||||
<p className="text-sm text-gray-900 font-medium mt-1 flex items-center gap-2">
|
|
||||||
<MapPin className="w-4 h-4 text-gray-400" />
|
|
||||||
{claim.claimDetails?.location || 'N/A'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-xs font-medium text-gray-500 uppercase tracking-wide">Activity Date</label>
|
|
||||||
<p className="text-sm text-gray-900 font-medium mt-1">{claim.claimDetails?.activityDate || 'N/A'}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-xs font-medium text-gray-500 uppercase tracking-wide">Estimated Budget</label>
|
|
||||||
<p className="text-sm text-gray-900 font-medium mt-1 flex items-center gap-2">
|
|
||||||
<DollarSign className="w-4 h-4 text-green-600" />
|
|
||||||
{claim.claimDetails?.estimatedBudget || 'N/A'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-xs font-medium text-gray-500 uppercase tracking-wide">Period</label>
|
|
||||||
<p className="text-sm text-gray-900 font-medium mt-1">
|
|
||||||
{claim.claimDetails?.periodStart || 'N/A'} - {claim.claimDetails?.periodEnd || 'N/A'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-4 border-t">
|
|
||||||
<label className="text-xs font-medium text-gray-500 uppercase tracking-wide">Description</label>
|
|
||||||
<p className="text-sm text-gray-700 mt-2 bg-gray-50 p-3 rounded-lg whitespace-pre-line">
|
|
||||||
{claim.claimDetails?.requestDescription || claim.description || 'N/A'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Dealer Information */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<CardTitle className="flex items-center gap-2 text-base">
|
|
||||||
<Building className="w-5 h-5 text-purple-600" />
|
|
||||||
Dealer Information
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="text-xs font-medium text-gray-500 uppercase tracking-wide">Dealer Code</label>
|
|
||||||
<p className="text-sm text-gray-900 font-medium mt-1">{claim.claimDetails?.dealerCode || 'N/A'}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-xs font-medium text-gray-500 uppercase tracking-wide">Dealer Name</label>
|
|
||||||
<p className="text-sm text-gray-900 font-medium mt-1">{claim.claimDetails?.dealerName || 'N/A'}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="text-xs font-medium text-gray-500 uppercase tracking-wide">Contact Information</label>
|
|
||||||
<div className="mt-2 space-y-2">
|
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-700">
|
|
||||||
<Mail className="w-4 h-4 text-gray-400" />
|
|
||||||
<span>{claim.claimDetails?.dealerEmail || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-700">
|
|
||||||
<Phone className="w-4 h-4 text-gray-400" />
|
|
||||||
<span>{claim.claimDetails?.dealerPhone || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
{claim.claimDetails?.dealerAddress && (
|
|
||||||
<div className="flex items-start gap-2 text-sm text-gray-700">
|
|
||||||
<MapPin className="w-4 h-4 text-gray-400 mt-0.5" />
|
|
||||||
<span>{claim.claimDetails.dealerAddress}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Initiator Information */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<CardTitle className="text-base">Request Initiator</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<Avatar className="h-14 w-14 ring-2 ring-white shadow-md">
|
|
||||||
<AvatarFallback className="bg-gray-700 text-white font-semibold text-lg">
|
|
||||||
{claim.initiator?.avatar || 'U'}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="font-semibold text-gray-900">{claim.initiator?.name || 'N/A'}</h3>
|
|
||||||
<p className="text-sm text-gray-600">{claim.initiator?.role || 'N/A'}</p>
|
|
||||||
<p className="text-sm text-gray-500">{claim.initiator?.department || 'N/A'}</p>
|
|
||||||
|
|
||||||
<div className="mt-3 space-y-2">
|
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
|
||||||
<Mail className="w-4 h-4" />
|
|
||||||
<span>{claim.initiator?.email || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
|
||||||
<Phone className="w-4 h-4" />
|
|
||||||
<span>{claim.initiator?.phone || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Column - Quick Actions Sidebar (1/3 width) */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Quick Actions */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-base">Quick Actions</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="w-full justify-start gap-2 border-gray-300"
|
|
||||||
onClick={() => onOpenModal?.('work-note')}
|
|
||||||
>
|
|
||||||
<MessageSquare className="w-4 h-4" />
|
|
||||||
Add Work Note
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="w-full justify-start gap-2 border-gray-300"
|
|
||||||
onClick={() => onOpenModal?.('add-spectator')}
|
|
||||||
>
|
|
||||||
<Eye className="w-4 h-4" />
|
|
||||||
Add Spectator
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="pt-4 space-y-2">
|
|
||||||
<Button
|
|
||||||
className="w-full bg-green-600 hover:bg-green-700 text-white"
|
|
||||||
onClick={() => onOpenModal?.('approve')}
|
|
||||||
>
|
|
||||||
<CheckCircle className="w-4 h-4 mr-2" />
|
|
||||||
Approve Step
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => onOpenModal?.('reject')}
|
|
||||||
>
|
|
||||||
<XCircle className="w-4 h-4 mr-2" />
|
|
||||||
Reject Step
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Spectators */}
|
|
||||||
{claim.spectators && claim.spectators.length > 0 && (
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-base">Spectators</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-3">
|
|
||||||
{claim.spectators.map((spectator: any, index: number) => (
|
|
||||||
<div key={index} className="flex items-center gap-3">
|
|
||||||
<Avatar className="h-8 w-8">
|
|
||||||
<AvatarFallback className="bg-blue-100 text-blue-800 text-xs font-semibold">
|
|
||||||
{spectator.avatar}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-sm font-medium text-gray-900">{spectator.name}</p>
|
|
||||||
<p className="text-xs text-gray-500 truncate">{spectator.role}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* Workflow Tab - 8 Step Process */}
|
|
||||||
<TabsContent value="workflow">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<TrendingUp className="w-5 h-5 text-purple-600" />
|
|
||||||
Claim Management Workflow
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription className="mt-2">
|
|
||||||
8-Step approval process for dealer claim management
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
<Badge variant="outline" className="font-medium">
|
|
||||||
Step {claim.currentStep} of {claim.totalSteps}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{claim.approvalFlow && claim.approvalFlow.length > 0 ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{claim.approvalFlow.map((step: any, index: number) => {
|
|
||||||
const isActive = step.status === 'pending' || step.status === 'in-review';
|
|
||||||
const isCompleted = step.status === 'approved';
|
|
||||||
const isRejected = step.status === 'rejected';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`relative p-5 rounded-lg border-2 transition-all ${
|
|
||||||
isActive
|
|
||||||
? 'border-purple-500 bg-purple-50 shadow-md'
|
|
||||||
: isCompleted
|
|
||||||
? 'border-green-500 bg-green-50'
|
|
||||||
: isRejected
|
|
||||||
? 'border-red-500 bg-red-50'
|
|
||||||
: 'border-gray-200 bg-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div className={`p-3 rounded-xl ${
|
|
||||||
isActive ? 'bg-purple-100' :
|
|
||||||
isCompleted ? 'bg-green-100' :
|
|
||||||
isRejected ? 'bg-red-100' :
|
|
||||||
'bg-gray-100'
|
|
||||||
}`}>
|
|
||||||
{getStepIcon(step.status)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-start justify-between gap-4 mb-2">
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<h4 className="font-semibold text-gray-900">Step {step.step}: {step.role}</h4>
|
|
||||||
<Badge variant="outline" className={
|
|
||||||
isActive ? 'bg-purple-100 text-purple-800 border-purple-200' :
|
|
||||||
isCompleted ? 'bg-green-100 text-green-800 border-green-200' :
|
|
||||||
isRejected ? 'bg-red-100 text-red-800 border-red-200' :
|
|
||||||
'bg-gray-100 text-gray-800 border-gray-200'
|
|
||||||
}>
|
|
||||||
{step.status}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-gray-600">{step.approver}</p>
|
|
||||||
{step.description && (
|
|
||||||
<p className="text-sm text-gray-500 mt-2 italic">{step.description}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
{step.tatHours && (
|
|
||||||
<p className="text-xs text-gray-500">TAT: {step.tatHours}h</p>
|
|
||||||
)}
|
|
||||||
{step.elapsedHours !== undefined && step.elapsedHours > 0 && (
|
|
||||||
<p className="text-xs text-gray-600 font-medium">Elapsed: {step.elapsedHours}h</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{step.comment && (
|
|
||||||
<div className="mt-3 p-3 bg-white rounded-lg border border-gray-200">
|
|
||||||
<p className="text-sm text-gray-700">{step.comment}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{step.timestamp && (
|
|
||||||
<p className="text-xs text-gray-500 mt-2">
|
|
||||||
{isCompleted ? 'Approved' : isRejected ? 'Rejected' : 'Actioned'} on {step.timestamp}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Workflow-specific Action Buttons */}
|
|
||||||
{isActive && (
|
|
||||||
<div className="mt-4 flex gap-2">
|
|
||||||
{step.step === 1 && step.role === 'Dealer - Document Upload' && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setDealerDocModal(true)}
|
|
||||||
className="bg-purple-600 hover:bg-purple-700"
|
|
||||||
>
|
|
||||||
<Upload className="w-4 h-4 mr-2" />
|
|
||||||
Upload Documents
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{step.step === 2 && step.role === 'Initiator Evaluation' && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onOpenModal?.('approve')}
|
|
||||||
className="bg-green-600 hover:bg-green-700"
|
|
||||||
>
|
|
||||||
<CheckCircle className="w-4 h-4 mr-2" />
|
|
||||||
Approve & Continue
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{step.step === 4 && step.role === 'Department Lead Approval' && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onOpenModal?.('approve')}
|
|
||||||
className="bg-green-600 hover:bg-green-700"
|
|
||||||
>
|
|
||||||
<CheckCircle className="w-4 h-4 mr-2" />
|
|
||||||
Approve & Lock Budget
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{step.step === 5 && step.role === 'Dealer - Completion Documents' && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setDealerDocModal(true)}
|
|
||||||
className="bg-purple-600 hover:bg-purple-700"
|
|
||||||
>
|
|
||||||
<Upload className="w-4 h-4 mr-2" />
|
|
||||||
Upload Completion Docs
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{step.step === 6 && step.role === 'Initiator Verification' && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setInitiatorVerificationModal(true)}
|
|
||||||
className="bg-green-600 hover:bg-green-700"
|
|
||||||
>
|
|
||||||
<CheckCircle className="w-4 h-4 mr-2" />
|
|
||||||
Verify & Set Amount
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{step.step === 8 && step.role.includes('Credit Note') && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onOpenModal?.('approve')}
|
|
||||||
className="bg-green-600 hover:bg-green-700"
|
|
||||||
>
|
|
||||||
<CheckCircle className="w-4 h-4 mr-2" />
|
|
||||||
Issue Credit Note
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-gray-500 text-center py-8">No workflow steps defined</p>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* Documents Tab */}
|
|
||||||
<TabsContent value="documents">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<FileText className="w-5 h-5 text-purple-600" />
|
|
||||||
Claim Documents
|
|
||||||
</CardTitle>
|
|
||||||
<Button size="sm">
|
|
||||||
<Upload className="w-4 h-4 mr-2" />
|
|
||||||
Upload Document
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{claim.documents && claim.documents.length > 0 ? (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{claim.documents.map((doc: any, index: number) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:bg-gray-50 transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="p-2 bg-purple-100 rounded-lg">
|
|
||||||
<FileText className="w-5 h-5 text-purple-600" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-medium text-gray-900">{doc.name}</p>
|
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
{doc.size} • Uploaded by {doc.uploadedBy} on {doc.uploadedAt}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button variant="ghost" size="sm">
|
|
||||||
<Eye className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="sm">
|
|
||||||
<Download className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-gray-500 text-center py-8">No documents uploaded yet</p>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* Activity Tab - Audit Trail */}
|
|
||||||
<TabsContent value="activity">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<Activity className="w-5 h-5 text-orange-600" />
|
|
||||||
Claim Activity Timeline
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Complete audit trail of all claim management activities
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{claim.auditTrail && claim.auditTrail.length > 0 ? claim.auditTrail.map((entry: any, index: number) => (
|
|
||||||
<div key={index} className="flex items-start gap-4 p-4 rounded-lg hover:bg-gray-50 transition-colors border border-gray-100">
|
|
||||||
<div className="mt-1">
|
|
||||||
{getActionTypeIcon(entry.type)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-start justify-between gap-2">
|
|
||||||
<div>
|
|
||||||
<p className="font-medium text-gray-900">{entry.action}</p>
|
|
||||||
<p className="text-sm text-gray-600 mt-1">{entry.details}</p>
|
|
||||||
<p className="text-xs text-gray-500 mt-1">by {entry.user}</p>
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-gray-500 whitespace-nowrap">{entry.timestamp}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)) : (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<Activity className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
|
||||||
<p className="text-sm text-gray-500">No activity recorded yet</p>
|
|
||||||
<p className="text-xs text-gray-400 mt-2">Actions and updates will appear here</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Claim Management Modals */}
|
|
||||||
{dealerDocModal && (
|
|
||||||
<DealerDocumentModal
|
|
||||||
isOpen={dealerDocModal}
|
|
||||||
onClose={() => setDealerDocModal(false)}
|
|
||||||
onSubmit={async (_documents) => {
|
|
||||||
toast.success('Documents Uploaded', {
|
|
||||||
description: 'Your documents have been submitted for review.',
|
|
||||||
});
|
|
||||||
setDealerDocModal(false);
|
|
||||||
}}
|
|
||||||
dealerName={claim.claimDetails?.dealerName || 'Dealer'}
|
|
||||||
activityName={claim.claimDetails?.activityName || claim.title}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{initiatorVerificationModal && (
|
|
||||||
<InitiatorVerificationModal
|
|
||||||
isOpen={initiatorVerificationModal}
|
|
||||||
onClose={() => setInitiatorVerificationModal(false)}
|
|
||||||
onSubmit={async (data) => {
|
|
||||||
toast.success('Verification Complete', {
|
|
||||||
description: `Amount set to ${data.approvedAmount}. E-invoice will be generated.`,
|
|
||||||
});
|
|
||||||
setInitiatorVerificationModal(false);
|
|
||||||
}}
|
|
||||||
activityName={claim.claimDetails?.activityName || claim.title}
|
|
||||||
requestedAmount={claim.claimDetails?.estimatedBudget || claim.amount || 'TBD'}
|
|
||||||
documents={claim.documents || []}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export { ClaimManagementDetail } from './ClaimManagementDetail';
|
|
||||||
@ -1,684 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { Card, CardContent, 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 { Textarea } from '@/components/ui/textarea';
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Progress } from '@/components/ui/progress';
|
|
||||||
import { Calendar } from '@/components/ui/calendar';
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
|
||||||
import {
|
|
||||||
ArrowLeft,
|
|
||||||
ArrowRight,
|
|
||||||
Calendar as CalendarIcon,
|
|
||||||
Check,
|
|
||||||
Receipt,
|
|
||||||
Building,
|
|
||||||
MapPin,
|
|
||||||
Clock,
|
|
||||||
CheckCircle,
|
|
||||||
Info,
|
|
||||||
FileText,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { getAllDealers as fetchDealersFromAPI, getDealerByCode, type DealerInfo } from '@/services/dealerApi';
|
|
||||||
|
|
||||||
interface ClaimManagementWizardProps {
|
|
||||||
onBack?: () => void;
|
|
||||||
onSubmit?: (claimData: any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CLAIM_TYPES = [
|
|
||||||
'Marketing Activity',
|
|
||||||
'Promotional Event',
|
|
||||||
'Dealer Training',
|
|
||||||
'Infrastructure Development',
|
|
||||||
'Customer Experience Initiative',
|
|
||||||
'Service Campaign'
|
|
||||||
];
|
|
||||||
|
|
||||||
const STEP_NAMES = [
|
|
||||||
'Claim Details',
|
|
||||||
'Review & Submit'
|
|
||||||
];
|
|
||||||
|
|
||||||
export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizardProps) {
|
|
||||||
const [currentStep, setCurrentStep] = useState(1);
|
|
||||||
const [dealers, setDealers] = useState<DealerInfo[]>([]);
|
|
||||||
const [loadingDealers, setLoadingDealers] = useState(true);
|
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
activityName: '',
|
|
||||||
activityType: '',
|
|
||||||
dealerCode: '',
|
|
||||||
dealerName: '',
|
|
||||||
dealerEmail: '',
|
|
||||||
dealerPhone: '',
|
|
||||||
dealerAddress: '',
|
|
||||||
activityDate: undefined as Date | undefined,
|
|
||||||
location: '',
|
|
||||||
requestDescription: '',
|
|
||||||
periodStartDate: undefined as Date | undefined,
|
|
||||||
periodEndDate: undefined as Date | undefined,
|
|
||||||
estimatedBudget: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalSteps = STEP_NAMES.length;
|
|
||||||
|
|
||||||
// Fetch dealers from API on component mount
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchDealers = async () => {
|
|
||||||
setLoadingDealers(true);
|
|
||||||
try {
|
|
||||||
const fetchedDealers = await fetchDealersFromAPI();
|
|
||||||
setDealers(fetchedDealers);
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('Failed to load dealer list.');
|
|
||||||
console.error('Error fetching dealers:', error);
|
|
||||||
} finally {
|
|
||||||
setLoadingDealers(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchDealers();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const updateFormData = (field: string, value: any) => {
|
|
||||||
setFormData(prev => ({ ...prev, [field]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const isStepValid = () => {
|
|
||||||
switch (currentStep) {
|
|
||||||
case 1:
|
|
||||||
return formData.activityName &&
|
|
||||||
formData.activityType &&
|
|
||||||
formData.dealerCode &&
|
|
||||||
formData.dealerName &&
|
|
||||||
formData.activityDate &&
|
|
||||||
formData.location &&
|
|
||||||
formData.requestDescription;
|
|
||||||
case 2:
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const nextStep = () => {
|
|
||||||
if (currentStep < totalSteps && isStepValid()) {
|
|
||||||
setCurrentStep(currentStep + 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const prevStep = () => {
|
|
||||||
if (currentStep > 1) {
|
|
||||||
setCurrentStep(currentStep - 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDealerChange = async (dealerCode: string) => {
|
|
||||||
const selectedDealer = dealers.find(d => d.dealerCode === dealerCode);
|
|
||||||
if (selectedDealer) {
|
|
||||||
updateFormData('dealerCode', dealerCode);
|
|
||||||
updateFormData('dealerName', selectedDealer.dealerName);
|
|
||||||
updateFormData('dealerEmail', selectedDealer.email || '');
|
|
||||||
updateFormData('dealerPhone', selectedDealer.phone || '');
|
|
||||||
updateFormData('dealerAddress', ''); // Address not available in API response
|
|
||||||
|
|
||||||
// Try to fetch full dealer info from API if available
|
|
||||||
try {
|
|
||||||
const fullDealerInfo = await getDealerByCode(dealerCode);
|
|
||||||
if (fullDealerInfo) {
|
|
||||||
updateFormData('dealerEmail', fullDealerInfo.email || selectedDealer.email || '');
|
|
||||||
updateFormData('dealerPhone', fullDealerInfo.phone || selectedDealer.phone || '');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore error, use basic info from list
|
|
||||||
console.debug('Could not fetch full dealer info:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
const claimData = {
|
|
||||||
...formData,
|
|
||||||
templateType: 'claim-management',
|
|
||||||
submittedAt: new Date().toISOString(),
|
|
||||||
status: 'pending',
|
|
||||||
currentStep: 'initiator-review',
|
|
||||||
workflowSteps: [
|
|
||||||
{
|
|
||||||
step: 1,
|
|
||||||
name: 'Initiator Evaluation',
|
|
||||||
status: 'pending',
|
|
||||||
approver: 'Current User (Initiator)',
|
|
||||||
description: 'Review and confirm all claim details and documents'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 2,
|
|
||||||
name: 'IO Confirmation',
|
|
||||||
status: 'waiting',
|
|
||||||
approver: 'System',
|
|
||||||
description: 'Automatic IO generation upon initiator approval'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 3,
|
|
||||||
name: 'Department Lead Approval',
|
|
||||||
status: 'waiting',
|
|
||||||
approver: 'Department Lead',
|
|
||||||
description: 'Budget blocking and final approval'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 4,
|
|
||||||
name: 'Document Submission',
|
|
||||||
status: 'waiting',
|
|
||||||
approver: 'Dealer',
|
|
||||||
description: 'Dealer submits completion documents'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 5,
|
|
||||||
name: 'Document Verification',
|
|
||||||
status: 'waiting',
|
|
||||||
approver: 'Initiator',
|
|
||||||
description: 'Verify completion documents'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 6,
|
|
||||||
name: 'E-Invoice Generation',
|
|
||||||
status: 'waiting',
|
|
||||||
approver: 'System',
|
|
||||||
description: 'Auto-generate e-invoice based on approved amount'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 7,
|
|
||||||
name: 'Credit Note Issuance',
|
|
||||||
status: 'waiting',
|
|
||||||
approver: 'Finance',
|
|
||||||
description: 'Issue credit note to dealer'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
toast.success('Claim Request Created', {
|
|
||||||
description: 'Your claim management request has been submitted successfully.'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (onSubmit) {
|
|
||||||
onSubmit(claimData);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderStepContent = () => {
|
|
||||||
switch (currentStep) {
|
|
||||||
case 1:
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, y: -20 }}
|
|
||||||
className="space-y-6"
|
|
||||||
>
|
|
||||||
<div className="text-center mb-8">
|
|
||||||
<div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
|
||||||
<Receipt className="w-8 h-8 text-white" />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Claim Details</h2>
|
|
||||||
<p className="text-gray-600">
|
|
||||||
Provide comprehensive information about your claim request
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="max-w-3xl mx-auto space-y-6">
|
|
||||||
{/* Activity Name and Type */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="activityName" className="text-base font-semibold">Activity Name *</Label>
|
|
||||||
<Input
|
|
||||||
id="activityName"
|
|
||||||
placeholder="e.g., Himalayan Adventure Fest 2024"
|
|
||||||
value={formData.activityName}
|
|
||||||
onChange={(e) => updateFormData('activityName', e.target.value)}
|
|
||||||
className="mt-2 h-12"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="activityType" className="text-base font-semibold">Activity Type *</Label>
|
|
||||||
<Select value={formData.activityType} onValueChange={(value) => updateFormData('activityType', value)}>
|
|
||||||
<SelectTrigger className="mt-2 h-12">
|
|
||||||
<SelectValue placeholder="Select activity type" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{CLAIM_TYPES.map((type) => (
|
|
||||||
<SelectItem key={type} value={type}>{type}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Dealer Selection */}
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="dealer" className="text-base font-semibold">Dealer Code / Dealer Name *</Label>
|
|
||||||
<Select value={formData.dealerCode} onValueChange={handleDealerChange} disabled={loadingDealers}>
|
|
||||||
<SelectTrigger className="mt-2 h-12">
|
|
||||||
<SelectValue placeholder={loadingDealers ? "Loading dealers..." : "Select dealer"}>
|
|
||||||
{formData.dealerCode && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="font-mono text-sm">{formData.dealerCode}</span>
|
|
||||||
<span className="text-gray-400">•</span>
|
|
||||||
<span>{formData.dealerName}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</SelectValue>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{dealers.length === 0 && !loadingDealers ? (
|
|
||||||
<div className="p-2 text-sm text-gray-500">No dealers available</div>
|
|
||||||
) : (
|
|
||||||
dealers
|
|
||||||
.filter((dealer) => dealer.dealerCode && dealer.dealerCode.trim() !== '')
|
|
||||||
.map((dealer) => (
|
|
||||||
<SelectItem key={dealer.dealerCode} value={dealer.dealerCode}>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="font-mono text-sm font-semibold">{dealer.dealerCode}</span>
|
|
||||||
<span className="text-gray-400">•</span>
|
|
||||||
<span>{dealer.dealerName}</span>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
{formData.dealerCode && (
|
|
||||||
<p className="text-sm text-gray-600 mt-2">
|
|
||||||
Selected: <span className="font-semibold">{formData.dealerName}</span> ({formData.dealerCode})
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Date and Location */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<Label className="text-base font-semibold">Date *</Label>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="w-full justify-start text-left mt-2 h-12"
|
|
||||||
>
|
|
||||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
||||||
{formData.activityDate ? format(formData.activityDate, 'PPP') : 'Select date'}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-auto p-0" align="start">
|
|
||||||
<Calendar
|
|
||||||
mode="single"
|
|
||||||
selected={formData.activityDate}
|
|
||||||
onSelect={(date) => updateFormData('activityDate', date)}
|
|
||||||
initialFocus
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="location" className="text-base font-semibold">Location *</Label>
|
|
||||||
<Input
|
|
||||||
id="location"
|
|
||||||
placeholder="e.g., Mumbai, Maharashtra"
|
|
||||||
value={formData.location}
|
|
||||||
onChange={(e) => updateFormData('location', e.target.value)}
|
|
||||||
className="mt-2 h-12"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Request Detail */}
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="requestDescription" className="text-base font-semibold">Request in Detail - Brief Requirement *</Label>
|
|
||||||
<Textarea
|
|
||||||
id="requestDescription"
|
|
||||||
placeholder="Provide a detailed description of your claim requirement..."
|
|
||||||
value={formData.requestDescription}
|
|
||||||
onChange={(e) => updateFormData('requestDescription', e.target.value)}
|
|
||||||
className="mt-2 min-h-[120px]"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
|
||||||
Include key details about the claim, objectives, and expected outcomes
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Period (Optional) */}
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-2 mb-3">
|
|
||||||
<Label className="text-base font-semibold">Period (If Any)</Label>
|
|
||||||
<Badge variant="secondary" className="text-xs">Optional</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<Label className="text-sm text-gray-600">Start Date</Label>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="w-full justify-start text-left mt-2 h-12"
|
|
||||||
>
|
|
||||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
||||||
{formData.periodStartDate ? format(formData.periodStartDate, 'PPP') : 'Start date'}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-auto p-0" align="start">
|
|
||||||
<Calendar
|
|
||||||
mode="single"
|
|
||||||
selected={formData.periodStartDate}
|
|
||||||
onSelect={(date) => updateFormData('periodStartDate', date)}
|
|
||||||
initialFocus
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label className="text-sm text-gray-600">End Date</Label>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="w-full justify-start text-left mt-2 h-12"
|
|
||||||
>
|
|
||||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
||||||
{formData.periodEndDate ? format(formData.periodEndDate, 'PPP') : 'End date'}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-auto p-0" align="start">
|
|
||||||
<Calendar
|
|
||||||
mode="single"
|
|
||||||
selected={formData.periodEndDate}
|
|
||||||
onSelect={(date) => updateFormData('periodEndDate', date)}
|
|
||||||
initialFocus
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{(formData.periodStartDate || formData.periodEndDate) && (
|
|
||||||
<p className="text-xs text-gray-600 mt-2">
|
|
||||||
{formData.periodStartDate && formData.periodEndDate
|
|
||||||
? `Period: ${format(formData.periodStartDate, 'MMM dd, yyyy')} - ${format(formData.periodEndDate, 'MMM dd, yyyy')}`
|
|
||||||
: 'Please select both start and end dates for the period'}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 2:
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, y: -20 }}
|
|
||||||
className="space-y-6"
|
|
||||||
>
|
|
||||||
<div className="text-center mb-8">
|
|
||||||
<div className="w-16 h-16 bg-gradient-to-br from-green-500 to-emerald-600 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
|
||||||
<CheckCircle className="w-8 h-8 text-white" />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Review & Submit</h2>
|
|
||||||
<p className="text-gray-600">
|
|
||||||
Review your claim details before submission
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="max-w-3xl mx-auto space-y-6">
|
|
||||||
{/* Activity Information */}
|
|
||||||
<Card className="border-2">
|
|
||||||
<CardHeader className="bg-gradient-to-br from-blue-50 to-indigo-50">
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<Receipt className="w-5 h-5 text-blue-600" />
|
|
||||||
Activity Information
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="pt-6 space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs text-gray-600 uppercase tracking-wider">Activity Name</Label>
|
|
||||||
<p className="font-semibold text-gray-900 mt-1">{formData.activityName}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs text-gray-600 uppercase tracking-wider">Activity Type</Label>
|
|
||||||
<Badge variant="secondary" className="mt-1">{formData.activityType}</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Dealer Information */}
|
|
||||||
<Card className="border-2">
|
|
||||||
<CardHeader className="bg-gradient-to-br from-green-50 to-emerald-50">
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<Building className="w-5 h-5 text-green-600" />
|
|
||||||
Dealer Information
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="pt-6 space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs text-gray-600 uppercase tracking-wider">Dealer Code</Label>
|
|
||||||
<p className="font-semibold text-gray-900 mt-1 font-mono">{formData.dealerCode}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs text-gray-600 uppercase tracking-wider">Dealer Name</Label>
|
|
||||||
<p className="font-semibold text-gray-900 mt-1">{formData.dealerName}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs text-gray-600 uppercase tracking-wider">Email</Label>
|
|
||||||
<p className="text-gray-900 mt-1">{formData.dealerEmail}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs text-gray-600 uppercase tracking-wider">Phone</Label>
|
|
||||||
<p className="text-gray-900 mt-1">{formData.dealerPhone}</p>
|
|
||||||
</div>
|
|
||||||
{formData.dealerAddress && (
|
|
||||||
<div className="col-span-2">
|
|
||||||
<Label className="text-xs text-gray-600 uppercase tracking-wider">Address</Label>
|
|
||||||
<p className="text-gray-900 mt-1">{formData.dealerAddress}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Date & Location */}
|
|
||||||
<Card className="border-2">
|
|
||||||
<CardHeader className="bg-gradient-to-br from-purple-50 to-pink-50">
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<CalendarIcon className="w-5 h-5 text-purple-600" />
|
|
||||||
Date & Location
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="pt-6 space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs text-gray-600 uppercase tracking-wider">Date</Label>
|
|
||||||
<p className="font-semibold text-gray-900 mt-1">
|
|
||||||
{formData.activityDate ? format(formData.activityDate, 'PPP') : 'N/A'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs text-gray-600 uppercase tracking-wider">Location</Label>
|
|
||||||
<div className="flex items-center gap-2 mt-1">
|
|
||||||
<MapPin className="w-4 h-4 text-gray-500" />
|
|
||||||
<p className="font-semibold text-gray-900">{formData.location}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{formData.estimatedBudget && (
|
|
||||||
<div className="col-span-2">
|
|
||||||
<Label className="text-xs text-gray-600 uppercase tracking-wider">Estimated Budget</Label>
|
|
||||||
<p className="text-xl font-bold text-blue-900 mt-1">{formData.estimatedBudget}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Request Details */}
|
|
||||||
<Card className="border-2">
|
|
||||||
<CardHeader className="bg-gradient-to-br from-orange-50 to-amber-50">
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<FileText className="w-5 h-5 text-orange-600" />
|
|
||||||
Request Details
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="pt-6 space-y-4">
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs text-gray-600 uppercase tracking-wider">Brief Requirement</Label>
|
|
||||||
<div className="mt-2 p-4 bg-gray-50 rounded-lg border">
|
|
||||||
<p className="text-gray-900 whitespace-pre-wrap">{formData.requestDescription}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Period (if provided) */}
|
|
||||||
{(formData.periodStartDate || formData.periodEndDate) && (
|
|
||||||
<Card className="border-2">
|
|
||||||
<CardHeader className="bg-gradient-to-br from-cyan-50 to-blue-50">
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<Clock className="w-5 h-5 text-cyan-600" />
|
|
||||||
Period
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="pt-6 space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs text-gray-600 uppercase tracking-wider">Start Date</Label>
|
|
||||||
<p className="font-semibold text-gray-900 mt-1">
|
|
||||||
{formData.periodStartDate ? format(formData.periodStartDate, 'PPP') : 'Not specified'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs text-gray-600 uppercase tracking-wider">End Date</Label>
|
|
||||||
<p className="font-semibold text-gray-900 mt-1">
|
|
||||||
{formData.periodEndDate ? format(formData.periodEndDate, 'PPP') : 'Not specified'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Confirmation Message */}
|
|
||||||
<div className="bg-blue-50 border-2 border-blue-200 rounded-lg p-6">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<Info className="w-6 h-6 text-blue-600 flex-shrink-0 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<p className="font-semibold text-blue-900 mb-1">Ready to Submit</p>
|
|
||||||
<p className="text-sm text-blue-800">
|
|
||||||
Please review all the information above. Once submitted, your claim request will enter the approval workflow.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full bg-gradient-to-br from-gray-50 to-gray-100 py-4 sm:py-6 lg:py-8 px-3 sm:px-4 lg:px-6 overflow-y-auto">
|
|
||||||
<div className="max-w-6xl mx-auto pb-8">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-6 sm:mb-8">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={onBack}
|
|
||||||
className="mb-3 sm:mb-4 gap-2 text-sm sm:text-base"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-3 h-3 sm:w-4 sm:h-4" />
|
|
||||||
<span className="hidden sm:inline">Back to Templates</span>
|
|
||||||
<span className="sm:hidden">Back</span>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-0">
|
|
||||||
<div>
|
|
||||||
<Badge variant="secondary" className="mb-2 text-xs">Claim Management Template</Badge>
|
|
||||||
<h1 className="text-xl sm:text-2xl lg:text-3xl font-bold text-gray-900">New Claim Request</h1>
|
|
||||||
<p className="text-sm sm:text-base text-gray-600 mt-1">
|
|
||||||
Step {currentStep} of {totalSteps}: <span className="hidden sm:inline">{STEP_NAMES[currentStep - 1]}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progress Bar */}
|
|
||||||
<div className="mt-4 sm:mt-6">
|
|
||||||
<Progress value={(currentStep / totalSteps) * 100} className="h-2" />
|
|
||||||
<div className="flex justify-between mt-2 px-1">
|
|
||||||
{STEP_NAMES.map((_name, index) => (
|
|
||||||
<span
|
|
||||||
key={index}
|
|
||||||
className={`text-xs sm:text-sm ${
|
|
||||||
index + 1 <= currentStep ? 'text-blue-600 font-medium' : 'text-gray-400'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{index + 1}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Step Content */}
|
|
||||||
<Card className="mb-6 sm:mb-8">
|
|
||||||
<CardContent className="p-4 sm:p-6 lg:p-8">
|
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
{renderStepContent()}
|
|
||||||
</AnimatePresence>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Navigation */}
|
|
||||||
<div className="flex flex-col sm:flex-row justify-between gap-3 sm:gap-0 pb-4 sm:pb-0">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={prevStep}
|
|
||||||
disabled={currentStep === 1}
|
|
||||||
className="gap-2 w-full sm:w-auto order-2 sm:order-1"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-4 h-4" />
|
|
||||||
Previous
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{currentStep < totalSteps ? (
|
|
||||||
<Button
|
|
||||||
onClick={nextStep}
|
|
||||||
disabled={!isStepValid()}
|
|
||||||
className="gap-2 w-full sm:w-auto order-1 sm:order-2"
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
<ArrowRight className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={!isStepValid()}
|
|
||||||
className="gap-2 bg-green-600 hover:bg-green-700 w-full sm:w-auto order-1 sm:order-2"
|
|
||||||
>
|
|
||||||
<Check className="w-4 h-4" />
|
|
||||||
Submit Claim Request
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export { ClaimManagementWizard } from './ClaimManagementWizard';
|
|
||||||
@ -1,9 +1,679 @@
|
|||||||
/**
|
import { useState, useEffect } from 'react';
|
||||||
* Dealer Claim Request Creation Wizard
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
*
|
import { Button } from '@/components/ui/button';
|
||||||
* This component handles the creation of dealer claim requests.
|
import { Input } from '@/components/ui/input';
|
||||||
* Located in: src/dealer-claim/components/request-creation/
|
import { Label } from '@/components/ui/label';
|
||||||
*/
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { Calendar } from '@/components/ui/calendar';
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
ArrowLeft,
|
||||||
|
ArrowRight,
|
||||||
|
Calendar as CalendarIcon,
|
||||||
|
Check,
|
||||||
|
Receipt,
|
||||||
|
Building,
|
||||||
|
MapPin,
|
||||||
|
Clock,
|
||||||
|
CheckCircle,
|
||||||
|
Info,
|
||||||
|
FileText,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { getAllDealers as fetchDealersFromAPI, getDealerByCode, type DealerInfo } from '@/services/dealerApi';
|
||||||
|
|
||||||
// Re-export the original component
|
interface ClaimManagementWizardProps {
|
||||||
export { ClaimManagementWizard } from '@/components/workflow/ClaimManagementWizard';
|
onBack?: () => void;
|
||||||
|
onSubmit?: (claimData: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CLAIM_TYPES = [
|
||||||
|
'Marketing Activity',
|
||||||
|
'Promotional Event',
|
||||||
|
'Dealer Training',
|
||||||
|
'Infrastructure Development',
|
||||||
|
'Customer Experience Initiative',
|
||||||
|
'Service Campaign'
|
||||||
|
];
|
||||||
|
|
||||||
|
const STEP_NAMES = [
|
||||||
|
'Claim Details',
|
||||||
|
'Review & Submit'
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizardProps) {
|
||||||
|
const [currentStep, setCurrentStep] = useState(1);
|
||||||
|
const [dealers, setDealers] = useState<DealerInfo[]>([]);
|
||||||
|
const [loadingDealers, setLoadingDealers] = useState(true);
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
activityName: '',
|
||||||
|
activityType: '',
|
||||||
|
dealerCode: '',
|
||||||
|
dealerName: '',
|
||||||
|
dealerEmail: '',
|
||||||
|
dealerPhone: '',
|
||||||
|
dealerAddress: '',
|
||||||
|
activityDate: undefined as Date | undefined,
|
||||||
|
location: '',
|
||||||
|
requestDescription: '',
|
||||||
|
periodStartDate: undefined as Date | undefined,
|
||||||
|
periodEndDate: undefined as Date | undefined,
|
||||||
|
estimatedBudget: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalSteps = STEP_NAMES.length;
|
||||||
|
|
||||||
|
// Fetch dealers from API on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchDealers = async () => {
|
||||||
|
setLoadingDealers(true);
|
||||||
|
try {
|
||||||
|
const fetchedDealers = await fetchDealersFromAPI();
|
||||||
|
setDealers(fetchedDealers);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to load dealer list.');
|
||||||
|
console.error('Error fetching dealers:', error);
|
||||||
|
} finally {
|
||||||
|
setLoadingDealers(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchDealers();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateFormData = (field: string, value: any) => {
|
||||||
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const isStepValid = () => {
|
||||||
|
switch (currentStep) {
|
||||||
|
case 1:
|
||||||
|
return formData.activityName &&
|
||||||
|
formData.activityType &&
|
||||||
|
formData.dealerCode &&
|
||||||
|
formData.dealerName &&
|
||||||
|
formData.activityDate &&
|
||||||
|
formData.location &&
|
||||||
|
formData.requestDescription;
|
||||||
|
case 2:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextStep = () => {
|
||||||
|
if (currentStep < totalSteps && isStepValid()) {
|
||||||
|
setCurrentStep(currentStep + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevStep = () => {
|
||||||
|
if (currentStep > 1) {
|
||||||
|
setCurrentStep(currentStep - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDealerChange = async (dealerCode: string) => {
|
||||||
|
const selectedDealer = dealers.find(d => d.dealerCode === dealerCode);
|
||||||
|
if (selectedDealer) {
|
||||||
|
updateFormData('dealerCode', dealerCode);
|
||||||
|
updateFormData('dealerName', selectedDealer.dealerName);
|
||||||
|
updateFormData('dealerEmail', selectedDealer.email || '');
|
||||||
|
updateFormData('dealerPhone', selectedDealer.phone || '');
|
||||||
|
updateFormData('dealerAddress', ''); // Address not available in API response
|
||||||
|
|
||||||
|
// Try to fetch full dealer info from API
|
||||||
|
try {
|
||||||
|
const fullDealerInfo = await getDealerByCode(dealerCode);
|
||||||
|
if (fullDealerInfo) {
|
||||||
|
updateFormData('dealerEmail', fullDealerInfo.email || selectedDealer.email || '');
|
||||||
|
updateFormData('dealerPhone', fullDealerInfo.phone || selectedDealer.phone || '');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore error, use basic info from list
|
||||||
|
console.debug('Could not fetch full dealer info:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
const claimData = {
|
||||||
|
...formData,
|
||||||
|
templateType: 'claim-management',
|
||||||
|
submittedAt: new Date().toISOString(),
|
||||||
|
status: 'pending',
|
||||||
|
currentStep: 'initiator-review',
|
||||||
|
workflowSteps: [
|
||||||
|
{
|
||||||
|
step: 1,
|
||||||
|
name: 'Initiator Evaluation',
|
||||||
|
status: 'pending',
|
||||||
|
approver: 'Current User (Initiator)',
|
||||||
|
description: 'Review and confirm all claim details and documents'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: 2,
|
||||||
|
name: 'IO Confirmation',
|
||||||
|
status: 'waiting',
|
||||||
|
approver: 'System',
|
||||||
|
description: 'Automatic IO generation upon initiator approval'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: 3,
|
||||||
|
name: 'Department Lead Approval',
|
||||||
|
status: 'waiting',
|
||||||
|
approver: 'Department Lead',
|
||||||
|
description: 'Budget blocking and final approval'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: 4,
|
||||||
|
name: 'Document Submission',
|
||||||
|
status: 'waiting',
|
||||||
|
approver: 'Dealer',
|
||||||
|
description: 'Dealer submits completion documents'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: 5,
|
||||||
|
name: 'Document Verification',
|
||||||
|
status: 'waiting',
|
||||||
|
approver: 'Initiator',
|
||||||
|
description: 'Verify completion documents'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: 6,
|
||||||
|
name: 'E-Invoice Generation',
|
||||||
|
status: 'waiting',
|
||||||
|
approver: 'System',
|
||||||
|
description: 'Auto-generate e-invoice based on approved amount'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: 7,
|
||||||
|
name: 'Credit Note Issuance',
|
||||||
|
status: 'waiting',
|
||||||
|
approver: 'Finance',
|
||||||
|
description: 'Issue credit note to dealer'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Don't show toast here - let the parent component handle success/error after API call
|
||||||
|
if (onSubmit) {
|
||||||
|
onSubmit(claimData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderStepContent = () => {
|
||||||
|
switch (currentStep) {
|
||||||
|
case 1:
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||||
|
<Receipt className="w-8 h-8 text-white" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">Claim Details</h2>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Provide comprehensive information about your claim request
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-3xl mx-auto space-y-6">
|
||||||
|
{/* Activity Name and Type */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="activityName" className="text-base font-semibold">Activity Name *</Label>
|
||||||
|
<Input
|
||||||
|
id="activityName"
|
||||||
|
placeholder="e.g., Himalayan Adventure Fest 2024"
|
||||||
|
value={formData.activityName}
|
||||||
|
onChange={(e) => updateFormData('activityName', e.target.value)}
|
||||||
|
className="mt-2 h-12"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="activityType" className="text-base font-semibold">Activity Type *</Label>
|
||||||
|
<Select value={formData.activityType} onValueChange={(value) => updateFormData('activityType', value)}>
|
||||||
|
<SelectTrigger className="mt-2 !h-12 data-[size=default]:!h-12" id="activityType">
|
||||||
|
<SelectValue placeholder="Select activity type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{CLAIM_TYPES.map((type) => (
|
||||||
|
<SelectItem key={type} value={type}>{type}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dealer Selection */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-base font-semibold">Dealer Code / Dealer Name *</Label>
|
||||||
|
<Select value={formData.dealerCode} onValueChange={handleDealerChange} disabled={loadingDealers}>
|
||||||
|
<SelectTrigger className="mt-2 !h-12 data-[size=default]:!h-12" id="dealer-select">
|
||||||
|
<SelectValue placeholder={loadingDealers ? "Loading dealers..." : "Select dealer"}>
|
||||||
|
{formData.dealerCode && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-mono text-sm">{formData.dealerCode}</span>
|
||||||
|
<span className="text-gray-400">•</span>
|
||||||
|
<span>{formData.dealerName}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{dealers.length === 0 && !loadingDealers ? (
|
||||||
|
<div className="p-2 text-sm text-gray-500">No dealers available</div>
|
||||||
|
) : (
|
||||||
|
dealers.map((dealer) => (
|
||||||
|
<SelectItem key={dealer.userId} value={dealer.dealerCode}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-mono text-sm font-semibold">{dealer.dealerCode}</span>
|
||||||
|
<span className="text-gray-400">•</span>
|
||||||
|
<span>{dealer.dealerName}</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{formData.dealerCode && (
|
||||||
|
<p className="text-sm text-gray-600 mt-2">
|
||||||
|
Selected: <span className="font-semibold">{formData.dealerName}</span> ({formData.dealerCode})
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Date and Location */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<Label className="text-base font-semibold">Date *</Label>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-start text-left mt-2 h-12"
|
||||||
|
>
|
||||||
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
|
{formData.activityDate ? format(formData.activityDate, 'PPP') : 'Select date'}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={formData.activityDate}
|
||||||
|
onSelect={(date) => updateFormData('activityDate', date)}
|
||||||
|
initialFocus
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="location" className="text-base font-semibold">Location *</Label>
|
||||||
|
<Input
|
||||||
|
id="location"
|
||||||
|
placeholder="e.g., Mumbai, Maharashtra"
|
||||||
|
value={formData.location}
|
||||||
|
onChange={(e) => updateFormData('location', e.target.value)}
|
||||||
|
className="mt-2 h-12"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Request Detail */}
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="requestDescription" className="text-base font-semibold">Request in Detail - Brief Requirement *</Label>
|
||||||
|
<Textarea
|
||||||
|
id="requestDescription"
|
||||||
|
placeholder="Provide a detailed description of your claim requirement..."
|
||||||
|
value={formData.requestDescription}
|
||||||
|
onChange={(e) => updateFormData('requestDescription', e.target.value)}
|
||||||
|
className="mt-2 min-h-[120px]"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Include key details about the claim, objectives, and expected outcomes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Period (Optional) */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<Label className="text-base font-semibold">Period (If Any)</Label>
|
||||||
|
<Badge variant="secondary" className="text-xs">Optional</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm text-gray-600">Start Date</Label>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-start text-left mt-2 h-12"
|
||||||
|
>
|
||||||
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
|
{formData.periodStartDate ? format(formData.periodStartDate, 'PPP') : 'Start date'}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={formData.periodStartDate}
|
||||||
|
onSelect={(date) => updateFormData('periodStartDate', date)}
|
||||||
|
initialFocus
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm text-gray-600">End Date</Label>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-start text-left mt-2 h-12"
|
||||||
|
>
|
||||||
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
|
{formData.periodEndDate ? format(formData.periodEndDate, 'PPP') : 'End date'}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={formData.periodEndDate}
|
||||||
|
onSelect={(date) => updateFormData('periodEndDate', date)}
|
||||||
|
initialFocus
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{(formData.periodStartDate || formData.periodEndDate) && (
|
||||||
|
<p className="text-xs text-gray-600 mt-2">
|
||||||
|
{formData.periodStartDate && formData.periodEndDate
|
||||||
|
? `Period: ${format(formData.periodStartDate, 'MMM dd, yyyy')} - ${format(formData.periodEndDate, 'MMM dd, yyyy')}`
|
||||||
|
: 'Please select both start and end dates for the period'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<div className="w-16 h-16 bg-gradient-to-br from-green-500 to-emerald-600 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||||
|
<CheckCircle className="w-8 h-8 text-white" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">Review & Submit</h2>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Review your claim details before submission
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-3xl mx-auto space-y-6">
|
||||||
|
{/* Activity Information */}
|
||||||
|
<Card className="border-2">
|
||||||
|
<CardHeader className="bg-gradient-to-br from-blue-50 to-indigo-50">
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Receipt className="w-5 h-5 text-blue-600" />
|
||||||
|
Activity Information
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pt-6 space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs text-gray-600 uppercase tracking-wider">Activity Name</Label>
|
||||||
|
<p className="font-semibold text-gray-900 mt-1">{formData.activityName}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs text-gray-600 uppercase tracking-wider">Activity Type</Label>
|
||||||
|
<Badge variant="secondary" className="mt-1">{formData.activityType}</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Dealer Information */}
|
||||||
|
<Card className="border-2">
|
||||||
|
<CardHeader className="bg-gradient-to-br from-green-50 to-emerald-50">
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Building className="w-5 h-5 text-green-600" />
|
||||||
|
Dealer Information
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pt-6 space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs text-gray-600 uppercase tracking-wider">Dealer Code</Label>
|
||||||
|
<p className="font-semibold text-gray-900 mt-1 font-mono">{formData.dealerCode}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs text-gray-600 uppercase tracking-wider">Dealer Name</Label>
|
||||||
|
<p className="font-semibold text-gray-900 mt-1">{formData.dealerName}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs text-gray-600 uppercase tracking-wider">Email</Label>
|
||||||
|
<p className="text-gray-900 mt-1">{formData.dealerEmail}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs text-gray-600 uppercase tracking-wider">Phone</Label>
|
||||||
|
<p className="text-gray-900 mt-1">{formData.dealerPhone}</p>
|
||||||
|
</div>
|
||||||
|
{formData.dealerAddress && (
|
||||||
|
<div className="col-span-2">
|
||||||
|
<Label className="text-xs text-gray-600 uppercase tracking-wider">Address</Label>
|
||||||
|
<p className="text-gray-900 mt-1">{formData.dealerAddress}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Date & Location */}
|
||||||
|
<Card className="border-2">
|
||||||
|
<CardHeader className="bg-gradient-to-br from-purple-50 to-pink-50">
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<CalendarIcon className="w-5 h-5 text-purple-600" />
|
||||||
|
Date & Location
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pt-6 space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs text-gray-600 uppercase tracking-wider">Date</Label>
|
||||||
|
<p className="font-semibold text-gray-900 mt-1">
|
||||||
|
{formData.activityDate ? format(formData.activityDate, 'PPP') : 'N/A'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs text-gray-600 uppercase tracking-wider">Location</Label>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<MapPin className="w-4 h-4 text-gray-500" />
|
||||||
|
<p className="font-semibold text-gray-900">{formData.location}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{formData.estimatedBudget && (
|
||||||
|
<div className="col-span-2">
|
||||||
|
<Label className="text-xs text-gray-600 uppercase tracking-wider">Estimated Budget</Label>
|
||||||
|
<p className="text-xl font-bold text-blue-900 mt-1">{formData.estimatedBudget}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Request Details */}
|
||||||
|
<Card className="border-2">
|
||||||
|
<CardHeader className="bg-gradient-to-br from-orange-50 to-amber-50">
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<FileText className="w-5 h-5 text-orange-600" />
|
||||||
|
Request Details
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pt-6 space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs text-gray-600 uppercase tracking-wider">Brief Requirement</Label>
|
||||||
|
<div className="mt-2 p-4 bg-gray-50 rounded-lg border">
|
||||||
|
<p className="text-gray-900 whitespace-pre-wrap">{formData.requestDescription}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Period (if provided) */}
|
||||||
|
{(formData.periodStartDate || formData.periodEndDate) && (
|
||||||
|
<Card className="border-2">
|
||||||
|
<CardHeader className="bg-gradient-to-br from-cyan-50 to-blue-50">
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Clock className="w-5 h-5 text-cyan-600" />
|
||||||
|
Period
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pt-6 space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs text-gray-600 uppercase tracking-wider">Start Date</Label>
|
||||||
|
<p className="font-semibold text-gray-900 mt-1">
|
||||||
|
{formData.periodStartDate ? format(formData.periodStartDate, 'PPP') : 'Not specified'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs text-gray-600 uppercase tracking-wider">End Date</Label>
|
||||||
|
<p className="font-semibold text-gray-900 mt-1">
|
||||||
|
{formData.periodEndDate ? format(formData.periodEndDate, 'PPP') : 'Not specified'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Confirmation Message */}
|
||||||
|
<div className="bg-blue-50 border-2 border-blue-200 rounded-lg p-6">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Info className="w-6 h-6 text-blue-600 flex-shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-blue-900 mb-1">Ready to Submit</p>
|
||||||
|
<p className="text-sm text-blue-800">
|
||||||
|
Please review all the information above. Once submitted, your claim request will enter the approval workflow.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full bg-gradient-to-br from-gray-50 to-gray-100 py-4 sm:py-6 lg:py-8 px-3 sm:px-4 lg:px-6 overflow-y-auto">
|
||||||
|
<div className="max-w-6xl mx-auto pb-8">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-6 sm:mb-8">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={onBack}
|
||||||
|
className="mb-3 sm:mb-4 gap-2 text-sm sm:text-base"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-3 h-3 sm:w-4 sm:h-4" />
|
||||||
|
<span className="hidden sm:inline">Back to Templates</span>
|
||||||
|
<span className="sm:hidden">Back</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-0">
|
||||||
|
<div>
|
||||||
|
<Badge variant="secondary" className="mb-2 text-xs">Claim Management Template</Badge>
|
||||||
|
<h1 className="text-xl sm:text-2xl lg:text-3xl font-bold text-gray-900">New Claim Request</h1>
|
||||||
|
<p className="text-sm sm:text-base text-gray-600 mt-1">
|
||||||
|
Step {currentStep} of {totalSteps}: <span className="hidden sm:inline">{STEP_NAMES[currentStep - 1]}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress Bar */}
|
||||||
|
<div className="mt-4 sm:mt-6">
|
||||||
|
<Progress value={(currentStep / totalSteps) * 100} className="h-2" />
|
||||||
|
<div className="flex justify-between mt-2 px-1">
|
||||||
|
{STEP_NAMES.map((_name, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className={`text-xs sm:text-sm ${
|
||||||
|
index + 1 <= currentStep ? 'text-blue-600 font-medium' : 'text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step Content */}
|
||||||
|
<Card className="mb-6 sm:mb-8">
|
||||||
|
<CardContent className="p-4 sm:p-6 lg:p-8">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{renderStepContent()}
|
||||||
|
</AnimatePresence>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<div className="flex flex-col sm:flex-row justify-between gap-3 sm:gap-0 pb-4 sm:pb-0">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={prevStep}
|
||||||
|
disabled={currentStep === 1}
|
||||||
|
className="gap-2 w-full sm:w-auto order-2 sm:order-1"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4" />
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{currentStep < totalSteps ? (
|
||||||
|
<Button
|
||||||
|
onClick={nextStep}
|
||||||
|
disabled={!isStepValid()}
|
||||||
|
className="gap-2 w-full sm:w-auto order-1 sm:order-2"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
<ArrowRight className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!isStepValid()}
|
||||||
|
className="gap-2 bg-green-600 hover:bg-green-700 w-full sm:w-auto order-1 sm:order-2"
|
||||||
|
>
|
||||||
|
<Check className="w-4 h-4" />
|
||||||
|
Submit Claim Request
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -5,5 +5,389 @@
|
|||||||
* Located in: src/dealer-claim/components/request-detail/
|
* Located in: src/dealer-claim/components/request-detail/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Re-export the original component
|
import { useState, useEffect } from 'react';
|
||||||
export { IOTab } from '@/pages/RequestDetail/components/tabs/IOTab';
|
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<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 && 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{/* IO Budget Management Card */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<DollarSign className="w-5 h-5 text-[#2d4a3e]" />
|
||||||
|
IO Budget Management
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Enter IO number to fetch available budget from SAP
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
{/* IO Number Input */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label htmlFor="ioNumber">IO Number *</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
id="ioNumber"
|
||||||
|
placeholder="Enter IO number (e.g., IO-2024-12345)"
|
||||||
|
value={ioNumber}
|
||||||
|
onChange={(e) => setIoNumber(e.target.value)}
|
||||||
|
disabled={fetchingAmount || !!blockedDetails}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={handleFetchAmount}
|
||||||
|
disabled={!ioNumber.trim() || fetchingAmount || !!blockedDetails}
|
||||||
|
className="bg-[#2d4a3e] hover:bg-[#1f3329]"
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4 mr-2" />
|
||||||
|
{fetchingAmount ? 'Fetching...' : 'Fetch Amount'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Fetched Amount Display */}
|
||||||
|
{fetchedAmount !== null && !blockedDetails && (
|
||||||
|
<>
|
||||||
|
<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-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="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>
|
||||||
|
|
||||||
|
{/* 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 || !amountToBlock || parseFloat(amountToBlock) <= 0 || parseFloat(amountToBlock) > fetchedAmount}
|
||||||
|
className="w-full bg-[#2d4a3e] hover:bg-[#1f3329]"
|
||||||
|
>
|
||||||
|
<Target className="w-4 h-4 mr-2" />
|
||||||
|
{blockingBudget ? 'Blocking in SAP...' : 'Block IO in SAP'}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* IO Blocked Details Card */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<CircleCheckBig className="w-5 h-5 text-green-600" />
|
||||||
|
IO Blocked Details
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Details of IO blocked in SAP system
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{blockedDetails ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Success Banner */}
|
||||||
|
<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-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="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="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="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="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="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="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>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<DollarSign className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||||
|
<p className="text-sm text-gray-500 mb-2">No IO blocked yet</p>
|
||||||
|
<p className="text-xs text-gray-400">
|
||||||
|
Enter IO number and fetch amount to block budget
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -5,5 +5,117 @@
|
|||||||
* Located in: src/dealer-claim/components/request-detail/
|
* Located in: src/dealer-claim/components/request-detail/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Re-export the original component
|
import {
|
||||||
export { ClaimManagementOverviewTab as DealerClaimOverviewTab } from '@/pages/RequestDetail/components/tabs/ClaimManagementOverviewTab';
|
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 (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
<p>This is not a claim management request.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
<p>Unable to load claim management data.</p>
|
||||||
|
<p className="text-xs mt-2">Please ensure the request has been properly initialized.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<div className={`space-y-6 ${className}`}>
|
||||||
|
{/* Activity Information - Always visible */}
|
||||||
|
<ActivityInformationCard activityInfo={claimRequest.activityInfo} />
|
||||||
|
|
||||||
|
{/* Dealer Information - Always visible */}
|
||||||
|
<DealerInformationCard dealerInfo={claimRequest.dealerInfo} />
|
||||||
|
|
||||||
|
{/* Proposal Details - Only shown after dealer submits proposal */}
|
||||||
|
{visibility.showProposalDetails && claimRequest.proposalDetails && (
|
||||||
|
<ProposalDetailsCard proposalDetails={claimRequest.proposalDetails} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Request Initiator */}
|
||||||
|
<RequestInitiatorCard initiatorInfo={initiatorInfo} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export as DealerClaimOverviewTab for consistency
|
||||||
|
export { ClaimManagementOverviewTab as DealerClaimOverviewTab };
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Calendar, MapPin, DollarSign, Receipt } from 'lucide-react';
|
import { Calendar, MapPin, DollarSign, Receipt } from 'lucide-react';
|
||||||
import { ClaimActivityInfo } from '../../types/claimManagement.types';
|
import { ClaimActivityInfo } from '@/pages/RequestDetail/types/claimManagement.types';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
interface ActivityInformationCardProps {
|
interface ActivityInformationCardProps {
|
||||||
@ -173,5 +173,3 @@ export function ActivityInformationCard({ activityInfo, className }: ActivityInf
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Building, Mail, Phone, MapPin } from 'lucide-react';
|
import { Building, Mail, Phone, MapPin } from 'lucide-react';
|
||||||
import { DealerInfo } from '../../types/claimManagement.types';
|
import { DealerInfo } from '@/pages/RequestDetail/types/claimManagement.types';
|
||||||
|
|
||||||
interface DealerInformationCardProps {
|
interface DealerInformationCardProps {
|
||||||
dealerInfo: DealerInfo;
|
dealerInfo: DealerInfo;
|
||||||
@ -96,5 +96,3 @@ export function DealerInformationCard({ dealerInfo, className }: DealerInformati
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -289,5 +289,3 @@ export function ProcessDetailsCard({
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -157,5 +157,3 @@ export function ProposalDetailsCard({ proposalDetails, className }: ProposalDeta
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -73,5 +73,3 @@ export function RequestInitiatorCard({ initiatorInfo, className }: RequestInitia
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -5,8 +5,8 @@
|
|||||||
* Located in: src/dealer-claim/components/request-detail/claim-cards/
|
* Located in: src/dealer-claim/components/request-detail/claim-cards/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { ActivityInformationCard } from '@/pages/RequestDetail/components/claim-cards/ActivityInformationCard';
|
export { ActivityInformationCard } from './ActivityInformationCard';
|
||||||
export { DealerInformationCard } from '@/pages/RequestDetail/components/claim-cards/DealerInformationCard';
|
export { DealerInformationCard } from './DealerInformationCard';
|
||||||
export { ProcessDetailsCard } from '@/pages/RequestDetail/components/claim-cards/ProcessDetailsCard';
|
export { ProcessDetailsCard } from './ProcessDetailsCard';
|
||||||
export { ProposalDetailsCard } from '@/pages/RequestDetail/components/claim-cards/ProposalDetailsCard';
|
export { ProposalDetailsCard } from './ProposalDetailsCard';
|
||||||
export { RequestInitiatorCard } from '@/pages/RequestDetail/components/claim-cards/RequestInitiatorCard';
|
export { RequestInitiatorCard } from './RequestInitiatorCard';
|
||||||
|
|||||||
@ -267,4 +267,3 @@ export function CreditNoteSAPModal({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -0,0 +1,376 @@
|
|||||||
|
/**
|
||||||
|
* DMSPushModal Component
|
||||||
|
* Modal for Step 6: Push to DMS Verification
|
||||||
|
* Allows user to verify completion details and expenses before pushing to DMS
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import {
|
||||||
|
Receipt,
|
||||||
|
DollarSign,
|
||||||
|
TriangleAlert,
|
||||||
|
Activity,
|
||||||
|
CheckCircle2,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface ExpenseItem {
|
||||||
|
description: string;
|
||||||
|
amount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CompletionDetails {
|
||||||
|
activityCompletionDate?: string;
|
||||||
|
numberOfParticipants?: number;
|
||||||
|
closedExpenses?: ExpenseItem[];
|
||||||
|
totalClosedExpenses?: number;
|
||||||
|
completionDescription?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IODetails {
|
||||||
|
ioNumber?: string;
|
||||||
|
blockedAmount?: number;
|
||||||
|
availableBalance?: number;
|
||||||
|
remainingBalance?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DMSPushModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onPush: (comments: string) => Promise<void>;
|
||||||
|
completionDetails?: CompletionDetails | null;
|
||||||
|
ioDetails?: IODetails | null;
|
||||||
|
requestTitle?: string;
|
||||||
|
requestNumber?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DMSPushModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onPush,
|
||||||
|
completionDetails,
|
||||||
|
ioDetails,
|
||||||
|
requestTitle,
|
||||||
|
requestNumber,
|
||||||
|
}: DMSPushModalProps) {
|
||||||
|
const [comments, setComments] = useState('');
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const commentsChars = comments.length;
|
||||||
|
const maxCommentsChars = 500;
|
||||||
|
|
||||||
|
// Calculate total closed expenses
|
||||||
|
const totalClosedExpenses = useMemo(() => {
|
||||||
|
if (completionDetails?.totalClosedExpenses) {
|
||||||
|
return completionDetails.totalClosedExpenses;
|
||||||
|
}
|
||||||
|
if (completionDetails?.closedExpenses && Array.isArray(completionDetails.closedExpenses)) {
|
||||||
|
return completionDetails.closedExpenses.reduce((sum, item) => {
|
||||||
|
const amount = typeof item === 'object' ? (item.amount || 0) : 0;
|
||||||
|
return sum + (Number(amount) || 0);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}, [completionDetails]);
|
||||||
|
|
||||||
|
// Format date
|
||||||
|
const formatDate = (dateString?: string) => {
|
||||||
|
if (!dateString) return '—';
|
||||||
|
try {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString('en-IN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format currency
|
||||||
|
const formatCurrency = (amount: number) => {
|
||||||
|
return `₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!comments.trim()) {
|
||||||
|
toast.error('Please provide comments before pushing to DMS');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSubmitting(true);
|
||||||
|
await onPush(comments.trim());
|
||||||
|
handleReset();
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to push to DMS:', error);
|
||||||
|
toast.error('Failed to push to DMS. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setComments('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (!submitting) {
|
||||||
|
handleReset();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||||
|
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<div className="p-2 rounded-lg bg-indigo-100">
|
||||||
|
<Activity className="w-6 h-6 text-indigo-600" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<DialogTitle className="font-semibold text-xl">
|
||||||
|
Push to DMS - Verification
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-sm mt-1">
|
||||||
|
Review completion details and expenses before pushing to DMS for e-invoice generation
|
||||||
|
</DialogDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Request Info Card */}
|
||||||
|
<div className="space-y-3 p-4 bg-gray-50 rounded-lg border">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="font-medium text-gray-900">Workflow Step:</span>
|
||||||
|
<Badge variant="outline" className="font-mono">Step 6</Badge>
|
||||||
|
</div>
|
||||||
|
{requestNumber && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-900">Request Number:</span>
|
||||||
|
<p className="text-gray-700 mt-1 font-mono">{requestNumber}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-900">Title:</span>
|
||||||
|
<p className="text-gray-700 mt-1">{requestTitle || '—'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium text-gray-900">Action:</span>
|
||||||
|
<Badge className="bg-indigo-100 text-indigo-800 border-indigo-200">
|
||||||
|
<Activity className="w-3 h-3 mr-1" />
|
||||||
|
PUSH TO DMS
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Completion Details Card */}
|
||||||
|
{completionDetails && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
|
<CheckCircle2 className="w-5 h-5 text-green-600" />
|
||||||
|
Completion Details
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Review activity completion information
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
{completionDetails.activityCompletionDate && (
|
||||||
|
<div className="flex items-center justify-between py-2 border-b">
|
||||||
|
<span className="text-sm text-gray-600">Activity Completion Date:</span>
|
||||||
|
<span className="text-sm font-semibold text-gray-900">
|
||||||
|
{formatDate(completionDetails.activityCompletionDate)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{completionDetails.numberOfParticipants !== undefined && (
|
||||||
|
<div className="flex items-center justify-between py-2 border-b">
|
||||||
|
<span className="text-sm text-gray-600">Number of Participants:</span>
|
||||||
|
<span className="text-sm font-semibold text-gray-900">
|
||||||
|
{completionDetails.numberOfParticipants}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{completionDetails.completionDescription && (
|
||||||
|
<div className="pt-2">
|
||||||
|
<p className="text-xs text-gray-600 mb-1">Completion Description:</p>
|
||||||
|
<p className="text-sm text-gray-900">
|
||||||
|
{completionDetails.completionDescription}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Expense Breakdown Card */}
|
||||||
|
{completionDetails?.closedExpenses && completionDetails.closedExpenses.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
|
<DollarSign className="w-5 h-5 text-blue-600" />
|
||||||
|
Expense Breakdown
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Review closed expenses before pushing to DMS
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{completionDetails.closedExpenses.map((expense, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center justify-between py-2 px-3 bg-gray-50 rounded border"
|
||||||
|
>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm font-medium text-gray-900">
|
||||||
|
{expense.description || `Expense ${index + 1}`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<p className="text-sm font-semibold text-gray-900">
|
||||||
|
{formatCurrency(typeof expense === 'object' ? (expense.amount || 0) : 0)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="flex items-center justify-between py-3 px-3 bg-blue-50 rounded border-2 border-blue-200 mt-3">
|
||||||
|
<span className="text-sm font-semibold text-gray-900">Total Closed Expenses:</span>
|
||||||
|
<span className="text-lg font-bold text-blue-700">
|
||||||
|
{formatCurrency(totalClosedExpenses)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* IO Details Card */}
|
||||||
|
{ioDetails && ioDetails.ioNumber && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
|
<Receipt className="w-5 h-5 text-purple-600" />
|
||||||
|
IO Details
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Internal Order information for budget reference
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<div className="flex items-center 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 font-mono">
|
||||||
|
{ioDetails.ioNumber}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{ioDetails.blockedAmount !== undefined && ioDetails.blockedAmount > 0 && (
|
||||||
|
<div className="flex items-center justify-between py-2 border-b">
|
||||||
|
<span className="text-sm text-gray-600">Blocked Amount:</span>
|
||||||
|
<span className="text-sm font-bold text-green-700">
|
||||||
|
{formatCurrency(ioDetails.blockedAmount)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{ioDetails.remainingBalance !== undefined && (
|
||||||
|
<div className="flex items-center justify-between py-2">
|
||||||
|
<span className="text-sm text-gray-600">Remaining Balance:</span>
|
||||||
|
<span className="text-sm font-semibold text-gray-900">
|
||||||
|
{formatCurrency(ioDetails.remainingBalance)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Verification Warning */}
|
||||||
|
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<TriangleAlert className="w-5 h-5 text-yellow-600 flex-shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold text-yellow-900">
|
||||||
|
Please verify all details before pushing to DMS
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-yellow-700 mt-1">
|
||||||
|
Once pushed, the system will automatically generate an e-invoice and the workflow will proceed to Step 7.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Comments & Remarks */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="comment" className="text-sm font-semibold text-gray-900 flex items-center gap-2">
|
||||||
|
Comments & Remarks <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="comment"
|
||||||
|
placeholder="Enter your comments about pushing to DMS (e.g., verified expenses, ready for invoice generation)..."
|
||||||
|
value={comments}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (value.length <= maxCommentsChars) {
|
||||||
|
setComments(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
rows={4}
|
||||||
|
className="text-sm min-h-[80px] resize-none"
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-between text-xs text-gray-500">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<TriangleAlert className="w-3 h-3" />
|
||||||
|
Required and visible to all
|
||||||
|
</div>
|
||||||
|
<span>{commentsChars}/{maxCommentsChars}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="flex flex-col-reverse sm:flex-row sm:justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleClose}
|
||||||
|
disabled={submitting}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!comments.trim() || submitting}
|
||||||
|
className="bg-indigo-600 hover:bg-indigo-700 text-white"
|
||||||
|
>
|
||||||
|
{submitting ? (
|
||||||
|
'Pushing to DMS...'
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Activity className="w-4 h-4 mr-2" />
|
||||||
|
Push to DMS
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@ -613,4 +613,3 @@ export function DealerCompletionDocumentsModal({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -483,4 +483,3 @@ export function DealerProposalSubmissionModal({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
|
import * as React from 'react';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -32,6 +33,9 @@ interface DeptLeadIOApprovalModalProps {
|
|||||||
onReject: (comments: string) => Promise<void>;
|
onReject: (comments: string) => Promise<void>;
|
||||||
requestTitle?: string;
|
requestTitle?: string;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
|
// Pre-filled IO data from IO tab
|
||||||
|
preFilledIONumber?: string;
|
||||||
|
preFilledBlockedAmount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DeptLeadIOApprovalModal({
|
export function DeptLeadIOApprovalModal({
|
||||||
@ -41,13 +45,22 @@ export function DeptLeadIOApprovalModal({
|
|||||||
onReject,
|
onReject,
|
||||||
requestTitle,
|
requestTitle,
|
||||||
requestId: _requestId,
|
requestId: _requestId,
|
||||||
|
preFilledIONumber,
|
||||||
|
preFilledBlockedAmount,
|
||||||
}: DeptLeadIOApprovalModalProps) {
|
}: DeptLeadIOApprovalModalProps) {
|
||||||
const [actionType, setActionType] = useState<'approve' | 'reject'>('approve');
|
const [actionType, setActionType] = useState<'approve' | 'reject'>('approve');
|
||||||
const [ioNumber, setIoNumber] = useState('');
|
const [ioNumber, setIoNumber] = useState(preFilledIONumber || '');
|
||||||
const [ioRemark, setIoRemark] = useState('');
|
const [ioRemark, setIoRemark] = useState('');
|
||||||
const [comments, setComments] = useState('');
|
const [comments, setComments] = useState('');
|
||||||
const [submitting, setSubmitting] = useState(false);
|
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 ioRemarkChars = ioRemark.length;
|
||||||
const commentsChars = comments.length;
|
const commentsChars = comments.length;
|
||||||
const maxIoRemarkChars = 300;
|
const maxIoRemarkChars = 300;
|
||||||
@ -110,7 +123,7 @@ export function DeptLeadIOApprovalModal({
|
|||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
setActionType('approve');
|
setActionType('approve');
|
||||||
setIoNumber('');
|
setIoNumber(preFilledIONumber || '');
|
||||||
setIoRemark('');
|
setIoRemark('');
|
||||||
setComments('');
|
setComments('');
|
||||||
};
|
};
|
||||||
@ -211,8 +224,26 @@ export function DeptLeadIOApprovalModal({
|
|||||||
onChange={(e) => setIoNumber(e.target.value)}
|
onChange={(e) => setIoNumber(e.target.value)}
|
||||||
className="bg-white h-8"
|
className="bg-white h-8"
|
||||||
/>
|
/>
|
||||||
|
{preFilledIONumber && (
|
||||||
|
<p className="text-xs text-blue-600 mt-1">
|
||||||
|
Pre-filled from IO tab
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Blocked Amount Display (if available) */}
|
||||||
|
{preFilledBlockedAmount !== undefined && preFilledBlockedAmount > 0 && (
|
||||||
|
<div className="p-2 bg-green-50 border border-green-200 rounded">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs font-semibold text-gray-700">Blocked Amount:</span>
|
||||||
|
<span className="text-sm font-bold text-green-700">
|
||||||
|
₹{preFilledBlockedAmount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-600 mt-1">Amount already blocked in SAP from IO tab</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* IO Remark */}
|
{/* IO Remark */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="ioRemark" className="text-sm font-semibold text-gray-900 flex items-center gap-2">
|
<Label htmlFor="ioRemark" className="text-sm font-semibold text-gray-900 flex items-center gap-2">
|
||||||
@ -301,4 +332,3 @@ export function DeptLeadIOApprovalModal({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,5 +192,3 @@ export function EditClaimAmountModal({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -145,4 +145,3 @@ This is an automated message.`;
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -440,4 +440,3 @@ export function InitiatorProposalApprovalModal({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5,10 +5,11 @@
|
|||||||
* Located in: src/dealer-claim/components/request-detail/modals/
|
* Located in: src/dealer-claim/components/request-detail/modals/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { CreditNoteSAPModal } from '@/pages/RequestDetail/components/modals/CreditNoteSAPModal';
|
export { CreditNoteSAPModal } from './CreditNoteSAPModal';
|
||||||
export { DealerCompletionDocumentsModal } from '@/pages/RequestDetail/components/modals/DealerCompletionDocumentsModal';
|
export { DealerCompletionDocumentsModal } from './DealerCompletionDocumentsModal';
|
||||||
export { DealerProposalSubmissionModal } from '@/pages/RequestDetail/components/modals/DealerProposalSubmissionModal';
|
export { DealerProposalSubmissionModal } from './DealerProposalSubmissionModal';
|
||||||
export { DeptLeadIOApprovalModal } from '@/pages/RequestDetail/components/modals/DeptLeadIOApprovalModal';
|
export { DeptLeadIOApprovalModal } from './DeptLeadIOApprovalModal';
|
||||||
export { EditClaimAmountModal } from '@/pages/RequestDetail/components/modals/EditClaimAmountModal';
|
export { DMSPushModal } from './DMSPushModal';
|
||||||
export { EmailNotificationTemplateModal } from '@/pages/RequestDetail/components/modals/EmailNotificationTemplateModal';
|
export { EditClaimAmountModal } from './EditClaimAmountModal';
|
||||||
export { InitiatorProposalApprovalModal } from '@/pages/RequestDetail/components/modals/InitiatorProposalApprovalModal';
|
export { EmailNotificationTemplateModal } from './EmailNotificationTemplateModal';
|
||||||
|
export { InitiatorProposalApprovalModal } from './InitiatorProposalApprovalModal';
|
||||||
|
|||||||
@ -139,30 +139,43 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Determine if user is department lead (whoever is in step 3 / approval level 3)
|
// Determine if user is department lead (whoever is in step 3 / approval level 3)
|
||||||
const approvalLevels = apiRequest?.approvalLevels || [];
|
// Use approvalFlow (transformed) or approvals (raw) - both have step/levelNumber
|
||||||
const step3Level = approvalLevels.find((level: any) =>
|
const approvalFlow = apiRequest?.approvalFlow || [];
|
||||||
|
const approvals = apiRequest?.approvals || [];
|
||||||
|
|
||||||
|
// Try to find Step 3 from approvalFlow first (has 'step' property), then from approvals (has 'levelNumber')
|
||||||
|
const step3Level = approvalFlow.find((level: any) =>
|
||||||
|
(level.step || level.levelNumber || level.level_number) === 3
|
||||||
|
) || approvals.find((level: any) =>
|
||||||
(level.levelNumber || level.level_number) === 3
|
(level.levelNumber || level.level_number) === 3
|
||||||
);
|
);
|
||||||
const deptLeadUserId = step3Level?.approverId || step3Level?.approver?.userId;
|
|
||||||
const deptLeadEmail = (step3Level?.approverEmail || step3Level?.approver?.email || step3Level?.approverEmail || '').toLowerCase().trim();
|
|
||||||
|
|
||||||
|
const deptLeadUserId = step3Level?.approverId || step3Level?.approver_id || step3Level?.approver?.userId;
|
||||||
|
const deptLeadEmail = (step3Level?.approverEmail || step3Level?.approver_email || step3Level?.approver?.email || '').toLowerCase().trim();
|
||||||
|
|
||||||
|
// User is department lead if they match the Step 3 approver (regardless of status)
|
||||||
const isDeptLead = (deptLeadUserId && deptLeadUserId === currentUserId) ||
|
const isDeptLead = (deptLeadUserId && deptLeadUserId === currentUserId) ||
|
||||||
(deptLeadEmail && currentUserEmail && deptLeadEmail === currentUserEmail);
|
(deptLeadEmail && currentUserEmail && deptLeadEmail === currentUserEmail);
|
||||||
|
|
||||||
const step3Status = step3Level?.status ? String(step3Level.status).toUpperCase() : '';
|
const step3Status = step3Level?.status ? String(step3Level.status).toUpperCase() : '';
|
||||||
const isStep3PendingOrInProgress = step3Status === 'PENDING' ||
|
const isStep3PendingOrInProgress = step3Status === 'PENDING' ||
|
||||||
step3Status === 'IN_PROGRESS';
|
step3Status === 'IN_PROGRESS' ||
|
||||||
|
step3Status === 'PAUSED';
|
||||||
|
|
||||||
const currentLevel = apiRequest?.currentLevel || apiRequest?.current_level || 0;
|
const currentLevel = apiRequest?.currentLevel || apiRequest?.current_level || apiRequest?.currentStep || 0;
|
||||||
const isStep3CurrentLevel = currentLevel === 3;
|
const isStep3CurrentLevel = currentLevel === 3;
|
||||||
|
|
||||||
|
// User is current approver for Step 3 if Step 3 is active and they are the approver
|
||||||
const isStep3CurrentApprover = step3Level && isStep3PendingOrInProgress && isStep3CurrentLevel && (
|
const isStep3CurrentApprover = step3Level && isStep3PendingOrInProgress && isStep3CurrentLevel && (
|
||||||
(deptLeadUserId && deptLeadUserId === currentUserId) ||
|
(deptLeadUserId && deptLeadUserId === currentUserId) ||
|
||||||
(deptLeadEmail && currentUserEmail && deptLeadEmail === currentUserEmail)
|
(deptLeadEmail && currentUserEmail && deptLeadEmail === currentUserEmail)
|
||||||
);
|
);
|
||||||
|
|
||||||
// IO tab visibility for dealer claims
|
// IO tab visibility for dealer claims
|
||||||
const showIOTab = isUserInitiator || isDeptLead || isStep3CurrentApprover;
|
// Show IO tab if user is initiator, department lead (Step 3 approver), or current Step 3 approver
|
||||||
|
// Also show if Step 3 has been approved (to view IO details)
|
||||||
|
const isStep3Approved = step3Status === 'APPROVED';
|
||||||
|
const showIOTab = isUserInitiator || isDeptLead || isStep3CurrentApprover || isStep3Approved;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mergedMessages,
|
mergedMessages,
|
||||||
|
|||||||
@ -558,6 +558,19 @@ export function useRequestDetails(
|
|||||||
proposalDetails = claimData.proposalDetails || claimData.proposal_details;
|
proposalDetails = claimData.proposalDetails || claimData.proposal_details;
|
||||||
completionDetails = claimData.completionDetails || claimData.completion_details;
|
completionDetails = claimData.completionDetails || claimData.completion_details;
|
||||||
internalOrder = claimData.internalOrder || claimData.internal_order || null;
|
internalOrder = claimData.internalOrder || claimData.internal_order || null;
|
||||||
|
// New normalized tables
|
||||||
|
const budgetTracking = claimData.budgetTracking || claimData.budget_tracking || null;
|
||||||
|
const invoice = claimData.invoice || null;
|
||||||
|
const creditNote = claimData.creditNote || claimData.credit_note || null;
|
||||||
|
const completionExpenses = claimData.completionExpenses || claimData.completion_expenses || null;
|
||||||
|
|
||||||
|
// Store new fields in claimDetails for backward compatibility and easy access
|
||||||
|
if (claimDetails) {
|
||||||
|
(claimDetails as any).budgetTracking = budgetTracking;
|
||||||
|
(claimDetails as any).invoice = invoice;
|
||||||
|
(claimDetails as any).creditNote = creditNote;
|
||||||
|
(claimDetails as any).completionExpenses = completionExpenses;
|
||||||
|
}
|
||||||
|
|
||||||
console.debug('[useRequestDetails] Initial load - Extracted details:', {
|
console.debug('[useRequestDetails] Initial load - Extracted details:', {
|
||||||
hasClaimDetails: !!claimDetails,
|
hasClaimDetails: !!claimDetails,
|
||||||
@ -565,6 +578,10 @@ export function useRequestDetails(
|
|||||||
hasProposalDetails: !!proposalDetails,
|
hasProposalDetails: !!proposalDetails,
|
||||||
hasCompletionDetails: !!completionDetails,
|
hasCompletionDetails: !!completionDetails,
|
||||||
hasInternalOrder: !!internalOrder,
|
hasInternalOrder: !!internalOrder,
|
||||||
|
hasBudgetTracking: !!budgetTracking,
|
||||||
|
hasInvoice: !!invoice,
|
||||||
|
hasCreditNote: !!creditNote,
|
||||||
|
hasCompletionExpenses: Array.isArray(completionExpenses) && completionExpenses.length > 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -614,6 +631,11 @@ export function useRequestDetails(
|
|||||||
proposalDetails: proposalDetails || null,
|
proposalDetails: proposalDetails || null,
|
||||||
completionDetails: completionDetails || null,
|
completionDetails: completionDetails || null,
|
||||||
internalOrder: internalOrder || null,
|
internalOrder: internalOrder || null,
|
||||||
|
// New normalized tables (also available via claimDetails for backward compatibility)
|
||||||
|
budgetTracking: (claimDetails as any)?.budgetTracking || null,
|
||||||
|
invoice: (claimDetails as any)?.invoice || null,
|
||||||
|
creditNote: (claimDetails as any)?.creditNote || null,
|
||||||
|
completionExpenses: (claimDetails as any)?.completionExpenses || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
setApiRequest(mapped);
|
setApiRequest(mapped);
|
||||||
|
|||||||
@ -61,6 +61,32 @@ export function ClosedRequestCard({ request, onViewRequest }: ClosedRequestCardP
|
|||||||
>
|
>
|
||||||
{request.priority}
|
{request.priority}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{/* 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 (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={`${templateColor} text-xs px-2.5 py-0.5 shrink-0 hidden md:inline-flex`}
|
||||||
|
data-testid="template-type-badge"
|
||||||
|
>
|
||||||
|
{templateLabel}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
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 { motion } from 'framer-motion';
|
||||||
import { MyRequest } from '../types/myRequests.types';
|
import { MyRequest } from '../types/myRequests.types';
|
||||||
import { getPriorityConfig, getStatusConfig } from '../utils/configMappers';
|
import { getPriorityConfig, getStatusConfig } from '../utils/configMappers';
|
||||||
@ -97,16 +97,32 @@ export function RequestCard({ request, index, onViewRequest }: RequestCardProps)
|
|||||||
<PriorityIcon className="w-3 h-3 mr-1" />
|
<PriorityIcon className="w-3 h-3 mr-1" />
|
||||||
{request.priority}
|
{request.priority}
|
||||||
</Badge>
|
</Badge>
|
||||||
{request.templateType && (
|
{/* Template Type Badge */}
|
||||||
<Badge
|
{(() => {
|
||||||
variant="secondary"
|
const templateType = request?.templateType || (request as any)?.template_type || '';
|
||||||
className="bg-purple-100 text-purple-700 text-xs shrink-0 hidden sm:inline-flex"
|
const templateTypeUpper = templateType?.toUpperCase() || '';
|
||||||
data-testid="template-badge"
|
|
||||||
>
|
// Direct mapping from templateType
|
||||||
<FileText className="w-3 h-3 mr-1" />
|
let templateLabel = 'Custom';
|
||||||
Template: {request.templateName}
|
let templateColor = 'bg-purple-100 !text-purple-600 border-purple-200';
|
||||||
</Badge>
|
|
||||||
)}
|
if (templateTypeUpper === 'DEALER CLAIM') {
|
||||||
|
templateLabel = 'Claim Management';
|
||||||
|
templateColor = 'bg-blue-100 !text-blue-700 border-blue-200';
|
||||||
|
} else if (templateTypeUpper === 'TEMPLATE') {
|
||||||
|
templateLabel = 'Template';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={`${templateColor} font-medium text-xs shrink-0`}
|
||||||
|
data-testid="template-type-badge"
|
||||||
|
>
|
||||||
|
{templateLabel}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs sm:text-sm text-gray-600 mb-2 sm:mb-3 line-clamp-2 leading-relaxed" data-testid="request-description">
|
<p className="text-xs sm:text-sm text-gray-600 mb-2 sm:mb-3 line-clamp-2 leading-relaxed" data-testid="request-description">
|
||||||
{stripHtmlTags(request.description || '') || 'No description provided'}
|
{stripHtmlTags(request.description || '') || 'No description provided'}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export interface MyRequest {
|
|||||||
currentApprover?: string;
|
currentApprover?: string;
|
||||||
approverLevel?: string;
|
approverLevel?: string;
|
||||||
templateType?: string;
|
templateType?: string;
|
||||||
|
workflowType?: string;
|
||||||
templateName?: string;
|
templateName?: string;
|
||||||
pauseInfo?: {
|
pauseInfo?: {
|
||||||
isPaused: boolean;
|
isPaused: boolean;
|
||||||
|
|||||||
@ -29,8 +29,9 @@ export function transformRequest(req: any): MyRequest {
|
|||||||
: req.currentStep && req.totalSteps
|
: req.currentStep && req.totalSteps
|
||||||
? `${req.currentStep} of ${req.totalSteps}`
|
? `${req.currentStep} of ${req.totalSteps}`
|
||||||
: '—',
|
: '—',
|
||||||
templateType: req.templateType,
|
templateType: req.templateType || req.template_type,
|
||||||
templateName: req.templateName,
|
workflowType: req.workflowType || req.workflow_type,
|
||||||
|
templateName: req.templateName || req.template_name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,7 @@ interface Request {
|
|||||||
currentLevelSLA?: any; // Backend-provided SLA for current level
|
currentLevelSLA?: any; // Backend-provided SLA for current level
|
||||||
isPaused?: boolean; // Pause status
|
isPaused?: boolean; // Pause status
|
||||||
pauseInfo?: any; // Pause details
|
pauseInfo?: any; // Pause details
|
||||||
|
templateType?: string; // Template type for badge display
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OpenRequestsProps {
|
interface OpenRequestsProps {
|
||||||
@ -178,6 +179,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
|
|||||||
approvalStep: r.currentLevel ? `Step ${r.currentLevel} of ${r.totalLevels || '?'}` : undefined,
|
approvalStep: r.currentLevel ? `Step ${r.currentLevel} of ${r.totalLevels || '?'}` : undefined,
|
||||||
department: r.department,
|
department: r.department,
|
||||||
currentLevelSLA: r.currentLevelSLA, // ← Backend-calculated current level SLA
|
currentLevelSLA: r.currentLevelSLA, // ← Backend-calculated current level SLA
|
||||||
|
templateType: r.templateType || r.template_type, // ← Template type for badge display
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
setItems(mapped);
|
setItems(mapped);
|
||||||
@ -458,6 +460,32 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
|
|||||||
>
|
>
|
||||||
{request.priority}
|
{request.priority}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{/* 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 (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={`${templateColor} text-xs px-2.5 py-0.5 shrink-0 hidden md:inline-flex`}
|
||||||
|
data-testid="template-type-badge"
|
||||||
|
>
|
||||||
|
{templateLabel}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { UserPlus, Eye, CheckCircle, XCircle, Share2, Pause, Play, AlertCircle }
|
|||||||
import { getSharedRecipients, type SharedRecipient } from '@/services/summaryApi';
|
import { getSharedRecipients, type SharedRecipient } from '@/services/summaryApi';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import notificationApi, { type Notification } from '@/services/notificationApi';
|
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 { isClaimManagementRequest } from '@/utils/claimRequestUtils';
|
||||||
import { determineUserRole, getRoleBasedVisibility, mapToClaimManagementRequest } from '@/utils/claimDataMapper';
|
import { determineUserRole, getRoleBasedVisibility, mapToClaimManagementRequest } from '@/utils/claimDataMapper';
|
||||||
|
|
||||||
|
|||||||
@ -50,7 +50,7 @@ export function RequestDetailHeader({ request, refreshing, onBack, onRefresh, on
|
|||||||
<h1 className="text-sm sm:text-base md:text-lg font-bold text-gray-900 truncate" data-testid="request-id">
|
<h1 className="text-sm sm:text-base md:text-lg font-bold text-gray-900 truncate" data-testid="request-id">
|
||||||
{request.id || 'N/A'}
|
{request.id || 'N/A'}
|
||||||
</h1>
|
</h1>
|
||||||
{/* Priority and Status Badges */}
|
{/* Priority, Status, and Template Type Badges */}
|
||||||
<div className="flex flex-wrap items-center gap-1.5 sm:gap-2">
|
<div className="flex flex-wrap items-center gap-1.5 sm:gap-2">
|
||||||
<Badge
|
<Badge
|
||||||
className={`${priorityConfig.color} rounded-full px-2 sm:px-3 text-xs capitalize shrink-0`}
|
className={`${priorityConfig.color} rounded-full px-2 sm:px-3 text-xs capitalize shrink-0`}
|
||||||
@ -66,6 +66,26 @@ export function RequestDetailHeader({ request, refreshing, onBack, onRefresh, on
|
|||||||
>
|
>
|
||||||
{statusConfig.label}
|
{statusConfig.label}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{/* 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 (
|
||||||
|
<Badge
|
||||||
|
className={`${templateColor} rounded-full px-2 sm:px-3 text-xs shrink-0`}
|
||||||
|
variant="outline"
|
||||||
|
data-testid="template-type-badge"
|
||||||
|
>
|
||||||
|
{templateLabel}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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';
|
|
||||||
|
|
||||||
|
|
||||||
@ -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<void>; // Optional: Custom download handler
|
|
||||||
onSendToDealer?: () => Promise<void>; // 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 && (
|
|
||||||
<Button
|
|
||||||
className="bg-green-600 hover:bg-green-700"
|
|
||||||
onClick={() => setShowCreditNoteModal(true)}
|
|
||||||
>
|
|
||||||
<Receipt className="w-4 h-4 mr-2" />
|
|
||||||
View Credit Note
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Add Modal Component
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
<CreditNoteSAPModal
|
|
||||||
isOpen={showCreditNoteModal}
|
|
||||||
onClose={() => 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
|
|
||||||
|
|
||||||
@ -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 (
|
|
||||||
<div className="text-center py-8 text-gray-500">
|
|
||||||
<p>This is not a claim management request.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 (
|
|
||||||
<div className="text-center py-8 text-gray-500">
|
|
||||||
<p>Unable to load claim management data.</p>
|
|
||||||
<p className="text-xs mt-2">Please ensure the request has been properly initialized.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 (
|
|
||||||
<div className={`space-y-6 ${className}`}>
|
|
||||||
{/* Activity Information - Always visible */}
|
|
||||||
<ActivityInformationCard activityInfo={claimRequest.activityInfo} />
|
|
||||||
|
|
||||||
{/* Dealer Information - Always visible */}
|
|
||||||
<DealerInformationCard dealerInfo={claimRequest.dealerInfo} />
|
|
||||||
|
|
||||||
{/* Proposal Details - Only shown after dealer submits proposal */}
|
|
||||||
{visibility.showProposalDetails && claimRequest.proposalDetails && (
|
|
||||||
<ProposalDetailsCard proposalDetails={claimRequest.proposalDetails} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Request Initiator */}
|
|
||||||
<RequestInitiatorCard initiatorInfo={initiatorInfo} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<any>;
|
|
||||||
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 (
|
|
||||||
<ClaimManagementOverviewTab
|
|
||||||
request={request}
|
|
||||||
apiRequest={apiRequest}
|
|
||||||
currentUserId={currentUserId}
|
|
||||||
isInitiator={isInitiator}
|
|
||||||
onEditClaimAmount={onEditClaimAmount}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render regular overview if provided
|
|
||||||
if (RegularOverview) {
|
|
||||||
return <RegularOverview {...regularOverviewProps} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback
|
|
||||||
return (
|
|
||||||
<div className="text-center py-8 text-gray-500">
|
|
||||||
<p>No overview available for this request type.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@ -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 <CircleCheckBig className="w-5 h-5 text-green-600" />;
|
|
||||||
case 'in_progress':
|
|
||||||
return <Clock className="w-5 h-5 text-blue-600" />;
|
|
||||||
case 'rejected':
|
|
||||||
return <AlertCircle className="w-5 h-5 text-red-600" />;
|
|
||||||
case 'pending':
|
|
||||||
return <Clock className="w-5 h-5 text-gray-400" />;
|
|
||||||
default:
|
|
||||||
return <Clock className="w-5 h-5 text-gray-400" />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<Card className={className}>
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<CardTitle className="leading-none flex items-center gap-2">
|
|
||||||
<TrendingUp className="w-5 h-5 text-purple-600" />
|
|
||||||
Claim Management Workflow
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription className="mt-2">
|
|
||||||
8-Step approval process for dealer claim management
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
<Badge variant="outline" className="font-medium">
|
|
||||||
Step {currentStep} of {totalSteps}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{steps.map((step) => (
|
|
||||||
<div
|
|
||||||
key={step.stepNumber}
|
|
||||||
className={`relative p-5 rounded-lg border-2 transition-all ${getStepBorderColor(step.status)}`}
|
|
||||||
>
|
|
||||||
{/* Step Content */}
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
{/* Icon */}
|
|
||||||
<div className={`p-3 rounded-xl ${getStepIconBg(step.status)}`}>
|
|
||||||
{getStepIcon(step.status)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Step Details */}
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
{/* Header Row */}
|
|
||||||
<div className="flex items-start justify-between gap-4 mb-2">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<h4 className="font-semibold text-gray-900">
|
|
||||||
Step {step.stepNumber}: {step.stepName}
|
|
||||||
</h4>
|
|
||||||
<Badge className={getStatusBadgeColor(step.status)}>
|
|
||||||
{step.status}
|
|
||||||
</Badge>
|
|
||||||
|
|
||||||
{/* Action Buttons */}
|
|
||||||
{step.hasEmailNotification && onViewEmailTemplate && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-6 w-6 p-0 hover:bg-blue-100"
|
|
||||||
onClick={() => onViewEmailTemplate(step.stepNumber)}
|
|
||||||
title="View email template"
|
|
||||||
>
|
|
||||||
<Mail className="w-3.5 h-3.5 text-blue-600" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{step.hasDownload && onDownloadDocument && step.downloadUrl && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-6 w-6 p-0 hover:bg-green-100"
|
|
||||||
onClick={() => onDownloadDocument(step.stepNumber, step.downloadUrl!)}
|
|
||||||
title="Download E-Invoice"
|
|
||||||
>
|
|
||||||
<Download className="w-3.5 h-3.5 text-green-600" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-sm text-gray-600">{step.assignedTo}</p>
|
|
||||||
<p className="text-sm text-gray-500 mt-2 italic">
|
|
||||||
{step.stepDescription}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* TAT Info */}
|
|
||||||
<div className="text-right">
|
|
||||||
<p className="text-xs text-gray-500">TAT: {step.tatHours}h</p>
|
|
||||||
{step.elapsedHours !== undefined && (
|
|
||||||
<p className="text-xs text-gray-600 font-medium">
|
|
||||||
Elapsed: {step.elapsedHours}h
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Remarks */}
|
|
||||||
{step.remarks && (
|
|
||||||
<div className="mt-3 p-3 bg-white rounded-lg border border-gray-200">
|
|
||||||
<p className="text-sm text-gray-700">{step.remarks}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* IO Details */}
|
|
||||||
{step.ioDetails && (
|
|
||||||
<div className="mt-3 p-3 bg-blue-50 rounded-lg border border-blue-200">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<Receipt className="w-4 h-4 text-blue-600" />
|
|
||||||
<p className="text-xs font-semibold text-blue-900 uppercase tracking-wide">
|
|
||||||
IO Organisation Details
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-xs text-gray-600">IO Number:</span>
|
|
||||||
<span className="text-sm font-semibold text-gray-900">
|
|
||||||
{step.ioDetails.ioNumber}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="pt-1.5 border-t border-blue-100">
|
|
||||||
<p className="text-xs text-gray-600 mb-1">IO Remark:</p>
|
|
||||||
<p className="text-sm text-gray-900">{step.ioDetails.ioRemarks}</p>
|
|
||||||
</div>
|
|
||||||
<div className="pt-1.5 border-t border-blue-100 text-xs text-gray-500">
|
|
||||||
Organised by {step.ioDetails.organisedBy} on{' '}
|
|
||||||
{formatDate(step.ioDetails.organisedAt)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* DMS Details */}
|
|
||||||
{step.dmsDetails && (
|
|
||||||
<div className="mt-3 p-3 bg-purple-50 rounded-lg border border-purple-200">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<Activity className="w-4 h-4 text-purple-600" />
|
|
||||||
<p className="text-xs font-semibold text-purple-900 uppercase tracking-wide">
|
|
||||||
DMS Processing Details
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-xs text-gray-600">DMS Number:</span>
|
|
||||||
<span className="text-sm font-semibold text-gray-900">
|
|
||||||
{step.dmsDetails.dmsNumber}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="pt-1.5 border-t border-purple-100">
|
|
||||||
<p className="text-xs text-gray-600 mb-1">DMS Remarks:</p>
|
|
||||||
<p className="text-sm text-gray-900">{step.dmsDetails.dmsRemarks}</p>
|
|
||||||
</div>
|
|
||||||
<div className="pt-1.5 border-t border-purple-100 text-xs text-gray-500">
|
|
||||||
Pushed by {step.dmsDetails.pushedBy} on{' '}
|
|
||||||
{formatDate(step.dmsDetails.pushedAt)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Approval Timestamp */}
|
|
||||||
{step.approvedAt && (
|
|
||||||
<p className="text-xs text-gray-500 mt-2">
|
|
||||||
{step.status === 'approved' ? 'Approved' : 'Updated'} on{' '}
|
|
||||||
{formatDate(step.approvedAt)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -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<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 && 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 (
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
{/* IO Budget Management Card */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<DollarSign className="w-5 h-5 text-[#2d4a3e]" />
|
|
||||||
IO Budget Management
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Enter IO number to fetch available budget from SAP
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
{/* IO Number Input */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Label htmlFor="ioNumber">IO Number *</Label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
id="ioNumber"
|
|
||||||
placeholder="Enter IO number (e.g., IO-2024-12345)"
|
|
||||||
value={ioNumber}
|
|
||||||
onChange={(e) => setIoNumber(e.target.value)}
|
|
||||||
disabled={fetchingAmount || !!blockedDetails}
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={handleFetchAmount}
|
|
||||||
disabled={!ioNumber.trim() || fetchingAmount || !!blockedDetails}
|
|
||||||
className="bg-[#2d4a3e] hover:bg-[#1f3329]"
|
|
||||||
>
|
|
||||||
<Download className="w-4 h-4 mr-2" />
|
|
||||||
{fetchingAmount ? 'Fetching...' : 'Fetch Amount'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Fetched Amount Display */}
|
|
||||||
{fetchedAmount !== null && !blockedDetails && (
|
|
||||||
<>
|
|
||||||
<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-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="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>
|
|
||||||
|
|
||||||
{/* 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 || !amountToBlock || parseFloat(amountToBlock) <= 0 || parseFloat(amountToBlock) > fetchedAmount}
|
|
||||||
className="w-full bg-[#2d4a3e] hover:bg-[#1f3329]"
|
|
||||||
>
|
|
||||||
<Target className="w-4 h-4 mr-2" />
|
|
||||||
{blockingBudget ? 'Blocking in SAP...' : 'Block IO in SAP'}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* IO Blocked Details Card */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<CircleCheckBig className="w-5 h-5 text-green-600" />
|
|
||||||
IO Blocked Details
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Details of IO blocked in SAP system
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{blockedDetails ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Success Banner */}
|
|
||||||
<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-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="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="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="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="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="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="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>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<DollarSign className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
|
||||||
<p className="text-sm text-gray-500 mb-2">No IO blocked yet</p>
|
|
||||||
<p className="text-xs text-gray-400">
|
|
||||||
Enter IO number and fetch amount to block budget
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -96,6 +96,32 @@ export function RequestCard({ request, index, onViewRequest }: RequestCardProps)
|
|||||||
<PriorityIcon className="w-3 h-3 mr-1" />
|
<PriorityIcon className="w-3 h-3 mr-1" />
|
||||||
{request.priority}
|
{request.priority}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{/* 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 (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={`${templateColor} font-medium text-xs shrink-0`}
|
||||||
|
data-testid="template-type-badge"
|
||||||
|
>
|
||||||
|
{templateLabel}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
{request.department && (
|
{request.department && (
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|||||||
@ -62,6 +62,7 @@ export interface ConvertedRequest {
|
|||||||
currentApprover: string;
|
currentApprover: string;
|
||||||
approverLevel: string;
|
approverLevel: string;
|
||||||
templateType?: string;
|
templateType?: string;
|
||||||
|
workflowType?: string;
|
||||||
templateName?: string;
|
templateName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -97,8 +97,9 @@ export function transformRequest(req: any): ConvertedRequest {
|
|||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
currentApprover: currentApprover,
|
currentApprover: currentApprover,
|
||||||
approverLevel: approverLevel,
|
approverLevel: approverLevel,
|
||||||
templateType: req.templateType,
|
templateType: req.templateType || req.template_type,
|
||||||
templateName: req.templateName
|
workflowType: req.workflowType || req.workflow_type,
|
||||||
|
templateName: req.templateName || req.template_name
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { NavigateFunction } from 'react-router-dom';
|
import { NavigateFunction } from 'react-router-dom';
|
||||||
import { getRequestDetailRoute, getRequestFlowType, RequestFlowType } from './requestTypeUtils';
|
import { getRequestDetailRoute, RequestFlowType } from './requestTypeUtils';
|
||||||
|
|
||||||
export interface RequestNavigationOptions {
|
export interface RequestNavigationOptions {
|
||||||
requestId: string;
|
requestId: string;
|
||||||
@ -33,7 +33,7 @@ export interface RequestNavigationOptions {
|
|||||||
* - Status-based routing
|
* - Status-based routing
|
||||||
*/
|
*/
|
||||||
export function navigateToRequest(options: RequestNavigationOptions): void {
|
export function navigateToRequest(options: RequestNavigationOptions): void {
|
||||||
const { requestId, requestTitle, status, request, navigate } = options;
|
const { requestId, status, request, navigate } = options;
|
||||||
|
|
||||||
// Check if request is a draft - if so, route to edit form instead of detail view
|
// Check if request is a draft - if so, route to edit form instead of detail view
|
||||||
const isDraft = status?.toLowerCase() === 'draft' || status === 'DRAFT';
|
const isDraft = status?.toLowerCase() === 'draft' || status === 'DRAFT';
|
||||||
|
|||||||
@ -77,9 +77,7 @@ export function getRequestFlowType(request: any): RequestFlowType {
|
|||||||
/**
|
/**
|
||||||
* Get the route path for a request detail page based on flow type
|
* Get the route path for a request detail page based on flow type
|
||||||
*/
|
*/
|
||||||
export function getRequestDetailRoute(requestId: string, request?: any): string {
|
export function getRequestDetailRoute(requestId: string, _request?: any): string {
|
||||||
const flowType = request ? getRequestFlowType(request) : null;
|
|
||||||
|
|
||||||
// For now, all requests use the same route
|
// For now, all requests use the same route
|
||||||
// In the future, you can customize routes per flow type:
|
// In the future, you can customize routes per flow type:
|
||||||
// if (flowType === 'DEALER_CLAIM') {
|
// if (flowType === 'DEALER_CLAIM') {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user