diff --git a/src/pages/CreateTenantWizard.tsx b/src/pages/CreateTenantWizard.tsx index 026fafd..0463418 100644 --- a/src/pages/CreateTenantWizard.tsx +++ b/src/pages/CreateTenantWizard.tsx @@ -9,7 +9,7 @@ import { FormField, FormSelect, PrimaryButton, SecondaryButton, MultiselectPagin import { tenantService } from '@/services/tenant-service'; import { moduleService } from '@/services/module-service'; import { showToast } from '@/utils/toast'; -import { ChevronRight, ChevronLeft } from 'lucide-react'; +import { ChevronRight, ChevronLeft, Image as ImageIcon } from 'lucide-react'; // Step 1: Tenant Details Schema - matches NewTenantModal const tenantDetailsSchema = z.object({ @@ -61,6 +61,9 @@ const contactDetailsSchema = z 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; @@ -144,9 +147,16 @@ const CreateTenantWizard = (): ReactElement => { defaultValues: { enable_sso: false, enable_2fa: false, + primary_color: '#112868', + secondary_color: '#23DCE1', + accent_color: '#084CC8', }, }); + // File upload state for branding + const [logoFile, setLogoFile] = useState(null); + const [faviconFile, setFaviconFile] = useState(null); + // Auto-generate slug and domain from name const nameValue = tenantDetailsForm.watch('name'); const baseUrlWithoutProtocol = getBaseUrlWithoutProtocol(); @@ -265,12 +275,25 @@ const CreateTenantWizard = (): ReactElement => { // Extract confirmPassword from contactDetails (not needed in API call) const { confirmPassword, ...contactData } = contactDetails; + // Extract branding colors from settings + const { enable_sso, enable_2fa, primary_color, secondary_color, accent_color } = settings; + const tenantData = { ...restTenantDetails, module_ids: selectedModules.length > 0 ? selectedModules : undefined, settings: { - ...settings, + enable_sso, + enable_2fa, contact: contactData, // Include first_name, last_name, email, password + branding: { + primary_color: primary_color || undefined, + secondary_color: secondary_color || undefined, + accent_color: accent_color || undefined, + // Note: logo and favicon files would need to be uploaded separately via FormData + // For now, we're just storing the file references + logo_file: logoFile ? logoFile.name : undefined, + favicon_file: faviconFile ? faviconFile.name : undefined, + }, }, }; @@ -663,13 +686,209 @@ const CreateTenantWizard = (): ReactElement => { {/* Step 3: Settings */} {currentStep === 3 && ( -
+

Configuration & Limits

Set resource limits and security preferences for this tenant.

+ + {/* Branding Section */} +
+ {/* Section Header */} +
+

Branding

+

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

+
+ + {/* Logo and Favicon Upload */} +
+ {/* Company Logo */} +
+ + + {logoFile && ( +
+ Selected: {logoFile.name} +
+ )} +
+ + {/* Favicon */} +
+ + + {faviconFile && ( +
+ Selected: {faviconFile.name} +
+ )} +
+
+ + {/* 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 */} +
+ {/* Secondary Color */} +
+ +
+
+
+ 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. +

+
+ + {/* Accent Color */} +
+ +
+
+
+ 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 */}
diff --git a/src/pages/TenantDetails.tsx b/src/pages/TenantDetails.tsx index ffccbfc..2c8951a 100644 --- a/src/pages/TenantDetails.tsx +++ b/src/pages/TenantDetails.tsx @@ -12,6 +12,8 @@ import { Edit, CheckCircle2, XCircle, + Settings, + Image as ImageIcon, } from 'lucide-react'; import { Layout } from '@/components/layout/Layout'; import { @@ -28,13 +30,14 @@ import type { Tenant, AssignedModule } from '@/types/tenant'; import type { AuditLog } from '@/types/audit-log'; import { formatDate } from '@/utils/format-date'; -type TabType = 'overview' | 'users' | 'roles' | 'modules' | 'license' | 'audit-logs' | 'billing'; +type TabType = 'overview' | 'users' | 'roles' | 'modules' | 'settings' | 'license' | 'audit-logs' | 'billing'; const tabs: Array<{ id: TabType; label: string; icon: ReactElement }> = [ { id: 'overview', label: 'Overview', icon: }, { id: 'users', label: 'Users', icon: }, { id: 'roles', label: 'Roles', icon: }, { id: 'modules', label: 'Modules', icon: }, + { id: 'settings', label: 'Settings', icon: }, { id: 'license', label: 'License', icon: }, { id: 'audit-logs', label: 'Audit Logs', icon: }, { id: 'billing', label: 'Billing', icon: }, @@ -276,6 +279,9 @@ const TenantDetails = (): ReactElement => { modules={tenant.assignedModules || []} /> )} + {activeTab === 'settings' && tenant && ( + + )} {activeTab === 'license' && } {activeTab === 'audit-logs' && ( { + const [logoFile, setLogoFile] = useState(null); + const [faviconFile, setFaviconFile] = useState(null); + const [primaryColor, setPrimaryColor] = useState('#112868'); + const [secondaryColor, setSecondaryColor] = useState('#23DCE1'); + const [accentColor, setAccentColor] = useState('#084CC8'); + + const handleLogoChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + // Validate file size (2MB max) + if (file.size > 2 * 1024 * 1024) { + alert('Logo file size must be less than 2MB'); + return; + } + // Validate file type + const validTypes = ['image/png', 'image/svg+xml', 'image/jpeg', 'image/jpg']; + if (!validTypes.includes(file.type)) { + alert('Logo must be PNG, SVG, or JPG format'); + return; + } + setLogoFile(file); + } + }; + + const handleFaviconChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + // Validate file size (500KB max) + if (file.size > 500 * 1024) { + alert('Favicon file size must be less than 500KB'); + return; + } + // Validate file type + const validTypes = ['image/x-icon', 'image/png', 'image/vnd.microsoft.icon']; + if (!validTypes.includes(file.type)) { + alert('Favicon must be ICO or PNG format'); + return; + } + setFaviconFile(file); + } + }; + + return ( +
+ {/* Branding Section */} +
+ {/* Section Header */} +
+

Branding

+

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

+
+ + {/* Logo and Favicon Upload */} +
+ {/* Company Logo */} +
+ + + {logoFile && ( +
+ Selected: {logoFile.name} +
+ )} +
+ + {/* Favicon */} +
+ + + {faviconFile && ( +
+ Selected: {faviconFile.name} +
+ )} +
+
+ + {/* Primary Color */} +
+ +
+
+
+ setPrimaryColor(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" + /> +
+ setPrimaryColor(e.target.value)} + className="size-10 rounded-md border border-[#d1d5db] cursor-pointer" + /> +
+

+ Used for navigation, headers, and key actions. +

+
+ + {/* Secondary and Accent Colors */} +
+ {/* Secondary Color */} +
+ +
+
+
+ setSecondaryColor(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" + /> +
+ setSecondaryColor(e.target.value)} + className="size-10 rounded-md border border-[#d1d5db] cursor-pointer" + /> +
+

+ Used for highlights and supporting elements. +

+
+ + {/* Accent Color */} +
+ +
+
+
+ setAccentColor(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" + /> +
+ setAccentColor(e.target.value)} + className="size-10 rounded-md border border-[#d1d5db] cursor-pointer" + /> +
+

+ Used for alerts and special notices. +

+
+
+
+
+ ); +}; + // Billing Tab Component interface BillingTabProps { tenant: Tenant;