import { useState, useRef, useEffect } from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Badge } from '@/components/ui/badge'; import { Users, X, AtSign, Clock, Shield, CheckCircle, XCircle, AlertCircle, Lightbulb } from 'lucide-react'; import { searchUsers, ensureUserExists, type UserSummary } from '@/services/userApi'; interface ApprovalLevelInfo { levelNumber: number; approverName: string; status: string; tatHours: number; } interface AddApproverModalProps { open: boolean; onClose: () => void; onConfirm: (email: string, tatHours: number, level: number) => Promise | void; requestIdDisplay?: string; requestTitle?: string; existingParticipants?: Array<{ email: string; participantType: string; name?: string }>; currentLevels?: ApprovalLevelInfo[]; // Current approval levels } export function AddApproverModal({ open, onClose, onConfirm, requestIdDisplay, requestTitle, existingParticipants = [], currentLevels = [] }: AddApproverModalProps) { const [email, setEmail] = useState(''); const [tatHours, setTatHours] = useState(24); const [selectedLevel, setSelectedLevel] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); const [selectedUser, setSelectedUser] = useState(null); // Track if user was selected via @ search const searchTimer = useRef(null); // Validation modal state const [validationModal, setValidationModal] = useState<{ open: boolean; type: 'error' | 'not-found'; email: string; message: string; }>({ open: false, type: 'error', email: '', message: '' }); // Calculate available levels (after completed levels) const completedLevels = currentLevels.filter(l => l && (l.status === 'approved' || l.status === 'rejected' || l.status === 'skipped') ); const minLevel = Math.max(1, completedLevels.length + 1); const maxLevel = Math.max(1, currentLevels.length + 1); const availableLevels = maxLevel >= minLevel ? Array.from({ length: maxLevel - minLevel + 1 }, (_, i) => minLevel + i) : [minLevel]; // Auto-select first available level useEffect(() => { if (availableLevels.length > 0 && selectedLevel === null) { setSelectedLevel(availableLevels[0] || null); } }, [availableLevels.length, selectedLevel]); const handleConfirm = async () => { const emailToAdd = email.trim().toLowerCase(); if (!emailToAdd) { setValidationModal({ open: true, type: 'error', email: '', message: 'Please enter an email address' }); return; } // Basic email validation const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(emailToAdd)) { setValidationModal({ open: true, type: 'error', email: emailToAdd, message: 'Please enter a valid email address' }); return; } // Validate TAT hours if (!tatHours || tatHours <= 0) { setValidationModal({ open: true, type: 'error', email: '', message: 'Please enter valid TAT hours (minimum 1 hour)' }); return; } if (tatHours > 720) { setValidationModal({ open: true, type: 'error', email: '', message: 'TAT hours cannot exceed 720 hours (30 days)' }); return; } // Validate level if (!selectedLevel) { setValidationModal({ open: true, type: 'error', email: '', message: 'Please select an approval level' }); return; } if (selectedLevel < minLevel) { setValidationModal({ open: true, type: 'error', email: '', message: `Cannot add approver at level ${selectedLevel}. Minimum allowed level is ${minLevel} (after completed levels)` }); return; } // Check if user is already a participant const existingParticipant = existingParticipants.find( p => (p.email || '').toLowerCase() === emailToAdd ); if (existingParticipant) { const participantType = existingParticipant.participantType?.toUpperCase() || 'PARTICIPANT'; const userName = existingParticipant.name || emailToAdd; if (participantType === 'INITIATOR') { setValidationModal({ open: true, type: 'error', email: emailToAdd, message: `${userName} is the request initiator and cannot be added as an approver.` }); return; } else if (participantType === 'APPROVER') { setValidationModal({ open: true, type: 'error', email: emailToAdd, message: `${userName} is already an approver on this request.` }); return; } else if (participantType === 'SPECTATOR') { setValidationModal({ open: true, type: 'error', email: emailToAdd, message: `${userName} is currently a spectator on this request and cannot be added as an approver. Please remove them as spectator first.` }); return; } else { setValidationModal({ open: true, type: 'error', email: emailToAdd, message: `${userName} is already a participant on this request.` }); return; } } // If user was NOT selected via @ search, validate against Okta if (!selectedUser || selectedUser.email.toLowerCase() !== emailToAdd) { try { const searchOktaResults = await searchUsers(emailToAdd, 1); if (searchOktaResults.length === 0) { // User not found in Okta setValidationModal({ open: true, type: 'not-found', email: emailToAdd, message: '' }); return; } // User found - ensure they exist in DB const foundUser = searchOktaResults[0]; await ensureUserExists({ userId: foundUser.userId, email: foundUser.email, displayName: foundUser.displayName, firstName: foundUser.firstName, lastName: foundUser.lastName, department: foundUser.department }); console.log(`✅ Validated approver: ${foundUser.displayName} (${foundUser.email})`); } catch (error) { console.error('Failed to validate approver:', error); setValidationModal({ open: true, type: 'error', email: emailToAdd, message: 'Failed to validate user. Please try again.' }); return; } } try { setIsSubmitting(true); await onConfirm(emailToAdd, tatHours, selectedLevel); setEmail(''); setTatHours(24); setSelectedLevel(null); setSelectedUser(null); onClose(); } catch (error) { console.error('Failed to add approver:', error); // Error handling is done in the parent component } finally { setIsSubmitting(false); } }; const handleClose = () => { if (!isSubmitting) { setEmail(''); setTatHours(24); setSelectedLevel(null); setSelectedUser(null); setSearchResults([]); setIsSearching(false); onClose(); } }; // Get status icon const getStatusIcon = (status: string) => { const statusLower = status.toLowerCase(); if (statusLower === 'approved') return ; if (statusLower === 'rejected') return ; if (statusLower === 'skipped') return ; if (statusLower === 'in-review' || statusLower === 'pending') return ; return ; }; // Cleanup search timer on unmount useEffect(() => { return () => { if (searchTimer.current) { clearTimeout(searchTimer.current); } }; }, []); // Handle user search with @ mention const handleEmailChange = (value: string) => { setEmail(value); // Clear selectedUser when manually editing (forces revalidation) if (selectedUser && selectedUser.email.toLowerCase() !== value.toLowerCase()) { setSelectedUser(null); } // Clear existing timer if (searchTimer.current) { clearTimeout(searchTimer.current); } // Only trigger search when using @ sign if (!value || !value.startsWith('@') || value.length < 2) { setSearchResults([]); setIsSearching(false); return; } // Start search with debounce setIsSearching(true); searchTimer.current = setTimeout(async () => { try { const term = value.slice(1); // Remove @ prefix const results = await searchUsers(term, 10); setSearchResults(results); } catch (error) { console.error('Search failed:', error); setSearchResults([]); } finally { setIsSearching(false); } }, 300); }; // Select user from search results const handleSelectUser = async (user: UserSummary) => { // Ensure user exists in DB when selected via @ search try { await ensureUserExists({ userId: user.userId, email: user.email, displayName: user.displayName, firstName: user.firstName, lastName: user.lastName, department: user.department }); setEmail(user.email); setSelectedUser(user); // Track that user was selected via @ search setSearchResults([]); setIsSearching(false); console.log(`✅ User selected and verified: ${user.displayName} (${user.email})`); } catch (error) { console.error('Failed to ensure user exists:', error); setValidationModal({ open: true, type: 'error', email: user.email, message: 'Failed to verify user in database. Please try again.' }); } }; return (
Add Approver
{/* Description */}

Add a new approver at a specific level. Existing approvers at and after the selected level will be shifted down.

{/* Current Levels Display */} {currentLevels.length > 0 && (
{currentLevels.map((level) => (
{level.levelNumber}

{level.approverName}

{level.tatHours}h TAT

{getStatusIcon(level.status)} {level.status}
))}

â„šī¸ New approver can only be added at level {minLevel} or higher (after completed levels)

)} {/* Level Selection */}

Choose where to insert the new approver. Existing levels will be automatically shifted.

{/* TAT Hours Input */}
setTatHours(Number(e.target.value))} className="h-11 border-gray-300 flex-1" disabled={isSubmitting} placeholder="24" />
hours

Maximum time for this approver to respond (1-720 hours)

{/* Email Input with @ Search */}
handleEmailChange(e.target.value)} className="pl-10 h-11 border-gray-300" disabled={isSubmitting} autoFocus /> {/* Search Results Dropdown */} {(isSearching || searchResults.length > 0) && (
{isSearching ? (
Searching users...
) : searchResults.length > 0 ? (
    {searchResults.map((user) => (
  • handleSelectUser(user)} >
    {(user.displayName || user.email) .split(' ') .map(s => s[0]) .join('') .slice(0, 2) .toUpperCase()}

    {user.displayName || [user.firstName, user.lastName].filter(Boolean).join(' ') || user.email}

    {user.email}

    {user.designation && (

    {user.designation}

    )}
  • ))}
) : null}
)}

Type @username to search for users, or enter email directly.

{/* Action Buttons */}
{/* Validation Error Modal */} setValidationModal(prev => ({ ...prev, open: isOpen }))}> {validationModal.type === 'not-found' ? ( <> User Not Found ) : ( <> Validation Error )}
{validationModal.type === 'not-found' && ( <>

User {validationModal.email} was not found in the organization directory.

Please verify:

  • Email address is spelled correctly
  • User exists in Okta/SSO system
  • User has an active account

Tip: Use @ sign to search users from the directory.

)} {validationModal.type === 'error' && ( <> {validationModal.email && (

Failed to validate {validationModal.email}.

)} {validationModal.message && (

{validationModal.message}

)} )}
); }