Re_Figma_Code/src/dealer-claim/components/request-creation/ClaimManagementWizard.tsx

1070 lines
46 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useRef, 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 { RichTextEditor } from '@/components/ui/rich-text-editor';
import { FormattedDescription } from '@/components/common/FormattedDescription';
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,
Users,
XCircle,
Loader2,
} from 'lucide-react';
import { format } from 'date-fns';
import { toast } from 'sonner';
import { getAllDealers as fetchDealersFromAPI, verifyDealerLogin, type DealerInfo } from '@/services/dealerApi';
import { ClaimApproverSelectionStep } from './ClaimApproverSelectionStep';
import { useAuth } from '@/contexts/AuthContext';
// CLAIM_STEPS definition (same as in ClaimApproverSelectionStep)
const CLAIM_STEPS = [
{ level: 1, name: 'Dealer Proposal Submission', description: 'Dealer submits proposal documents', defaultTat: 72, isAuto: false, approverType: 'dealer' },
{ level: 2, name: 'Requestor Evaluation', description: 'Initiator evaluates dealer proposal', defaultTat: 48, isAuto: false, approverType: 'initiator' },
{ level: 3, name: 'Department Lead Approval', description: 'Department lead approves and blocks IO budget', defaultTat: 72, isAuto: false, approverType: 'manual' },
{ level: 4, name: 'Activity Creation', description: 'System auto-processes activity creation', defaultTat: 1, isAuto: true, approverType: 'system' },
{ level: 5, name: 'Dealer Completion Documents', description: 'Dealer submits completion documents', defaultTat: 120, isAuto: false, approverType: 'dealer' },
{ level: 6, name: 'Requestor Claim Approval', description: 'Initiator approves completion', defaultTat: 48, isAuto: false, approverType: 'initiator' },
{ level: 7, name: 'E-Invoice Generation', description: 'System generates e-invoice via DMS', defaultTat: 1, isAuto: true, approverType: 'system' },
{ level: 8, name: 'Credit Note Confirmation', description: 'System/Finance processes credit note confirmation', defaultTat: 48, isAuto: true, approverType: 'system' },
];
interface ClaimManagementWizardProps {
onBack?: () => void;
onSubmit?: (claimData: any) => void;
}
const CLAIM_TYPES = [
'Riders Mania Claims',
'Marketing Cost Bike to Vendor',
'Media Bike Service',
'ARAI Motorcycle Liquidation',
'ARAI Certification STA Approval CNR',
'Procurement of Spares/Apparel/GMA for Events',
'Fuel for Media Bike Used for Event',
'Motorcycle Buyback and Goodwill Support',
'Liquidation of Used Motorcycle',
'Motorcycle Registration CNR (Owned or Gifted by RE)',
'Legal Claims Reimbursement',
'Service Camp Claims',
'Corporate Claims Institutional Sales PDI'
];
const STEP_NAMES = [
'Claim Details',
'Approver Selection',
'Review & Submit'
];
export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizardProps) {
const { user } = useAuth();
const [currentStep, setCurrentStep] = useState(1);
const [verifyingDealer, setVerifyingDealer] = useState(false);
const [dealerSearchResults, setDealerSearchResults] = useState<DealerInfo[]>([]);
const [dealerSearchLoading, setDealerSearchLoading] = useState(false);
const [dealerSearchInput, setDealerSearchInput] = useState('');
const dealerSearchTimer = useRef<any>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const submitTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (submitTimeoutRef.current) {
clearTimeout(submitTimeoutRef.current);
}
};
}, []);
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: '',
// Approvers array for all 8 steps
approvers: [] as Array<{
email: string;
name?: string;
userId?: string;
level: number;
tat?: number | string;
tatType?: 'hours' | 'days';
isAdditional?: boolean;
insertAfterLevel?: number;
stepName?: string;
originalStepLevel?: number;
}>
});
const totalSteps = STEP_NAMES.length;
// Handle dealer search input with debouncing
const handleDealerSearchInputChange = (value: string) => {
setDealerSearchInput(value);
// Clear previous timer
if (dealerSearchTimer.current) {
clearTimeout(dealerSearchTimer.current);
}
// If input is empty, clear results
if (!value || value.trim().length < 2) {
setDealerSearchResults([]);
setDealerSearchLoading(false);
return;
}
// Set loading state
setDealerSearchLoading(true);
// Debounce search
dealerSearchTimer.current = setTimeout(async () => {
try {
const results = await fetchDealersFromAPI(value, 10); // Limit to 10 results
setDealerSearchResults(results);
} catch (error) {
console.error('Error searching dealers:', error);
setDealerSearchResults([]);
} finally {
setDealerSearchLoading(false);
}
}, 300);
};
const updateFormData = (field: string, value: any) => {
setFormData(prev => {
const updated = { ...prev, [field]: value };
// Validate period dates
if (field === 'periodStartDate') {
// If start date is selected and end date exists, validate end date
if (value && updated.periodEndDate && value > updated.periodEndDate) {
// Clear end date if it's before the new start date
updated.periodEndDate = undefined;
toast.error('End date must be on or after the start date. End date has been cleared.');
}
} else if (field === 'periodEndDate') {
// If end date is selected and start date exists, validate end date
if (value && updated.periodStartDate && value < updated.periodStartDate) {
toast.error('End date must be on or after the start date.');
// Don't update the end date if it's invalid
return prev;
}
}
return updated;
});
};
const isStepValid = () => {
switch (currentStep) {
case 1:
return formData.activityName &&
formData.activityType &&
formData.dealerCode &&
formData.dealerName &&
formData.activityDate &&
formData.location &&
formData.requestDescription;
case 2:
// Validate that all required approvers are assigned (Step 3 only, Step 8 is now system/Finance)
const approvers = formData.approvers || [];
// Find step 3 approver by originalStepLevel first, then fallback to level
const step3Approver = approvers.find((a: any) =>
a.originalStepLevel === 3 || (!a.originalStepLevel && a.level === 3 && !a.isAdditional)
);
// Step 8 is now a system step, no validation needed
return step3Approver?.email && step3Approver?.userId && step3Approver?.tat;
case 3:
return true;
default:
return false;
}
};
const nextStep = () => {
if (currentStep < totalSteps) {
if (!isStepValid()) {
// Show specific error messages for step 2 (approver selection)
if (currentStep === 2) {
const approvers = formData.approvers || [];
// Find step 3 approver by originalStepLevel first, then fallback to level
const step3Approver = approvers.find((a: any) =>
a.originalStepLevel === 3 || (!a.originalStepLevel && a.level === 3 && !a.isAdditional)
);
const missingSteps: string[] = [];
if (!step3Approver?.email || !step3Approver?.userId || !step3Approver?.tat) {
missingSteps.push('Department Lead Approval');
}
if (missingSteps.length > 0) {
toast.error(`Please add missing approvers: ${missingSteps.join(', ')}`);
} else {
toast.error('Please complete all required approver selections (email, user verification, and TAT) before proceeding.');
}
} else {
toast.error('Please complete all required fields before proceeding.');
}
return;
}
setCurrentStep(currentStep + 1);
}
};
const prevStep = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};
const handleDealerSelect = async (selectedDealer: DealerInfo) => {
// Verify dealer is logged in
setVerifyingDealer(true);
try {
const verifiedDealer = await verifyDealerLogin(selectedDealer.dealerCode);
if (!verifiedDealer.isLoggedIn) {
toast.error(
`Dealer "${verifiedDealer.dealerName || verifiedDealer.displayName}" (${verifiedDealer.dealerCode}) has not logged in to the system. Please ask them to log in first.`,
{ duration: 5000 }
);
// Clear the selection
setDealerSearchInput('');
setDealerSearchResults([]);
updateFormData('dealerCode', '');
updateFormData('dealerName', '');
updateFormData('dealerEmail', '');
updateFormData('dealerPhone', '');
updateFormData('dealerAddress', '');
setVerifyingDealer(false);
return;
}
// Dealer is logged in, update form data
updateFormData('dealerCode', verifiedDealer.dealerCode);
updateFormData('dealerName', verifiedDealer.dealerName || verifiedDealer.displayName);
updateFormData('dealerEmail', verifiedDealer.email || '');
updateFormData('dealerPhone', verifiedDealer.phone || '');
updateFormData('dealerAddress', ''); // Address not available in API response
// Clear search input and results
setDealerSearchInput(verifiedDealer.dealerName || verifiedDealer.displayName);
setDealerSearchResults([]);
toast.success(`Dealer "${verifiedDealer.dealerName || verifiedDealer.displayName}" verified and logged in`);
} catch (error: any) {
const errorMessage = error.message || 'Failed to verify dealer login';
toast.error(errorMessage, { duration: 5000 });
// Clear the selection
setDealerSearchInput('');
setDealerSearchResults([]);
updateFormData('dealerCode', '');
updateFormData('dealerName', '');
updateFormData('dealerEmail', '');
updateFormData('dealerPhone', '');
updateFormData('dealerAddress', '');
} finally {
setVerifyingDealer(false);
}
};
const handleSubmit = () => {
// Prevent multiple submissions
if (isSubmitting) {
return;
}
// Approvers are already using integer levels with proper shifting
// Just sort them and prepare for submission
const approvers = formData.approvers || [];
const sortedApprovers = [...approvers].sort((a, b) => a.level - b.level);
// Check for duplicate levels (should not happen, but safeguard)
const levelMap = new Map<number, typeof sortedApprovers[0]>();
const duplicates: number[] = [];
sortedApprovers.forEach((approver) => {
if (levelMap.has(approver.level)) {
duplicates.push(approver.level);
} else {
levelMap.set(approver.level, approver);
}
});
if (duplicates.length > 0) {
toast.error(`Duplicate approver levels detected: ${duplicates.join(', ')}. Please refresh and try again.`);
console.error('Duplicate levels found:', duplicates, sortedApprovers);
return;
}
// Prepare final approvers array - preserve stepName for additional approvers
// The backend will use stepName to set the levelName for approval levels
// Also preserve originalStepLevel so backend can identify which step each approver belongs to
const finalApprovers = sortedApprovers.map((approver) => {
const result: any = {
email: approver.email,
name: approver.name,
userId: approver.userId,
level: approver.level,
tat: approver.tat,
tatType: approver.tatType,
};
// Preserve stepName for additional approvers
if (approver.isAdditional && approver.stepName) {
result.stepName = approver.stepName;
result.isAdditional = true;
}
// Preserve originalStepLevel for fixed steps (so backend can identify which step this is)
if (approver.originalStepLevel) {
result.originalStepLevel = approver.originalStepLevel;
}
return result;
});
const claimData = {
...formData,
templateType: 'claim-management',
submittedAt: new Date().toISOString(),
status: 'pending',
currentStep: 'initiator-review',
// Pass normalized approvers array to backend
approvers: finalApprovers
};
// Set submitting state to prevent multiple clicks
setIsSubmitting(true);
// Clear any existing timeout
if (submitTimeoutRef.current) {
clearTimeout(submitTimeoutRef.current);
}
// Set a timeout as a fallback to reset loading state (30 seconds)
// In most cases, the parent component will navigate away on success,
// but this prevents the button from being stuck in loading state if there's an error
submitTimeoutRef.current = setTimeout(() => {
setIsSubmitting(false);
submitTimeoutRef.current = null;
}, 30000);
// Don't show toast here - let the parent component handle success/error after API call
if (onSubmit) {
try {
onSubmit(claimData);
// Note: On success, the component will unmount when parent navigates away (timeout cleared in useEffect)
// On error, the timeout will reset the state after 30 seconds
} catch (error) {
// If onSubmit throws synchronously, reset state immediately
if (submitTimeoutRef.current) {
clearTimeout(submitTimeoutRef.current);
submitTimeoutRef.current = null;
}
setIsSubmitting(false);
console.error('Error submitting claim:', error);
}
} else {
// If no onSubmit handler, reset immediately
if (submitTimeoutRef.current) {
clearTimeout(submitTimeoutRef.current);
submitTimeoutRef.current = null;
}
setIsSubmitting(false);
}
};
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>
<div className="mt-2">
<div className="relative">
<Input
placeholder="Type dealer code, name, or email to search..."
value={formData.dealerCode ? `${formData.dealerName} (${formData.dealerCode})` : dealerSearchInput}
onChange={(e) => {
if (formData.dealerCode) {
// If dealer is already selected, clear selection first
updateFormData('dealerCode', '');
updateFormData('dealerName', '');
updateFormData('dealerEmail', '');
updateFormData('dealerPhone', '');
updateFormData('dealerAddress', '');
setDealerSearchInput(e.target.value);
} else {
handleDealerSearchInputChange(e.target.value);
}
}}
onFocus={() => {
// When input is focused, show search results if input has value
if (dealerSearchInput && dealerSearchInput.length >= 2) {
handleDealerSearchInputChange(dealerSearchInput);
}
}}
className="h-12 border-2 border-gray-300 focus:border-blue-500"
disabled={verifyingDealer}
/>
{formData.dealerCode && (
<div className="absolute right-3 top-1/2 -translate-y-1/2">
<Badge variant="outline" className="text-xs bg-green-50 text-green-700 border-green-300">
<CheckCircle className="w-3 h-3 mr-1" />
Verified
</Badge>
</div>
)}
{/* Search suggestions dropdown */}
{(dealerSearchLoading || dealerSearchResults.length > 0) && !formData.dealerCode && (
<div className="absolute left-0 right-0 top-full mt-1 z-50 border rounded-md bg-white shadow-lg">
{dealerSearchLoading ? (
<div className="p-2 text-xs text-gray-500">Searching...</div>
) : (
<ul className="max-h-56 overflow-auto divide-y">
{dealerSearchResults.map((dealer) => (
<li
key={dealer.dealerId}
className="p-2 text-sm cursor-pointer hover:bg-gray-50"
onClick={() => handleDealerSelect(dealer)}
>
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="font-medium text-gray-900">
{dealer.dealerName || dealer.displayName}
</div>
<div className="text-xs text-gray-600">
<span className="font-mono">{dealer.dealerCode}</span>
{dealer.email && (
<>
<span className="mx-1"></span>
<span>{dealer.email}</span>
</>
)}
</div>
{dealer.city && dealer.state && (
<div className="text-xs text-gray-500">
{dealer.city}, {dealer.state}
</div>
)}
</div>
<div className="ml-2 flex-shrink-0">
{dealer.isLoggedIn ? (
<CheckCircle className="w-4 h-4 text-green-600" />
) : (
<XCircle className="w-4 h-4 text-red-500" />
)}
</div>
</div>
</li>
))}
</ul>
)}
</div>
)}
</div>
</div>
<div className="mt-2 flex items-center gap-2 text-xs text-gray-500">
<span>Status:</span>
<div className="flex items-center gap-1">
<CheckCircle className="w-3 h-3 text-green-600" />
<span>Logged in</span>
</div>
<span className="mx-1"></span>
<div className="flex items-center gap-1">
<XCircle className="w-3 h-3 text-red-500" />
<span>Not logged in</span>
</div>
</div>
{formData.dealerCode && (
<div className="mt-2 space-y-1">
<p className="text-sm text-gray-600">
Selected: <span className="font-semibold">{formData.dealerName}</span> ({formData.dealerCode})
</p>
{formData.dealerEmail && (
<p className="text-xs text-gray-500">Email: {formData.dealerEmail}</p>
)}
</div>
)}
</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 pl-3"
>
<CalendarIcon className="mr-2 h-4 w-4 flex-shrink-0" />
<span className="flex-1 text-left">{formData.activityDate ? format(formData.activityDate, 'PPP') : 'Select date'}</span>
</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>
<p className="text-sm text-gray-600 mb-3">
Explain what you need approval for, why it's needed, and any relevant background information.
<span className="block mt-1 text-xs text-blue-600">
💡 Tip: You can paste formatted content (lists, tables) and the formatting will be preserved.
</span>
</p>
<RichTextEditor
value={formData.requestDescription || ''}
onChange={(html) => updateFormData('requestDescription', html)}
placeholder="Provide comprehensive details about your claim requirement including scope, objectives, expected outcomes, and any supporting context that will help approvers make an informed decision."
className="min-h-[120px] text-base border-2 border-gray-300 focus-within:border-blue-500 bg-white shadow-sm"
minHeight="120px"
/>
</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 pl-3"
>
<CalendarIcon className="mr-2 h-4 w-4 flex-shrink-0" />
<span className="flex-1 text-left">{formData.periodStartDate ? format(formData.periodStartDate, 'PPP') : 'Start date'}</span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={formData.periodStartDate}
onSelect={(date) => updateFormData('periodStartDate', date)}
initialFocus
// Maximum date is the end date (if selected)
toDate={formData.periodEndDate || undefined}
/>
</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 pl-3"
disabled={!formData.periodStartDate}
>
<CalendarIcon className="mr-2 h-4 w-4 flex-shrink-0" />
<span className="flex-1 text-left">{formData.periodEndDate ? format(formData.periodEndDate, 'PPP') : 'End date'}</span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={formData.periodEndDate}
onSelect={(date) => updateFormData('periodEndDate', date)}
initialFocus
// Minimum date is the start date (if selected)
fromDate={formData.periodStartDate || undefined}
/>
</PopoverContent>
</Popover>
{!formData.periodStartDate && (
<p className="text-xs text-gray-500 mt-1">Please select start date first</p>
)}
</div>
</div>
{(formData.periodStartDate || formData.periodEndDate) && (
<div className="mt-2">
{formData.periodStartDate && formData.periodEndDate ? (
<p className="text-xs text-gray-600">
Period: {format(formData.periodStartDate, 'MMM dd, yyyy')} - {format(formData.periodEndDate, 'MMM dd, yyyy')}
</p>
) : (
<p className="text-xs text-gray-500">
{formData.periodStartDate
? 'Please select end date for the period'
: 'Please select start date first'}
</p>
)}
</div>
)}
</div>
</div>
</motion.div>
);
case 2:
return (
<ClaimApproverSelectionStep
formData={formData}
updateFormData={updateFormData}
currentUserEmail={(user as any)?.email || ''}
currentUserId={(user as any)?.userId || ''}
currentUserName={
(user as any)?.displayName ||
(user as any)?.name ||
((user as any)?.firstName && (user as any)?.lastName
? `${(user as any).firstName} ${(user as any).lastName}`.trim()
: (user as any)?.email || 'User')
}
/>
);
case 3:
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>
{/* Approver Information */}
<Card className="border-2">
<CardHeader className="bg-gradient-to-br from-purple-50 to-indigo-50">
<CardTitle className="flex items-center gap-2">
<Users className="w-5 h-5 text-purple-600" />
Selected Approvers
</CardTitle>
</CardHeader>
<CardContent className="pt-6 space-y-4">
<div className="space-y-3">
{(() => {
// Sort approvers by level and filter out system approvers
const sortedApprovers = [...(formData.approvers || [])]
.filter((a: any) => !a.email?.includes('system@') && !a.email?.includes('finance@'))
.sort((a: any, b: any) => a.level - b.level);
return sortedApprovers.map((approver: any) => {
const tat = Number(approver.tat || 0);
const tatType = approver.tatType || 'hours';
const hours = tatType === 'days' ? tat * 24 : tat;
// Find step name - handle additional approvers and shifted levels
let stepName = 'Unknown';
let stepLabel = '';
if (approver.isAdditional) {
// Additional approver - use stepName if available
stepName = approver.stepName || 'Additional Approver';
const afterStep = CLAIM_STEPS.find(s => s.level === approver.insertAfterLevel);
stepLabel = approver.stepName || `Additional Approver (after "${afterStep?.name || 'Unknown'}")`;
} else {
// Fixed step - find by originalStepLevel first, then fallback to level
const step = approver.originalStepLevel
? CLAIM_STEPS.find(s => s.level === approver.originalStepLevel)
: CLAIM_STEPS.find(s => s.level === approver.level && !s.isAuto);
stepName = step?.name || 'Unknown';
stepLabel = stepName;
}
return (
<div key={`${approver.level}-${approver.email}`} className={`p-3 rounded-lg border ${
approver.isAdditional ? 'bg-purple-50 border-purple-200' : 'bg-gray-50 border-gray-200'
}`}>
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<Label className="text-xs text-gray-600 uppercase tracking-wider">
{stepLabel}
</Label>
{approver.isAdditional && (
<Badge variant="outline" className="text-xs bg-purple-100 text-purple-700 border-purple-300">
ADDITIONAL
</Badge>
)}
</div>
<p className="font-semibold text-gray-900 mt-1">{approver.name || approver.email || 'Not selected'}</p>
{approver.email && (
<p className="text-xs text-gray-500 mt-1">{approver.email}</p>
)}
</div>
<div className="text-right ml-4">
<p className="text-sm font-semibold text-gray-900">{hours} hours</p>
<p className="text-xs text-gray-500">TAT</p>
</div>
</div>
</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">
<FormattedDescription
content={formData.requestDescription || ''}
className="text-sm"
/>
</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}
className={`gap-2 w-full sm:w-auto order-1 sm:order-2 ${
!isStepValid()
? 'opacity-50 cursor-pointer hover:opacity-60'
: ''
}`}
>
Next
<ArrowRight className="w-4 h-4" />
</Button>
) : (
<Button
onClick={handleSubmit}
disabled={!isStepValid() || isSubmitting}
className="gap-2 bg-green-600 hover:bg-green-700 w-full sm:w-auto order-1 sm:order-2"
>
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Submitting...
</>
) : (
<>
<Check className="w-4 h-4" />
Submit Claim Request
</>
)}
</Button>
)}
</div>
</div>
</div>
);
}