import { useState, useMemo, useRef } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import { adminSettingsApi, SettingDefinition } from '../api/adminSettings' import { brandingApi, setCachedBranding } from '../api/branding' import { setCachedAnimationEnabled } from '../components/AnimatedBackground' import { setCachedFullscreenEnabled } from '../hooks/useTelegramWebApp' import { themeColorsApi } from '../api/themeColors' import { ThemeColors, DEFAULT_THEME_COLORS } from '../types/theme' import { ColorPicker } from '../components/ColorPicker' import { applyThemeColors } from '../hooks/useThemeColors' import { updateEnabledThemesCache } from '../hooks/useTheme' import { useFavoriteSettings } from '../hooks/useFavoriteSettings' // ============ ICONS ============ const BackIcon = () => ( ) const SearchIcon = () => ( ) const StarIcon = ({ filled }: { filled?: boolean }) => ( ) const ChevronDownIcon = () => ( ) const ChevronRightIcon = () => ( ) const UploadIcon = () => ( ) const TrashIcon = () => ( ) const PencilIcon = () => ( ) const RefreshIcon = () => ( ) const LockIcon = () => ( ) const CheckIcon = () => ( ) const CloseIcon = () => ( ) const SunIcon = () => ( ) const MoonIcon = () => ( ) const MenuIcon = () => ( ) // ============ HELPER FUNCTIONS ============ // Форматирование названия настройки (Snake_Case / CamelCase -> читаемый текст) function formatSettingName(name: string): string { if (!name) return '' // Убираем префиксы типа "Miniapp ", "Happ " и т.д. let formatted = name // CamelCase -> пробелы .replace(/([a-z])([A-Z])/g, '$1 $2') // snake_case -> пробелы .replace(/_/g, ' ') // Убираем лишние пробелы .replace(/\s+/g, ' ') .trim() // Делаем первую букву заглавной, остальные как есть return formatted.charAt(0).toUpperCase() + formatted.slice(1) } // Очистка HTML тегов из описания function stripHtml(html: string): string { if (!html) return '' return html .replace(/<[^>]*>/g, '') .replace(/ /g, ' ') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .trim() } // ============ SIDEBAR MENU ITEMS ============ interface MenuItem { id: string icon: ((props: { filled?: boolean }) => JSX.Element) | null categories?: string[] } interface MenuSection { id: string items: MenuItem[] } const MENU_SECTIONS: MenuSection[] = [ { id: 'main', items: [ { id: 'favorites', icon: StarIcon }, { id: 'branding', icon: null }, { id: 'theme', icon: null }, ] }, { id: 'settings', items: [ { id: 'payments', icon: null, categories: ['PAYMENT', 'PAYMENT_VERIFICATION', 'YOOKASSA', 'CRYPTOBOT', 'HELEKET', 'PLATEGA', 'TRIBUTE', 'MULENPAY', 'PAL24', 'WATA', 'TELEGRAM'] }, { id: 'subscriptions', icon: null, categories: ['SUBSCRIPTIONS_CORE', 'SIMPLE_SUBSCRIPTION', 'PERIODS', 'SUBSCRIPTION_PRICES', 'TRAFFIC', 'TRAFFIC_PACKAGES', 'TRIAL', 'AUTOPAY'] }, { id: 'interface', icon: null, categories: ['INTERFACE', 'INTERFACE_BRANDING', 'INTERFACE_SUBSCRIPTION', 'CONNECT_BUTTON', 'MINIAPP', 'HAPP', 'SKIP', 'ADDITIONAL'] }, { id: 'notifications', icon: null, categories: ['NOTIFICATIONS', 'ADMIN_NOTIFICATIONS', 'ADMIN_REPORTS'] }, { id: 'database', icon: null, categories: ['DATABASE', 'POSTGRES', 'SQLITE', 'REDIS'] }, { id: 'system', icon: null, categories: ['CORE', 'REMNAWAVE', 'SERVER_STATUS', 'MONITORING', 'MAINTENANCE', 'BACKUP', 'VERSION', 'WEB_API', 'WEBHOOK', 'LOG', 'DEBUG', 'EXTERNAL_ADMIN'] }, { id: 'users', icon: null, categories: ['SUPPORT', 'LOCALIZATION', 'CHANNEL', 'TIMEZONE', 'REFERRAL', 'MODERATION'] }, ] } ] // ============ THEME PRESETS ============ const THEME_PRESETS: { id: string; colors: ThemeColors }[] = [ { id: 'standard', colors: DEFAULT_THEME_COLORS }, { id: 'ocean', colors: { accent: '#0ea5e9', darkBackground: '#0c1222', darkSurface: '#1e293b', darkText: '#f1f5f9', darkTextSecondary: '#94a3b8', lightBackground: '#e0f2fe', lightSurface: '#f0f9ff', lightText: '#0c4a6e', lightTextSecondary: '#0369a1', success: '#22c55e', warning: '#f59e0b', error: '#ef4444' } }, { id: 'forest', colors: { accent: '#22c55e', darkBackground: '#0a1a0f', darkSurface: '#14532d', darkText: '#f0fdf4', darkTextSecondary: '#86efac', lightBackground: '#dcfce7', lightSurface: '#f0fdf4', lightText: '#14532d', lightTextSecondary: '#166534', success: '#22c55e', warning: '#f59e0b', error: '#ef4444' } }, { id: 'sunset', colors: { accent: '#f97316', darkBackground: '#1c1009', darkSurface: '#2d1a0e', darkText: '#fff7ed', darkTextSecondary: '#fdba74', lightBackground: '#ffedd5', lightSurface: '#fff7ed', lightText: '#7c2d12', lightTextSecondary: '#c2410c', success: '#22c55e', warning: '#f59e0b', error: '#ef4444' } }, { id: 'violet', colors: { accent: '#a855f7', darkBackground: '#0f0a1a', darkSurface: '#1e1b2e', darkText: '#faf5ff', darkTextSecondary: '#c4b5fd', lightBackground: '#f3e8ff', lightSurface: '#faf5ff', lightText: '#581c87', lightTextSecondary: '#7e22ce', success: '#22c55e', warning: '#f59e0b', error: '#ef4444' } }, { id: 'rose', colors: { accent: '#f43f5e', darkBackground: '#1a0a10', darkSurface: '#2d1520', darkText: '#fff1f2', darkTextSecondary: '#fda4af', lightBackground: '#ffe4e6', lightSurface: '#fff1f2', lightText: '#881337', lightTextSecondary: '#be123c', success: '#22c55e', warning: '#f59e0b', error: '#ef4444' } }, { id: 'midnight', colors: { accent: '#6366f1', darkBackground: '#030712', darkSurface: '#111827', darkText: '#f9fafb', darkTextSecondary: '#9ca3af', lightBackground: '#e5e7eb', lightSurface: '#f3f4f6', lightText: '#111827', lightTextSecondary: '#4b5563', success: '#22c55e', warning: '#f59e0b', error: '#ef4444' } }, { id: 'turquoise', colors: { accent: '#14b8a6', darkBackground: '#0a1614', darkSurface: '#134e4a', darkText: '#f0fdfa', darkTextSecondary: '#5eead4', lightBackground: '#ccfbf1', lightSurface: '#f0fdfa', lightText: '#134e4a', lightTextSecondary: '#0f766e', success: '#22c55e', warning: '#f59e0b', error: '#ef4444' } }, ] // ============ MAIN COMPONENT ============ export default function AdminSettings() { const { t } = useTranslation() const queryClient = useQueryClient() const fileInputRef = useRef(null) // State const [activeSection, setActiveSection] = useState('branding') const [activeSubCategory, setActiveSubCategory] = useState(null) const [searchQuery, setSearchQuery] = useState('') const [editingName, setEditingName] = useState(false) const [newName, setNewName] = useState('') const [expandedSections, setExpandedSections] = useState>(new Set(['presets'])) const [mobileMenuOpen, setMobileMenuOpen] = useState(false) // Favorites hook const { favorites, toggleFavorite, isFavorite } = useFavoriteSettings() // ============ QUERIES ============ const { data: branding } = useQuery({ queryKey: ['branding'], queryFn: brandingApi.getBranding, }) const { data: animationSettings } = useQuery({ queryKey: ['animation-enabled'], queryFn: brandingApi.getAnimationEnabled, }) const { data: fullscreenSettings } = useQuery({ queryKey: ['fullscreen-enabled'], queryFn: brandingApi.getFullscreenEnabled, }) const { data: themeColors } = useQuery({ queryKey: ['theme-colors'], queryFn: themeColorsApi.getColors, }) const { data: enabledThemes } = useQuery({ queryKey: ['enabled-themes'], queryFn: themeColorsApi.getEnabledThemes, }) const { data: allSettings } = useQuery({ queryKey: ['admin-settings'], queryFn: () => adminSettingsApi.getSettings(), }) // ============ MUTATIONS ============ const updateBrandingMutation = useMutation({ mutationFn: brandingApi.updateName, onSuccess: (data) => { setCachedBranding(data) queryClient.invalidateQueries({ queryKey: ['branding'] }) setEditingName(false) }, }) const uploadLogoMutation = useMutation({ mutationFn: brandingApi.uploadLogo, onSuccess: (data) => { setCachedBranding(data) queryClient.invalidateQueries({ queryKey: ['branding'] }) }, }) const deleteLogoMutation = useMutation({ mutationFn: brandingApi.deleteLogo, onSuccess: (data) => { setCachedBranding(data) queryClient.invalidateQueries({ queryKey: ['branding'] }) }, }) const updateAnimationMutation = useMutation({ mutationFn: (enabled: boolean) => brandingApi.updateAnimationEnabled(enabled), onSuccess: (data) => { setCachedAnimationEnabled(data.enabled) queryClient.invalidateQueries({ queryKey: ['animation-enabled'] }) }, }) const updateFullscreenMutation = useMutation({ mutationFn: (enabled: boolean) => brandingApi.updateFullscreenEnabled(enabled), onSuccess: (data) => { setCachedFullscreenEnabled(data.enabled) queryClient.invalidateQueries({ queryKey: ['fullscreen-enabled'] }) }, }) const updateColorsMutation = useMutation({ mutationFn: themeColorsApi.updateColors, onSuccess: (data) => { applyThemeColors(data) queryClient.invalidateQueries({ queryKey: ['theme-colors'] }) }, }) const resetColorsMutation = useMutation({ mutationFn: themeColorsApi.resetColors, onSuccess: (data) => { applyThemeColors(data) queryClient.invalidateQueries({ queryKey: ['theme-colors'] }) }, }) const updateEnabledThemesMutation = useMutation({ mutationFn: themeColorsApi.updateEnabledThemes, onSuccess: (data) => { updateEnabledThemesCache(data) queryClient.invalidateQueries({ queryKey: ['enabled-themes'] }) }, }) const updateSettingMutation = useMutation({ mutationFn: ({ key, value }: { key: string; value: string }) => adminSettingsApi.updateSetting(key, value), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-settings'] }) }, }) const resetSettingMutation = useMutation({ mutationFn: (key: string) => adminSettingsApi.resetSetting(key), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-settings'] }) }, }) // ============ HANDLERS ============ const handleLogoUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (file) { uploadLogoMutation.mutate(file) } } const toggleSection = (section: string) => { setExpandedSections(prev => { const next = new Set(prev) if (next.has(section)) { next.delete(section) } else { next.add(section) } return next }) } // Get current menu item configuration const currentMenuItem = useMemo(() => { for (const section of MENU_SECTIONS) { const item = section.items.find(i => i.id === activeSection) if (item) return item } return null }, [activeSection]) // Get categories for current section const currentCategories = useMemo(() => { if (!currentMenuItem?.categories || !allSettings || !Array.isArray(allSettings)) return [] const categoryMap = new Map() for (const setting of allSettings) { if (currentMenuItem.categories.includes(setting.category.key)) { if (!categoryMap.has(setting.category.key)) { categoryMap.set(setting.category.key, []) } categoryMap.get(setting.category.key)!.push(setting) } } return Array.from(categoryMap.entries()).map(([key, settings]) => ({ key, label: t(`admin.settings.categories.${key}`, key), settings })) }, [currentMenuItem, allSettings, t]) // Filter settings for current view const filteredSettings = useMemo(() => { if (!allSettings || !Array.isArray(allSettings)) return [] let settings: SettingDefinition[] = [] if (activeSubCategory) { settings = allSettings.filter((s: SettingDefinition) => s.category.key === activeSubCategory) } else if (currentMenuItem?.categories) { settings = allSettings.filter((s: SettingDefinition) => currentMenuItem.categories!.includes(s.category.key) ) } if (searchQuery) { const q = searchQuery.toLowerCase() settings = settings.filter((s: SettingDefinition) => s.key.toLowerCase().includes(q) || s.name?.toLowerCase().includes(q) ) } return settings }, [allSettings, activeSection, activeSubCategory, currentMenuItem, searchQuery]) // Favorite settings const favoriteSettings = useMemo(() => { if (!allSettings || !Array.isArray(allSettings)) return [] return allSettings.filter((s: SettingDefinition) => favorites.includes(s.key)) }, [allSettings, favorites]) // ============ RENDER HELPERS ============ const renderToggle = (checked: boolean, onChange: () => void, disabled?: boolean) => ( ) const renderSettingRow = (setting: SettingDefinition) => { const isFav = isFavorite(setting.key) const displayName = formatSettingName(setting.name || setting.key) const description = setting.hint?.description ? stripHtml(setting.hint.description) : null return (
{/* Top row - name and badge */}
{displayName} {setting.has_override && ( {t('admin.settings.modified')} )}
{description && (

{description}

)}
{/* Favorite button */}
{/* Bottom row - control */}
{setting.key}
{/* Setting control */} {setting.read_only ? (
{String(setting.current ?? '-')}
) : setting.type === 'bool' ? ( renderToggle( setting.current === true || setting.current === 'true', () => updateSettingMutation.mutate({ key: setting.key, value: setting.current === true || setting.current === 'true' ? 'false' : 'true' }), updateSettingMutation.isPending ) ) : ( updateSettingMutation.mutate({ key: setting.key, value })} disabled={updateSettingMutation.isPending} /> )} {/* Reset button */} {setting.has_override && !setting.read_only && ( )}
) } // ============ CONTENT RENDERERS ============ const renderBrandingContent = () => (
{/* Logo & Name */}

{t('admin.settings.logoAndName')}

{/* Logo */}
{branding?.has_custom_logo ? ( Logo ) : ( branding?.logo_letter || 'V' )}
{branding?.has_custom_logo && ( )}
{/* Name */}
{editingName ? (
setNewName(e.target.value)} className="flex-1 px-4 py-2 rounded-xl bg-dark-700 border border-dark-600 text-dark-100 focus:outline-none focus:border-accent-500" maxLength={50} />
) : (
{branding?.name || t('admin.settings.notSpecified')}
)}
{/* Animation & Fullscreen toggles */}

{t('admin.settings.interfaceOptions')}

{t('admin.settings.animatedBackground')}

{t('admin.settings.animatedBackgroundDesc')}

{renderToggle( animationSettings?.enabled ?? true, () => updateAnimationMutation.mutate(!(animationSettings?.enabled ?? true)), updateAnimationMutation.isPending )}
{t('admin.settings.autoFullscreen')}

{t('admin.settings.autoFullscreenDesc')}

{renderToggle( fullscreenSettings?.enabled ?? false, () => updateFullscreenMutation.mutate(!(fullscreenSettings?.enabled ?? false)), updateFullscreenMutation.isPending )}
) const renderThemeContent = () => (
{/* Theme toggles */}

{t('admin.settings.availableThemes')}

{t('admin.settings.darkTheme')}
{renderToggle( enabledThemes?.dark ?? true, () => { if ((enabledThemes?.dark ?? true) && !(enabledThemes?.light ?? true)) return updateEnabledThemesMutation.mutate({ dark: !(enabledThemes?.dark ?? true) }) }, updateEnabledThemesMutation.isPending )}
{t('admin.settings.lightTheme')}
{renderToggle( enabledThemes?.light ?? true, () => { if ((enabledThemes?.light ?? true) && !(enabledThemes?.dark ?? true)) return updateEnabledThemesMutation.mutate({ light: !(enabledThemes?.light ?? true) }) }, updateEnabledThemesMutation.isPending )}
{/* Quick Presets */}
{expandedSections.has('presets') && (
{THEME_PRESETS.map((preset) => ( ))}
)}
{/* Custom Colors */}
{expandedSections.has('colors') && (
{/* Accent */}

{t('admin.settings.accentColor')}

updateColorsMutation.mutate({ accent: color })} disabled={updateColorsMutation.isPending} />
{/* Dark theme */}

{t('admin.settings.darkTheme')}

updateColorsMutation.mutate({ darkBackground: color })} disabled={updateColorsMutation.isPending} /> updateColorsMutation.mutate({ darkSurface: color })} disabled={updateColorsMutation.isPending} /> updateColorsMutation.mutate({ darkText: color })} disabled={updateColorsMutation.isPending} /> updateColorsMutation.mutate({ darkTextSecondary: color })} disabled={updateColorsMutation.isPending} />
{/* Light theme */}

{t('admin.settings.lightTheme')}

updateColorsMutation.mutate({ lightBackground: color })} disabled={updateColorsMutation.isPending} /> updateColorsMutation.mutate({ lightSurface: color })} disabled={updateColorsMutation.isPending} /> updateColorsMutation.mutate({ lightText: color })} disabled={updateColorsMutation.isPending} /> updateColorsMutation.mutate({ lightTextSecondary: color })} disabled={updateColorsMutation.isPending} />
{/* Status colors */}

{t('admin.settings.statusColors')}

updateColorsMutation.mutate({ success: color })} disabled={updateColorsMutation.isPending} /> updateColorsMutation.mutate({ warning: color })} disabled={updateColorsMutation.isPending} /> updateColorsMutation.mutate({ error: color })} disabled={updateColorsMutation.isPending} />
{/* Reset button */}
)}
) const renderFavoritesContent = () => (
{favoriteSettings.length === 0 ? (

{t('admin.settings.favoritesEmpty')}

{t('admin.settings.favoritesHint')}

) : (
{favoriteSettings.map(renderSettingRow)}
)}
) const renderSettingsContent = () => (
{/* Sub-categories navigation */} {currentCategories.length > 1 && !activeSubCategory && (
{currentCategories.map((cat) => ( ))}
)} {/* Back button if in sub-category */} {activeSubCategory && ( )} {/* Settings grid/list */} {filteredSettings.length === 0 ? (

{t('admin.settings.noSettings')}

) : (
{filteredSettings.map(renderSettingRow)}
)}
) // ============ RENDER ============ return (
{/* Mobile overlay */} {mobileMenuOpen && (
setMobileMenuOpen(false)} /> )} {/* Sidebar - hidden on mobile, visible on desktop */}
{/* Header */}

{t('admin.settings.title')}

{/* Close button on mobile */}
{/* Menu */}
{/* Main content */}
{/* Header */}
{/* Mobile menu button */}

{t(`admin.settings.${activeSection}`)} {activeSubCategory && ( / {t(`admin.settings.categories.${activeSubCategory}`, activeSubCategory)} )}

{/* Search - hidden on very small screens */}
setSearchQuery(e.target.value)} placeholder={t('admin.settings.searchPlaceholder')} className="w-48 lg:w-64 pl-10 pr-4 py-2 rounded-xl bg-dark-800 border border-dark-700 text-dark-100 placeholder-dark-500 focus:outline-none focus:border-accent-500 text-sm" />
{/* Mobile search */}
setSearchQuery(e.target.value)} placeholder={t('admin.settings.searchPlaceholder')} className="w-full pl-10 pr-4 py-2 rounded-xl bg-dark-800 border border-dark-700 text-dark-100 placeholder-dark-500 focus:outline-none focus:border-accent-500 text-sm" />
{/* Content */}
{activeSection === 'favorites' && renderFavoritesContent()} {activeSection === 'branding' && renderBrandingContent()} {activeSection === 'theme' && renderThemeContent()} {['payments', 'subscriptions', 'interface', 'notifications', 'database', 'system', 'users'].includes(activeSection) && renderSettingsContent()}
) } // ============ SETTING INPUT COMPONENT ============ function SettingInput({ setting, onUpdate, disabled }: { setting: SettingDefinition onUpdate: (value: string) => void disabled?: boolean }) { const [isEditing, setIsEditing] = useState(false) const [value, setValue] = useState('') const handleStart = () => { setValue(String(setting.current ?? '')) setIsEditing(true) } const handleSave = () => { onUpdate(value) setIsEditing(false) } const handleCancel = () => { setIsEditing(false) setValue('') } if (setting.choices && setting.choices.length > 0) { return ( ) } if (isEditing) { return (
setValue(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleSave() if (e.key === 'Escape') handleCancel() }} autoFocus className="bg-dark-700 border border-accent-500 rounded-lg px-3 py-1.5 text-sm text-dark-100 focus:outline-none w-32" />
) } return ( ) }