FactuGES_ERP/docs/settings/frontend-erp-settings-system-design.md
david 64b90888c3 feat(identity): add Spanish localization for account management
- Introduced `es.json` for Spanish translations in account management.
- Created account routes and controllers for managing user accounts.
- Implemented hooks and API calls for fetching and updating account profiles.
- Developed forms and validation schemas for account profile and password management.
- Added error handling and feedback mechanisms for account actions.
- Built UI components for account settings page, including password change dialog.
- Integrated translations for new UI components and error messages.
2026-08-18 00:56:04 +02:00

1202 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 │ │
│ ... │ │
│ │ <Outlet /> │
│ 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
RdxPageStack
```
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
<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:
```txt
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:
```txt
/settings existe
deep link a una route modular
refresh
ruta inexistente bajo settings
```
### UI
Testing Library:
```txt
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:
```txt
manifest
routing modular
lazy routes
i18n por namespace
public exports
```
El trabajo previo necesario está en el shell:
```txt
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:
```txt
obligada/exenta
certificados
AEAT
régimen
calificación de operación
causa de exención
```
---
## 30. No objetivos V1
No implementar todavía:
```txt
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:
1. `/settings` representa únicamente la empresa activa.
2. `/account` permanece separado.
3. Existe una única entrada Configuración en la navegación principal.
4. Las secciones son definidas por el shell.
5. Varios módulos pueden publicar entries en la misma sección.
6. Las entries se descubren desde `IModuleClient[]`.
7. El shell no importa Pages de dominio.
8. Cada entry apunta a una route real del módulo.
9. El router sigue siendo la única fuente de verdad de navegación.
10. No se introduce persistencia genérica.
11. No se crea un segundo contexto de company.
12. `rdx-ui` no conoce routing ni manifests.
13. Añadir una nueva entry no requiere modificar `SettingsLayout` ni `SettingsNavigation`.
---
## 32. Decisiones abiertas
Pendientes de cerrar durante la implementación del shell:
1. Si `order` resulta necesario en el primer segundo consumidor.
2. Cuándo introducir `permission?: PermissionCode`.
3. Si en el futuro se necesita namespace i18n explícito.
Estas decisiones no deben bloquear la V1 mínima.
---
## 33. Regla mental final
```txt
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`, `SettingsNavigation` ni un switch central de Pages.