feat: roles con modulos por rol + mejoras Mi dia y descarte

- Matriz de roles (vendedor, administracion_venta, director_ventas,
  escrituracion, direccion_general, admin) con modulos visibles por rol;
  auth/me y login incluyen 'modulos'; PATCH /usuarios/:id/rol (admin).
- Landing: Cobranza -> Clientes, Chat IA -> Chat Hermes, nuevo modulo
  Contactos (vista interna leyendo el tablero Contactos), grid filtrado
  por rol, rol visible en Configuracion de cuenta.
- Dashboards: tab 'Tablero comercial' y tabs filtrados por rol.
- Boton '<- Menu' en el header de todas las apps.
- Mi dia: cada lead muestra el ultimo cambio (que, quien, cuando) desde
  el activity log de Monday (normalizacion de created_at 100ns/us/ms).
- Descarte: el motivo se crea en el grupo de su categoria en el tablero
  Motivos No Compra.
This commit is contained in:
2026-09-04 09:12:01 +00:00
parent 42cfe937cf
commit f72354f775
33 changed files with 1224 additions and 114 deletions

View File

@@ -1,9 +1,50 @@
import { MODULOS } from './data/modulos'
import { useEffect, useState } from 'react'
import { MODULOS, esModuloVisible } from './data/modulos'
import { Navbar } from './components/Navbar'
import { TarjetaModulo } from './components/TarjetaModulo'
import { ConfiguracionCuenta } from './components/ConfiguracionCuenta'
import { VistaContactos } from './components/VistaContactos'
import { getToken } from './services/auth'
import { getMe } from './services/api'
type Vista = 'home' | 'contactos'
export default function App() {
const [vista, setVista] = useState<Vista>('home')
// Módulos permitidos por el rol (GET /auth/me); null = sin sesión → se muestran todos
const [permitidos, setPermitidos] = useState<string[] | null>(null)
useEffect(() => {
let cancelado = false
const cargar = () => {
if (!getToken()) {
setPermitidos(null)
return
}
// Si /me falla se conserva lo anterior (o todos): el login wall de cada app sigue
getMe()
.then((usuario) => {
if (!cancelado) setPermitidos(usuario.modulos ?? [])
})
.catch(() => {})
}
cargar()
window.addEventListener('frasa:sesion', cargar)
return () => {
cancelado = true
window.removeEventListener('frasa:sesion', cargar)
}
}, [])
if (vista === 'contactos') {
return (
<div className="flex min-h-screen flex-col">
<Navbar />
<VistaContactos onVolver={() => setVista('home')} />
</div>
)
}
return (
<div className="flex min-h-screen flex-col">
<Navbar />
@@ -24,8 +65,12 @@ export default function App() {
<nav aria-label="Módulos del ecosistema">
<ul className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{MODULOS.map((modulo) => (
<TarjetaModulo key={modulo.id} modulo={modulo} />
{MODULOS.filter((modulo) => esModuloVisible(modulo, permitidos)).map((modulo) => (
<TarjetaModulo
key={modulo.id}
modulo={modulo}
onAbrir={modulo.id === 'contactos' ? () => setVista('contactos') : undefined}
/>
))}
</ul>
</nav>