import { useEffect, useState, useRef } from 'react'; import type { ReactElement } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { Loader2, ChevronRight, ChevronLeft, Image as ImageIcon } from 'lucide-react'; import { Modal, FormField, FormSelect, PrimaryButton, SecondaryButton, MultiselectPaginatedSelect } from '@/components/shared'; import type { Tenant } from '@/types/tenant'; import { moduleService } from '@/services/module-service'; import { fileService } from '@/services/file-service'; import { showToast } from '@/utils/toast'; // Step 1: Tenant Details Schema const tenantDetailsSchema = z.object({ name: z .string() .min(1, 'name is required') .min(3, 'name must be at least 3 characters') .max(100, 'name must be at most 100 characters'), slug: z .string() .min(1, 'slug is required') .min(3, 'slug must be at least 3 characters') .max(100, 'slug must be at most 100 characters') .regex(/^[a-z0-9-]+$/, 'slug format is invalid'), domain: z.string().optional().nullable(), status: z.enum(['active', 'suspended', 'deleted'], { message: 'Status is required', }), subscription_tier: z.enum(['basic', 'professional', 'enterprise'], { message: 'Invalid subscription tier', }).optional().nullable(), max_users: z.number().int().min(1, 'max_users must be at least 1').optional().nullable(), max_modules: z.number().int().min(1, 'max_modules must be at least 1').optional().nullable(), modules: z.array(z.string().uuid()).optional().nullable(), }); // Step 2: Contact Details Schema - NO password fields const contactDetailsSchema = z.object({ email: z.string().min(1, 'Email is required').email('Please enter a valid email address'), first_name: z.string().min(1, 'First name is required'), last_name: z.string().min(1, 'Last name is required'), contact_phone: z .string() .optional() .nullable() .refine( (val) => { if (!val || val.trim() === '') return true; const phoneRegex = /^[\+]?[(]?[0-9]{1,4}[)]?[-\s\.]?[(]?[0-9]{1,4}[)]?[-\s\.]?[0-9]{1,9}[-\s\.]?[0-9]{1,9}$/; return phoneRegex.test(val.replace(/\s/g, '')); }, { message: 'Please enter a valid phone number (e.g., +1234567890, (123) 456-7890, 123-456-7890)', } ), address_line1: z.string().min(1, 'Address is required'), address_line2: z.string().optional().nullable(), city: z.string().min(1, 'City is required'), state: z.string().min(1, 'State is required'), postal_code: z .string() .min(1, 'Postal code is required') .regex(/^[A-Za-z0-9\s\-]{3,10}$/, 'Postal code must be 3-10 characters (letters, numbers, spaces, or hyphens)'), country: z.string().min(1, 'Country is required'), }); // Step 3: Settings Schema const settingsSchema = z.object({ enable_sso: z.boolean(), enable_2fa: z.boolean(), primary_color: z.string().optional().nullable(), secondary_color: z.string().optional().nullable(), accent_color: z.string().optional().nullable(), }); type TenantDetailsForm = z.infer; type ContactDetailsForm = z.infer; type SettingsForm = z.infer; const statusOptions = [ { value: 'active', label: 'Active' }, { value: 'suspended', label: 'Suspended' }, { value: 'deleted', label: 'Deleted' }, ]; const subscriptionTierOptions = [ { value: 'basic', label: 'Basic' }, { value: 'professional', label: 'Professional' }, { value: 'enterprise', label: 'Enterprise' }, ]; interface EditTenantModalProps { isOpen: boolean; onClose: () => void; tenantId: string | null; onLoadTenant: (id: string) => Promise; onSubmit: (id: string, data: any) => Promise; isLoading?: boolean; } export const EditTenantModal = ({ isOpen, onClose, tenantId, onLoadTenant, onSubmit, isLoading = false, }: EditTenantModalProps): ReactElement | null => { const [currentStep, setCurrentStep] = useState(1); const [isLoadingTenant, setIsLoadingTenant] = useState(false); const [loadError, setLoadError] = useState(null); const loadedTenantIdRef = useRef(null); const [selectedModules, setSelectedModules] = useState([]); const [initialModuleOptions, setInitialModuleOptions] = useState>([]); // File upload state for branding const [logoFile, setLogoFile] = useState(null); const [faviconFile, setFaviconFile] = useState(null); const [logoFilePath, setLogoFilePath] = useState(null); const [logoFileUrl, setLogoFileUrl] = useState(null); const [logoPreviewUrl, setLogoPreviewUrl] = useState(null); const [faviconFilePath, setFaviconFilePath] = useState(null); const [faviconFileUrl, setFaviconFileUrl] = useState(null); const [faviconPreviewUrl, setFaviconPreviewUrl] = useState(null); const [isUploadingLogo, setIsUploadingLogo] = useState(false); const [isUploadingFavicon, setIsUploadingFavicon] = useState(false); // Form instances for each step const tenantDetailsForm = useForm({ resolver: zodResolver(tenantDetailsSchema), defaultValues: { name: '', slug: '', domain: '', status: 'active', subscription_tier: null, max_users: null, max_modules: null, modules: [], }, }); const contactDetailsForm = useForm({ resolver: zodResolver(contactDetailsSchema), defaultValues: { email: '', first_name: '', last_name: '', contact_phone: '', address_line1: '', address_line2: '', city: '', state: '', postal_code: '', country: '', }, }); const settingsForm = useForm({ resolver: zodResolver(settingsSchema), defaultValues: { enable_sso: false, enable_2fa: false, primary_color: '#112868', secondary_color: '#23DCE1', accent_color: '#084CC8', }, }); // Load modules for multiselect const loadModules = async (page: number, limit: number) => { const response = await moduleService.getRunningModules(page, limit); return { options: response.data.map((module) => ({ value: module.id, label: module.name, })), pagination: response.pagination, }; }; // Load tenant data when modal opens useEffect(() => { if (isOpen && tenantId) { if (loadedTenantIdRef.current !== tenantId) { const loadTenant = async (): Promise => { try { setIsLoadingTenant(true); setLoadError(null); const tenant = await onLoadTenant(tenantId); loadedTenantIdRef.current = tenantId; // Extract contact info from settings const contactInfo = (tenant.settings as any)?.contact || {}; // Extract branding from settings or direct tenant fields const branding = (tenant.settings as any)?.branding || {}; const primaryColor = tenant.primary_color || branding.primary_color || '#112868'; const secondaryColor = tenant.secondary_color || branding.secondary_color || '#23DCE1'; const accentColor = tenant.accent_color || branding.accent_color || '#084CC8'; const logoPath = tenant.logo_file_path || branding.logo_file_path || null; const faviconPath = tenant.favicon_file_path || branding.favicon_file_path || null; // Set file paths and URLs if they exist if (logoPath) { setLogoFilePath(logoPath); // Construct URL from file path const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api/v1'; const baseUrl = apiBaseUrl.replace('/api/v1', ''); setLogoFileUrl(logoPath.startsWith('http') ? logoPath : `${baseUrl}/uploads/${logoPath}`); } if (faviconPath) { setFaviconFilePath(faviconPath); const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api/v1'; const baseUrl = apiBaseUrl.replace('/api/v1', ''); setFaviconFileUrl(faviconPath.startsWith('http') ? faviconPath : `${baseUrl}/uploads/${faviconPath}`); } // Validate subscription_tier const validSubscriptionTier = tenant.subscription_tier === 'basic' || tenant.subscription_tier === 'professional' || tenant.subscription_tier === 'enterprise' ? tenant.subscription_tier : null; // Extract module IDs const tenantModules = tenant.assignedModules ? tenant.assignedModules.map((module) => module.id) : tenant.modules || []; // Create initial options from assignedModules const initialOptions = tenant.assignedModules ? tenant.assignedModules.map((module) => ({ value: module.id, label: module.name, })) : []; setSelectedModules(tenantModules); setInitialModuleOptions(initialOptions); // Reset forms with tenant data tenantDetailsForm.reset({ name: tenant.name, slug: tenant.slug, domain: tenant.domain || '', status: tenant.status, subscription_tier: validSubscriptionTier, max_users: tenant.max_users, max_modules: tenant.max_modules, modules: tenantModules, }); contactDetailsForm.reset({ email: contactInfo.email || '', first_name: contactInfo.first_name || '', last_name: contactInfo.last_name || '', contact_phone: contactInfo.contact_phone || '', address_line1: contactInfo.address_line1 || '', address_line2: contactInfo.address_line2 || '', city: contactInfo.city || '', state: contactInfo.state || '', postal_code: contactInfo.postal_code || '', country: contactInfo.country || '', }); settingsForm.reset({ enable_sso: tenant.enable_sso || false, enable_2fa: tenant.enable_2fa || false, primary_color: primaryColor, secondary_color: secondaryColor, accent_color: accentColor, }); setCurrentStep(1); } catch (err: any) { setLoadError(err?.response?.data?.error?.message || 'Failed to load tenant details'); } finally { setIsLoadingTenant(false); } }; loadTenant(); } } else if (!isOpen) { loadedTenantIdRef.current = null; setSelectedModules([]); setInitialModuleOptions([]); setCurrentStep(1); setLogoFile(null); setFaviconFile(null); setLogoFilePath(null); setLogoFileUrl(null); setLogoPreviewUrl(null); setFaviconFilePath(null); setFaviconFileUrl(null); setFaviconPreviewUrl(null); tenantDetailsForm.reset(); contactDetailsForm.reset(); settingsForm.reset(); setLoadError(null); } }, [isOpen, tenantId, onLoadTenant, tenantDetailsForm, contactDetailsForm, settingsForm]); const handleNext = async (): Promise => { if (currentStep === 1) { const isValid = await tenantDetailsForm.trigger(); if (isValid) { setCurrentStep(2); } } else if (currentStep === 2) { const isValid = await contactDetailsForm.trigger(); if (isValid) { setCurrentStep(3); } } }; const handlePrevious = (): void => { if (currentStep > 1) { setCurrentStep(currentStep - 1); } }; const handleFormSubmit = async (): Promise => { if (!tenantId) return; const isValid = await settingsForm.trigger(); if (!isValid) return; try { const tenantDetails = tenantDetailsForm.getValues(); const contactDetails = contactDetailsForm.getValues(); const settings = settingsForm.getValues(); const { modules, ...restTenantDetails } = tenantDetails; const { enable_sso, enable_2fa, primary_color, secondary_color, accent_color } = settings; const tenantData = { ...restTenantDetails, module_ids: selectedModules.length > 0 ? selectedModules : undefined, settings: { enable_sso, enable_2fa, contact: contactDetails, branding: { primary_color: primary_color || undefined, secondary_color: secondary_color || undefined, accent_color: accent_color || undefined, logo_file_path: logoFilePath || undefined, favicon_file_path: faviconFilePath || undefined, }, }, }; await onSubmit(tenantId, tenantData); } catch (error: any) { // Handle validation errors from API if (error?.response?.data?.details && Array.isArray(error.response.data.details)) { const validationErrors = error.response.data.details; validationErrors.forEach((detail: { path: string; message: string }) => { if ( detail.path === 'name' || detail.path === 'slug' || detail.path === 'status' || detail.path === 'subscription_tier' || detail.path === 'max_users' || detail.path === 'max_modules' || detail.path === 'module_ids' ) { const fieldPath = detail.path === 'module_ids' ? 'modules' : detail.path; tenantDetailsForm.setError(fieldPath as keyof TenantDetailsForm, { type: 'server', message: detail.message, }); } else if ( detail.path === 'email' || detail.path === 'first_name' || detail.path === 'last_name' || detail.path === 'contact_phone' || detail.path === 'address_line1' || detail.path === 'city' || detail.path === 'state' || detail.path === 'postal_code' || detail.path === 'country' ) { contactDetailsForm.setError(detail.path as keyof ContactDetailsForm, { type: 'server', message: detail.message, }); } }); } else { const errorObj = error?.response?.data?.error; const errorMessage = (typeof errorObj === 'object' && errorObj !== null && 'message' in errorObj ? errorObj.message : null) || (typeof errorObj === 'string' ? errorObj : null) || error?.response?.data?.message || error?.message || 'Failed to update tenant. Please try again.'; tenantDetailsForm.setError('root', { type: 'server', message: typeof errorMessage === 'string' ? errorMessage : 'Failed to update tenant. Please try again.', }); } } }; const steps = [ { number: 1, title: 'Tenant Details', description: 'Basic organization information', isActive: currentStep === 1, isCompleted: currentStep > 1, }, { number: 2, title: 'Contact Details', description: 'Primary contact & address', isActive: currentStep === 2, isCompleted: currentStep > 2, }, { number: 3, title: 'Settings', description: 'Security & branding', isActive: currentStep === 3, isCompleted: false, }, ]; if (!isOpen) return null; return (
{isLoadingTenant && (
)} {loadError && (

{loadError}

)} {!isLoadingTenant && (
{/* Steps Sidebar */}

Steps

{steps.map((step) => (
{step.number}
{step.title}
{step.description}
))}
{/* Main Content */}
{/* Step 1: Tenant Details */} {currentStep === 1 && (

Tenant Details

Basic information for the organization.

{tenantDetailsForm.formState.errors.root && (

{tenantDetailsForm.formState.errors.root.message}

)}
tenantDetailsForm.setValue('status', value as 'active' | 'suspended' | 'deleted') } error={tenantDetailsForm.formState.errors.status?.message} />
tenantDetailsForm.setValue( 'subscription_tier', value === '' ? null : (value as 'basic' | 'professional' | 'enterprise') ) } error={tenantDetailsForm.formState.errors.subscription_tier?.message} />
{ if (value === '' || value === null || value === undefined) return null; const num = Number(value); return isNaN(num) ? null : num; }, })} />
{ if (value === '' || value === null || value === undefined) return null; const num = Number(value); return isNaN(num) ? null : num; }, })} />
{ setSelectedModules(values); tenantDetailsForm.setValue('modules', values.length > 0 ? values : []); }} onLoadOptions={loadModules} initialOptions={initialModuleOptions} error={tenantDetailsForm.formState.errors.modules?.message} />
)} {/* Step 2: Contact Details - NO PASSWORD FIELDS */} {currentStep === 2 && (

Contact Details

Contact information for the main account administrator.

{contactDetailsForm.formState.errors.root && (

{contactDetailsForm.formState.errors.root.message}

)}

Organization Address

)} {/* Step 3: Settings with Branding */} {currentStep === 3 && (

Configuration & Limits

Set resource limits and security preferences for this tenant.

{/* Branding Section */}

Branding

Customize logo, favicon, and colors for this tenant experience.

{/* Logo and Favicon Upload */}
{/* Company Logo */}
{logoFile && (
{isUploadingLogo ? 'Uploading...' : `Selected: ${logoFile.name}`}
{(logoPreviewUrl || logoFileUrl) && (
Logo preview { console.error('Failed to load logo preview image', { logoFileUrl, logoPreviewUrl, src: e.currentTarget.src, }); }} onLoad={() => { console.log('Logo preview loaded successfully', { logoFileUrl, logoPreviewUrl, src: logoFileUrl || logoPreviewUrl, }); }} />
)}
)} {!logoFile && logoFileUrl && (
Current logo
)}
{/* Favicon */}
{faviconFile && (
{isUploadingFavicon ? 'Uploading...' : `Selected: ${faviconFile.name}`}
{(faviconPreviewUrl || faviconFileUrl) && (
Favicon preview { console.error('Failed to load favicon preview image', { faviconFileUrl, faviconPreviewUrl, src: e.currentTarget.src, }); }} onLoad={() => { console.log('Favicon preview loaded successfully', { faviconFileUrl, faviconPreviewUrl, src: faviconFileUrl || faviconPreviewUrl, }); }} />
)}
)} {!faviconFile && faviconFileUrl && (
Current favicon
)}
{/* Primary Color */}
settingsForm.setValue('primary_color', e.target.value)} className="bg-[#f5f7fa] border border-[#d1d5db] h-10 px-3 py-1 rounded-md text-sm text-[#0f1724] w-full focus:outline-none focus:ring-2 focus:ring-[#112868] focus:border-transparent" placeholder="#112868" />
settingsForm.setValue('primary_color', e.target.value)} className="size-10 rounded-md border border-[#d1d5db] cursor-pointer" />

Used for navigation, headers, and key actions.

{/* Secondary and Accent Colors */}
settingsForm.setValue('secondary_color', e.target.value)} className="bg-[#f5f7fa] border border-[#d1d5db] h-10 px-3 py-1 rounded-md text-sm text-[#0f1724] w-full focus:outline-none focus:ring-2 focus:ring-[#112868] focus:border-transparent" placeholder="#23DCE1" />
settingsForm.setValue('secondary_color', e.target.value)} className="size-10 rounded-md border border-[#d1d5db] cursor-pointer" />

Used for highlights and supporting elements.

settingsForm.setValue('accent_color', e.target.value)} className="bg-[#f5f7fa] border border-[#d1d5db] h-10 px-3 py-1 rounded-md text-sm text-[#0f1724] w-full focus:outline-none focus:ring-2 focus:ring-[#112868] focus:border-transparent" placeholder="#084CC8" />
settingsForm.setValue('accent_color', e.target.value)} className="size-10 rounded-md border border-[#d1d5db] cursor-pointer" />

Used for alerts and special notices.

{/* Security Settings */}

Enable Single Sign-On (SSO)

Allow users to log in using their organization's identity provider.

Enable Two-Factor Authentication (2FA)

Enforce 2FA for all users in this tenant organization.

)} {/* Footer Navigation */}
{currentStep > 1 && ( Previous )} {currentStep < 3 ? ( Next ) : ( {isLoading ? 'Updating...' : 'Update Tenant'} )}
)}
); };