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

@@ -0,0 +1,79 @@
import { useCallback, useEffect, useState } from 'react'
import { getContactos, type Contacto } from '../services/api'
interface Props {
onVolver: () => void
}
/**
* Vista interna "Contactos": lista del tablero Contactos de Monday (lo más
* reciente primero). Vive dentro de la landing (no hay router); se abre desde
* la tarjeta Contactos del grid.
*/
export function VistaContactos({ onVolver }: Props) {
const [contactos, setContactos] = useState<Contacto[] | null>(null)
const [cargando, setCargando] = useState(true)
const [error, setError] = useState('')
const cargar = useCallback(() => {
setCargando(true)
setError('')
getContactos()
.then(setContactos)
.catch(() => setError('No se pudieron cargar los contactos.'))
.finally(() => setCargando(false))
}, [])
useEffect(() => cargar(), [cargar])
return (
<main className="mx-auto w-full max-w-[1280px] flex-1 px-6 py-12 sm:py-16">
<div className="flex items-center gap-4">
<button
type="button"
onClick={onVolver}
className="rounded-lg border border-[#D1D5DB] bg-white px-4 py-2 text-sm font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
>
&larr; Volver
</button>
<h1 className="text-2xl font-semibold tracking-tight text-[#111827] sm:text-3xl">Contactos</h1>
</div>
<section aria-label="Lista de contactos" className="mt-8">
{cargando ? (
<p className="text-sm text-[#64748B]">Cargando contactos…</p>
) : error ? (
<div>
<p role="alert" className="text-sm text-red-600">
{error}
</p>
<button
type="button"
onClick={cargar}
className="mt-3 rounded-lg border border-[#D1D5DB] bg-white px-4 py-2 text-sm font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
>
Reintentar
</button>
</div>
) : !contactos || contactos.length === 0 ? (
<p className="text-sm text-[#64748B]">No hay contactos para mostrar.</p>
) : (
<ul className="divide-y divide-[#E5E7EB] rounded-xl border border-[#E5E7EB] bg-white shadow-sm">
{contactos.map((contacto) => (
<li key={contacto.id} className="flex flex-wrap items-center gap-x-4 gap-y-1 px-6 py-4">
<span className="text-sm font-semibold text-[#111827]">{contacto.nombre}</span>
{contacto.estado && (
<span className="rounded-full bg-blue-50 px-2.5 py-0.5 text-xs font-medium text-accent">
{contacto.estado}
</span>
)}
<span className="text-sm text-[#64748B]">{contacto.telefono || 'Sin teléfono'}</span>
<span className="text-sm text-[#64748B]">{contacto.email || 'Sin email'}</span>
</li>
))}
</ul>
)}
</section>
</main>
)
}