24 KiB
Frontend ERP – Sistema modular de Settings
1. Propósito
Este documento define la arquitectura del sistema de Configuración (Settings) del ERP.
El objetivo es disponer de una única experiencia de configuración para la empresa activa sin convertir la aplicación base en conocedora de las opciones particulares de cada módulo.
Principio rector:
La aplicación conoce el sistema de Settings.
La aplicación NO conoce los settings concretos de cada módulo.
Cada módulo declara las opciones de configuración que aporta.
Este diseño debe integrarse con la arquitectura modular existente del frontend, manteniendo la separación entre routing, datos, controllers y UI.
2. Alcance
2.1 /settings
/settings representa exclusivamente:
Configuración de la empresa activa
Ejemplos potenciales:
/settings/company/details
/settings/billing/verifactu
/settings/billing/document-series
/settings/billing/certificates
Una modificación realizada aquí afecta a la empresa activa y, potencialmente, a todos los usuarios que trabajan con ella.
2.2 /account
La configuración personal del usuario queda fuera de este sistema:
/account
→ configuración personal del usuario
Ejemplos:
idioma
apariencia
preferencias personales
seguridad de cuenta
Por tanto:
/settings ≠ /account
No se introduce en esta fase ningún scope: "company" | "user", porque /settings tiene un único ámbito semántico: la empresa activa.
3. Objetivos
El sistema debe permitir:
- Disponer de una única entrada Configuración en la navegación principal.
- Reunir en
/settingsconfiguraciones procedentes de múltiples módulos. - Permitir que varios módulos aporten opciones a una misma sección.
- Mantener a cada módulo como propietario de sus páginas, lógica y datos.
- Añadir una configuración nueva sin modificar el shell de Settings.
- Soportar navegación directa mediante URL.
- Aplicar permisos sobre opciones concretas.
- Mantener una experiencia visual consistente.
- Evitar un almacenamiento genérico de configuración.
- Evitar un framework dinámico excesivamente complejo.
4. No objetivos
El sistema de Settings no debe convertirse en:
un almacén genérico key/value
un framework de formularios
un sistema de permisos
un router alternativo
un gestor de estado global
un registro genérico de componentes React
Tampoco debe decidir cómo persiste cada dominio su configuración.
Ejemplos:
VeriFactu → persistencia y reglas propias
Series documentales → módulo document-series
Datos de empresa → módulo propietario de company/identity
Certificados → módulo propietario correspondiente
Regla:
Settings framework ≠ Settings persistence
5. Principio de propiedad
La propiedad se divide en dos niveles.
5.1 Shell de Settings
Responsable de:
descubrimiento de configuraciones
agrupación por secciones
ordenación
navegación
layout
estado de sección/entrada activa
filtrado por permisos
5.2 Módulo propietario
Responsable de:
routing de su configuración
Page
Page Controller
Controllers
Entities
Hooks
API
validación
persistencia
reglas de negocio
UI específica
La arquitectura interna de cada setting debe seguir el flujo general del ERP:
API
→ DTO
→ Adapter
→ Entity
→ Hook
→ Controller
→ Page Controller
→ UI
Los controllers no deben conocer routing. La integración con URL/navegación sigue correspondiendo al Page Controller/Page.
6. Arquitectura general
apps/web
│
├── navegación principal
│ │
│ └── Configuración
│ │
│ ▼
│ /settings
│
├── Settings Shell
│ ├── SettingsLayout / SettingsPage
│ ├── SettingsNavigation
│ └── SettingsRegistry
│
└── módulos registrados
│
├──────────────────────────┬─────────────────────────┐
▼ ▼ ▼
customer-invoices document-series identity
│ │ │
└─ contribution └─ contribution └─ contribution
│ │ │
└───────────── Settings Registry ──────────────────┘
El shell consume metadatos declarativos, no implementaciones de dominio.
7. Contribuciones de módulos
Cada módulo puede declarar las opciones de configuración que aporta.
El punto natural es el manifest.ts del módulo, siempre que la auditoría del repositorio confirme que este es el contrato correcto para ampliar metadatos de módulo.
Conceptualmente:
export const customerInvoicesManifest = {
// configuración existente del módulo
settings: {
sections: [],
entries: [],
},
};
El contrato exacto debe adaptarse al shape real del manifest existente.
8. Secciones compartidas entre módulos
Una sección de Settings no pertenece necesariamente a un único módulo.
Ejemplo:
Facturación
├── VeriFactu customer-invoices
├── Series documentales document-series
└── Certificados módulo propietario correspondiente
Varios módulos pueden contribuir a:
sectionId = "billing"
sin conocerse entre ellos.
El registry se encarga de fusionar esas contribuciones.
9. Contratos conceptuales
9.1 Sección
export interface ModuleSettingsSectionContribution {
id: string;
labelKey: string;
order?: number;
}
9.2 Entrada
export interface ModuleSettingsEntryContribution {
id: string;
sectionId: string;
labelKey: string;
descriptionKey?: string;
path: string;
order?: number;
permission?: string;
}
9.3 Contribución completa
export interface ModuleSettingsContribution {
sections?: ModuleSettingsSectionContribution[];
entries?: ModuleSettingsEntryContribution[];
}
Este contrato debe mantenerse deliberadamente pequeño.
No añadir en V1 salvo necesidad real:
component
loader
icon renderer
badge provider
status resolver
search keywords
feature flag resolver
nested tree arbitrario
custom layout
10. Identidad de secciones
Las secciones deben utilizar IDs semánticos y estables.
Ejemplos:
company
billing
sales
purchases
users
integrations
No es necesario convertir inicialmente el conjunto en una unión TypeScript cerrada si eso dificulta la extensibilidad modular.
Ejemplo:
{
id: "billing",
labelKey: "settings.sections.billing",
order: 200,
}
Si dos módulos declaran billing, el registry debe reconocer que representan la misma sección.
11. Conflictos de secciones
El mismo sectionId no puede representar semánticas diferentes.
Ejemplo inválido:
billing + "Facturación"
billing + "Ventas"
Regla:
mismo sectionId
→ misma identidad semántica
→ labels compatibles
En desarrollo, el registry debe detectar conflictos en lugar de resolverlos silenciosamente.
12. Identidad de entradas
El id identifica una opción dentro del sistema de Settings.
Debe ser:
estable
semántico
independiente del texto visible
Ejemplos:
verifactu
document-series
certificates
company-details
Evitar:
setting1
optionA
config
Si existe riesgo real de colisión, el registry puede usar internamente una identidad compuesta:
moduleId + entryId
sin obligar a exponer esa representación al consumidor.
13. Routing
Regla principal
Settings registra destinos de navegación, no componentes React.
Evitar:
{
id: "verifactu",
component: VerifactuSettingsPage,
}
Preferir:
{
id: "verifactu",
path: "/settings/billing/verifactu",
}
El módulo propietario continúa registrando su propia route.
Motivo
Settings Registry
→ conoce navegación
Módulo
→ conoce página
Esto evita convertir Settings en un segundo router o sistema de carga dinámica de React.
14. Namespace de rutas
Convención objetivo:
/settings/:section/:setting
Ejemplos:
/settings/company/details
/settings/billing/verifactu
/settings/billing/document-series
/settings/billing/certificates
La ruta exacta debe verificarse contra el routing real antes de implementarla.
15. Ruta raíz /settings
La raíz puede:
redirigir a la primera configuración disponible
o presentar un índice/resumen de configuraciones.
La decisión final es de UX y puede tomarse al implementar el shell.
No debe contener lógica de dominio.
16. Settings Registry
El registry es la pieza responsable de combinar las contribuciones de todos los módulos.
Entrada conceptual:
ModuleManifest[]
Salida conceptual:
export interface SettingsRegistry {
sections: SettingsSection[];
}
export interface SettingsSection {
id: string;
labelKey: string;
order: number;
entries: SettingsEntry[];
}
17. Responsabilidades del registry
Debe:
recoger contribuciones
fusionar secciones comunes
validar IDs
detectar conflictos
ordenar secciones
ordenar entradas
filtrar entradas no disponibles por permisos
producir un resultado determinista
No debe:
consultar APIs de negocio
cargar configuración
persistir valores
renderizar formularios
resolver reglas fiscales
18. Registro declarativo, no imperativo
Preferir:
export const manifest = {
settings: ...,
};
frente a:
registerSetting(...);
registerSetting(...);
Ventajas:
- evita side effects durante imports;
- facilita validación;
- hace visible la superficie pública del módulo;
- permite inspección;
- permite ordenar antes de renderizar;
- encaja con una arquitectura basada en manifests.
19. Lazy loading
El metadata usado para construir la navegación debe ser ligero.
El shell no debería necesitar importar toda una página para saber que existe una opción.
manifest metadata
≠
page implementation
El routing del módulo puede mantener lazy loading si la infraestructura actual ya lo soporta.
20. Layout
Modelo recomendado:
┌───────────────────────────────────────────────────────────┐
│ Configuración │
│ Configura el comportamiento de la empresa activa │
├─────────────────┬─────────────────────────────────────────┤
│ │ │
│ Empresa │ │
│ Datos │ │
│ │ contenido del módulo │
│ Facturación │ │
│ VeriFactu │ │
│ Series │ │
│ Certificados │ │
│ │ │
│ Integraciones │ │
│ │ │
└─────────────────┴─────────────────────────────────────────┘
El panel izquierdo pertenece al framework.
El área de contenido pertenece a la ruta del módulo.
21. Sistema visual
El shell debe reutilizar los componentes estructurales Rdx existentes cuando sean aplicables.
Por ejemplo:
RdxPageContainer
RdxPageHeader
RdxPageStack
La infraestructura dependiente de routing, manifests, permisos o empresa activa no debe vivir en packages/rdx-ui.
rdx-ui puede contener únicamente piezas visuales genéricas y desacopladas.
22. Navegación principal
La aplicación debe exponer una sola entrada:
Configuración
El menú principal no debe contener directamente:
VeriFactu
Series
Certificados
Configuración de clientes
...
Estas opciones aparecen dentro de /settings.
App navigation
└── Configuración
└── SettingsNavigation
├── Empresa
├── Facturación
├── ...
23. Empresa activa
Todo /settings debe operar sobre la empresa activa.
No debe introducirse un segundo selector o una segunda fuente de verdad.
Conceptualmente:
active company changes
│
▼
Settings context changes
│
▼
module queries resolve data for new company
La implementación debe reutilizar la infraestructura de empresa activa existente en el ERP.
24. Cambio de empresa activa
Una página de configuración no debe conservar datos pertenecientes a la empresa anterior después de cambiar la empresa activa.
Debe resolverse mediante la infraestructura ya existente de:
company context
headers
query keys
query invalidation
No mediante estado duplicado dentro del framework Settings.
25. Persistencia
Regla fundamental:
Settings framework
≠
Settings persistence
No crear únicamente por este sistema:
settings table
settings(key, value)
GenericSettingsRepository
useSettings()
Cada dominio conserva su propia persistencia y validación.
26. Formularios
Cada setting mantiene sus propios formularios.
Ejemplo:
VerifactuSettingsPage
│
▼
VerifactuSettingsEditor
│
▼
React Hook Form
Settings no proporciona un GenericSettingsForm.
Los formularios deben seguir las convenciones ya consolidadas del ERP para fields y React Hook Form.
27. Guardado
No debe existir un botón global:
Guardar toda la configuración del ERP
Cada entrada mantiene su propia transacción de guardado.
Ejemplos:
/settings/billing/verifactu
→ Guardar VeriFactu
/settings/company/details
→ Guardar datos de empresa
Esto evita mezclar dominios y transacciones.
28. Errores
El shell gestiona únicamente errores estructurales del sistema Settings.
Ejemplos:
ruta inválida
entry inexistente
acceso no permitido al subtree
error de render del subtree
El módulo sigue gestionando errores de carga/guardado de su configuración mediante los patrones generales del ERP.
29. Permisos
Una entrada puede declarar opcionalmente:
permission?: string;
El shell reutiliza la infraestructura central de autorización para decidir si la entrada está disponible.
Ejemplos conceptuales:
VeriFactu
→ manage_verifactu_settings
Certificados
→ manage_company_certificates
Settings no interpreta el permiso ni implementa autorización propia.
30. Ocultar vs deshabilitar
Para una opción de Settings sin permiso, la decisión inicial es:
no mostrarla
La navegación representa capacidades administrativas disponibles.
Esto no sustituye la autorización backend.
31. Seguridad
Ocultar una entrada del menú no constituye autorización.
El backend debe seguir validando:
usuario
permiso
empresa activa
operación solicitada
El frontend únicamente adapta la experiencia de navegación.
32. i18n
Los manifests deben declarar claves, no textos hardcodeados:
labelKey
descriptionKey
Ejemplo:
labelKey: "customerInvoices.settings.verifactu.title";
El módulo propietario mantiene sus traducciones.
Las secciones compartidas deben utilizar claves semánticamente compatibles.
33. Ordenación
Cada sección y entrada puede declarar:
order?: number;
Ejemplo:
100 Empresa
200 Facturación
300 Ventas
400 Compras
500 Integraciones
Dentro de Facturación:
100 VeriFactu
200 Series documentales
300 Certificados
No usar orden alfabético como criterio principal porque los labels dependen del idioma.
Si order no existe, el resultado debe seguir siendo determinista mediante un valor por defecto y un desempate estable por id.
34. Conflictos
El registry debe detectar al menos en desarrollo:
Section ID conflict
mismo id
labels incompatibles
Entry ID conflict
misma identidad registrada más de una vez
Path conflict
dos entries incompatibles apuntan a la misma ruta
Missing section
entry apunta a una section inexistente
Los conflictos no deben resolverse silenciosamente.
35. Responsive
Desktop:
navegación lateral + contenido
Viewport estrecho:
navegación adaptada
→ selección/listado de settings
→ contenido
Debe mantenerse el mismo registry y las mismas rutas.
No crear dos sistemas de Settings diferentes.
36. Accesibilidad
La navegación debe usar semántica real:
<nav>
<a>
Requisitos mínimos:
focus-visible
aria-current="page"
uso completo con teclado
jerarquía correcta de headings
estado activo perceptible sin depender solo del color
37. Deep links
Cada configuración debe ser direccionable directamente.
Ejemplo:
/settings/billing/verifactu
Debe funcionar con:
refresh
deep-link
bookmark
navegación interna
El estado activo se deriva de la URL, no de un setSelectedSetting(...) global.
38. Cambios sin guardar
El shell no debe inspeccionar ni gestionar globalmente formularios dirty de los módulos.
Cada página propietaria debe usar el mecanismo estándar del ERP para:
unsaved changes
navigation guard
cancel
39. Queries y caché
Cada módulo conserva sus query keys y estrategia de caché.
El framework no debe crear una query genérica:
["settings", ...]
para todos los dominios.
Ejemplos:
VeriFactu settings query key
→ customer-invoices
Document Series query keys
→ document-series
40. Caso piloto: VeriFactu
VeriFactu será el primer consumidor real del framework.
Conceptualmente:
customer-invoices
└── manifest
└── settings
├── section: billing
└── entry: verifactu
Ruta objetivo:
/settings/billing/verifactu
La ubicación física de su implementación debe definirse después de auditar el módulo real.
41. Independencia entre Settings y módulos
Debe cumplirse:
eliminar customer-invoices
→ desaparecen automáticamente sus settings
Settings continúa funcionando
Y también:
añadir módulo nuevo
→ puede aportar Settings
→ sin modificar SettingsPage
→ sin modificar SettingsNavigation
Este es el principal criterio de aceptación del sistema.
42. Independencia entre módulos
Un módulo no debe importar otro únicamente para registrar sus settings.
Incorrecto:
customer-invoices
→ importa document-series settings
Correcto:
customer-invoices manifest ─┐
├→ registry
document-series manifest ───┘
43. Anti-patterns prohibidos
Configuración central hardcoded
const SETTINGS = [
VerifactuSettings,
DocumentSeriesSettings,
CertificateSettings,
];
Registro central de componentes de dominio
{
component: VerifactuSettingsPage,
}
como API principal del registry.
Persistencia genérica indiscriminada
settings
├── key
└── value
para almacenar configuraciones de dominios diferentes.
Switch central
switch (setting.id) {
case "verifactu":
case "certificates":
}
Imports cruzados por Settings
apps/web/settings
→ importa páginas concretas de todos los módulos
si el sistema modular de routing puede evitarlos.
Routing o manifests en rdx-ui
No permitido.
44. Extensiones futuras posibles
Solo cuando aparezca necesidad real:
buscador de configuración
badges de estado
feature flags
keywords
settings favoritos
navegación reciente
secciones anidadas
links externos
estado "requiere atención"
No forman parte de V1.
45. Criterios de aceptación
El diseño se considera correctamente implementado si:
- Añadir un setting nuevo no modifica el shell.
- Eliminar un módulo elimina automáticamente sus settings.
- Varios módulos pueden contribuir a la misma sección.
- Cada setting tiene URL propia.
- El módulo conserva control sobre sus datos y UI.
- Settings no persiste datos de negocio.
rdx-uino conoce routing ni manifests.- No existen imports cruzados entre módulos por Settings.
- La empresa activa es el único scope de
/settings. /accountpermanece independiente.- El resultado del registry es determinista.
- Los conflictos de metadata se detectan en desarrollo.
46. Fases de implementación recomendadas
Fase 0 — Auditoría
Investigar sin modificar código:
manifest actual
module registry
routing composition
sidebar
active company
permissions
i18n
layouts
Fase 1 — Contrato de contribuciones
Añadir únicamente:
module manifest
→ settings metadata
y el registry correspondiente.
Fase 2 — Shell de Settings
Implementar:
/settings
SettingsLayout
SettingsNavigation
agrupación por sección
routing
permissions
Sin introducir todavía lógica de dominios concretos.
Fase 3 — Consumidor piloto
Integrar VeriFactu como primera contribución real.
Fase 4 — Segundo módulo
Integrar una configuración aportada por otro módulo, preferentemente document-series, para validar que dos módulos contribuyen a billing sin conocerse.
Fase 5 — Consolidación
Solo después revisar abstracciones comunes, tests y documentación del UI Kit.
47. Tests mínimos
Registry
Debe comprobar:
fusiona secciones
ordena entries
detecta IDs duplicados
detecta conflictos
respeta permisos
resultado determinista
Routing
Debe comprobar:
deep-link directo
ruta inexistente
navegación entre entries
refresh
UI
Con Testing Library:
navegación accesible
aria-current
entry visible/no visible por permisos
render del contenido de la route
No testear implementación interna de cada módulo desde Settings.
48. Decisiones consolidadas
/settings
→ configuración de la empresa activa
/account
→ configuración personal del usuario
Una única entrada "Configuración"
en la navegación principal.
Varios módulos pueden contribuir
a una misma sección.
La aplicación base no conoce
los settings específicos de los módulos.
Los módulos declaran sus settings
mediante metadata.
Settings registra navegación,
no implementaciones React.
Cada dominio conserva
su propia persistencia.
Settings no pertenece a rdx-ui.
VeriFactu será el primer consumidor real.
49. Pendientes que requieren auditoría
No deben cerrarse sin inspeccionar el repositorio:
shape exacto de manifest.ts
ubicación física del SettingsRegistry
forma de agregar manifests
routing modular actual
sistema real de permisos
fuente concreta de active company
API de traducciones en manifests
estrategia de lazy routes
ruta exacta del SettingsLayout
mecanismo de ErrorBoundary del shell
50. Regla mental final
App
→ conoce Settings
Settings
→ conoce contribuciones
Contribución
→ conoce metadata
Módulo
→ conoce dominio
Ruta
→ conecta Settings con el módulo
Criterio principal:
Añadir una opción de configuración desde un módulo
NO debe requerir modificar el shell de Settings.