Arquitectura
El Panel es una SPA de React construida con Vite. El código se organiza por feature: cada dominio (llaves, propiedades, reservas, usuarios…) agrupa sus peticiones, contratos, queries y componentes. Las páginas solo componen features. Las convenciones completas están en elCLAUDE.md del repo. Esta página resume la estructura y el flujo para que sepas dónde tocar.
Estructura de carpetas
Flujo de una petición
Todas las llamadas a la API siguen la misma cadena. Tomando el alta de usuario como ejemplo:- Request: una función por endpoint en
api/requests/. Traduce del payload del panel al formato de la API. - DTO: el schema Zod valida la respuesta y el mapper la convierte en un modelo de
core/. Se crea conMapper.create(schema, mapping)(src/core/shared/Mapper.ts). Si la API cambia su contrato, falla aquí y Sentry lo etiqueta comocontract_violation. - Query: el hook de TanStack Query envuelve la request. Las mutaciones invalidan las queries relacionadas y muestran un toast en español. Muchas registran también un evento de analytics.
- Página: usa el hook y pinta skeletons mientras carga.
El cliente HTTP
src/lib/apiClient.ts es una instancia de Axios con baseURL = VITE_API_BASE_URL y timeout de 30 s.
vivla-apiresponde con un sobre{ code, msg, data }.code: 20es éxito. Cualquier otro código se convierte en unApiError.- Si la API devuelve
code: 41(token inválido) o un HTTP401, el cliente fuerza un refresh del token y reintenta la petición una sola vez. - Los códigos esperados (
41,43,44,49) no se envían a Sentry. El resto sí. Ver Observabilidad.
Router y guards
El router está ensrc/router/index.tsx (createBrowserRouter envuelto por Sentry). Todas las páginas se cargan en lazy con lazyWithReload. Si un chunk falla (típico justo después de un despliegue), recarga la página una vez.
- Si no tienes el permiso de una ruta,
PermissionBasedRedirectte lleva a la primera sección del menú a la que sí tienes acceso. Si no tienes ninguna, acabas en/unauthorized. - Las rutas se construyen siempre con
src/config/paths.ts(paths.bookings.getHref({ user })), nunca con strings sueltos. src/config/routes.ts(navigableRoutes) define las entradas del menú lateral y el permiso de cada una. Las etiquetas salen delocales/es/sidebar.json.
Estado en la URL
Los filtros y el modo de vista viven en query params, así que un enlace a una pantalla filtrada se puede compartir. Los detalles se abren en un panel lateral también controlado por la URL (?bookingId=, ?userId=, ?homeId=).
Al cambiar de sección desde el menú, src/lib/sectionSearchMemory.ts recuerda los filtros de cada sección y los restaura al volver. Los parámetros de detalle son efímeros y no se recuerdan.
Providers globales
src/main.tsx monta, en este orden:
Autenticación
El login no usa el SDK de Auth0. El panel llama directamente a los endpoints de Auth0:- Dominios permitidos: el login y la recuperación de contraseña solo aceptan correos de los dominios de
allowedEmailDomains(features/auth/constants). - Almacenamiento: el access token y el refresh token se guardan en
localStoragecon Zustandpersist. Las pestañas se sincronizan entre sí con el eventostorage. - Refresh proactivo: se refresca 2 minutos antes de caducar, o si el token tiene más de 10 minutos. También se comprueba cada minuto y al volver el foco a la pestaña. Un lock de
navigator.locksevita que dos pestañas refresquen a la vez. - Sesión caducada: si Auth0 rechaza el refresh con un 4xx, se borra la sesión y aparece el toast Tu sesión ha caducado. Vuelve a iniciar sesión.
- Permisos: salen del claim
permissionsdel access token. Ver Permisos.
Convenciones de código
Resumen de las reglas delCLAUDE.md:
- TypeScript estricto: sin
any.typeantes queinterface, salvo para props (interface ComponentNameProps). - Estilos: styled-components en un
Styles.tsjunto al componente. Todo enrem. SufijoWrapper, nuncaContainer. Props transitorias con$($isOpen). Colores desde el tema. - Imports: primero los estilos con imports nombrados desde
./Styles(nuncaimport * as S). Después librerías, tipos, assets e imports locales. - React Query: query keys en kebab-case (
["booking-details", id]) ythrowOnError: false. Las mutaciones invalidan lo relacionado y muestran un toast en español. - Formularios: React Hook Form + Zod, con el schema en
schema.tsy mensajes en español. - Textos: nada hardcodeado. Todo pasa por i18next, con un namespace por feature en
src/assets/locales/es/. - Carga: siempre skeletons (
react-loading-skeleton) con la misma forma que el componente final.
Añadir un módulo nuevo
1
Crea la feature
Genera
src/features/{feature}/ con /create-feature. Añade requests, DTOs y queries con sus skills.2
Define el permiso
Añade las constantes
read: y edit: en PERMISSIONS (src/features/auth/constants/index.ts). El permiso tiene que existir también en la API de Auth0.3
Crea la página y la ruta
Crea
src/pages/{Page}/, añade su href en src/config/paths.ts y registra la ruta en src/router/index.tsx dentro de un PermissionGuard.4
Añádela al menú
Añade la entrada en
navigableRoutes (src/config/routes.ts) y su etiqueta en locales/es/sidebar.json.5
Documenta el módulo
Crea su página en
docs/wiki/, añádela a docs/wiki/docs.json y a la tabla de módulos del CLAUDE.md.