feat(mejoras): 25 mejoras UX en todos los módulos + purga Monday
Backend: mi-dia siempre local, vocabulario canónico, AplicacionPago con usuario Mi día: modal español real, cita desde modal, score falso eliminado Cotizador: barra sticky, persistencia local con folio Alta Clientes: redirect eliminado, foto conectada, asesor desde sesión, refs dinámicas Cobranza: contrato Mora arreglado, link a Plan, abono próximo pago, WhatsApp Landing: contactos locales, ModalContacto eliminado, ModoTrabajo eliminado Dashboards: MiTablero unificado, funnel con descartados, drill-down, rango fechas Chat FRASA: corpus 5 desarrollos, 6 herramientas, historial conversaciones 69 archivos, 7 apps construidas
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
@@ -43,10 +44,28 @@ const COLORES_STATUS: Record<string, string> = {
|
||||
const COLORES_FUNNEL = ['#94A3B8', '#64748B', '#3B82F6', '#2563EB']
|
||||
const COLORES_COBRANZA = ['#2563EB', '#F59E0B']
|
||||
|
||||
/** Rangos de fecha disponibles para filtrar las gráficas (días hacia atrás) */
|
||||
const RANGOS_DIAS = [7, 30, 90] as const
|
||||
|
||||
/** Rutas de drill-down a otras apps del ecosistema (NGINX las sirve como SPAs aparte) */
|
||||
const RUTA_PLANES_VENCIDOS = '/pay/?vista=todos&status=ACTIVO&sort=atraso&dir=desc'
|
||||
const RUTA_MI_DIA = '/hoy/'
|
||||
|
||||
/**
|
||||
* Drill-down cruzado de apps: /pay (cobranza) y /hoy (mi-dia) son SPAs
|
||||
* distintas detrás de NGINX; navigate() de react-router no puede salir del
|
||||
* basename de esta app, así que el salto es a nivel document.
|
||||
*/
|
||||
function irAApp(ruta: string): void {
|
||||
window.location.assign(ruta)
|
||||
}
|
||||
|
||||
/**
|
||||
* Vista "General": el tablero comercial de todo el ecosistema (KPIs, inventario,
|
||||
* pipeline, funnel y cobranza). Con `mostrarEquipo` (coordinador/corporativo/
|
||||
* admin) agrega la sección Equipo con leads por vendedor.
|
||||
* El rango de fechas (?dias=7|30|90) vive en la URL y filtra leads,
|
||||
* oportunidades y perfilamiento; inventario y cobranza son posición actual.
|
||||
*/
|
||||
export default function TableroGeneral({
|
||||
mostrarEquipo,
|
||||
@@ -58,23 +77,39 @@ export default function TableroGeneral({
|
||||
const [resumen, setResumen] = useState<ResumenReportes | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [cargando, setCargando] = useState(true)
|
||||
// Rango de fechas persistido en la URL: ?dias=7|30|90 (default 30)
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const paramDias = Number(searchParams.get('dias'))
|
||||
const dias = (RANGOS_DIAS as readonly number[]).includes(paramDias) ? paramDias : 30
|
||||
|
||||
const cargar = useCallback(async () => {
|
||||
setCargando(true)
|
||||
try {
|
||||
setResumen(await fetchResumen())
|
||||
setResumen(await fetchResumen(dias))
|
||||
setError(null)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'No se pudo cargar el tablero')
|
||||
} finally {
|
||||
setCargando(false)
|
||||
}
|
||||
}, [])
|
||||
}, [dias])
|
||||
|
||||
useEffect(() => {
|
||||
void cargar()
|
||||
}, [cargar])
|
||||
|
||||
/** Cambia el rango y lo persiste en la URL (compartible) */
|
||||
const cambiarRango = (nuevo: number) => {
|
||||
setSearchParams(
|
||||
(prev) => {
|
||||
const next = new URLSearchParams(prev)
|
||||
next.set('dias', String(nuevo))
|
||||
return next
|
||||
},
|
||||
{ replace: true },
|
||||
)
|
||||
}
|
||||
|
||||
// Expone "Actualizar" al header sticky de DashboardPage
|
||||
useEffect(() => {
|
||||
alCambiarAcciones({ actualizar: () => void cargar(), cargando })
|
||||
@@ -89,13 +124,33 @@ export default function TableroGeneral({
|
||||
|
||||
return (
|
||||
<main className="px-4 py-8 sm:px-6 lg:px-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-xl font-semibold text-[#111827] dark:text-[#E2E8F0]">Tablero comercial</h1>
|
||||
{resumen && (
|
||||
<p className="mt-1 text-sm text-[#64748B] dark:text-[#94A3B8]">
|
||||
Actualizado: {new Date(resumen.generadoEn).toLocaleString('es-MX')}
|
||||
</p>
|
||||
)}
|
||||
<div className="mb-8 flex flex-wrap items-end justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-[#111827] dark:text-[#E2E8F0]">Tablero comercial</h1>
|
||||
{resumen && (
|
||||
<p className="mt-1 text-sm text-[#64748B] dark:text-[#94A3B8]">
|
||||
Actualizado: {new Date(resumen.generadoEn).toLocaleString('es-MX')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1" role="group" aria-label="Rango de fechas de las gráficas">
|
||||
<span className="mr-1 text-xs font-medium uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">Rango:</span>
|
||||
{RANGOS_DIAS.map((r) => (
|
||||
<button
|
||||
key={r}
|
||||
type="button"
|
||||
onClick={() => cambiarRango(r)}
|
||||
aria-pressed={dias === r}
|
||||
className={
|
||||
dias === r
|
||||
? 'rounded-lg bg-accent px-3 py-1.5 text-xs font-medium text-white shadow-sm transition-colors'
|
||||
: 'rounded-lg border border-[#D1D5DB] bg-white px-3 py-1.5 text-xs font-medium text-[#64748B] transition-colors hover:text-[#111827] dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#94A3B8] dark:hover:text-[#E2E8F0]'
|
||||
}
|
||||
>
|
||||
{r} días
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
@@ -114,6 +169,7 @@ export default function TableroGeneral({
|
||||
titulo="Leads totales"
|
||||
valor={resumen.leads.total}
|
||||
detalle={`${resumen.leads.nuevosHoy} hoy · ${resumen.leads.nuevosSemana} esta semana`}
|
||||
onClick={() => irAApp(RUTA_MI_DIA)}
|
||||
/>
|
||||
<KpiCard
|
||||
titulo="Oportunidades activas"
|
||||
@@ -132,7 +188,8 @@ export default function TableroGeneral({
|
||||
<KpiCard
|
||||
titulo="Cartera vencida"
|
||||
valor={formatoMoneda(resumen.cobranza.montoVencido)}
|
||||
detalle={`${resumen.cobranza.pagosVencidos} pago(s) vencido(s)`}
|
||||
detalle={`${resumen.cobranza.pagosVencidos} pago(s) vencido(s) · click para ver planes`}
|
||||
onClick={() => irAApp(RUTA_PLANES_VENCIDOS)}
|
||||
/>
|
||||
<KpiCard
|
||||
titulo="Planes activos"
|
||||
|
||||
Reference in New Issue
Block a user