RF-USER-005: Preferencias de Usuario
Identificacion
| Campo |
Valor |
| ID |
RF-USER-005 |
| Modulo |
MGN-002 |
| Nombre Modulo |
Users - Gestion de Usuarios |
| Prioridad |
P2 |
| Complejidad |
Baja |
| Estado |
Aprobado |
| Autor |
System |
| Fecha |
2025-12-05 |
Descripcion
El sistema debe permitir a cada usuario configurar sus preferencias personales, incluyendo idioma, zona horaria, formato de fecha/hora, tema visual y preferencias de notificaciones. Estas configuraciones personalizan la experiencia del usuario sin afectar a otros usuarios del tenant.
Contexto de Negocio
Las preferencias de usuario permiten:
- Experiencia personalizada por usuario
- Soporte multi-idioma
- Adaptacion a zonas horarias locales
- Control sobre notificaciones recibidas
- Accesibilidad (tema oscuro, tamano de fuente)
Criterios de Aceptacion
Ejemplos de Verificacion
Scenario: Cambiar idioma
Given un usuario con idioma "es" (español)
When cambia su preferencia a "en" (ingles)
Then el sistema guarda la preferencia
And la interfaz cambia a ingles inmediatamente
And las fechas se formatean en ingles
Scenario: Configurar zona horaria
Given un usuario en Mexico (America/Mexico_City)
When configura su zona horaria
Then todas las fechas/horas se muestran en esa zona
And los eventos del calendario se ajustan
Scenario: Preferencias de notificaciones
Given un usuario con notificaciones por email activadas
When desactiva "notificaciones de marketing"
Then deja de recibir ese tipo de emails
And mantiene otras notificaciones activas
Scenario: Tema oscuro
Given un usuario con tema claro
When activa tema oscuro
Then la interfaz cambia a colores oscuros
And la preferencia se mantiene al recargar
Reglas de Negocio
| ID |
Regla |
Validacion |
| RN-001 |
Idiomas soportados: es, en, pt |
Enum validation |
| RN-002 |
Zona horaria debe ser valida IANA |
Timezone validation |
| RN-003 |
Preferencias son por usuario, no por tenant |
user_id scope |
| RN-004 |
Valores por defecto del tenant si no hay preferencia |
Fallback logic |
| RN-005 |
Tema "sistema" sigue preferencia del OS |
CSS media query |
Preferencias Disponibles
interface UserPreferences {
// Localizacion
language: 'es' | 'en' | 'pt';
timezone: string; // IANA timezone
dateFormat: 'DD/MM/YYYY' | 'MM/DD/YYYY' | 'YYYY-MM-DD';
timeFormat: '12h' | '24h';
currency: string; // ISO 4217
numberFormat: 'es-MX' | 'en-US' | 'pt-BR';
// Apariencia
theme: 'light' | 'dark' | 'system';
sidebarCollapsed: boolean;
compactMode: boolean;
fontSize: 'small' | 'medium' | 'large';
// Notificaciones
notifications: {
email: {
enabled: boolean;
digest: 'instant' | 'daily' | 'weekly';
marketing: boolean;
security: boolean;
updates: boolean;
};
push: {
enabled: boolean;
sound: boolean;
};
inApp: {
enabled: boolean;
desktop: boolean;
};
};
// Dashboard
dashboard: {
defaultView: string;
widgets: string[];
};
}
Impacto en Capas
Database
| Elemento |
Accion |
Descripcion |
| Tabla |
crear |
user_preferences |
| Columna |
- |
id UUID PK |
| Columna |
- |
user_id UUID FK UNIQUE |
| Columna |
- |
tenant_id UUID FK |
| Columna |
- |
language VARCHAR(5) |
| Columna |
- |
timezone VARCHAR(50) |
| Columna |
- |
date_format VARCHAR(20) |
| Columna |
- |
time_format VARCHAR(5) |
| Columna |
- |
theme VARCHAR(10) |
| Columna |
- |
notifications JSONB |
| Columna |
- |
dashboard JSONB |
| Columna |
- |
metadata JSONB |
| Columna |
- |
updated_at TIMESTAMPTZ |
Backend
| Elemento |
Accion |
Descripcion |
| Controller |
crear |
PreferencesController |
| Service |
crear |
PreferencesService |
| Method |
crear |
getPreferences() |
| Method |
crear |
updatePreferences() |
| Method |
crear |
resetPreferences() |
| DTO |
crear |
UpdatePreferencesDto |
| DTO |
crear |
PreferencesResponseDto |
| Entity |
crear |
UserPreferences |
| Endpoint |
crear |
GET /api/v1/users/me/preferences |
| Endpoint |
crear |
PATCH /api/v1/users/me/preferences |
| Endpoint |
crear |
POST /api/v1/users/me/preferences/reset |
Frontend
| Elemento |
Accion |
Descripcion |
| Pagina |
crear |
PreferencesPage |
| Componente |
crear |
LanguageSelector |
| Componente |
crear |
TimezoneSelector |
| Componente |
crear |
ThemeToggle |
| Componente |
crear |
NotificationSettings |
| Store |
crear |
preferencesStore |
| Hook |
crear |
usePreferences |
| Context |
crear |
PreferencesContext |
Dependencias
Depende de (Bloqueantes)
| ID |
Requerimiento |
Estado |
| RF-USER-001 |
CRUD Usuarios |
Tabla users |
Dependencias Relacionadas
| ID |
Requerimiento |
Relacion |
| RF-SETTINGS-001 |
Tenant Settings |
Valores por defecto |
Especificaciones Tecnicas
Endpoint GET /api/v1/users/me/preferences
// Response 200
{
"language": "es",
"timezone": "America/Mexico_City",
"dateFormat": "DD/MM/YYYY",
"timeFormat": "24h",
"currency": "MXN",
"numberFormat": "es-MX",
"theme": "dark",
"sidebarCollapsed": false,
"compactMode": false,
"fontSize": "medium",
"notifications": {
"email": {
"enabled": true,
"digest": "daily",
"marketing": false,
"security": true,
"updates": true
},
"push": {
"enabled": true,
"sound": true
},
"inApp": {
"enabled": true,
"desktop": false
}
},
"dashboard": {
"defaultView": "overview",
"widgets": ["sales", "inventory", "tasks"]
}
}
Endpoint PATCH /api/v1/users/me/preferences
// Request - Actualizacion parcial
{
"theme": "light",
"notifications": {
"email": {
"marketing": false
}
}
}
// Response 200
{
// PreferencesResponseDto completo actualizado
}
Merge de Preferencias
// preferences.service.ts
async updatePreferences(
userId: string,
updates: Partial<UserPreferences>,
): Promise<UserPreferences> {
let preferences = await this.preferencesRepository.findOne({
where: { userId },
});
if (!preferences) {
// Crear con defaults del tenant
const tenantDefaults = await this.getTenantDefaults(userId);
preferences = this.preferencesRepository.create({
userId,
...tenantDefaults,
});
}
// Deep merge para objetos anidados (notifications, dashboard)
const merged = deepMerge(preferences, updates);
return this.preferencesRepository.save(merged);
}
Aplicacion en Frontend
// PreferencesContext.tsx
export const PreferencesProvider: React.FC = ({ children }) => {
const [preferences, setPreferences] = useState<UserPreferences | null>(null);
useEffect(() => {
loadPreferences();
}, []);
useEffect(() => {
if (preferences) {
// Aplicar tema
document.documentElement.setAttribute('data-theme', preferences.theme);
// Aplicar idioma
i18n.changeLanguage(preferences.language);
// Configurar moment/dayjs timezone
dayjs.tz.setDefault(preferences.timezone);
}
}, [preferences]);
return (
<PreferencesContext.Provider value={{ preferences, updatePreferences }}>
{children}
</PreferencesContext.Provider>
);
};
Datos de Prueba
| Escenario |
Entrada |
Resultado |
| Obtener preferencias |
GET /preferences |
200, preferencias o defaults |
| Cambiar idioma |
language: "en" |
200, idioma actualizado |
| Zona horaria invalida |
timezone: "Invalid/Zone" |
400, "Zona horaria invalida" |
| Tema valido |
theme: "dark" |
200, tema actualizado |
| Tema invalido |
theme: "purple" |
400, "Valor no permitido" |
| Desactivar notificaciones |
notifications.email.enabled: false |
200, actualizado |
| Reset preferencias |
POST /reset |
200, defaults del tenant |
Estimacion
| Capa |
Story Points |
Notas |
| Database |
1 |
Tabla user_preferences |
| Backend |
2 |
CRUD preferencias |
| Frontend |
4 |
UI de configuracion + contexto |
| Total |
7 |
|
Notas Adicionales
- Las preferencias se cargan al inicio de sesion y se cachean
- Cambios de tema deben ser instantaneos (sin reload)
- Considerar preferencias sincronizadas entre dispositivos
- Exportar/importar preferencias para usuarios
- Preferencias de accesibilidad (alto contraste, reducir animaciones)
Historial de Cambios
| Version |
Fecha |
Autor |
Cambios |
| 1.0 |
2025-12-05 |
System |
Creacion inicial |
Aprobaciones
| Rol |
Nombre |
Fecha |
Firma |
| Analista |
System |
2025-12-05 |
[x] |
| Tech Lead |
- |
- |
[ ] |
| Product Owner |
- |
- |
[ ] |