Mi día: cancelación con motivo, conteo escrituración real, cita sin retroceder, calendario completo Cobranza: MoraDashboard respeta alcance, hoy dinámico, cancelar con motivo Landing: contactos paginados, error vs vacío diferenciado Dashboards: KPI renombrado, drill-down planes activos Chat: historial recarga, preguntas rotativas, error solo toast Transversal: shadow-sm corrupto corregido en 9 archivos 553 API tests + 7 apps construidas
74 lines
3.0 KiB
TypeScript
74 lines
3.0 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react'
|
|
import { EstadoVacio, TituloSeccion } from './comunes'
|
|
import type { ResumenMiDia } from '../lib/mi-tablero'
|
|
import { fetchResumenMiDia } from '../services/mi-dia'
|
|
|
|
const CLASE_CELDA = 'px-4 py-3 text-sm text-[#111827] dark:text-[#E2E8F0]'
|
|
const CLASE_ENCABEZADO = 'bg-[#F8FAFC] px-4 py-2 text-left text-xs font-semibold uppercase text-[#64748B] dark:text-[#94A3B8]'
|
|
|
|
/**
|
|
* Sección "Equipo" (solo coordinador/corporativo/admin): tabla vendedor →
|
|
* leads asignados y nuevos esta semana, más los leads nuevos sin asignar.
|
|
* Fuente: `equipo` de GET /api/mi-dia/resumen (el API solo la incluye para
|
|
* esos roles).
|
|
*/
|
|
export default function SeccionEquipo() {
|
|
const [equipo, setEquipo] = useState<ResumenMiDia['equipo'] | null>(null)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
const cargar = useCallback(async () => {
|
|
try {
|
|
setEquipo((await fetchResumenMiDia()).equipo ?? null)
|
|
setError(null)
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'No se pudo cargar el equipo')
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
void cargar()
|
|
}, [cargar])
|
|
|
|
return (
|
|
<section aria-label="Equipo" className="mt-10">
|
|
<TituloSeccion>Equipo: leads por vendedor</TituloSeccion>
|
|
{error && (
|
|
<p role="alert" className="rounded-lg bg-amber-50 px-4 py-3 text-xs font-medium text-amber-700">
|
|
Sin datos del API — {error}
|
|
</p>
|
|
)}
|
|
{!error && !equipo && <EstadoVacio mensaje="Sin datos de equipo" />}
|
|
{!error && equipo && (
|
|
<>
|
|
<p className="mb-4 text-xs text-[#4B5563] dark:text-[#CBD5E1]">
|
|
Leads nuevos sin asignar:{' '}
|
|
<span className="font-semibold text-[#111827] dark:text-[#E2E8F0]">{equipo.leadsNuevosSinAsignar}</span>
|
|
</p>
|
|
{equipo.porVendedor.length === 0 ? (
|
|
<EstadoVacio mensaje="Sin leads asignados al equipo" />
|
|
) : (
|
|
<table className="w-full overflow-hidden rounded-xl border border-[#E5E7EB] bg-white shadow-sm dark:border-[#1E293B] dark:bg-[#1E293B]">
|
|
<thead className="border-b border-[#E5E7EB]">
|
|
<tr>
|
|
<th className={CLASE_ENCABEZADO}>Vendedor</th>
|
|
<th className={CLASE_ENCABEZADO}>Leads asignados</th>
|
|
<th className={CLASE_ENCABEZADO}>Nuevos esta semana</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-[#E5E7EB]">
|
|
{equipo.porVendedor.map((v) => (
|
|
<tr key={v.usuarioId} className="transition-colors duration-100 hover:bg-[#F8FAFC] dark:bg-[#0F172A]">
|
|
<td className={CLASE_CELDA}>{v.nombre ?? 'Sin nombre'}</td>
|
|
<td className={CLASE_CELDA}>{v.leadsAsignados}</td>
|
|
<td className={CLASE_CELDA}>{v.leadsNuevosSemana}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</>
|
|
)}
|
|
</section>
|
|
)
|
|
}
|