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.
114 lines
5.0 KiB
TypeScript
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]"
|
|
>
|
|
← 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>
|
|
)
|
|
}
|