1070 lines
46 KiB
TypeScript
1070 lines
46 KiB
TypeScript
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>
|
||
);
|
||
}
|