diff --git a/src/App.tsx b/src/App.tsx index e75783f..3c383d9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -275,7 +275,7 @@ function AppRoutes({ onLogout }: AppProps) { setApprovalAction(null); }; - const handleClaimManagementSubmit = async (claimData: any, selectedManagerEmail?: string) => { + const handleClaimManagementSubmit = async (claimData: any, _selectedManagerEmail?: string) => { try { // Prepare payload for API const payload = { @@ -292,7 +292,7 @@ function AppRoutes({ onLogout }: AppProps) { periodStartDate: claimData.periodStartDate ? new Date(claimData.periodStartDate).toISOString() : undefined, periodEndDate: claimData.periodEndDate ? new Date(claimData.periodEndDate).toISOString() : undefined, estimatedBudget: claimData.estimatedBudget || undefined, - selectedManagerEmail: selectedManagerEmail || undefined, + approvers: claimData.approvers || [], // Pass approvers array }; // Call API to create claim request diff --git a/src/dealer-claim/components/request-creation/ClaimApproverSelectionStep.tsx b/src/dealer-claim/components/request-creation/ClaimApproverSelectionStep.tsx new file mode 100644 index 0000000..3fc16f4 --- /dev/null +++ b/src/dealer-claim/components/request-creation/ClaimApproverSelectionStep.tsx @@ -0,0 +1,573 @@ +/** + * ClaimApproverSelectionStep Component + * Step 2: Manual approver selection for all 8 steps in dealer claim workflow + * Similar to ApprovalWorkflowStep but fixed to 8 steps with predefined step names + */ + +import { motion } from 'framer-motion'; +import { useEffect } from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Badge } from '@/components/ui/badge'; +import { Users, Shield, CheckCircle, Info, Clock, User } from 'lucide-react'; +import { useMultiUserSearch } from '@/hooks/useUserSearch'; +import { ensureUserExists } from '@/services/userApi'; +import { toast } from 'sonner'; + +// Fixed 8-step workflow for dealer claims +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 ClaimApprover { + email: string; + name?: string; + userId?: string; + level: number; + tat?: number | string; + tatType?: 'hours' | 'days'; +} + +interface ClaimApproverSelectionStepProps { + formData: { + dealerEmail?: string; + dealerName?: string; + approvers?: ClaimApprover[]; + }; + updateFormData: (field: string, value: any) => void; + onValidationError?: (error: { type: string; email: string; message: string }) => void; + currentUserEmail?: string; + currentUserId?: string; + currentUserName?: string; + onValidate?: (isValid: boolean) => void; +} + +export function ClaimApproverSelectionStep({ + formData, + updateFormData, + onValidationError, + currentUserEmail = '', + currentUserId = '', + currentUserName = '', + onValidate, +}: ClaimApproverSelectionStepProps) { + const { userSearchResults, userSearchLoading, searchUsersForIndex, clearSearchForIndex } = useMultiUserSearch(); + + // Validation function to check for missing approvers + const validateApprovers = (): { isValid: boolean; missingSteps: string[] } => { + const approvers = formData.approvers || []; + const missingSteps: string[] = []; + + CLAIM_STEPS.forEach((step) => { + // Skip auto steps (system steps) and pre-filled steps (dealer, initiator) + // Step 8 is now a system step, so it should be skipped from validation + if (step.isAuto || step.approverType === 'dealer' || step.approverType === 'initiator') { + return; + } + + // For manual steps (3 and 8), check if approver is assigned, verified, and has TAT + const approver = approvers.find((a: ClaimApprover) => a.level === step.level); + + if (!approver || !approver.email || !approver.userId || !approver.tat) { + missingSteps.push(`Step ${step.level}: ${step.name}`); + } + }); + + return { + isValid: missingSteps.length === 0, + missingSteps, + }; + }; + + // Expose validation to parent component + useEffect(() => { + if (onValidate) { + const validation = validateApprovers(); + onValidate(validation.isValid); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [formData.approvers]); + + // Initialize approvers array for all 8 steps + useEffect(() => { + const currentApprovers = formData.approvers || []; + const newApprovers: ClaimApprover[] = []; + + CLAIM_STEPS.forEach((step) => { + const existingApprover = currentApprovers.find((a: ClaimApprover) => a.level === step.level); + + if (step.isAuto) { + // System steps - no approver needed + // Step 8 is System/Finance, use finance email + const systemEmail = step.level === 8 ? 'finance@royalenfield.com' : 'system@royalenfield.com'; + const systemName = step.level === 8 ? 'System/Finance' : 'System'; + newApprovers.push({ + email: systemEmail, + name: systemName, + level: step.level, + tat: step.defaultTat, + tatType: 'hours', + }); + } else if (step.approverType === 'dealer') { + // Dealer steps - use dealer email + newApprovers.push({ + email: formData.dealerEmail || '', + name: formData.dealerName || '', + level: step.level, + tat: step.defaultTat, + tatType: 'hours', + }); + } else if (step.approverType === 'initiator') { + // Initiator steps - use current user + newApprovers.push({ + email: currentUserEmail || '', + name: currentUserName || currentUserEmail || 'User', + userId: currentUserId, + level: step.level, + tat: step.defaultTat, + tatType: 'hours', + }); + } else { + // Manual steps - use existing or create empty + newApprovers.push(existingApprover || { + email: '', + name: '', + level: step.level, + tat: step.defaultTat, + tatType: 'hours', + }); + } + }); + + // Only update if approvers array is empty or structure changed + if (currentApprovers.length === 0 || currentApprovers.length !== newApprovers.length) { + updateFormData('approvers', newApprovers); + } + }, [formData.dealerEmail, formData.dealerName, currentUserEmail, currentUserId, currentUserName]); + + const handleApproverEmailChange = (level: number, value: string) => { + const approvers = [...(formData.approvers || [])]; + const index = approvers.findIndex((a: ClaimApprover) => a.level === level); + + if (index === -1) { + // Create new approver entry + const step = CLAIM_STEPS.find(s => s.level === level); + approvers.push({ + email: value, + name: '', + level: level, + tat: step?.defaultTat || 48, + tatType: 'hours', + }); + } else { + // Update existing approver + const existingApprover = approvers[index]; + if (existingApprover) { + const previousEmail = existingApprover.email; + approvers[index] = { + ...existingApprover, + email: value, + // Clear name and userId if email changed + name: value !== previousEmail ? '' : existingApprover.name, + userId: value !== previousEmail ? undefined : existingApprover.userId, + }; + } + } + + updateFormData('approvers', approvers); + + if (!value || !value.startsWith('@') || value.length < 2) { + clearSearchForIndex(level - 1); + return; + } + searchUsersForIndex(level - 1, value, 10); + }; + + const handleUserSelect = async (level: number, selectedUser: any) => { + try { + // Check if user is trying to select themselves for non-initiator steps + const step = CLAIM_STEPS.find(s => s.level === level); + if (step && !step.isAuto && step.approverType !== 'initiator' && selectedUser.email?.toLowerCase() === currentUserEmail?.toLowerCase()) { + toast.error(`You cannot assign yourself as ${step.name} approver.`); + if (onValidationError) { + onValidationError({ + type: 'self-assign', + email: selectedUser.email, + message: `You cannot assign yourself as ${step.name} approver.` + }); + } + return; + } + + // Check for duplicates across other steps + const approvers = formData.approvers || []; + const isDuplicate = approvers.some( + (a: ClaimApprover) => + a.level !== level && + (a.userId === selectedUser.userId || a.email?.toLowerCase() === selectedUser.email?.toLowerCase()) + ); + + if (isDuplicate) { + toast.error('This user is already assigned to another step.'); + if (onValidationError) { + onValidationError({ + type: 'error', + email: selectedUser.email, + message: 'This user is already assigned to another step.' + }); + } + return; + } + + // Ensure user exists in database (create from Okta if needed) + const dbUser = await ensureUserExists({ + userId: selectedUser.userId, + email: selectedUser.email, + displayName: selectedUser.displayName, + firstName: selectedUser.firstName, + lastName: selectedUser.lastName, + department: selectedUser.department, + phone: selectedUser.phone, + mobilePhone: selectedUser.mobilePhone, + designation: selectedUser.designation, + jobTitle: selectedUser.jobTitle, + manager: selectedUser.manager, + employeeId: selectedUser.employeeId, + employeeNumber: selectedUser.employeeNumber, + secondEmail: selectedUser.secondEmail, + location: selectedUser.location + }); + + // Update approver in array + const updatedApprovers = [...(formData.approvers || [])]; + const approverIndex = updatedApprovers.findIndex((a: ClaimApprover) => a.level === level); + + if (approverIndex === -1) { + const step = CLAIM_STEPS.find(s => s.level === level); + updatedApprovers.push({ + email: selectedUser.email, + name: selectedUser.displayName || [selectedUser.firstName, selectedUser.lastName].filter(Boolean).join(' '), + userId: dbUser.userId, + level: level, + tat: step?.defaultTat || 48, + tatType: 'hours' as const, + }); + } else { + const existingApprover = updatedApprovers[approverIndex]; + if (existingApprover) { + updatedApprovers[approverIndex] = { + ...existingApprover, + email: selectedUser.email, + name: selectedUser.displayName || [selectedUser.firstName, selectedUser.lastName].filter(Boolean).join(' '), + userId: dbUser.userId, + }; + } + } + + updateFormData('approvers', updatedApprovers); + clearSearchForIndex(level - 1); + toast.success(`Approver for ${CLAIM_STEPS.find(s => s.level === level)?.name} selected successfully.`); + } catch (err) { + console.error('Failed to ensure user exists:', err); + toast.error('Failed to validate user. Please try again.'); + if (onValidationError) { + onValidationError({ + type: 'error', + email: selectedUser.email, + message: 'Failed to validate user. Please try again.' + }); + } + } + }; + + const handleTatChange = (level: number, tat: number | string) => { + const approvers = [...(formData.approvers || [])]; + const index = approvers.findIndex((a: ClaimApprover) => a.level === level); + + if (index !== -1) { + const existingApprover = approvers[index]; + if (existingApprover) { + approvers[index] = { + ...existingApprover, + tat: tat, + }; + updateFormData('approvers', approvers); + } + } + }; + + const handleTatTypeChange = (level: number, tatType: 'hours' | 'days') => { + const approvers = [...(formData.approvers || [])]; + const index = approvers.findIndex((a: ClaimApprover) => a.level === level); + + if (index !== -1) { + const existingApprover = approvers[index]; + if (existingApprover) { + approvers[index] = { + ...existingApprover, + tatType: tatType, + tat: '', // Clear TAT when changing type + }; + updateFormData('approvers', approvers); + } + } + }; + + const approvers = formData.approvers || []; + + return ( + +
+
+ +
+

Approver Selection

+

+ Assign approvers for all 8 workflow steps with TAT (Turn Around Time) +

+
+ +
+ {/* Info Card */} + + + + + Workflow Steps Information + + + Some steps are pre-filled (Dealer, Initiator, System). You need to assign approvers for Step 3 only. Step 8 is handled by System/Finance. + + + + + {/* Approval Hierarchy */} + + + + + Approval Hierarchy (8 Steps) + + + Define approvers and TAT for each step. Steps 1, 2, 4, 5, 6, 7, 8 are pre-filled. Only Step 3 requires manual assignment. + + + + {/* Initiator Card */} +
+
+
+ +
+
+
+ Request Initiator + YOU +
+

Creates and submits the request

+
+
+
+ + {/* Dynamic Approver Cards - Filter out system steps (auto-processed) */} + {CLAIM_STEPS.filter((step) => !step.isAuto).map((step, index, filteredSteps) => { + const approver = approvers.find((a: ClaimApprover) => a.level === step.level) || { + email: '', + name: '', + level: step.level, + tat: step.defaultTat, + tatType: 'hours' as const, + }; + + const isLast = index === filteredSteps.length - 1; + const isPreFilled = step.isAuto || step.approverType === 'dealer' || step.approverType === 'initiator'; + const isEditable = !step.isAuto; + + return ( +
+
+
+
+ +
+
+
+ {step.level} +
+
+
+ + {step.name} + + {isLast && ( + FINAL + )} + {isPreFilled && ( + PRE-FILLED + )} +
+

{step.description}

+ + {isEditable && ( +
+
+
+ + {approver.email && approver.userId && ( + + + Verified + + )} +
+
+ { + const newValue = e.target.value; + if (!isPreFilled) { + handleApproverEmailChange(step.level, newValue); + } + }} + disabled={isPreFilled || step.isAuto} + className="h-9 border-2 border-gray-300 focus:border-blue-500 mt-1 w-full text-sm" + /> + {/* Search suggestions dropdown */} + {!isPreFilled && !step.isAuto && (userSearchLoading[step.level - 1] || (userSearchResults[step.level - 1]?.length || 0) > 0) && ( +
+ {userSearchLoading[step.level - 1] ? ( +
Searching...
+ ) : ( +
    + {userSearchResults[step.level - 1]?.map((u) => ( +
  • handleUserSelect(step.level, u)} + > +
    {u.displayName || u.email}
    +
    {u.email}
    + {u.department && ( +
    {u.department}
    + )} +
  • + ))} +
+ )} +
+ )} +
+ {approver.name && ( +

+ Selected: {approver.name} +

+ )} +
+ +
+ +
+ handleTatChange(step.level, parseInt(e.target.value) || '')} + disabled={step.isAuto} + className="h-9 border-2 border-gray-300 focus:border-blue-500 flex-1 text-sm" + /> + +
+
+
+ )} +
+
+
+
+ ); + })} +
+
+ + {/* TAT Summary */} + + + + + TAT Summary + + + +
+ {approvers.map((approver: ClaimApprover) => { + const step = CLAIM_STEPS.find(s => s.level === approver.level); + if (!step || step.isAuto) return null; + + const tat = Number(approver.tat || 0); + const tatType = approver.tatType || 'hours'; + const hours = tatType === 'days' ? tat * 24 : tat; + if (!tat) return null; + + return ( +
+ Step {approver.level}: {step.name} + {hours} hours +
+ ); + })} +
+
+
+
+
+ ); +} + diff --git a/src/dealer-claim/components/request-creation/ClaimManagementWizard.tsx b/src/dealer-claim/components/request-creation/ClaimManagementWizard.tsx index 492d839..5b68d5d 100644 --- a/src/dealer-claim/components/request-creation/ClaimManagementWizard.tsx +++ b/src/dealer-claim/components/request-creation/ClaimManagementWizard.tsx @@ -22,10 +22,13 @@ import { CheckCircle, Info, FileText, + Users, } from 'lucide-react'; import { format } from 'date-fns'; import { toast } from 'sonner'; import { getAllDealers as fetchDealersFromAPI, getDealerByCode, type DealerInfo } from '@/services/dealerApi'; +import { ClaimApproverSelectionStep } from './ClaimApproverSelectionStep'; +import { useAuth } from '@/contexts/AuthContext'; interface ClaimManagementWizardProps { onBack?: () => void; @@ -33,20 +36,29 @@ interface ClaimManagementWizardProps { } const CLAIM_TYPES = [ - 'Marketing Activity', - 'Promotional Event', - 'Dealer Training', - 'Infrastructure Development', - 'Customer Experience Initiative', - 'Service Campaign' + '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 [dealers, setDealers] = useState([]); const [loadingDealers, setLoadingDealers] = useState(true); @@ -64,7 +76,16 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar requestDescription: '', periodStartDate: undefined as Date | undefined, periodEndDate: undefined as Date | undefined, - estimatedBudget: '' + estimatedBudget: '', + // Approvers array for all 8 steps + approvers: [] as Array<{ + email: string; + name?: string; + userId?: string; + level: number; + tat?: number | string; + tatType?: 'hours' | 'days'; + }> }); const totalSteps = STEP_NAMES.length; @@ -87,7 +108,28 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar }, []); const updateFormData = (field: string, value: any) => { - setFormData(prev => ({ ...prev, [field]: value })); + 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 = () => { @@ -101,6 +143,12 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar 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 || []; + const step3Approver = approvers.find((a: any) => a.level === 3); + // Step 8 is now a system step, no validation needed + return step3Approver?.email && step3Approver?.userId && step3Approver?.tat; + case 3: return true; default: return false; @@ -108,7 +156,28 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar }; const nextStep = () => { - if (currentStep < totalSteps && isStepValid()) { + if (currentStep < totalSteps) { + if (!isStepValid()) { + // Show specific error messages for step 2 (approver selection) + if (currentStep === 2) { + const approvers = formData.approvers || []; + const step3Approver = approvers.find((a: any) => a.level === 3); + const missingSteps: string[] = []; + + if (!step3Approver?.email || !step3Approver?.userId || !step3Approver?.tat) { + missingSteps.push('Step 3: 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); } }; @@ -149,57 +218,8 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar 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' - } - ] + // Pass approvers array to backend + approvers: formData.approvers || [] }; // Don't show toast here - let the parent component handle success/error after API call @@ -372,6 +392,8 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar selected={formData.periodStartDate} onSelect={(date) => updateFormData('periodStartDate', date)} initialFocus + // Maximum date is the end date (if selected) + toDate={formData.periodEndDate || undefined} /> @@ -384,6 +406,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar