- Updated ScopedListProformasPageView, ProformaUpdateEditorForm, ProformaUpdatePage, RecurringProformaCreatePage, and others to use RdxPageContent instead of RdxEditorLayout. - Removed RdxPageStack and replaced it with RdxPageContent for consistent layout management. - Deleted RdxEditorLayout component and its associated tests. - Introduced RdxPageContent component to handle main content and optional aside layout. - Adjusted tests to reflect changes in component structure and layout.
22 KiB
Frontend ERP – Sistema modular de Settings
1. Estado del documento
Estado: DISEÑO V1 AJUSTADO A LA ARQUITECTURA REAL DEL REPOSITORIO
Este documento sustituye la versión inicial del diseño de Settings y recoge las correcciones derivadas de la auditoría del monorepo realizada el 11 de agosto de 2026.
Decisiones ya consolidadas:
/settings
→ configuración de la empresa activa
/account
→ configuración personal del usuario
Settings shell != dominio != persistencia
El shell conoce la navegación de Settings.
Los módulos conocen sus páginas, rutas y lógica.
La arquitectura modular real del frontend ya compone rutas aportadas por módulos mediante IModuleClient, pero no dispone todavía de navegación modular, permisos frontend para navegación ni un subtree /settings/* compartido.
2. Propósito
Definir una infraestructura mínima y extensible para la configuración empresarial del ERP.
Objetivo principal:
Añadir una nueva opción de configuración desde un módulo no debe requerir modificar la Page ni la navegación interna del shell de Settings.
El sistema debe aprovechar la infraestructura modular existente y no crear un registry paralelo de rutas ni un framework genérico de persistencia.
3. Alcance
3.1 /settings
/settings representa exclusivamente configuración de la empresa activa.
Ejemplos previstos:
/settings/company/details
/settings/billing/verifactu
/settings/billing/document-series
/settings/billing/certificates
Una configuración situada bajo /settings debe operar sobre la empresa activa resuelta por la infraestructura global de identidad.
Caso ya implementado:
/settings/company/details
-> GET /companies/current
-> PATCH /companies/current
La pantalla no selecciona el target mediante companyId.
3.2 /account
/account queda reservado para configuración personal del usuario autenticado.
En la implementación actual se resuelve como pantalla única y no debe mezclarse con /settings.
Backend ya preparado actualmente para el subtree de usuario:
GET /identity/account
PATCH /identity/account
POST /identity/account/change-password
Regla de acceso:
/account
→ requiere usuario autenticado
→ no requiere empresa activa
→ no requiere X-Company-Id
Composición funcional actual de la pantalla:
/account
├─ información personal
│ ├─ name
│ ├─ email
│ └─ language_code
├─ seguridad
│ └─ cambio de contraseña mediante dialog
└─ sesión
└─ logout reutilizado
Restricciones conocidas de la V1 actual:
- el avatar solo se muestra; no existe upload real confirmado para exponer edición en la UI
- el backend mantiene
avatar_urlen el contrato por compatibilidad actual - enable/disable global de usuarios sigue siendo una capacidad de dominio pendiente de exposición administrativa
No se introduce scope: "company" | "user" en V1.
4. Arquitectura real de referencia
La aplicación compone actualmente los módulos así:
App
└─ apps/web/src/app.tsx
├─ QueryClientProvider
├─ DataSourceProvider
├─ IdentityAuthSessionProvider
└─ RouterProvider(getAppRouter)
└─ apps/web/src/routes/app-routes.tsx
├─ modules = apps/web/src/register-modules.tsx
├─ buildModuleRoutes(modules, "auth")
├─ buildModuleRoutes(modules, "app-sidebar")
├─ buildModuleRoutes(modules, "app-fullscreen")
└─ createBrowserRouter(...)
├─ AuthLayout
├─ AppFullscreenLayout
└─ AppSidebarLayout
Piezas reales relevantes:
apps/web/src/register-modules.tsx
→ modules: IModuleClient[]
modules/core/src/web/lib/modules/module-client.interface.ts
→ IModuleClient
→ ModuleRouteObject
→ ModuleRouteHandle
apps/web/src/components/module-routes.tsx
→ buildModuleRoutes()
→ normalizeRoute()
apps/web/src/routes/app-routes.tsx
→ router raíz
V1 de Settings debe extender esta arquitectura, no sustituirla.
5. Principios de diseño
5.1 El shell no conoce dominio
apps/web puede conocer:
secciones de Settings
entries publicadas por módulos
paths
labels
layout
navegación
No debe conocer:
DTOs
Entities
formularios
controllers
endpoints
reglas VeriFactu
reglas de Series
reglas de certificados
5.2 Cada módulo conserva ownership
El módulo propietario de una configuración mantiene:
route
Page
Page Controller
Controllers
shared/api
shared/adapters
shared/entities
shared/hooks
validación
persistencia
reglas de negocio
UI específica
En company/details, los campos editables actuales son:
legal_name
trade_name
tin
email
phone
website
default_payment_term_id
La UI actual de esta pantalla se organiza en:
Datos generales
→ legal_name
→ trade_name
→ tin
Datos de contacto
→ email
→ phone
→ website
Condiciones comerciales
→ default_payment_term_id
Semántica de guardado esperada para esta pantalla:
PATCH parcial
→ solo viajan los campos dirty
campo omitido
→ no modificar
campo nullable enviado como null
→ limpiar valor
La pantalla sigue operando exclusivamente contra:
GET /companies/current
PATCH /companies/current
sin usar companyId como selector del target.
Tras un guardado correcto, la respuesta del servidor debe seguir siendo la fuente única de verdad:
response
→ set/reset del formulario
→ sincronización de active company visible
Política de errores consolidada para esta pantalla:
error inicial sin datos requeridos
→ PageErrorState
error de refetch con datos previos
→ mantener contenido
→ ErrorAlert inline
cancelled
→ no elevar a pantalla bloqueante
422 de backend con path mapeable
→ error de campo en RHF
error general de mutation
→ feedback local
Fuera de alcance del formulario actual:
slug
status
fiscalidad avanzada
VeriFactu
document series
payment methods
branding
bank accounts
defaultPaymentMethodId
El hecho de que una Page aparezca dentro de /settings no cambia su arquitectura interna.
Flujo obligatorio:
API
→ DTO
→ Adapter
→ Entity
→ Hook
→ Controller
→ Page Controller
→ UI
5.3 Settings no es persistencia
No crear:
GenericSettingsRepository
settings(key, value)
useSettings()
tabla genérica de settings
Cada dominio conserva su propio modelo de persistencia.
5.4 Settings no es un router alternativo
La navegación de Settings referencia rutas reales ya registradas en el router modular.
No registrar componentes React dentro de metadata de Settings.
6. Arquitectura objetivo V1
apps/web
│
├── register-modules.tsx
│ └── IModuleClient[]
│
├── routes/app-routes.tsx
│ └── nueva rama /settings/*
│ └── SettingsLayout
│ └── rutas de módulos bajo namespace /settings/...
│
└── settings/
├── settings-section-catalog.ts
├── build-settings-navigation.ts
├── build-settings-routes.tsx
├── settings-layout.tsx
├── settings-navigation.tsx
└── settings-index-page.tsx
▲
│
metadata ligera
│
┌─────────┴──────────────────┐
│ │
customer-invoices document-series
manifest manifest
settings entries settings entries
No se introduce un SettingsRegistry autónomo con ciclo de vida propio.
La pieza de agregación debe ser una función/estructura ligera sobre los IModuleClient[] ya registrados en apps/web.
7. IModuleClient como extension point
El manifest.ts real del módulo utiliza IModuleClient.
Actualmente IModuleClient contiene, entre otros conceptos:
name
version
description?
dependencies?
protected?
layout?
icon?
routes?
No contiene Settings.
V1 puede ampliar el contrato compartido con metadata ligera de Settings.
Dirección conceptual:
export interface ModuleSettingsEntryContribution {
id: string;
sectionId: string;
labelKey: string;
path: string;
}
export interface ModuleSettingsContribution {
entries: ModuleSettingsEntryContribution[];
}
export interface IModuleClient {
// contrato existente
settings?: ModuleSettingsContribution;
}
El shape definitivo debe adaptarse al tipo real sin duplicar contratos existentes.
No crear un segundo manifest específico para Settings salvo que aparezca una razón técnica durante la implementación.
8. Secciones: catálogo central en V1
En V1, las secciones se definen en el shell, no en los módulos.
Motivo:
- la navegación actual es responsabilidad de
apps/web; - las secciones son conceptos de información global;
- evita conflictos de namespace i18n;
- evita implementar prematuramente conflict detection;
- permite que varios módulos contribuyan a una misma sección sin conocerse.
Ejemplo:
export interface SettingsSectionDefinition {
id: string;
labelKey: string;
}
export const SETTINGS_SECTIONS: SettingsSectionDefinition[] = [
{
id: "company",
labelKey: "settings.sections.company",
},
{
id: "billing",
labelKey: "settings.sections.billing",
},
];
Esto no implica que el shell conozca las entries concretas.
El shell conoce:
billing = Facturación
pero no:
billing contiene VeriFactu
billing contiene Series documentales
Estas entries proceden de módulos.
9. Entries modulares
Contrato mínimo V1:
export interface ModuleSettingsEntryContribution {
id: string;
sectionId: string;
labelKey: string;
path: string;
}
Semántica:
id
Identificador estable dentro del módulo.
Ejemplos:
verifactu
document-series
certificates
company-details
sectionId
Referencia a una sección conocida por el shell.
Ejemplos:
company
billing
labelKey
Clave i18n del módulo.
El namespace se resuelve con manifest.name, siguiendo el sistema de traducciones existente.
path
Ruta absoluta real de la aplicación.
Ejemplo:
/settings/billing/verifactu
La metadata no registra ni importa la Page.
10. Campos diferidos
No forman parte del contrato mínimo obligatorio V1:
descriptionKey
order
permission
icon
badge
status
feature flags
keywords
children anidados
descriptionKey
Se elimina de V1 porque no existe un patrón real de navegación que lo requiera.
order
Se difiere hasta comprobar que el orden central/default no sea suficiente.
Si se introduce posteriormente, deberá tener semántica explícita y determinista.
permission
Existe PermissionCode = string en el proyecto, pero el shell no dispone todavía de un mecanismo frontend operativo para filtrar navegación o proteger routes por permiso.
Por tanto, no se promete enforcement en V1.
Cuando exista infraestructura frontend real, el contrato podrá evolucionar a:
permission?: PermissionCode;
sin crear un sistema paralelo de autorización.
11. i18n
El sistema real usa namespaces por módulo mediante:
registerTranslations(moduleName, locale, resources)
Regla V1:
sección
→ traducción del shell/apps-web
entry
→ traducción del módulo propietario
Ejemplo conceptual:
settings: {
entries: [
{
id: "verifactu",
sectionId: "billing",
labelKey: "settings.verifactu.title",
path: "/settings/billing/verifactu",
},
],
}
El shell resolverá esa key usando el namespace del manifest.name.
No introducir namespace + key explícitos salvo que aparezca una necesidad real.
12. Routing
12.1 Estado actual
El router ya permite rutas modulares por layout, pero no existe todavía una rama compartida:
/settings/*
con SettingsLayout.
Existe /settings como placeholder, no como feature implementada.
12.2 Cambio requerido
V1 debe añadir una rama explícita en el router shell para Settings.
Objetivo conceptual:
AppSidebarLayout
└── /settings
└── SettingsLayout
├── index
└── rutas de módulos bajo /settings/...
El detalle exacto debe integrarse con buildModuleRoutes() y ModuleRouteObject.
No crear un router secundario.
12.3 Regla de acoplamiento
SettingsNavigation
→ path
Router modular
→ route correspondiente
Módulo
→ Page
Nunca:
SettingsNavigation
→ React component
13. Ruta raíz /settings
Decisión cerrada en V1:
si existe al menos una entry
→ redirección determinista a la primera entry agregada
si no existe ninguna entry
→ estado vacío útil del shell
La elección sigue sin afectar al contrato de los módulos.
14. Agregación de entries
No crear un objeto SettingsRegistry con estado propio.
Usar una transformación determinista sobre los módulos registrados:
IModuleClient[]
→ flatMap(settings.entries)
→ validar sectionId conocido
→ agrupar por sectionId
→ resolver labels
→ SettingsNavigation
La agregación no debe:
consultar APIs
mantener estado
crear context global
resolver permisos inexistentes
registrar routes
Debe ser una transformación pequeña y testeable.
15. Navegación principal
La navegación actual está hardcodeada en apps/web.
Existe ya un menú de Configuración con links placeholder.
V1 debe:
- conservar una única entrada principal Configuración;
- eliminar/reemplazar los links placeholder que dupliquen el nuevo sistema;
- dirigir a
/settings; - construir la navegación interna de Settings a partir de las secciones centrales + entries modulares.
No crear un segundo sistema general de navegación para toda la aplicación.
La modularización se limita inicialmente a las entries internas de Settings.
16. Layout de Settings
Responsabilidad de apps/web.
Modelo recomendado:
┌───────────────────────────────────────────────────────────┐
│ Configuración │
│ Configura el comportamiento de la empresa activa │
├─────────────────┬─────────────────────────────────────────┤
│ Empresa │ │
│ ... │ │
│ │ <Outlet /> │
│ Facturación │ │
│ VeriFactu │ │
│ Series │ │
│ │ │
└─────────────────┴─────────────────────────────────────────┘
Responsabilidades:
SettingsLayout
→ estructura + Outlet
SettingsNavigation
→ secciones + links
route del módulo
→ contenido
El estado activo debe derivarse de la URL, no de useState.
17. Sistema visual
Settings debe reutilizar los componentes Rdx existentes cuando sean adecuados.
Ejemplos:
RdxPageContainer
RdxPageHeader
RdxPageContent
No mover a packages/rdx-ui:
routing
SettingsLayout dependiente del router
agregación de manifests
active company
permissions de aplicación
rdx-ui solo puede albergar componentes visuales genéricos y desacoplados.
18. Empresa activa
Fuente real:
IdentityAuthSessionProvider
Persistencia:
active-company-storage.ts
→ react-secure-storage
HTTP:
setup-interceptors.ts
→ X-Company-Id
Backend:
require-company-context.middleware.ts
→ valida company/membership
→ req.user.companyId
Regla consolidada:
/settings NO incluye companyId en la URL
pero esta decisión depende de mantener:
X-Company-Id
RequireCompany guard
reset global de React Query al cambiar empresa
No introducir un segundo selector o contexto de company dentro de Settings.
19. React Query y cambio de empresa
Las query keys actuales no incorporan companyId.
La coherencia entre empresas se mantiene globalmente mediante el cambio de empresa:
cancelQueries()
removeQueries({ type: "inactive" })
resetQueries({ type: "active" })
Settings debe reutilizar este mecanismo.
No crear query keys especiales con companyId solo para Settings si el resto del ERP no sigue esa estrategia.
Debe considerarse una invariante del shell:
Cambiar la empresa activa debe resetear los datos visibles antes de reutilizar cualquier Page de Settings.
20. Persistencia
Regla consolidada:
Settings framework != Settings persistence
Ejemplos:
VeriFactu
→ configuración propia del dominio
DocumentSeries
→ document-series
Company
→ identity/company
Certificados
→ módulo propietario
La coexistencia en una misma UI no implica modelo de datos compartido.
21. Page / Controller / Shared
Cada setting conserva las reglas generales del ERP.
Page Controller
integra contexto/routing
compone controllers
Controller
gestiona caso de uso/formulario
construye Params
maneja side effects
No usa directamente:
useParams
useNavigate
DTOs
Settings shell
shared
shared/api
→ transporte
shared/adapters
→ DTO → Entity
shared/entities
→ contratos frontend
shared/hooks
→ React Query
No conoce routing ni Settings.
22. Formularios y guardado
Cada setting mantiene su propio formulario y transacción.
No existe:
Guardar toda la configuración
Ejemplo:
/settings/billing/verifactu
→ Guardar configuración VeriFactu
Una Page de configuración puede usar el footer/editor pattern existente del ERP si corresponde, pero el shell no impone un GenericSettingsForm.
23. Loading y errores
El shell solo gestiona errores estructurales de routing/render.
Cada módulo gestiona:
loading inicial
error de query
retry
submit
validation errors
refetch errors
mediante los patrones vigentes:
PageErrorState
ErrorAlert
ErrorBoundary / route error cuando corresponda
El shell no interpreta ApiClientError de dominios concretos.
24. Deep links
Cada entry debe ser una ruta real y refrescable:
/settings/billing/verifactu
Debe funcionar con:
refresh
bookmark
deep link
navegación interna
No depender de estado previo del shell.
25. Unsaved changes
Cada módulo es responsable de su política de cambios sin guardar.
No crear un dirty global dentro del Settings shell.
26. Accesibilidad
La navegación debe usar semántica real:
<nav>
<a>/<Link>
aria-current="page"
focus-visible
jerarquía correcta de headings
No utilizar div clicables.
En responsive debe conservarse el mismo modelo de rutas y metadata.
27. Seguridad
La navegación frontend no es una barrera de seguridad.
Aunque en el futuro una entry declare permission, el backend debe validar siempre autorización y company context.
No duplicar autorización backend dentro de Settings.
28. Testing V1
Agregador
Probar:
recoge entries de varios módulos
agrupa por sectionId
ignora módulos sin settings
rechaza sectionId desconocido
resultado determinista
No implementar todavía conflict detection compleja.
Routing
Probar:
/settings existe
deep link a una route modular
refresh
ruta inexistente bajo settings
UI
Testing Library:
nav semántica
aria-current
navegación entre entries
render de Outlet
29. Caso piloto: VeriFactu
customer-invoices es un primer consumidor viable.
El módulo ya dispone de:
manifest
routing modular
lazy routes
i18n por namespace
public exports
El trabajo previo necesario está en el shell:
rama /settings/*
SettingsLayout
agregación de entries
navegación interna
La implementación funcional de VeriFactu se documentará por separado.
Settings no debe incorporar conocimiento de:
obligada/exenta
certificados
AEAT
régimen
calificación de operación
causa de exención
30. No objetivos V1
No implementar todavía:
settings sections dinámicas por módulo
registro modular de navegación global
permission filtering real
order configurable por módulo
descriptionKey
search de settings
badges
feature flags
favoritos
recent settings
nested sections
GenericSettingsRepository
SettingsProvider global
SettingsRegistry con estado
31. Criterios de aceptación
La V1 es correcta si:
/settingsrepresenta únicamente la empresa activa./accountpermanece separado.- Existe una única entrada Configuración en la navegación principal.
- Las secciones son definidas por el shell.
- Varios módulos pueden publicar entries en la misma sección.
- Las entries se descubren desde
IModuleClient[]. - El shell no importa Pages de dominio.
- Cada entry apunta a una route real del módulo.
- El router sigue siendo la única fuente de verdad de navegación.
- No se introduce persistencia genérica.
- No se crea un segundo contexto de company.
rdx-uino conoce routing ni manifests.- Añadir una nueva entry no requiere modificar
SettingsLayoutniSettingsNavigation.
32. Decisiones abiertas
Pendientes de cerrar durante la implementación del shell:
- Si
orderresulta necesario en el primer segundo consumidor. - Cuándo introducir
permission?: PermissionCode. - Si en el futuro se necesita namespace i18n explícito.
Estas decisiones no deben bloquear la V1 mínima.
33. Regla mental final
App shell
→ define secciones y layout
IModuleClient
→ declara entries
Router
→ resuelve paths
Módulo
→ implementa dominio
Settings
→ compone navegación, no negocio
Criterio principal:
Añadir una opción de configuración desde un módulo no debe requerir modificar
SettingsLayout,SettingsNavigationni un switch central de Pages.