Files
FRASA-CAS/apps/dashboards/src/pages/DashboardPage.tsx
Consult AS Technology c77b4c0628 feat(mejoras): 14 mejoras al CRM Frasa
Fase 1 - UI: dark mode, skeletons, toasts
Fase 2 - Filtros: avanzados Mi día, búsqueda global Ctrl+K
Fase 3 - Datos: export Excel, notas apartados, auditoría
Fase 4 - Dashboard: gráficas vendedor + métricas uso
Fase 5 - Responsive: tablets optimizadas
Fase 6 - PWA: IndexedDB offline, push notifications

Backend: 3 tablas nuevas, 6 services, 6 endpoints nuevos
721 API tests + fronts verdes.
2026-09-07 06:04:49 +00:00

114 lines
5.0 KiB
TypeScript

import { useEffect, useState } from 'react'
import MiTablero from '../components/MiTablero'
import TableroGeneral from '../components/TableroGeneral'
import type { AccionesTablero } from '../components/comunes'
import { esRolConEquipo } from '../lib/mi-tablero'
import { getMe, getSesion } from '../services/auth'
import BannerModoLocal from '../components/BannerModoLocal'
import DarkModeToggle from '../components/DarkModeToggle'
type Vista = 'mia' | 'general'
const VISTAS: { id: Vista; etiqueta: string; clave: string }[] = [
{ id: 'mia', etiqueta: 'Mi tablero', clave: 'mi-tablero' },
{ id: 'general', etiqueta: 'Tablero comercial', clave: 'tablero-comercial' },
]
/**
* Tableros con vista por rol: cada tab se muestra solo si el módulo está
* permitido en la matriz de roles del API (GET /auth/me → modulos). Con un
* solo tab permitido no hay segmented control: la vista va directa. La
* sección Equipo del tablero comercial sigue la regla de rol (esRolConEquipo).
*/
export default function DashboardPage() {
const sesion = getSesion()
const rol = sesion?.usuario.rol ?? 'vendedor'
const conEquipo = esRolConEquipo(rol)
// null = aún sin dato del API (sesiones viejas sin modulos): no se filtra
const [permitidos, setPermitidos] = useState<string[] | null>(() => sesion?.usuario.modulos ?? null)
const [elegida, setElegida] = useState<Vista | null>(null)
const [acciones, setAcciones] = useState<AccionesTablero | null>(null)
useEffect(() => {
getMe()
.then((usuario) => setPermitidos(usuario.modulos ?? null))
.catch(() => {}) // si /me falla se mantiene lo que hubiera
}, [])
const permitidas = VISTAS.filter((v) => permitidos === null || permitidos.includes(v.clave))
const vista: Vista | null =
permitidas.length === 0
? null
: permitidas.some((v) => v.id === elegida)
? elegida
: permitidas[0]!.id
return (
<div>
<BannerModoLocal />
<header className="sticky top-0 z-10 border-b border-[#E5E7EB] bg-white/95 backdrop-blur dark:border-[#1E293B] dark:bg-[#1E293B]/95">
<div className="mx-auto flex min-h-14 max-w-[1280px] flex-wrap items-center justify-between gap-x-4 gap-y-2 px-4 py-2 sm:px-6">
<div className="flex items-center gap-2.5">
<a
href="/"
className="rounded-lg px-2 py-1.5 text-xs font-medium text-[#64748B] dark:text-[#94A3B8] transition-all duration-100 ease-out hover:bg-[#F9FAFB] hover:text-[#111827] dark:text-[#E2E8F0]"
>
&larr; Menú
</a>
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-accent text-xs font-bold text-white">
F
</span>
<span className="text-sm font-semibold text-[#111827] dark:text-[#E2E8F0]">FRASA Desarrollos</span>
</div>
<div className="flex items-center gap-2 sm:gap-3">
<DarkModeToggle />
{permitidas.length > 1 && (
<div
className="inline-flex rounded-lg border border-[#E5E7EB] bg-[#F1F5F9] p-0.5 sm:p-1"
role="tablist"
aria-label="Vista del tablero"
>
{permitidas.map((v) => (
<button
key={v.id}
type="button"
role="tab"
aria-selected={vista === v.id}
onClick={() => setElegida(v.id)}
className={
vista === v.id
? 'rounded-md bg-white px-2 py-1.5 text-[10px] font-medium text-[#111827] dark:text-[#E2E8F0] shadow-sm transition-all duration-100 ease-out sm:px-3 sm:text-xs'
: 'rounded-md px-2 py-1.5 text-[10px] font-medium text-[#64748B] dark:text-[#94A3B8] transition-all duration-100 ease-out hover:text-[#111827] dark:text-[#E2E8F0] sm:px-3 sm:text-xs'
}
>
{v.etiqueta}
</button>
))}
</div>
)}
{vista !== null && (
<button
type="button"
onClick={() => acciones?.actualizar()}
disabled={!acciones || acciones.cargando}
className="rounded-lg border border-[#D1D5DB] bg-white px-4 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] py-1.5 text-xs font-medium text-accent shadow-sm transition-all duration-100 ease-out hover:bg-blue-50 disabled:opacity-50"
>
{acciones?.cargando ? 'Actualizando…' : 'Actualizar'}
</button>
)}
</div>
</div>
</header>
{vista === null ? (
<main className="mx-auto max-w-[1280px] px-4 py-16 sm:px-6">
<p className="text-sm text-[#64748B] dark:text-[#94A3B8]">Tu rol no tiene tableros asignados por ahora.</p>
</main>
) : vista === 'mia' ? (
<MiTablero alCambiarAcciones={setAcciones} />
) : (
<TableroGeneral mostrarEquipo={conEquipo} alCambiarAcciones={setAcciones} />
)}
</div>
)
}