# ET-SETTINGS-FRONTEND: Componentes React ## Identificacion | Campo | Valor | |-------|-------| | **ID** | ET-SETTINGS-FRONTEND | | **Modulo** | MGN-006 Settings | | **Version** | 1.0 | | **Estado** | En Diseno | | **Framework** | React + TypeScript | | **UI Library** | shadcn/ui | | **State** | Zustand | | **Autor** | Requirements-Analyst | | **Fecha** | 2025-12-05 | --- ## Descripcion General Especificacion tecnica del modulo frontend de Settings. Incluye paginas de configuracion del sistema, tenant, preferencias de usuario y gestion de feature flags con interfaces jerarquicas y validacion en tiempo real. ### Estructura de Archivos ``` apps/frontend/src/modules/settings/ ├── index.ts ├── routes.tsx ├── pages/ │ ├── SettingsLayout.tsx │ ├── SystemSettingsPage.tsx │ ├── TenantSettingsPage.tsx │ ├── UserPreferencesPage.tsx │ ├── FeatureFlagsPage.tsx │ └── SettingsOverviewPage.tsx ├── components/ │ ├── system/ │ │ ├── SystemSettingsForm.tsx │ │ ├── SettingsCategoryNav.tsx │ │ ├── SettingsSearch.tsx │ │ └── SettingsResetConfirm.tsx │ ├── tenant/ │ │ ├── TenantSettingsForm.tsx │ │ ├── TenantBrandingSection.tsx │ │ ├── TenantLocaleSection.tsx │ │ └── TenantLimitsSection.tsx │ ├── preferences/ │ │ ├── UserPreferencesForm.tsx │ │ ├── ThemeSelector.tsx │ │ ├── LocaleSelector.tsx │ │ ├── NotificationPrefs.tsx │ │ └── TimezoneSelector.tsx │ ├── feature-flags/ │ │ ├── FeatureFlagList.tsx │ │ ├── FeatureFlagForm.tsx │ │ ├── FeatureFlagCard.tsx │ │ ├── FeatureFlagToggle.tsx │ │ ├── RolloutSlider.tsx │ │ ├── VariantEditor.tsx │ │ └── FeatureFlagHistory.tsx │ └── shared/ │ ├── SettingItem.tsx │ ├── SettingGroup.tsx │ ├── SettingDescription.tsx │ ├── SettingInput.tsx │ └── SettingToggle.tsx ├── stores/ │ ├── system-settings.store.ts │ ├── tenant-settings.store.ts │ ├── user-preferences.store.ts │ └── feature-flags.store.ts ├── hooks/ │ ├── useSystemSettings.ts │ ├── useTenantSettings.ts │ ├── useUserPreferences.ts │ ├── useFeatureFlag.ts │ └── useEffectiveSetting.ts ├── services/ │ ├── system-settings.service.ts │ ├── tenant-settings.service.ts │ ├── user-preferences.service.ts │ └── feature-flags.service.ts └── types/ ├── system-settings.types.ts ├── tenant-settings.types.ts ├── user-preferences.types.ts └── feature-flag.types.ts ``` --- ## Types ### System Settings Types ```typescript // types/system-settings.types.ts export type SettingDataType = 'string' | 'integer' | 'boolean' | 'json' | 'decimal'; export interface SystemSettingDefinition { id: string; key: string; category: string; label: string; description?: string; dataType: SettingDataType; defaultValue: string; validationRules?: ValidationRule[]; options?: SettingOption[]; isEncrypted: boolean; requiresRestart: boolean; } export interface SystemSettingValue { id: string; definitionId: string; definition?: SystemSettingDefinition; value: string; level: 'system' | 'plan' | 'tenant'; levelId?: string; createdAt: string; updatedAt: string; } export interface SettingOption { value: string; label: string; } export interface ValidationRule { type: 'min' | 'max' | 'regex' | 'enum'; value: string | number; message: string; } export interface SettingCategory { key: string; label: string; icon: string; order: number; settings: SystemSettingDefinition[]; } export interface UpdateSettingDto { value: string; } export interface SettingsFilters { category?: string; search?: string; level?: 'system' | 'plan' | 'tenant'; } ``` ### Tenant Settings Types ```typescript // types/tenant-settings.types.ts export interface TenantSettings { id: string; tenantId: string; // Branding companyName: string; legalName?: string; logo?: string; favicon?: string; primaryColor: string; secondaryColor?: string; // Locale defaultLocale: string; timezone: string; dateFormat: string; timeFormat: string; currencyId: string; // Business fiscalYearStart: number; // 1-12 weekStart: number; // 0-6 // Limits maxUsers?: number; maxStorage?: number; // MB features: string[]; // Contact supportEmail?: string; website?: string; createdAt: string; updatedAt: string; } export interface UpdateTenantSettingsDto { companyName?: string; legalName?: string; logo?: string; primaryColor?: string; secondaryColor?: string; defaultLocale?: string; timezone?: string; dateFormat?: string; timeFormat?: string; currencyId?: string; fiscalYearStart?: number; weekStart?: number; supportEmail?: string; website?: string; } export interface BrandingPreview { primaryColor: string; secondaryColor: string; logo?: string; } ``` ### User Preferences Types ```typescript // types/user-preferences.types.ts export type ThemeMode = 'light' | 'dark' | 'system'; export interface UserPreferences { id: string; userId: string; // UI theme: ThemeMode; locale: string; timezone: string; dateFormat?: string; timeFormat?: string; density: 'compact' | 'normal' | 'comfortable'; // Notifications emailNotifications: boolean; pushNotifications: boolean; inAppNotifications: boolean; digestFrequency: 'realtime' | 'daily' | 'weekly' | 'never'; notificationCategories: Record; // Navigation sidebarCollapsed: boolean; defaultLandingPage?: string; recentItems: string[]; // Accessibility highContrast: boolean; reducedMotion: boolean; fontSize: 'small' | 'medium' | 'large'; createdAt: string; updatedAt: string; } export interface UpdateUserPreferencesDto { theme?: ThemeMode; locale?: string; timezone?: string; dateFormat?: string; timeFormat?: string; density?: 'compact' | 'normal' | 'comfortable'; emailNotifications?: boolean; pushNotifications?: boolean; inAppNotifications?: boolean; digestFrequency?: 'realtime' | 'daily' | 'weekly' | 'never'; notificationCategories?: Record; sidebarCollapsed?: boolean; defaultLandingPage?: string; highContrast?: boolean; reducedMotion?: boolean; fontSize?: 'small' | 'medium' | 'large'; } ``` ### Feature Flag Types ```typescript // types/feature-flag.types.ts export type FlagType = 'boolean' | 'percentage' | 'variant'; export type FlagStatus = 'active' | 'inactive' | 'scheduled'; export interface FeatureFlag { id: string; key: string; name: string; description?: string; flagType: FlagType; status: FlagStatus; // Boolean enabled?: boolean; // Percentage (rollout) percentage?: number; // Variant (A/B) variants?: FlagVariant[]; // Targeting tenantIds?: string[]; userIds?: string[]; planIds?: string[]; rules?: FlagRule[]; // Scheduling startDate?: string; endDate?: string; // Metadata tags?: string[]; createdBy: string; createdAt: string; updatedAt: string; } export interface FlagVariant { key: string; name: string; weight: number; // 0-100, suma = 100 payload?: Record; } export interface FlagRule { id: string; attribute: string; // 'userId', 'tenantId', 'plan', 'country', etc operator: 'equals' | 'contains' | 'startsWith' | 'in' | 'notIn'; value: string | string[]; enabled: boolean; } export interface CreateFeatureFlagDto { key: string; name: string; description?: string; flagType: FlagType; enabled?: boolean; percentage?: number; variants?: FlagVariant[]; } export interface UpdateFeatureFlagDto { name?: string; description?: string; status?: FlagStatus; enabled?: boolean; percentage?: number; variants?: FlagVariant[]; tenantIds?: string[]; userIds?: string[]; planIds?: string[]; rules?: FlagRule[]; startDate?: string; endDate?: string; tags?: string[]; } export interface FlagEvaluation { flagKey: string; enabled: boolean; variant?: string; payload?: Record; reason: 'default' | 'targeting' | 'rollout' | 'scheduled' | 'disabled'; } export interface FlagHistory { id: string; flagId: string; action: 'created' | 'updated' | 'enabled' | 'disabled'; changes: Record; userId: string; userName: string; createdAt: string; } ``` --- ## Stores (Zustand) ### User Preferences Store ```typescript // stores/user-preferences.store.ts import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { UserPreferences, UpdateUserPreferencesDto, ThemeMode } from '../types/user-preferences.types'; import { userPreferencesService } from '../services/user-preferences.service'; interface UserPreferencesState { preferences: UserPreferences | null; isLoading: boolean; error: string | null; // Computed effectiveTheme: 'light' | 'dark'; // Actions fetchPreferences: () => Promise; updatePreferences: (dto: UpdateUserPreferencesDto) => Promise; setTheme: (theme: ThemeMode) => Promise; setLocale: (locale: string) => Promise; toggleSidebar: () => Promise; resetPreferences: () => Promise; } export const useUserPreferencesStore = create()( persist( (set, get) => ({ preferences: null, isLoading: false, error: null, effectiveTheme: 'light', fetchPreferences: async () => { set({ isLoading: true, error: null }); try { const preferences = await userPreferencesService.get(); const effectiveTheme = resolveTheme(preferences.theme); set({ preferences, effectiveTheme, isLoading: false }); applyTheme(effectiveTheme); applyAccessibility(preferences); } catch (error: any) { set({ error: error.message, isLoading: false }); } }, updatePreferences: async (dto: UpdateUserPreferencesDto) => { const current = get().preferences; if (!current) return; // Optimistic update set({ preferences: { ...current, ...dto }, error: null }); try { const updated = await userPreferencesService.update(dto); const effectiveTheme = resolveTheme(updated.theme); set({ preferences: updated, effectiveTheme }); if (dto.theme) applyTheme(effectiveTheme); if (dto.highContrast !== undefined || dto.reducedMotion !== undefined || dto.fontSize) { applyAccessibility(updated); } } catch (error: any) { // Revert on error set({ preferences: current, error: error.message }); throw error; } }, setTheme: async (theme: ThemeMode) => { await get().updatePreferences({ theme }); }, setLocale: async (locale: string) => { await get().updatePreferences({ locale }); }, toggleSidebar: async () => { const current = get().preferences; if (!current) return; await get().updatePreferences({ sidebarCollapsed: !current.sidebarCollapsed }); }, resetPreferences: async () => { set({ isLoading: true }); try { const preferences = await userPreferencesService.reset(); const effectiveTheme = resolveTheme(preferences.theme); set({ preferences, effectiveTheme, isLoading: false }); applyTheme(effectiveTheme); } catch (error: any) { set({ error: error.message, isLoading: false }); } }, }), { name: 'user-preferences', partialize: (state) => ({ preferences: state.preferences, effectiveTheme: state.effectiveTheme, }), } ) ); function resolveTheme(theme: ThemeMode): 'light' | 'dark' { if (theme === 'system') { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } return theme; } function applyTheme(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); } function applyAccessibility(prefs: UserPreferences) { document.documentElement.classList.toggle('high-contrast', prefs.highContrast); document.documentElement.classList.toggle('reduce-motion', prefs.reducedMotion); document.documentElement.setAttribute('data-font-size', prefs.fontSize); } ``` ### Feature Flags Store ```typescript // stores/feature-flags.store.ts import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; import { FeatureFlag, CreateFeatureFlagDto, UpdateFeatureFlagDto, FlagEvaluation } from '../types/feature-flag.types'; import { featureFlagsService } from '../services/feature-flags.service'; interface FeatureFlagsState { flags: FeatureFlag[]; evaluations: Record; isLoading: boolean; error: string | null; // Actions fetchFlags: () => Promise; createFlag: (dto: CreateFeatureFlagDto) => Promise; updateFlag: (id: string, dto: UpdateFeatureFlagDto) => Promise; deleteFlag: (id: string) => Promise; toggleFlag: (id: string) => Promise; evaluateFlag: (key: string) => Promise; evaluateAllFlags: () => Promise; // Helpers isEnabled: (key: string) => boolean; getVariant: (key: string) => string | undefined; } export const useFeatureFlagsStore = create()( devtools( (set, get) => ({ flags: [], evaluations: {}, isLoading: false, error: null, fetchFlags: async () => { set({ isLoading: true, error: null }); try { const flags = await featureFlagsService.getAll(); set({ flags, isLoading: false }); } catch (error: any) { set({ error: error.message, isLoading: false }); } }, createFlag: async (dto: CreateFeatureFlagDto) => { set({ isLoading: true, error: null }); try { const flag = await featureFlagsService.create(dto); set((state) => ({ flags: [...state.flags, flag], isLoading: false, })); return flag; } catch (error: any) { set({ error: error.message, isLoading: false }); throw error; } }, updateFlag: async (id: string, dto: UpdateFeatureFlagDto) => { set({ isLoading: true, error: null }); try { const flag = await featureFlagsService.update(id, dto); set((state) => ({ flags: state.flags.map((f) => (f.id === id ? flag : f)), isLoading: false, })); return flag; } catch (error: any) { set({ error: error.message, isLoading: false }); throw error; } }, deleteFlag: async (id: string) => { set({ isLoading: true, error: null }); try { await featureFlagsService.delete(id); set((state) => ({ flags: state.flags.filter((f) => f.id !== id), isLoading: false, })); } catch (error: any) { set({ error: error.message, isLoading: false }); throw error; } }, toggleFlag: async (id: string) => { const flag = get().flags.find((f) => f.id === id); if (!flag) return; await get().updateFlag(id, { enabled: !flag.enabled }); }, evaluateFlag: async (key: string) => { try { const evaluation = await featureFlagsService.evaluate(key); set((state) => ({ evaluations: { ...state.evaluations, [key]: evaluation }, })); return evaluation; } catch (error: any) { // Return disabled on error return { flagKey: key, enabled: false, reason: 'disabled' as const }; } }, evaluateAllFlags: async () => { try { const evaluations = await featureFlagsService.evaluateAll(); const evaluationsMap: Record = {}; evaluations.forEach((e) => { evaluationsMap[e.flagKey] = e; }); set({ evaluations: evaluationsMap }); } catch (error: any) { console.error('Failed to evaluate flags:', error); } }, isEnabled: (key: string) => { const evaluation = get().evaluations[key]; return evaluation?.enabled ?? false; }, getVariant: (key: string) => { const evaluation = get().evaluations[key]; return evaluation?.variant; }, }), { name: 'feature-flags-store' } ) ); ``` --- ## Custom Hooks ### useFeatureFlag ```typescript // hooks/useFeatureFlag.ts import { useEffect, useState } from 'react'; import { useFeatureFlagsStore } from '../stores/feature-flags.store'; export interface UseFeatureFlagOptions { fallback?: boolean; } export function useFeatureFlag( key: string, options: UseFeatureFlagOptions = {} ): boolean { const { evaluations, evaluateFlag, isEnabled } = useFeatureFlagsStore(); const [loading, setLoading] = useState(!evaluations[key]); useEffect(() => { if (!evaluations[key]) { setLoading(true); evaluateFlag(key).finally(() => setLoading(false)); } }, [key, evaluations, evaluateFlag]); if (loading) { return options.fallback ?? false; } return isEnabled(key); } export function useFeatureFlagVariant(key: string): { enabled: boolean; variant?: string; payload?: Record; loading: boolean; } { const { evaluations, evaluateFlag } = useFeatureFlagsStore(); const [loading, setLoading] = useState(!evaluations[key]); useEffect(() => { if (!evaluations[key]) { setLoading(true); evaluateFlag(key).finally(() => setLoading(false)); } }, [key, evaluations, evaluateFlag]); const evaluation = evaluations[key]; return { enabled: evaluation?.enabled ?? false, variant: evaluation?.variant, payload: evaluation?.payload, loading, }; } ``` ### useEffectiveSetting ```typescript // hooks/useEffectiveSetting.ts import { useMemo } from 'react'; import { useSystemSettingsStore } from '../stores/system-settings.store'; import { useTenantSettingsStore } from '../stores/tenant-settings.store'; import { useUserPreferencesStore } from '../stores/user-preferences.store'; /** * Resuelve el valor efectivo de una configuracion * considerando la jerarquia: user > tenant > plan > system */ export function useEffectiveSetting( key: string, options?: { userOverride?: T; tenantOverride?: T; defaultValue?: T; } ): T | undefined { const { getSystemValue } = useSystemSettingsStore(); const { settings: tenantSettings } = useTenantSettingsStore(); const { preferences } = useUserPreferencesStore(); return useMemo(() => { // 1. Check user preference override if (options?.userOverride !== undefined) { return options.userOverride; } // 2. Check user preferences const userValue = preferences?.[key as keyof typeof preferences]; if (userValue !== undefined) { return userValue as T; } // 3. Check tenant override if (options?.tenantOverride !== undefined) { return options.tenantOverride; } // 4. Check tenant settings const tenantValue = tenantSettings?.[key as keyof typeof tenantSettings]; if (tenantValue !== undefined) { return tenantValue as T; } // 5. Check system setting const systemValue = getSystemValue(key); if (systemValue !== undefined) { return systemValue as T; } // 6. Return default return options?.defaultValue; }, [key, preferences, tenantSettings, getSystemValue, options]); } ``` --- ## Components ### ThemeSelector ```tsx // components/preferences/ThemeSelector.tsx import { Moon, Sun, Monitor } from 'lucide-react'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Label } from '@/components/ui/label'; import { Card, CardContent } from '@/components/ui/card'; import { ThemeMode } from '../../types/user-preferences.types'; import { cn } from '@/lib/utils'; interface ThemeSelectorProps { value: ThemeMode; onChange: (theme: ThemeMode) => void; } const themes: { value: ThemeMode; label: string; icon: any; description: string }[] = [ { value: 'light', label: 'Claro', icon: Sun, description: 'Tema claro para uso diurno', }, { value: 'dark', label: 'Oscuro', icon: Moon, description: 'Tema oscuro para reducir fatiga visual', }, { value: 'system', label: 'Sistema', icon: Monitor, description: 'Sigue la preferencia del sistema operativo', }, ]; export function ThemeSelector({ value, onChange }: ThemeSelectorProps) { return ( {themes.map((theme) => { const Icon = theme.icon; const isSelected = value === theme.value; return ( ); })} ); } ``` ### FeatureFlagCard ```tsx // components/feature-flags/FeatureFlagCard.tsx import { Clock, Users, Percent, ToggleLeft, ToggleRight, Layers } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; import { Button } from '@/components/ui/button'; import { Progress } from '@/components/ui/progress'; import { FeatureFlag, FlagType } from '../../types/feature-flag.types'; import { cn } from '@/lib/utils'; import { formatDistanceToNow } from 'date-fns'; import { es } from 'date-fns/locale'; interface FeatureFlagCardProps { flag: FeatureFlag; onToggle: (id: string) => void; onEdit: (flag: FeatureFlag) => void; } const flagTypeConfig: Record = { boolean: { icon: ToggleLeft, label: 'Boolean', color: 'bg-blue-500' }, percentage: { icon: Percent, label: 'Rollout', color: 'bg-amber-500' }, variant: { icon: Layers, label: 'Variante', color: 'bg-purple-500' }, }; export function FeatureFlagCard({ flag, onToggle, onEdit }: FeatureFlagCardProps) { const config = flagTypeConfig[flag.flagType]; const Icon = config.icon; const isScheduled = flag.startDate || flag.endDate; const hasTargeting = (flag.tenantIds?.length ?? 0) > 0 || (flag.userIds?.length ?? 0) > 0 || (flag.planIds?.length ?? 0) > 0; return (
{config.label} {flag.status === 'scheduled' && ( Programado )}
{flag.name} {flag.key}
onToggle(flag.id)} disabled={flag.status === 'scheduled'} />
{flag.description && (

{flag.description}

)} {/* Rollout Progress */} {flag.flagType === 'percentage' && flag.percentage !== undefined && (
Rollout {flag.percentage}%
)} {/* Variants */} {flag.flagType === 'variant' && flag.variants && (
Variantes:
{flag.variants.map((variant) => ( {variant.name} ({variant.weight}%) ))}
)} {/* Targeting */} {hasTargeting && (
{[ flag.tenantIds?.length && `${flag.tenantIds.length} tenants`, flag.userIds?.length && `${flag.userIds.length} usuarios`, flag.planIds?.length && `${flag.planIds.length} planes`, ] .filter(Boolean) .join(', ')}
)} {/* Tags */} {flag.tags && flag.tags.length > 0 && (
{flag.tags.map((tag) => ( {tag} ))}
)} {/* Footer */}
Actualizado {formatDistanceToNow(new Date(flag.updatedAt), { addSuffix: true, locale: es, })}
); } ``` ### RolloutSlider ```tsx // components/feature-flags/RolloutSlider.tsx import { useState, useEffect } from 'react'; import { Slider } from '@/components/ui/slider'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { cn } from '@/lib/utils'; interface RolloutSliderProps { value: number; onChange: (value: number) => void; disabled?: boolean; } export function RolloutSlider({ value, onChange, disabled }: RolloutSliderProps) { const [localValue, setLocalValue] = useState(value); useEffect(() => { setLocalValue(value); }, [value]); const handleSliderChange = (values: number[]) => { const newValue = values[0]; setLocalValue(newValue); onChange(newValue); }; const handleInputChange = (e: React.ChangeEvent) => { const newValue = Math.min(100, Math.max(0, parseInt(e.target.value) || 0)); setLocalValue(newValue); onChange(newValue); }; const getColor = (percent: number) => { if (percent === 0) return 'bg-gray-200'; if (percent < 25) return 'bg-red-500'; if (percent < 50) return 'bg-orange-500'; if (percent < 75) return 'bg-yellow-500'; if (percent < 100) return 'bg-blue-500'; return 'bg-green-500'; }; return (
%
{/* Visual indicator */}
{localValue === 0 && 'Desactivado'} {localValue > 0 && localValue < 100 && `${localValue}% de usuarios`} {localValue === 100 && 'Todos los usuarios'}
{/* Quick buttons */}
{[0, 10, 25, 50, 75, 100].map((percent) => ( ))}
); } ``` ### SettingItem ```tsx // components/shared/SettingItem.tsx import { ReactNode } from 'react'; import { Info } from 'lucide-react'; import { Label } from '@/components/ui/label'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip'; import { Badge } from '@/components/ui/badge'; import { cn } from '@/lib/utils'; interface SettingItemProps { label: string; description?: string; children: ReactNode; badge?: string; badgeVariant?: 'default' | 'secondary' | 'destructive' | 'outline'; required?: boolean; disabled?: boolean; className?: string; } export function SettingItem({ label, description, children, badge, badgeVariant = 'secondary', required, disabled, className, }: SettingItemProps) { return (
{badge && ( {badge} )}
{description && (

{description}

)}
{children}
); } ``` --- ## Pages ### UserPreferencesPage ```tsx // pages/UserPreferencesPage.tsx import { useEffect } from 'react'; import { Loader2 } from 'lucide-react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Separator } from '@/components/ui/separator'; import { Switch } from '@/components/ui/switch'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { ThemeSelector } from '../components/preferences/ThemeSelector'; import { LocaleSelector } from '../components/preferences/LocaleSelector'; import { TimezoneSelector } from '../components/preferences/TimezoneSelector'; import { NotificationPrefs } from '../components/preferences/NotificationPrefs'; import { SettingItem } from '../components/shared/SettingItem'; import { useUserPreferencesStore } from '../stores/user-preferences.store'; import { toast } from 'sonner'; export function UserPreferencesPage() { const { preferences, isLoading, fetchPreferences, updatePreferences, resetPreferences, } = useUserPreferencesStore(); useEffect(() => { fetchPreferences(); }, [fetchPreferences]); const handleUpdate = async (updates: Partial) => { try { await updatePreferences(updates); toast.success('Preferencias actualizadas'); } catch (error) { toast.error('Error al actualizar preferencias'); } }; const handleReset = async () => { if (confirm('¿Restaurar todas las preferencias a valores predeterminados?')) { await resetPreferences(); toast.success('Preferencias restauradas'); } }; if (isLoading && !preferences) { return (
); } if (!preferences) return null; return (

Preferencias

Personaliza tu experiencia en la aplicacion

{/* Apariencia */} Apariencia Personaliza el aspecto visual de la aplicacion

Tema

handleUpdate({ theme })} />
handleUpdate({ sidebarCollapsed })} />
{/* Regional */} Regional Configura idioma, zona horaria y formatos handleUpdate({ locale })} /> handleUpdate({ timezone })} /> {/* Notificaciones */} Notificaciones Controla como y cuando recibes notificaciones {/* Accesibilidad */} Accesibilidad Opciones para mejorar la accesibilidad handleUpdate({ highContrast })} /> handleUpdate({ reducedMotion })} />
); } ``` ### FeatureFlagsPage ```tsx // pages/FeatureFlagsPage.tsx import { useEffect, useState } from 'react'; import { Plus, Search, Filter } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { FeatureFlagCard } from '../components/feature-flags/FeatureFlagCard'; import { FeatureFlagForm } from '../components/feature-flags/FeatureFlagForm'; import { useFeatureFlagsStore } from '../stores/feature-flags.store'; import { FeatureFlag, FlagType } from '../types/feature-flag.types'; export function FeatureFlagsPage() { const { flags, isLoading, fetchFlags, createFlag, updateFlag, toggleFlag, } = useFeatureFlagsStore(); const [search, setSearch] = useState(''); const [typeFilter, setTypeFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive'>('all'); const [isFormOpen, setIsFormOpen] = useState(false); const [editingFlag, setEditingFlag] = useState(null); useEffect(() => { fetchFlags(); }, [fetchFlags]); const filteredFlags = flags.filter((flag) => { const matchesSearch = flag.name.toLowerCase().includes(search.toLowerCase()) || flag.key.toLowerCase().includes(search.toLowerCase()); const matchesType = typeFilter === 'all' || flag.flagType === typeFilter; const matchesStatus = statusFilter === 'all' || (statusFilter === 'active' && flag.enabled) || (statusFilter === 'inactive' && !flag.enabled); return matchesSearch && matchesType && matchesStatus; }); const handleEdit = (flag: FeatureFlag) => { setEditingFlag(flag); setIsFormOpen(true); }; const handleClose = () => { setIsFormOpen(false); setEditingFlag(null); }; // Stats const activeCount = flags.filter((f) => f.enabled).length; const booleanCount = flags.filter((f) => f.flagType === 'boolean').length; const rolloutCount = flags.filter((f) => f.flagType === 'percentage').length; const variantCount = flags.filter((f) => f.flagType === 'variant').length; return (

Feature Flags

Gestiona funcionalidades y experimentos

{/* Stats */}
{flags.length}

Total Flags

{activeCount}

Activas

{rolloutCount}

En Rollout

{variantCount}

Con Variantes

{/* Filters */}
setSearch(e.target.value)} className="pl-10" />
{/* Flags Grid */}
{filteredFlags.map((flag) => ( ))}
{filteredFlags.length === 0 && (

No se encontraron feature flags

)} {/* Form Dialog */} {editingFlag ? 'Editar Feature Flag' : 'Nueva Feature Flag'} { if (editingFlag) { await updateFlag(editingFlag.id, data); } else { await createFlag(data); } handleClose(); }} onCancel={handleClose} />
); } ``` --- ## Routes ```tsx // routes.tsx import { lazy } from 'react'; import { RouteObject, Navigate } from 'react-router-dom'; const SettingsLayout = lazy(() => import('./pages/SettingsLayout')); const SettingsOverviewPage = lazy(() => import('./pages/SettingsOverviewPage')); const SystemSettingsPage = lazy(() => import('./pages/SystemSettingsPage')); const TenantSettingsPage = lazy(() => import('./pages/TenantSettingsPage')); const UserPreferencesPage = lazy(() => import('./pages/UserPreferencesPage')); const FeatureFlagsPage = lazy(() => import('./pages/FeatureFlagsPage')); export const settingsRoutes: RouteObject[] = [ { path: 'settings', element: , children: [ { index: true, element: , }, { path: 'system', element: , }, { path: 'tenant', element: , }, { path: 'preferences', element: , }, { path: 'feature-flags', element: , }, ], }, ]; ``` --- ## Wireframes ### Preferencias de Usuario ``` +------------------------------------------------------------------+ | Preferencias [Restaurar Predeterminados] | | Personaliza tu experiencia en la aplicacion | +------------------------------------------------------------------+ +------------------------------------------------------------------+ | Apariencia | | Personaliza el aspecto visual de la aplicacion | +------------------------------------------------------------------+ | Tema | | +------------------+ +------------------+ +------------------+ | | | [☀] | | [🌙] | | [🖥] | | | | Claro | | Oscuro | | Sistema | | | | Tema claro ... | | Tema oscuro ... | | Sigue sistema...| | | +------------------+ +------------------+ +------------------+ | | | | Densidad de Contenido | [Normal v] | | Ajusta el espaciado | | | | | Barra Lateral Colapsada | [ O ] | | Inicia minimizada | | +------------------------------------------------------------------+ +------------------------------------------------------------------+ | Regional | | Configura idioma, zona horaria y formatos | +------------------------------------------------------------------+ | Idioma | [Espanol (Mexico) v] | | Idioma de la interfaz | | | | | Zona Horaria | [America/Mexico_City v] | | Tu zona horaria local | | | | | Formato de Fecha | [DD/MM/YYYY v] | | Como se muestran las fechas | | +------------------------------------------------------------------+ +------------------------------------------------------------------+ | Notificaciones | | Controla como y cuando recibes notificaciones | +------------------------------------------------------------------+ | Notificaciones por Email | [ O ] | | Recibe alertas en tu correo | | | | | Notificaciones Push | [ O ] | | Alertas en tiempo real | | | | | Frecuencia de Resumen | [Diario v] | | Consolida multiples alertas | | +------------------------------------------------------------------+ ``` ### Feature Flags ``` +------------------------------------------------------------------+ | Feature Flags [+ Nueva Flag] | | Gestiona funcionalidades y experimentos | +------------------------------------------------------------------+ +--------+ +--------+ +--------+ +--------+ | 12 | | 8 | | 3 | | 1 | | Total | | Activas| | Rollout| | Variant| +--------+ +--------+ +--------+ +--------+ +------------------------------------------------------------------+ | [🔍 Buscar por nombre o clave... ] [Tipo ▼] [Estado ▼] | +------------------------------------------------------------------+ +--------------------------------+ +--------------------------------+ | [Boolean] NEW_CHECKOUT | | [Rollout] DARK_MODE | | new-checkout-flow | | dark-mode-beta | | [ON O] | | [ON O] | | Nuevo flujo de checkout con | | Modo oscuro para usuarios | | proceso simplificado | | seleccionados | | | | | | | | Rollout [=====> ] 35% | | Actualizado hace 2 dias [Editar] | Actualizado hace 1 hora [Editar] | +--------------------------------+ +--------------------------------+ +--------------------------------+ +--------------------------------+ | [Variant] PRICING_TEST | | [Boolean] BULK_EXPORT | | pricing-ab-test | | bulk-export-feature | | [ON O] | | [OFF O] | | Test A/B de pagina de precios | | Exportacion masiva de datos | | | | | | Variantes: | | | | [Control (50%)] [New (50%)] | | | | | | | | Actualizado hace 5 dias [Editar] | Actualizado hace 1 mes [Editar] | +--------------------------------+ +--------------------------------+ ``` --- ## Historial de Cambios | Version | Fecha | Autor | Cambios | |---------|-------|-------|---------| | 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | --- ## Aprobaciones | Rol | Nombre | Fecha | Firma | |-----|--------|-------|-------| | Frontend Lead | - | - | [ ] | | UX Designer | - | - | [ ] |