# 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: ```txt /settings → configuración de la empresa activa /account → configuración personal del usuario ``` ```txt Settings shell != dominio != persistencia ``` ```txt 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: ```txt /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: ```txt /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: ```txt GET /identity/account PATCH /identity/account POST /identity/account/change-password ``` Regla de acceso: ```txt /account → requiere usuario autenticado → no requiere empresa activa → no requiere X-Company-Id ``` Composición funcional actual de la pantalla: ```txt /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_url` en 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í: ```txt 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: ```txt 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: ```txt secciones de Settings entries publicadas por módulos paths labels layout navegación ``` No debe conocer: ```txt 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: ```txt 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: ```txt legal_name trade_name tin email phone website default_payment_term_id ``` La UI actual de esta pantalla se organiza en: ```txt 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: ```txt PATCH parcial → solo viajan los campos dirty campo omitido → no modificar campo nullable enviado como null → limpiar valor ``` La pantalla sigue operando exclusivamente contra: ```txt 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: ```txt response → set/reset del formulario → sincronización de active company visible ``` Política de errores consolidada para esta pantalla: ```txt 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: ```txt 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: ```txt API → DTO → Adapter → Entity → Hook → Controller → Page Controller → UI ``` ### 5.3 Settings no es persistencia No crear: ```txt 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 ```txt 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: ```txt 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: ```ts 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: ```ts 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: ```txt billing = Facturación ``` pero no: ```txt billing contiene VeriFactu billing contiene Series documentales ``` Estas entries proceden de módulos. --- ## 9. Entries modulares Contrato mínimo V1: ```ts export interface ModuleSettingsEntryContribution { id: string; sectionId: string; labelKey: string; path: string; } ``` Semántica: ### `id` Identificador estable dentro del módulo. Ejemplos: ```txt verifactu document-series certificates company-details ``` ### `sectionId` Referencia a una sección conocida por el shell. Ejemplos: ```txt 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: ```txt /settings/billing/verifactu ``` La metadata no registra ni importa la Page. --- ## 10. Campos diferidos No forman parte del contrato mínimo obligatorio V1: ```txt 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: ```ts permission?: PermissionCode; ``` sin crear un sistema paralelo de autorización. --- ## 11. i18n El sistema real usa namespaces por módulo mediante: ```txt registerTranslations(moduleName, locale, resources) ``` Regla V1: ```txt sección → traducción del shell/apps-web entry → traducción del módulo propietario ``` Ejemplo conceptual: ```ts 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: ```txt /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: ```txt 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 ```txt SettingsNavigation → path Router modular → route correspondiente Módulo → Page ``` Nunca: ```txt SettingsNavigation → React component ``` --- ## 13. Ruta raíz `/settings` Decisión cerrada en V1: ```txt 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: ```txt IModuleClient[] → flatMap(settings.entries) → validar sectionId conocido → agrupar por sectionId → resolver labels → SettingsNavigation ``` La agregación no debe: ```txt 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: 1. conservar una única entrada principal **Configuración**; 2. eliminar/reemplazar los links placeholder que dupliquen el nuevo sistema; 3. dirigir a `/settings`; 4. 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: ```txt ┌───────────────────────────────────────────────────────────┐ │ Configuración │ │ Configura el comportamiento de la empresa activa │ ├─────────────────┬─────────────────────────────────────────┤ │ Empresa │ │ │ ... │ │ │ │ │ │ Facturación │ │ │ VeriFactu │ │ │ Series │ │ │ │ │ └─────────────────┴─────────────────────────────────────────┘ ``` Responsabilidades: ```txt 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: ```txt RdxPageContainer RdxPageHeader RdxPageContent ``` No mover a `packages/rdx-ui`: ```txt 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: ```txt IdentityAuthSessionProvider ``` Persistencia: ```txt active-company-storage.ts → react-secure-storage ``` HTTP: ```txt setup-interceptors.ts → X-Company-Id ``` Backend: ```txt require-company-context.middleware.ts → valida company/membership → req.user.companyId ``` Regla consolidada: ```txt /settings NO incluye companyId en la URL ``` pero esta decisión depende de mantener: ```txt 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: ```txt 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: ```txt Settings framework != Settings persistence ``` Ejemplos: ```txt 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 ```txt integra contexto/routing compone controllers ``` ### Controller ```txt gestiona caso de uso/formulario construye Params maneja side effects ``` No usa directamente: ```txt useParams useNavigate DTOs Settings shell ``` ### shared ```txt 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: ```txt Guardar toda la configuración ``` Ejemplo: ```txt /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: ```txt loading inicial error de query retry submit validation errors refetch errors ``` mediante los patrones vigentes: ```txt 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: ```txt /settings/billing/verifactu ``` Debe funcionar con: ```txt 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: ```txt