import { Layout } from "@/components/layout/Layout"; import { ImageIcon, Loader2, X } from "lucide-react"; import { useState, useEffect, type ReactElement } from "react"; import { useAppSelector, useAppDispatch } from "@/hooks/redux-hooks"; import { tenantService } from "@/services/tenant-service"; import { fileService } from "@/services/file-service"; import { showToast } from "@/utils/toast"; import { updateTheme } from "@/store/themeSlice"; import { PrimaryButton, AuthenticatedImage } from "@/components/shared"; import { generateUUID } from "@/lib/utils"; import type { Tenant } from "@/types/tenant"; // Helper function to get base URL with protocol const getBaseUrlWithProtocol = (): string => { return import.meta.env.VITE_API_BASE_URL || "http://localhost:3000"; }; export interface SettingsProps { customTenantId?: string; hideLayout?: boolean; } const Settings = ({ customTenantId, hideLayout = false }: SettingsProps = {}): ReactElement => { const authTenantId = useAppSelector((state) => state.auth.tenantId); const tenantId = customTenantId || authTenantId; const dispatch = useAppDispatch(); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(null); // Tenant data const [tenant, setTenant] = useState(null); // Color states const [primaryColor, setPrimaryColor] = useState("#112868"); const [secondaryColor, setSecondaryColor] = useState("#23DCE1"); const [accentColor, setAccentColor] = useState("#084CC8"); // Logo states const [logoFile, setLogoFile] = useState(null); const [logoFilePath, setLogoFilePath] = useState(null); const [logoFileAttachmentUuid, setLogoFileAttachmentUuid] = useState< string | null >(null); const [logoFileUrl, setLogoFileUrl] = useState(null); const [logoPreviewUrl, setLogoPreviewUrl] = useState(null); const [isUploadingLogo, setIsUploadingLogo] = useState(false); // Favicon states const [faviconFile, setFaviconFile] = useState(null); const [faviconFilePath, setFaviconFilePath] = useState(null); const [faviconFileAttachmentUuid, setFaviconFileAttachmentUuid] = useState< string | null >(null); const [faviconFileUrl, setFaviconFileUrl] = useState(null); const [faviconPreviewUrl, setFaviconPreviewUrl] = useState( null, ); const [isUploadingFavicon, setIsUploadingFavicon] = useState(false); const [logoError, setLogoError] = useState(null); const [faviconError, setFaviconError] = useState(null); // Fetch tenant data on mount useEffect(() => { const fetchTenant = async (): Promise => { if (!tenantId) { setError("Tenant ID not found"); setIsLoading(false); return; } try { setIsLoading(true); setError(null); const response = await tenantService.getById(tenantId); if (response.success && response.data) { const tenantData = response.data; setTenant(tenantData); // Set colors setPrimaryColor(tenantData.primary_color || "#112868"); setSecondaryColor(tenantData.secondary_color || "#23DCE1"); setAccentColor(tenantData.accent_color || "#084CC8"); // Set logo if (tenantData.logo_file_path) { setLogoFileUrl(tenantData.logo_file_path); setLogoFilePath(tenantData.logo_file_path); setLogoFileAttachmentUuid( tenantData.logo_file_attachment_uuid || null, ); setLogoError(null); } // Set favicon if (tenantData.favicon_file_path) { setFaviconFileUrl(tenantData.favicon_file_path); setFaviconFilePath(tenantData.favicon_file_path); setFaviconFileAttachmentUuid( tenantData.favicon_file_attachment_uuid || null, ); setFaviconError(null); } } } catch (err: any) { const errorMessage = err?.response?.data?.error?.message || err?.response?.data?.message || err?.message || "Failed to load tenant settings"; setError(errorMessage); showToast.error(errorMessage); } finally { setIsLoading(false); } }; fetchTenant(); }, [tenantId]); // Cleanup preview URLs useEffect(() => { return () => { if (logoPreviewUrl) { URL.revokeObjectURL(logoPreviewUrl); } if (faviconPreviewUrl) { URL.revokeObjectURL(faviconPreviewUrl); } }; }, [logoPreviewUrl, faviconPreviewUrl]); const handleLogoChange = async ( e: React.ChangeEvent, ): Promise => { const file = e.target.files?.[0]; if (!file) return; // Validate file size (2MB max) if (file.size > 2 * 1024 * 1024) { showToast.error("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)) { showToast.error("Logo must be PNG, SVG, or JPG format"); return; } // Revoke previous preview URL if (logoPreviewUrl) { URL.revokeObjectURL(logoPreviewUrl); } const previewUrl = URL.createObjectURL(file); setLogoFile(file); setLogoPreviewUrl(previewUrl); setIsUploadingLogo(true); try { const response = await fileService.upload( file, "tenant", generateUUID(), tenantId || undefined, ); const fileId = response.data.id; setLogoFileAttachmentUuid(fileId); const baseUrl = getBaseUrlWithProtocol(); const formattedUrl = `${baseUrl}/files/${fileId}/preview`; setLogoFilePath(formattedUrl); setLogoFileUrl(formattedUrl); setLogoError(null); showToast.success("Logo uploaded successfully"); } catch (err: any) { const errorMessage = err?.response?.data?.error?.message || err?.response?.data?.message || err?.message || "Failed to upload logo. Please try again."; showToast.error(errorMessage); setLogoFile(null); URL.revokeObjectURL(previewUrl); setLogoPreviewUrl(null); setLogoFileUrl(null); setLogoFilePath(null); setLogoFileAttachmentUuid(null); } finally { setIsUploadingLogo(false); } }; const handleFaviconChange = async ( e: React.ChangeEvent, ): Promise => { const file = e.target.files?.[0]; if (!file) return; // Validate file size (500KB max) if (file.size > 500 * 1024) { showToast.error("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)) { showToast.error("Favicon must be ICO or PNG format"); return; } // Revoke previous preview URL if (faviconPreviewUrl) { URL.revokeObjectURL(faviconPreviewUrl); } const previewUrl = URL.createObjectURL(file); setFaviconFile(file); setFaviconPreviewUrl(previewUrl); setIsUploadingFavicon(true); try { const response = await fileService.upload( file, "tenant", generateUUID(), tenantId || undefined, ); const fileId = response.data.id; setFaviconFileAttachmentUuid(fileId); const baseUrl = getBaseUrlWithProtocol(); const formattedUrl = `${baseUrl}/files/${fileId}/preview`; setFaviconFilePath(formattedUrl); setFaviconFileUrl(formattedUrl); setFaviconError(null); showToast.success("Favicon uploaded successfully"); } catch (err: any) { const errorMessage = err?.response?.data?.error?.message || err?.response?.data?.message || err?.message || "Failed to upload favicon. Please try again."; showToast.error(errorMessage); setFaviconFile(null); URL.revokeObjectURL(previewUrl); setFaviconPreviewUrl(null); setFaviconFileUrl(null); setFaviconFilePath(null); setFaviconFileAttachmentUuid(null); } finally { setIsUploadingFavicon(false); } }; const handleDeleteLogo = (): void => { if (logoPreviewUrl) { URL.revokeObjectURL(logoPreviewUrl); } setLogoFile(null); setLogoPreviewUrl(null); setLogoFileUrl(null); setLogoFilePath(null); setLogoFileAttachmentUuid(null); setLogoError(null); // Reset the file input const fileInput = document.getElementById( "logo-upload", ) as HTMLInputElement; if (fileInput) { fileInput.value = ""; } }; const handleDeleteFavicon = (): void => { if (faviconPreviewUrl) { URL.revokeObjectURL(faviconPreviewUrl); } setFaviconFile(null); setFaviconPreviewUrl(null); setFaviconFileUrl(null); setFaviconFilePath(null); setFaviconFileAttachmentUuid(null); setFaviconError(null); // Reset the file input const fileInput = document.getElementById( "favicon-upload", ) as HTMLInputElement; if (fileInput) { fileInput.value = ""; } }; const handleSave = async (): Promise => { if (!tenantId || !tenant) return; // Validate logo and favicon are uploaded setLogoError(null); setFaviconError(null); const isLogoMissing = !logoFilePath; const isFaviconMissing = !faviconFilePath; if (isLogoMissing) { setLogoError("Logo is required"); } if (isFaviconMissing) { setFaviconError("Favicon is required"); } if (isLogoMissing || isFaviconMissing) { return; } try { setIsSaving(true); setError(null); // Build update data matching EditTenantModal format const existingSettings = (tenant.settings as Record) || {}; const existingContact = (existingSettings.contact as Record) || {}; const updateData = { name: tenant.name, slug: tenant.slug, status: tenant.status, domain: tenant.domain || null, subscription_tier: tenant.subscription_tier || null, max_users: tenant.max_users || null, max_modules: tenant.max_modules || null, settings: { enable_sso: tenant.enable_sso || false, enable_2fa: tenant.enable_2fa || false, contact: existingContact, branding: { primary_color: primaryColor || undefined, secondary_color: secondaryColor || undefined, accent_color: accentColor || undefined, logo_file_path: logoFilePath || undefined, logo_file_attachment_uuid: logoFileAttachmentUuid || undefined, favicon_file_path: faviconFilePath || undefined, favicon_file_attachment_uuid: faviconFileAttachmentUuid || undefined, }, }, }; const response = await tenantService.update(tenantId, updateData); if (response.success) { showToast.success("Settings updated successfully"); // Update theme in Redux for the active session, if not editing a different tenant context if (!customTenantId || customTenantId === authTenantId) { dispatch( updateTheme({ logo_file_path: logoFilePath, favicon_file_path: faviconFilePath, primary_color: primaryColor, secondary_color: secondaryColor, accent_color: accentColor, }), ); } // Update local tenant state setTenant({ ...tenant, primary_color: primaryColor, secondary_color: secondaryColor, accent_color: accentColor, logo_file_path: logoFilePath, logo_file_attachment_uuid: logoFileAttachmentUuid, favicon_file_path: faviconFilePath, favicon_file_attachment_uuid: faviconFileAttachmentUuid, }); } } catch (err: any) { const errorMessage = err?.response?.data?.error?.message || err?.response?.data?.message || err?.message || "Failed to update settings. Please try again."; setError(errorMessage); showToast.error(errorMessage); } finally { setIsSaving(false); } }; if (isLoading) { return (
); } if (error && !tenant) { return (

{error}

); } const content = (
{error && (

{error}

)} {/* Branding Section */}
{/* Section Header */}

Branding

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

{/* Logo and Favicon Upload */}
{/* Company Logo */}
{logoError && (

{logoError}

)} {(logoFile || logoFileUrl) && (
{logoFile && (
{isUploadingLogo ? "Uploading..." : `Selected: ${logoFile.name}`}
)} {(logoPreviewUrl || logoFileUrl) && (
)}
)}
{/* Favicon */}
{faviconError && (

{faviconError}

)} {(faviconFile || faviconFileUrl) && (
{faviconFile && (
{isUploadingFavicon ? "Uploading..." : `Selected: ${faviconFile.name}`}
)} {(faviconPreviewUrl || faviconFileUrl) && (
)}
)}
{/* 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.

{/* Save Button */}
{isSaving ? "Saving..." : "Save Changes"}
); if (hideLayout) { return content; } return ( {content} ); }; export default Settings;