import { useState, useRef, useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { searchUsers, ensureUserExists, type UserSummary } from '@/services/userApi'; import { createWorkflowMultipart, submitWorkflow, getWorkflowDetails, updateWorkflow, updateWorkflowMultipart, getDocumentPreviewUrl, downloadDocument } from '@/services/workflowApi'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Badge } from '@/components/ui/badge'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Separator } from '@/components/ui/separator'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { motion, AnimatePresence } from 'framer-motion'; import { TemplateSelectionModal } from '@/components/modals/TemplateSelectionModal'; import { FilePreview } from '@/components/common/FilePreview'; import { ArrowLeft, ArrowRight, Upload, X, User, Clock, FileText, Check, Users, Zap, Shield, Target, Flame, TrendingUp, DollarSign, AlertCircle, CheckCircle, Info, Rocket, Plus, Minus, Eye, Lightbulb, Settings, Loader2 } from 'lucide-react'; import { format } from 'date-fns'; import { useAuth } from '@/contexts/AuthContext'; import { getAllConfigurations, AdminConfiguration } from '@/services/adminApi'; import { toast } from 'sonner'; interface CreateRequestProps { onBack?: () => void; onSubmit?: (requestData: any) => void; requestId?: string; // For edit mode isEditMode?: boolean; // Flag to indicate edit mode } interface RequestTemplate { id: string; name: string; description: string; category: string; icon: React.ComponentType; estimatedTime: string; commonApprovers: string[]; suggestedSLA: number; priority: 'high' | 'medium' | 'low'; fields: { amount?: boolean; vendor?: boolean; timeline?: boolean; impact?: boolean; }; } const REQUEST_TEMPLATES: RequestTemplate[] = [ { id: 'custom', name: 'Custom Request', description: 'Create a custom request for unique business needs with full flexibility to define your own workflow and requirements', category: 'General', icon: Lightbulb, estimatedTime: 'Variable', commonApprovers: [], suggestedSLA: 3, priority: 'medium', fields: {} }, { id: 'existing-template', name: 'Existing Template', description: 'Use a pre-configured template with predefined approval workflows, timelines, and requirements for faster processing', category: 'Templates', icon: FileText, estimatedTime: '1-2 days', commonApprovers: ['Department Head', 'Manager'], suggestedSLA: 2, priority: 'medium', fields: { timeline: true } } ]; const MOCK_USERS = [ { id: '1', name: 'Mike Johnson', role: 'Team Lead', avatar: 'MJ', department: 'Operations', email: 'mike.johnson@royalenfield.com', level: 2, canClose: false }, { id: '2', name: 'Lisa Wong', role: 'Finance Manager', avatar: 'LW', department: 'Finance', email: 'lisa.wong@royalenfield.com', level: 3, canClose: false }, { id: '3', name: 'David Kumar', role: 'Department Head', avatar: 'DK', department: 'IT', email: 'david.kumar@royalenfield.com', level: 4, canClose: true }, { id: '4', name: 'Anna Smith', role: 'Marketing Coordinator', avatar: 'AS', department: 'Marketing', email: 'anna.smith@royalenfield.com', level: 1, canClose: false }, { id: '5', name: 'John Doe', role: 'Budget Analyst', avatar: 'JD', department: 'Finance', email: 'john.doe@royalenfield.com', level: 2, canClose: false }, { id: '6', name: 'Sarah Chen', role: 'Marketing Manager', avatar: 'SC', department: 'Marketing', email: 'sarah.chen@royalenfield.com', level: 3, canClose: false }, { id: '7', name: 'Emily Davis', role: 'Creative Director', avatar: 'ED', department: 'Marketing', email: 'emily.davis@royalenfield.com', level: 3, canClose: false }, { id: '8', name: 'Robert Kim', role: 'Legal Counsel', avatar: 'RK', department: 'Legal', email: 'robert.kim@royalenfield.com', level: 4, canClose: true }, { id: '9', name: 'Jennifer Lee', role: 'CEO', avatar: 'JL', department: 'Executive', email: 'jennifer.lee@royalenfield.com', level: 5, canClose: true }, { id: '10', name: 'Michael Brown', role: 'CFO', avatar: 'MB', department: 'Finance', email: 'michael.brown@royalenfield.com', level: 5, canClose: true } ]; // User levels - keeping for future use // const USER_LEVELS = [ // { level: 1, name: 'Junior Level', description: 'Junior staff and coordinators', color: 'bg-gray-100 text-gray-800' }, // { level: 2, name: 'Mid Level', description: 'Team leads and supervisors', color: 'bg-blue-100 text-blue-800' }, // { level: 3, name: 'Senior Level', description: 'Managers and senior staff', color: 'bg-green-100 text-green-800' }, // { level: 4, name: 'Executive Level', description: 'Department heads and directors', color: 'bg-orange-100 text-orange-800' }, // { level: 5, name: 'C-Suite Level', description: 'Executive leadership', color: 'bg-purple-100 text-purple-800' } // ]; // SLA Templates - keeping for future use // const SLA_TEMPLATES = [ // { id: 'urgent', name: 'Urgent', hours: 4, description: 'Critical business impact', color: 'bg-red-100 text-red-800' }, // { id: 'high', name: 'High Priority', hours: 24, description: 'High business impact', color: 'bg-orange-100 text-orange-800' }, // { id: 'medium', name: 'Medium Priority', hours: 72, description: 'Moderate business impact', color: 'bg-yellow-100 text-yellow-800' }, // { id: 'low', name: 'Low Priority', hours: 120, description: 'Low business impact', color: 'bg-green-100 text-green-800' }, // { id: 'custom', name: 'Custom SLA', hours: 0, description: 'Define your own timeline', color: 'bg-blue-100 text-blue-800' } // ]; const STEP_NAMES = [ 'Template Selection', 'Basic Information', 'Approval Workflow', 'Participants & Access', 'Documents & Attachments', 'Review & Submit' ]; export function CreateRequest({ onBack, onSubmit, requestId: propRequestId, isEditMode = false }: CreateRequestProps) { const params = useParams<{ requestId: string }>(); const editRequestId = params.requestId || propRequestId || ''; const isEditing = isEditMode && !!editRequestId; const { user } = useAuth(); const [currentStep, setCurrentStep] = useState(1); const [selectedTemplate, setSelectedTemplate] = useState(null); const [emailInput, setEmailInput] = useState(''); const [showTemplateModal, setShowTemplateModal] = useState(false); const [newUserData, setNewUserData] = useState({ name: '', email: '', role: '', department: '', level: 1 }); // Approver email search state const [userSearchResults, setUserSearchResults] = useState>({}); const [userSearchLoading, setUserSearchLoading] = useState>({}); const searchTimers = useRef>({}); // Participants search state (spectators & CC) const [spectatorSearchResults, setSpectatorSearchResults] = useState([]); const [spectatorSearchLoading, setSpectatorSearchLoading] = useState(false); const spectatorTimer = useRef(null); const fileInputRef = useRef(null); const [formData, setFormData] = useState({ // Template and basic info template: '', title: '', description: '', category: '', // Details priority: '', urgency: '', businessImpact: '', amount: '', currency: 'USD', vendor: '', timeline: '', // SLA and dates slaTemplate: '', slaHours: 0, customSlaHours: 0, slaEndDate: undefined as Date | undefined, expectedCompletionDate: undefined as Date | undefined, breachEscalation: true, reminderSchedule: '50' as '25' | '50' | '75', // Workflow workflowType: 'sequential' as 'sequential' | 'parallel', requiresAllApprovals: true, escalationEnabled: true, reminderEnabled: true, minimumLevel: 1, maxLevel: 1, // Participants approvers: [] as any[], approverCount: 1, spectators: [] as any[], ccList: [] as any[], invitedUsers: [] as any[], // Access settings allowComments: true, allowDocumentUpload: true, // Documents documents: [] as File[], // Additional metadata tags: [] as string[], relatedRequests: [] as string[], costCenter: '', project: '' }); const totalSteps = STEP_NAMES.length; const [loadingDraft, setLoadingDraft] = useState(isEditing); const [submitting, setSubmitting] = useState(false); const [savingDraft, setSavingDraft] = useState(false); const [existingDocuments, setExistingDocuments] = useState([]); // Track documents from backend const [documentsToDelete, setDocumentsToDelete] = useState([]); // Track document IDs to delete const [previewDocument, setPreviewDocument] = useState<{ fileName: string; fileType: string; fileUrl: string; fileSize?: number; file?: File; documentId?: string } | null>(null); // Document policy state const [documentPolicy, setDocumentPolicy] = useState<{ maxFileSizeMB: number; allowedFileTypes: string[]; }>({ maxFileSizeMB: 10, allowedFileTypes: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'jpg', 'jpeg', 'png', 'gif'] }); // Document validation error modal const [documentErrorModal, setDocumentErrorModal] = useState<{ open: boolean; errors: Array<{ fileName: string; reason: string }>; }>({ open: false, errors: [] }); // Validation modal states const [validationModal, setValidationModal] = useState<{ open: boolean; type: 'error' | 'self-assign' | 'not-found'; email: string; message: string; }>({ open: false, type: 'error', email: '', message: '' }); // Fetch document policy on mount useEffect(() => { const loadDocumentPolicy = async () => { try { const configs = await getAllConfigurations('DOCUMENT_POLICY'); const configMap: Record = {}; configs.forEach((c: AdminConfiguration) => { configMap[c.configKey] = c.configValue; }); const maxFileSizeMB = parseInt(configMap['MAX_FILE_SIZE_MB'] || '10'); const allowedFileTypesStr = configMap['ALLOWED_FILE_TYPES'] || 'pdf,doc,docx,xls,xlsx,ppt,pptx,jpg,jpeg,png,gif'; const allowedFileTypes = allowedFileTypesStr.split(',').map(ext => ext.trim().toLowerCase()); setDocumentPolicy({ maxFileSizeMB, allowedFileTypes }); } catch (error) { console.error('Failed to load document policy:', error); // Use defaults if loading fails } }; loadDocumentPolicy(); }, []); // Fetch draft data when in edit mode useEffect(() => { if (!isEditing || !editRequestId) return; let mounted = true; (async () => { try { setLoadingDraft(true); const details = await getWorkflowDetails(editRequestId); if (!mounted || !details) return; const wf = details.workflow || {}; const approvals = Array.isArray(details.approvals) ? details.approvals : []; const participants = Array.isArray(details.participants) ? details.participants : []; const documents = Array.isArray(details.documents) ? details.documents.filter((d: any) => !d.isDeleted) : []; // Store existing documents for tracking setExistingDocuments(documents); // Map priority const priority = (wf.priority || '').toString().toLowerCase(); const priorityMap: Record = { 'standard': 'standard', 'express': 'express' }; // Map template type const templateType = wf.templateType === 'TEMPLATE' ? 'existing-template' : 'custom'; const template = REQUEST_TEMPLATES.find(t => t.id === templateType) || REQUEST_TEMPLATES[0] || null; setSelectedTemplate(template); // Map approvers const mappedApprovers = approvals .sort((a: any, b: any) => (a.levelNumber || 0) - (b.levelNumber || 0)) .map((approval: any) => { const tatHours = Number(approval.tatHours || 24); const tatDays = Math.floor(tatHours / 24); const tatHoursRemainder = tatHours % 24; return { id: approval.approverId || `temp-${approval.levelNumber}`, name: approval.approverName || approval.approverEmail || '', email: approval.approverEmail || '', role: approval.levelName || `Level ${approval.levelNumber}`, department: '', avatar: (approval.approverName || approval.approverEmail || 'XX').substring(0, 2).toUpperCase(), level: approval.levelNumber || 1, canClose: false, tat: tatDays > 0 ? tatDays : tatHoursRemainder, tatType: tatDays > 0 ? 'days' as const : 'hours' as const, userId: approval.approverId }; }); // Map spectators - check both participantType and participant_type fields // Debug: log participants to understand the structure console.log('Loading draft - participants:', participants); const mappedSpectators = participants .filter((p: any) => { // Check multiple possible field names for participantType const pt = (p.participantType || p.participant_type || '').toString().toUpperCase().trim(); const isSpectator = pt === 'SPECTATOR'; if (!isSpectator) { return false; } // Also ensure we have at least an email const hasEmail = !!(p.userEmail || p.user_email || p.email); if (!hasEmail) { console.warn('Skipping spectator without email:', p); } return hasEmail; }) .map((p: any, index: number) => { // Use userId if available, otherwise generate a stable unique ID const userId = p.userId || p.user_id || p.id; const userName = p.userName || p.user_name || p.name || ''; const userEmail = p.userEmail || p.user_email || p.email || ''; // Generate avatar from name or email const avatarText = userName || userEmail || 'XX'; const avatar = avatarText .split(' ') .map((s: string) => s[0]) .filter(Boolean) .join('') .slice(0, 2) .toUpperCase(); return { id: userId || `spectator-${editRequestId}-${index}-${Date.now()}`, userId: userId, // Keep userId separate for reference name: userName || userEmail || 'Spectator', email: userEmail, role: 'Spectator', department: p.department || '', avatar: avatar, level: 1, canClose: false }; }); // Debug: log mapped spectators console.log('Mapped spectators:', mappedSpectators); // Update form data setFormData(prev => ({ ...prev, template: templateType, title: wf.title || '', description: wf.description || '', priority: priorityMap[priority] || 'standard', approvers: mappedApprovers, approverCount: mappedApprovers.length || 1, spectators: mappedSpectators, maxLevel: Math.max(...mappedApprovers.map((a: any) => a.level || 1), 1) })); // Skip template selection step if editing setCurrentStep(2); } catch (error) { console.error('Failed to load draft:', error); } finally { if (mounted) setLoadingDraft(false); } })(); return () => { mounted = false; }; }, [isEditing, editRequestId]); const updateFormData = (field: string, value: any) => { setFormData(prev => ({ ...prev, [field]: value })); }; const validateEmail = (email: string) => { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); }; const createUserFromEmail = (email: string, name: string = '', role: string = '', department: string = '', level: number = 1) => { const avatar = name ? name.split(' ').map(n => (n?.[0] || '')).join('').toUpperCase() : email.split('@')[0]?.substring(0, 2).toUpperCase() || 'XX'; return { id: `temp-${Date.now()}-${Math.random()}`, name: name || email.split('@')[0], email, role: role || 'External User', department: department || 'External', avatar, level, canClose: level >= 4, isInvited: true }; }; const getPriorityIcon = (priority: string) => { switch (priority) { case 'high': return ; case 'medium': return ; case 'low': return ; default: return ; } }; const isStepValid = () => { switch (currentStep) { case 1: return selectedTemplate !== null; case 2: return formData.title.trim() !== '' && formData.description.trim() !== '' && formData.priority !== ''; case 3: return (formData.approverCount || 1) > 0 && formData.approvers.length === (formData.approverCount || 1) && formData.approvers.every(approver => { if (!approver || !approver.email) return false; // Check if email is valid if (!validateEmail(approver.email)) return false; // Check if approver has a userId (selected via @ search) // If no userId, it means they manually typed the email if (!approver.userId) { // Will be validated and ensured when moving to next step return true; // Allow for now, will validate on next step } // Check TAT validation based on type const tatType = approver.tatType || 'hours'; if (tatType === 'hours') { return approver.tat && approver.tat > 0 && approver.tat <= 720; } else if (tatType === 'days') { return approver.tat && approver.tat > 0 && approver.tat <= 30; } return false; }); case 4: return true; // Participants are optional except approvers case 5: return true; // Documents are optional case 6: return true; // Review & Submit default: return false; } }; const nextStep = async () => { if (!isStepValid()) return; // Scroll to top on mobile to ensure buttons are visible if (window.innerWidth < 640) { window.scrollTo({ top: 0, behavior: 'smooth' }); } // Special validation when leaving step 3 (Approval Workflow) if (currentStep === 3) { // Validate and ensure all approvers with manually entered emails exist const approversToValidate = formData.approvers.filter(a => a && a.email && !a.userId); if (approversToValidate.length > 0) { try { // Show loading state (optional - can be added later) const updatedApprovers = [...formData.approvers]; for (let i = 0; i < updatedApprovers.length; i++) { const approver = updatedApprovers[i]; // Skip if already has userId (selected via @ search) if (approver.userId) continue; // Skip if no email if (!approver.email) continue; // Check if this email is the initiator's email const initiatorEmail = (user as any)?.email?.toLowerCase(); if (approver.email.toLowerCase() === initiatorEmail) { setValidationModal({ open: true, type: 'self-assign', email: approver.email, message: '' }); return; // Stop navigation } // Search for the user by email in Okta directory try { const response = await searchUsers(approver.email, 1); // Backend returns { success: true, data: [...users] } const searchResults = response.data?.data || []; if (searchResults.length === 0) { // User NOT found in Okta directory setValidationModal({ open: true, type: 'not-found', email: approver.email, message: '' }); return; // Stop navigation - user must fix the email } else { // User found in Okta - ensure they exist in our DB and get userId const foundUser = searchResults[0]; if (!foundUser) { setValidationModal({ open: true, type: 'error', email: approver.email, message: 'Could not retrieve user details. Please try again.' }); return; } // Ensure user exists in our database (creates if needed) const dbUser = await ensureUserExists({ userId: foundUser.userId, email: foundUser.email, displayName: foundUser.displayName, firstName: foundUser.firstName, lastName: foundUser.lastName, department: foundUser.department }); // Update approver with DB userId and full details updatedApprovers[i] = { ...approver, userId: dbUser.userId, name: dbUser.displayName || approver.name, department: dbUser.department || approver.department, avatar: (dbUser.displayName || dbUser.email).substring(0, 2).toUpperCase() }; console.log(`✅ Validated approver ${i + 1}: ${dbUser.displayName} (${dbUser.email})`); } } catch (error) { console.error(`Failed to validate approver ${approver.email}:`, error); setValidationModal({ open: true, type: 'error', email: approver.email, message: `Failed to validate user. Please try again or select a different user.` }); return; // Stop navigation } } // Update form data with validated approvers updateFormData('approvers', updatedApprovers); } catch (error) { console.error('Failed to validate approvers:', error); setValidationModal({ open: true, type: 'error', email: '', message: 'Failed to validate approvers. Please try again.' }); return; // Stop navigation } } } // Proceed to next step if (currentStep < totalSteps) { setCurrentStep(currentStep + 1); } }; const prevStep = () => { if (currentStep > 1) { setCurrentStep(currentStep - 1); // Scroll to top on mobile to ensure content is visible if (window.innerWidth < 640) { window.scrollTo({ top: 0, behavior: 'smooth' }); } } }; const selectTemplate = (template: RequestTemplate) => { setSelectedTemplate(template); updateFormData('template', template.id); updateFormData('category', template.category); updateFormData('priority', template.priority); // Auto-suggest SLA const suggestedDate = new Date(); suggestedDate.setDate(suggestedDate.getDate() + template.suggestedSLA); updateFormData('slaEndDate', suggestedDate); // If user selected "Existing Template", show the template selection modal if (template.id === 'existing-template') { setShowTemplateModal(true); } }; const handleTemplateSelection = (templateId: string) => { // This will be handled by routing to the specific template wizard in App.tsx if (onSubmit) { onSubmit({ templateType: templateId }); } }; const addUser = (user: any, type: 'approvers' | 'spectators' | 'ccList' | 'invitedUsers') => { const userEmail = (user.email || '').toLowerCase(); const currentList = formData[type]; // Check if user is already in the target list if (currentList.find((u: any) => u.id === user.id || (u.email || '').toLowerCase() === userEmail)) { setValidationModal({ open: true, type: 'error', email: user.email, message: `${user.name || user.email} is already added as ${type.slice(0, -1)}.` }); return; } // Prevent adding same user in different roles if (type === 'spectators') { // Check if user is already an approver const isApprover = formData.approvers.find((a: any) => a.id === user.id || (a.email || '').toLowerCase() === userEmail ); if (isApprover) { setValidationModal({ open: true, type: 'error', email: user.email, message: `${user.name || user.email} is already an approver and cannot be added as a spectator.` }); return; } } else if (type === 'approvers') { // Check if user is already a spectator const isSpectator = formData.spectators.find((s: any) => s.id === user.id || (s.email || '').toLowerCase() === userEmail ); if (isSpectator) { setValidationModal({ open: true, type: 'error', email: user.email, message: `${user.name || user.email} is already a spectator and cannot be added as an approver.` }); return; } } // Add user to the list const updatedList = [...currentList, user]; updateFormData(type, updatedList); // Update max level if adding approver if (type === 'approvers') { const maxApproverLevel = Math.max(...updatedList.map((a: any) => a.level), 0); updateFormData('maxLevel', maxApproverLevel); } }; const removeUser = (userId: string, type: 'approvers' | 'spectators' | 'ccList' | 'invitedUsers') => { const currentList = formData[type]; const updatedList = currentList.filter((u: any) => u.id !== userId); updateFormData(type, updatedList); // Update max level if removing approver if (type === 'approvers') { const maxApproverLevel = Math.max(...updatedList.map((a: any) => a.level), 0); updateFormData('maxLevel', maxApproverLevel); } }; const addUserByEmail = () => { if (!validateEmail(emailInput)) return; // Check if user already exists const existingUser = [...MOCK_USERS, ...formData.invitedUsers].find(u => u.email === emailInput); if (existingUser) { setEmailInput(''); return existingUser; } // Create new user const newUser = createUserFromEmail( emailInput, newUserData.name, newUserData.role, newUserData.department, newUserData.level ); addUser(newUser, 'invitedUsers'); setEmailInput(''); setNewUserData({ name: '', email: '', role: '', department: '', level: 1 }); return newUser; }; const inviteAndAddUser = async (type: 'approvers' | 'spectators' | 'ccList') => { // For spectators, validate against Okta before adding if (type === 'spectators' && emailInput) { // Check if this email is the initiator's email const initiatorEmail = (user as any)?.email?.toLowerCase(); if (emailInput.toLowerCase() === initiatorEmail) { setValidationModal({ open: true, type: 'self-assign', email: emailInput, message: 'You cannot add yourself as a spectator.' }); return; } // Search for user in Okta directory try { const response = await searchUsers(emailInput, 1); // Backend returns { success: true, data: [...users] } const searchResults = response.data?.data || []; if (searchResults.length === 0) { // User NOT found in Okta directory setValidationModal({ open: true, type: 'not-found', email: emailInput, message: '' }); return; } // User found in Okta - ensure they exist in DB const foundUser = searchResults[0]; if (!foundUser) { setValidationModal({ open: true, type: 'error', email: emailInput, message: 'Could not retrieve user details. Please try again.' }); return; } const dbUser = await ensureUserExists({ userId: foundUser.userId, email: foundUser.email, displayName: foundUser.displayName, firstName: foundUser.firstName, lastName: foundUser.lastName, department: foundUser.department }); // Create spectator object with verified data const spectator = { id: dbUser.userId, userId: dbUser.userId, name: dbUser.displayName || dbUser.email.split('@')[0], email: dbUser.email, avatar: (dbUser.displayName || dbUser.email).substring(0, 2).toUpperCase(), role: 'Spectator', department: dbUser.department || '', level: 1, canClose: false }; // Add spectator addUser(spectator, 'spectators'); setEmailInput(''); return; } catch (error) { console.error('Failed to validate spectator:', error); setValidationModal({ open: true, type: 'error', email: emailInput, message: 'Failed to validate user. Please try again.' }); return; } } // For other types (deprecated flow) const userObj = addUserByEmail(); if (userObj) { addUser(userObj, type); } }; const validateFile = (file: File): { valid: boolean; reason?: string } => { // Check file size const maxSizeBytes = documentPolicy.maxFileSizeMB * 1024 * 1024; if (file.size > maxSizeBytes) { return { valid: false, reason: `File size exceeds the maximum allowed size of ${documentPolicy.maxFileSizeMB}MB. Current size: ${(file.size / (1024 * 1024)).toFixed(2)}MB` }; } // Check file extension const fileName = file.name.toLowerCase(); const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1); if (!documentPolicy.allowedFileTypes.includes(fileExtension)) { return { valid: false, reason: `File type "${fileExtension}" is not allowed. Allowed types: ${documentPolicy.allowedFileTypes.join(', ')}` }; } return { valid: true }; }; const handleFileUpload = (event: React.ChangeEvent) => { const files = Array.from(event.target.files || []); if (files.length === 0) return; // Validate all files const validationErrors: Array<{ fileName: string; reason: string }> = []; const validFiles: File[] = []; files.forEach(file => { const validation = validateFile(file); if (!validation.valid) { validationErrors.push({ fileName: file.name, reason: validation.reason || 'Unknown validation error' }); } else { validFiles.push(file); } }); // If there are validation errors, show modal if (validationErrors.length > 0) { setDocumentErrorModal({ open: true, errors: validationErrors }); } // Add only valid files if (validFiles.length > 0) { updateFormData('documents', [...formData.documents, ...validFiles]); if (validFiles.length < files.length) { toast.warning(`${validFiles.length} of ${files.length} file(s) were added. ${validationErrors.length} file(s) were rejected.`); } else { toast.success(`${validFiles.length} file(s) added successfully`); } } // Reset file input if (event.target) { event.target.value = ''; } }; const handleSubmit = async () => { if (!isStepValid() || submitting || savingDraft) return; setSubmitting(true); // Participants mapping const initiatorId = user?.userId || ''; const initiatorEmail = (user as any)?.email || ''; const initiatorName = (user as any)?.displayName || (user as any)?.name || initiatorEmail.split('@')[0] || 'Initiator'; const participants = [ { userId: initiatorId, userEmail: initiatorEmail, userName: initiatorName, participantType: 'INITIATOR' as const, canComment: true, canViewDocuments: true, canDownloadDocuments: true, notificationEnabled: true, addedBy: initiatorId, }, // Approvers -> map to participants (APPROVER) ...((formData.approvers || []).filter((a: any) => a?.email).map((a: any) => ({ userId: a.userId || undefined, userEmail: a.email, userName: a.name || a.email.split('@')[0], participantType: 'APPROVER' as const, canComment: true, canViewDocuments: true, canDownloadDocuments: true, notificationEnabled: true, addedBy: initiatorId, })) as any[]), // Spectators -> map to participants (SPECTATOR) ...((formData.spectators || []).filter((s: any) => s?.email).map((s: any) => ({ userId: s.id || undefined, userEmail: s.email, userName: s.name || s.email.split('@')[0], participantType: 'SPECTATOR' as const, canComment: true, canViewDocuments: true, canDownloadDocuments: true, notificationEnabled: true, addedBy: initiatorId, })) as any[]), ]; // Ensure approver userIds are present (selected via @ lookup) const hasMissingApproverIds = (formData.approvers || []).slice(0, formData.approverCount || 1) .some((a: any) => !a?.userId || !a?.email); if (hasMissingApproverIds) { alert('Please select approvers using @ search so we can capture their user IDs.'); return; } const payload = { templateId: selectedTemplate?.id || null, templateType: selectedTemplate?.id === 'custom' ? 'CUSTOM' as const : 'TEMPLATE' as const, title: formData.title, description: formData.description, priorityUi: (formData.priority === 'express' ? 'express' : 'standard') as 'express' | 'standard', approverCount: formData.approverCount || 1, approvers: (formData.approvers || []).map((a: any) => ({ userId: a?.userId || '', email: a?.email || '', name: a?.name, tat: a?.tat || '', tatType: a?.tatType || 'hours', })), spectators: (formData.spectators || []) .filter((s: any) => { const approverIds = (formData.approvers || []).map((a: any) => a?.userId).filter(Boolean); const approverEmails = (formData.approvers || []).map((a: any) => a?.email?.toLowerCase?.()).filter(Boolean); return !approverIds.includes(s?.id) && !approverEmails.includes((s?.email || '').toLowerCase()); }) .map((s: any) => ({ userId: s?.id || '', name: s?.name || s?.email?.split('@')?.[0] || 'Spectator', email: s?.email || '', })), ccList: (formData.ccList || []).map((c: any) => ({ id: c?.id, name: c?.name || c?.email?.split('@')?.[0] || 'CC', email: c?.email || '', })), // Backend service supports participants field (optional) participants, }; // Handle edit mode - update existing draft with full structure if (isEditing && editRequestId) { // Build approval levels const approvalLevels = (formData.approvers || []).slice(0, formData.approverCount || 1).map((a: any, index: number) => { const tat = typeof a.tat === 'number' ? a.tat : 0; const tatHours = a.tatType === 'days' ? tat * 24 : tat || 24; return { levelNumber: index + 1, levelName: `Level ${index + 1}`, approverId: a.userId || '', approverEmail: a.email || '', approverName: a.name || a.email?.split('@')[0] || `Approver ${index + 1}`, tatHours: tatHours, isFinalApprover: index + 1 === (formData.approverCount || 1) }; }); // Build participants const participants = [ { userId: user?.userId || '', userEmail: (user as any)?.email || '', userName: (user as any)?.displayName || (user as any)?.name || (user as any)?.email?.split('@')[0] || 'Initiator', participantType: 'INITIATOR' as const, canComment: true, canViewDocuments: true, canDownloadDocuments: true, notificationEnabled: true, }, ...((formData.approvers || []).filter((a: any) => a?.email && a?.userId).map((a: any) => ({ userId: a.userId, userEmail: a.email, userName: a.name || a.email.split('@')[0], participantType: 'APPROVER' as const, canComment: true, canViewDocuments: true, canDownloadDocuments: true, notificationEnabled: true, })) as any[]), ...((formData.spectators || []).filter((s: any) => s?.email).map((s: any) => ({ userId: s.id || s.userId || undefined, userEmail: s.email, userName: s.name || s.email.split('@')[0], participantType: 'SPECTATOR' as const, canComment: true, canViewDocuments: true, canDownloadDocuments: true, notificationEnabled: true, })) as any[]), ]; // Build update payload const updatePayload = { title: formData.title, description: formData.description, priority: formData.priority === 'express' ? 'EXPRESS' : 'STANDARD', approvalLevels: approvalLevels, participants: participants, deleteDocumentIds: documentsToDelete.length > 0 ? documentsToDelete : undefined, }; // Determine if we need multipart (new files or deletions) const hasNewFiles = formData.documents && formData.documents.length > 0; const hasDeletions = documentsToDelete.length > 0; if (hasNewFiles || hasDeletions) { // Use multipart update try { await updateWorkflowMultipart(editRequestId, updatePayload, formData.documents || [], documentsToDelete); // Submit the updated workflow try { await submitWorkflow(editRequestId); onSubmit?.({ ...formData, backendId: editRequestId, template: selectedTemplate }); } catch (err) { console.error('Failed to submit workflow:', err); setSubmitting(false); } } catch (err) { console.error('Failed to update workflow:', err); setSubmitting(false); } } else { // Use regular update try { await updateWorkflow(editRequestId, updatePayload); // Submit the updated workflow try { await submitWorkflow(editRequestId); onSubmit?.({ ...formData, backendId: editRequestId, template: selectedTemplate }); } catch (err) { console.error('Failed to submit workflow:', err); setSubmitting(false); } } catch (err) { console.error('Failed to update workflow:', err); setSubmitting(false); } } return; } // Create new workflow try { const res = await createWorkflowMultipart(payload as any, formData.documents || [], 'SUPPORTING'); const id = (res as any).id; try { await submitWorkflow(id); onSubmit?.({ ...formData, backendId: id, template: selectedTemplate }); } catch (err) { console.error('Failed to submit workflow:', err); setSubmitting(false); } } catch (err) { console.error('Failed to create workflow:', err); setSubmitting(false); } }; const handleSaveDraft = async () => { // Same payload as submit, but do NOT call submit endpoint if (!selectedTemplate || !formData.title.trim() || !formData.description.trim() || !formData.priority) { // allow minimal validation for draft: require title/description/priority/template return; } if (submitting || savingDraft) return; setSavingDraft(true); // Handle edit mode - update existing draft with full structure if (isEditing && editRequestId) { // Build approval levels const approvalLevels = (formData.approvers || []).slice(0, formData.approverCount || 1).map((a: any, index: number) => { const tat = typeof a.tat === 'number' ? a.tat : 0; const tatHours = a.tatType === 'days' ? tat * 24 : tat || 24; return { levelNumber: index + 1, levelName: `Level ${index + 1}`, approverId: a.userId || '', approverEmail: a.email || '', approverName: a.name || a.email?.split('@')[0] || `Approver ${index + 1}`, tatHours: tatHours, isFinalApprover: index + 1 === (formData.approverCount || 1) }; }); // Build participants const participants = [ { userId: user?.userId || '', userEmail: (user as any)?.email || '', userName: (user as any)?.displayName || (user as any)?.name || (user as any)?.email?.split('@')[0] || 'Initiator', participantType: 'INITIATOR' as const, canComment: true, canViewDocuments: true, canDownloadDocuments: true, notificationEnabled: true, }, ...((formData.approvers || []).filter((a: any) => a?.email && a?.userId).map((a: any) => ({ userId: a.userId, userEmail: a.email, userName: a.name || a.email.split('@')[0], participantType: 'APPROVER' as const, canComment: true, canViewDocuments: true, canDownloadDocuments: true, notificationEnabled: true, })) as any[]), ...((formData.spectators || []).filter((s: any) => s?.email).map((s: any) => ({ userId: s.id || s.userId || undefined, userEmail: s.email, userName: s.name || s.email.split('@')[0], participantType: 'SPECTATOR' as const, canComment: true, canViewDocuments: true, canDownloadDocuments: true, notificationEnabled: true, })) as any[]), ]; // Build update payload const updatePayload = { title: formData.title, description: formData.description, priority: formData.priority === 'express' ? 'EXPRESS' : 'STANDARD', approvalLevels: approvalLevels, participants: participants, deleteDocumentIds: documentsToDelete.length > 0 ? documentsToDelete : undefined, }; // Determine if we need multipart (new files or deletions) const hasNewFiles = formData.documents && formData.documents.length > 0; const hasDeletions = documentsToDelete.length > 0; if (hasNewFiles || hasDeletions) { // Use multipart update try { await updateWorkflowMultipart(editRequestId, updatePayload, formData.documents || [], documentsToDelete); onSubmit?.({ ...formData, backendId: editRequestId, template: selectedTemplate }); } catch (err) { console.error('Failed to update draft:', err); setSavingDraft(false); } } else { // Use regular update try { await updateWorkflow(editRequestId, updatePayload); onSubmit?.({ ...formData, backendId: editRequestId, template: selectedTemplate }); } catch (err) { console.error('Failed to update draft:', err); setSavingDraft(false); } } return; } // Build participants array for draft (same as handleSubmit) const initiatorId = user?.userId || ''; const initiatorEmail = (user as any)?.email || ''; const initiatorName = (user as any)?.displayName || (user as any)?.name || initiatorEmail.split('@')[0] || 'Initiator'; const participants = [ { userId: initiatorId, userEmail: initiatorEmail, userName: initiatorName, participantType: 'INITIATOR' as const, canComment: true, canViewDocuments: true, canDownloadDocuments: true, notificationEnabled: true, addedBy: initiatorId, }, // Approvers -> map to participants (APPROVER) ...((formData.approvers || []).filter((a: any) => a?.email).map((a: any) => ({ userId: a.userId || undefined, userEmail: a.email, userName: a.name || a.email.split('@')[0], participantType: 'APPROVER' as const, canComment: true, canViewDocuments: true, canDownloadDocuments: true, notificationEnabled: true, addedBy: initiatorId, })) as any[]), // Spectators -> map to participants (SPECTATOR) ...((formData.spectators || []).filter((s: any) => s?.email).map((s: any) => ({ userId: s.id || undefined, userEmail: s.email, userName: s.name || s.email.split('@')[0], participantType: 'SPECTATOR' as const, canComment: true, canViewDocuments: true, canDownloadDocuments: true, notificationEnabled: true, addedBy: initiatorId, })) as any[]), ]; // Create new draft const payload = { templateId: selectedTemplate?.id || null, templateType: selectedTemplate?.id === 'custom' ? 'CUSTOM' as const : 'TEMPLATE' as const, title: formData.title, description: formData.description, priorityUi: (formData.priority === 'express' ? 'express' : 'standard') as 'express' | 'standard', approverCount: formData.approverCount || 1, approvers: (formData.approvers || []).map((a: any) => ({ userId: a?.userId || '', email: a?.email || '', name: a?.name, tat: a?.tat || '', tatType: a?.tatType || 'hours', })), spectators: (formData.spectators || []) .filter((s: any) => { const approverIds = (formData.approvers || []).map((a: any) => a?.userId).filter(Boolean); const approverEmails = (formData.approvers || []).map((a: any) => a?.email?.toLowerCase?.()).filter(Boolean); return !approverIds.includes(s?.id) && !approverEmails.includes((s?.email || '').toLowerCase()); }) .map((s: any) => ({ userId: s?.id || '', name: s?.name || s?.email?.split('@')?.[0] || 'Spectator', email: s?.email || '', })), ccList: (formData.ccList || []).map((c: any) => ({ id: c?.id, name: c?.name || c?.email?.split('@')?.[0] || 'CC', email: c?.email || '', })), participants: participants, // Include participants array for draft }; try { const res = await createWorkflowMultipart(payload as any, formData.documents || [], 'SUPPORTING'); onSubmit?.({ ...formData, backendId: (res as any).id, template: selectedTemplate }); } catch (err) { console.error('Failed to save draft:', err); setSavingDraft(false); } }; // Show loading state while fetching draft data if (loadingDraft) { return (

Loading draft...

); } const renderStepContent = () => { switch (currentStep) { case 1: return ( {/* Header Section */}

Choose Your Request Type

Start with a pre-built template for faster approvals, or create a custom request tailored to your needs.

{/* Template Cards Grid */}
{REQUEST_TEMPLATES.map((template) => ( selectTemplate(template)} >
{selectedTemplate?.id === template.id && (
)}
{template.name}
{template.category} {getPriorityIcon(template.priority)}

{template.description}

{template.estimatedTime}
{template.commonApprovers.length} approvers
))}
{/* Template Details Card - Only show when template is selected */} {selectedTemplate && ( {selectedTemplate.name} - Template Details

{selectedTemplate.suggestedSLA} days

{getPriorityIcon(selectedTemplate.priority)} {selectedTemplate.priority}

{selectedTemplate.estimatedTime}

{selectedTemplate.commonApprovers.map((approver, index) => ( {approver} ))}
)}
); case 2: return (

Basic Information

Provide the essential details for your {selectedTemplate?.name || 'request'}.

Be specific and descriptive. This will be visible to all participants.

updateFormData('title', e.target.value)} className="text-base h-12 border-2 border-gray-300 focus:border-blue-500 bg-white shadow-sm" />

Explain what you need approval for, why it's needed, and any relevant background information.