From 4226f67923b605c4bd3445440092f678d411cf9f Mon Sep 17 00:00:00 2001 From: Yashwin Date: Tue, 27 Jan 2026 19:44:40 +0530 Subject: [PATCH] Enhance tenant management UI by adding branding customization options in CreateTenantWizard and TenantDetails components. Implement file upload functionality for logo and favicon, and allow users to set primary, secondary, and accent colors for tenant branding. Update settings schema to accommodate new branding fields. --- src/pages/CreateTenantWizard.tsx | 225 ++++++++++++++++++++++++++++++- src/pages/TenantDetails.tsx | 223 +++++++++++++++++++++++++++++- 2 files changed, 444 insertions(+), 4 deletions(-) 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;