|
|
|
|
@@ -761,12 +761,12 @@ function FilaLead({
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-3 flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center">
|
|
|
|
|
<button type="button" disabled={enviando} onClick={() => onAccion('CONTACTADO')} className={CLASE_BOTON_ACCION}>
|
|
|
|
|
Contactado
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button" disabled={enviando} onClick={() => onAccion('Intento de contacto')} className={CLASE_BOTON_ACCION}>
|
|
|
|
|
Intento
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button" disabled={enviando} onClick={() => onAccion('CONTACTADO')} className={CLASE_BOTON_ACCION}>
|
|
|
|
|
Contactado
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button" disabled={enviando} onClick={() => onAccion('INTERESADO')} className={CLASE_BOTON_ACCION}>
|
|
|
|
|
Interesado
|
|
|
|
|
</button>
|
|
|
|
|
@@ -844,6 +844,10 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|
|
|
|
})
|
|
|
|
|
const filtros = filtrosPorTab[tabCrm]
|
|
|
|
|
const actualizarFiltros = (f: FiltrosCRM) => setFiltrosPorTab((prev) => ({ ...prev, [tabCrm]: f }))
|
|
|
|
|
// Filtro por vendedor: SOLO administradores. Un solo selector que aplica a
|
|
|
|
|
// las cuatro pestañas (leads / oportunidades / apartados / escrituración).
|
|
|
|
|
const esAdmin = rol === 'admin' || rol === 'manager'
|
|
|
|
|
const [filtroVendedor, setFiltroVendedor] = useState('')
|
|
|
|
|
// Estados posibles por tab: alimenta el select "Estado" de FiltrosAvanzados
|
|
|
|
|
const estadosPorTab: Partial<Record<TabCrm, readonly string[]>> = {
|
|
|
|
|
leads: ESTADOS_LEAD,
|
|
|
|
|
@@ -861,21 +865,22 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|
|
|
|
// columnas de solo-display: su drop abre el modal de descarte o no hace nada.
|
|
|
|
|
const columnasLeads = [
|
|
|
|
|
{ id: 'NUEVO', titulo: 'Nuevo', color: 'border-t-gray-400' },
|
|
|
|
|
{ id: 'CONTACTADO', titulo: 'Contactado', color: 'border-t-blue-500' },
|
|
|
|
|
{ id: 'Intento de contacto', titulo: 'Intento de contacto', color: 'border-t-sky-500' },
|
|
|
|
|
{ id: 'CONTACTADO', titulo: 'Contactado', color: 'border-t-blue-500' },
|
|
|
|
|
{ id: 'INTERESADO', titulo: 'Interesado', color: 'border-t-yellow-500' },
|
|
|
|
|
{ id: 'AGENDA CITA', titulo: 'Agenda Cita', color: 'border-t-green-500' },
|
|
|
|
|
{ id: 'CALIFICADO', titulo: 'Calificado', color: 'border-t-teal-500' },
|
|
|
|
|
{ id: 'DESCARTADO', titulo: 'Descartado', color: 'border-t-red-500' },
|
|
|
|
|
]
|
|
|
|
|
// Mismo orden y mismas etapas que la vista de lista: Confirmó Cita →
|
|
|
|
|
// Afluencia → Cotizar → Apartó → Descartar. PERDIDO es solo-display: su
|
|
|
|
|
// drop abre el modal de descarte (mismo comportamiento que el botón).
|
|
|
|
|
const columnasOportunidades = [
|
|
|
|
|
{ id: 'Agendo Cita', titulo: 'Confirmó Cita', color: 'border-t-green-500' },
|
|
|
|
|
{ id: 'Afluencia', titulo: 'Afluencia', color: 'border-t-purple-500' },
|
|
|
|
|
{ id: 'Cotizador', titulo: 'Cotizador', color: 'border-t-blue-500' },
|
|
|
|
|
{ id: 'Agendo Cita', titulo: 'Agendó Cita', color: 'border-t-green-500' },
|
|
|
|
|
{ id: 'Cotizador', titulo: 'Cotizar', color: 'border-t-blue-500' },
|
|
|
|
|
{ id: 'Aparto', titulo: 'Apartó', color: 'border-t-emerald-600' },
|
|
|
|
|
{ id: 'No Asistio', titulo: 'No Asistió', color: 'border-t-orange-500' },
|
|
|
|
|
{ id: 'Abandono', titulo: 'Abandono', color: 'border-t-yellow-600' },
|
|
|
|
|
{ id: 'PERDIDO', titulo: 'Perdido', color: 'border-t-red-500' },
|
|
|
|
|
{ id: 'PERDIDO', titulo: 'Descartar', color: 'border-t-red-500' },
|
|
|
|
|
]
|
|
|
|
|
const columnasApartados = [
|
|
|
|
|
{ id: 'Pago Apartado', titulo: 'Pago Apartado', color: 'border-t-blue-500' },
|
|
|
|
|
@@ -910,11 +915,11 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|
|
|
|
const obtenerColumnaOp = (op: OportunidadMiDia) => {
|
|
|
|
|
const e = (op.etapa ?? '').toUpperCase()
|
|
|
|
|
if (e.includes('PERDIDO') || e.includes('PERDI')) return 'PERDIDO'
|
|
|
|
|
if (e.includes('ASISTIO')) return 'No Asistio'
|
|
|
|
|
if (e.includes('ABANDONO')) return 'Abandono'
|
|
|
|
|
if (e.includes('APARTO')) return 'Aparto'
|
|
|
|
|
if (e.includes('COTIZ')) return 'Cotizador'
|
|
|
|
|
if (e.includes('AGENDO') || e.includes('CITA')) return 'Agendo Cita'
|
|
|
|
|
// 'No Asistio' vuelve a Confirmó Cita (se reagenda); 'Abandono' es
|
|
|
|
|
// terminal y ya no aparece en la lista de trabajo
|
|
|
|
|
if (e.includes('AGENDO') || e.includes('CITA') || e.includes('ASISTIO')) return 'Agendo Cita'
|
|
|
|
|
return 'Afluencia'
|
|
|
|
|
}
|
|
|
|
|
const obtenerColumnaApartado = (a: ApartadoMiDia) => {
|
|
|
|
|
@@ -1074,6 +1079,24 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|
|
|
|
}, 'No se pudo mover el lead')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Click en una actividad de "Mis actividades": abre la bitácora del cliente
|
|
|
|
|
* ligado (lead, oportunidad o apartado). Si no hay ficha ligada, solo avisa.
|
|
|
|
|
*/
|
|
|
|
|
function abrirDetalleDesdeActividad(leadCapturaId: string | null) {
|
|
|
|
|
if (!resumen) return
|
|
|
|
|
const item =
|
|
|
|
|
leadCapturaId !== null
|
|
|
|
|
? resumen.misLeads.find((l) => l.id === leadCapturaId) ??
|
|
|
|
|
resumen.misOportunidades.items.find((o) => o.id === leadCapturaId) ??
|
|
|
|
|
resumen.misApartados.find((a) => a.id === leadCapturaId) ??
|
|
|
|
|
(resumen.todosApartados ?? []).find((a) => a.id === leadCapturaId) ??
|
|
|
|
|
null
|
|
|
|
|
: null
|
|
|
|
|
if (item) setLeadSeleccionado(item)
|
|
|
|
|
else toast.error('Esta actividad no tiene una ficha de cliente ligada')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function enviarCaptura(e: FormEvent) {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
setCapEnviando(true)
|
|
|
|
|
@@ -1111,9 +1134,11 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Listas filtradas por los filtros avanzados; las usan el render por pestaña
|
|
|
|
|
// Y el botón de exportar (para que la exportación respete los filtros activos)
|
|
|
|
|
// Y el botón de exportar (para que la exportación respete los filtros activos).
|
|
|
|
|
// Admin: filtroVendedor ('' = todos) recorta además por vendedor asignado.
|
|
|
|
|
const coincideVendedor = (v: string) => filtroVendedor === '' || v === filtroVendedor
|
|
|
|
|
const leadsFiltrados = resumen
|
|
|
|
|
? aplicarFiltros(resumen.misLeads.filter((l) => l.fuente === 'lead'), filtros, {
|
|
|
|
|
? aplicarFiltros(resumen.misLeads.filter((l) => l.fuente === 'lead' && coincideVendedor(l.vendedor)), filtros, {
|
|
|
|
|
texto: (l) => `${l.nombre} ${l.telefono} ${l.email}`,
|
|
|
|
|
origen: (l) => l.origen,
|
|
|
|
|
estado: (l) => l.estado,
|
|
|
|
|
@@ -1121,7 +1146,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|
|
|
|
})
|
|
|
|
|
: []
|
|
|
|
|
const opsFiltradas = resumen
|
|
|
|
|
? aplicarFiltros(resumen.misOportunidades.items, filtros, {
|
|
|
|
|
? aplicarFiltros(resumen.misOportunidades.items.filter((o) => coincideVendedor(o.vendedor)), filtros, {
|
|
|
|
|
texto: (o) => `${o.nombre} ${o.telefono} ${o.email}`,
|
|
|
|
|
desarrollo: (o) => o.desarrollo,
|
|
|
|
|
estado: (o) => o.etapa,
|
|
|
|
|
@@ -1129,7 +1154,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|
|
|
|
})
|
|
|
|
|
: []
|
|
|
|
|
const apartadosFiltrados = resumen
|
|
|
|
|
? aplicarFiltros(resumen.misApartados, filtros, {
|
|
|
|
|
? aplicarFiltros(resumen.misApartados.filter((a) => coincideVendedor(a.vendedor)), filtros, {
|
|
|
|
|
texto: (a) => a.nombre,
|
|
|
|
|
estado: (a) => a.estado,
|
|
|
|
|
fecha: (a) => a.fechaLimite,
|
|
|
|
|
@@ -1140,6 +1165,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|
|
|
|
// para que el conteo del tab refleje la lista filtrada de escrituración.
|
|
|
|
|
const enEscrituracion = resumen
|
|
|
|
|
? (resumen.todosApartados || resumen.misApartados).filter((a) => {
|
|
|
|
|
if (!coincideVendedor(a.vendedor)) return false
|
|
|
|
|
const e = (a.estado ?? '').toUpperCase()
|
|
|
|
|
return (
|
|
|
|
|
e.includes('FIRMA') ||
|
|
|
|
|
@@ -1163,6 +1189,20 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|
|
|
|
fecha: (a) => a.fechaLimite,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Vendedores con registros visibles: alimenta el filtro por vendedor (admin)
|
|
|
|
|
const vendedoresOpciones = resumen
|
|
|
|
|
? [
|
|
|
|
|
...new Set(
|
|
|
|
|
[
|
|
|
|
|
...resumen.misLeads.map((l) => l.vendedor),
|
|
|
|
|
...resumen.misOportunidades.items.map((o) => o.vendedor),
|
|
|
|
|
...resumen.misApartados.map((a) => a.vendedor),
|
|
|
|
|
...(resumen.todosApartados ?? []).map((a) => a.vendedor),
|
|
|
|
|
].filter((v) => v.trim() !== ''),
|
|
|
|
|
),
|
|
|
|
|
].sort((a, b) => a.localeCompare(b))
|
|
|
|
|
: []
|
|
|
|
|
|
|
|
|
|
const hoy = hoyLocal()
|
|
|
|
|
const iniciales = nombreVendedor
|
|
|
|
|
.split(' ')
|
|
|
|
|
@@ -1294,6 +1334,19 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<FiltrosAvanzados valor={filtros} onChange={actualizarFiltros} estados={estadosPorTab[tabCrm]} />
|
|
|
|
|
</div>
|
|
|
|
|
{esAdmin && (
|
|
|
|
|
<select
|
|
|
|
|
aria-label="Filtrar por vendedor"
|
|
|
|
|
value={filtroVendedor}
|
|
|
|
|
onChange={(e) => setFiltroVendedor(e.target.value)}
|
|
|
|
|
className="shrink-0 rounded-lg border border-[#D1D5DB] bg-white px-2 py-2 text-xs font-medium text-[#374151] shadow-sm transition-colors focus:border-accent focus:outline-none dark:bg-[#0F172A] dark:border-[#334155] dark:text-[#CBD5E1]"
|
|
|
|
|
>
|
|
|
|
|
<option value="">Todos los vendedores</option>
|
|
|
|
|
{vendedoresOpciones.map((v) => (
|
|
|
|
|
<option key={v} value={v}>{v}</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
)}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
@@ -1340,7 +1393,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|
|
|
|
onDescartar={() => setDescartando(lead)}
|
|
|
|
|
onVerDetalle={() => setLeadSeleccionado(lead)}
|
|
|
|
|
acciones={acciones}
|
|
|
|
|
esAdmin={rol === 'admin' || rol === 'manager'}
|
|
|
|
|
esAdmin={esAdmin}
|
|
|
|
|
vendedores={resumen.equipo?.porVendedor.map((v) => ({ id: v.usuarioId, nombre: v.nombre ?? '' }))}
|
|
|
|
|
onReasignar={(usuarioId) => void reasignar(lead, usuarioId)}
|
|
|
|
|
onMover={() => void mover(lead)}
|
|
|
|
|
@@ -1615,7 +1668,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<aside className={`order-1 flex flex-col gap-6 ${vistaKanban && tabCrm === 'leads' ? 'hidden' : 'md:order-2 lg:sticky lg:top-20 lg:self-start'}`}>
|
|
|
|
|
<section aria-label="Mis actividades">
|
|
|
|
|
<section aria-label="Mis actividades" className="order-2">
|
|
|
|
|
<TituloSeccion>Mis actividades</TituloSeccion>
|
|
|
|
|
{resumen.misActividadesHoy.length === 0 ? (
|
|
|
|
|
<EstadoVacio mensaje="Sin actividades para hoy" />
|
|
|
|
|
@@ -1624,19 +1677,26 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|
|
|
|
{resumen.misActividadesHoy.map((a) => (
|
|
|
|
|
<li
|
|
|
|
|
key={a.id}
|
|
|
|
|
className="flex items-center justify-between gap-3 rounded-xl border border-[#E5E7EB] bg-white px-4 py-3 shadow-sm dark:bg-[#1E293B] dark:border-[#1E293B]"
|
|
|
|
|
className="rounded-xl border border-[#E5E7EB] bg-white shadow-sm dark:bg-[#1E293B] dark:border-[#1E293B]"
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => abrirDetalleDesdeActividad(a.leadCapturaId ?? null)}
|
|
|
|
|
title={a.leadCapturaId ? 'Ver información del cliente' : 'Actividad sin cliente ligado'}
|
|
|
|
|
className="flex w-full items-center justify-between gap-3 px-4 py-3 text-left transition-colors hover:bg-[#F9FAFB] dark:hover:bg-[#0F172A]"
|
|
|
|
|
>
|
|
|
|
|
<p className="text-sm">
|
|
|
|
|
{a.nombre}
|
|
|
|
|
{a.tipo ? <span className="ml-2 text-xs uppercase tracking-wider text-[#94A3B8]">{a.tipo}</span> : null}
|
|
|
|
|
</p>
|
|
|
|
|
<span
|
|
|
|
|
className={`text-xs font-medium ${
|
|
|
|
|
className={`shrink-0 text-xs font-medium ${
|
|
|
|
|
esActividadVencida(a.fecha, hoy) ? 'text-red-600' : 'text-[#64748B] dark:text-[#94A3B8]'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{formatoFechaCorta(a.fecha)}
|
|
|
|
|
</span>
|
|
|
|
|
</button>
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
@@ -1644,7 +1704,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
{resumen.apartadosPorVencer.length > 0 && (
|
|
|
|
|
<section aria-label="Apartados por vencer">
|
|
|
|
|
<section aria-label="Apartados por vencer" className="order-3">
|
|
|
|
|
<TituloSeccion>Apartados por vencer</TituloSeccion>
|
|
|
|
|
<ul className="space-y-2">
|
|
|
|
|
{resumen.apartadosPorVencer.map((a) => (
|
|
|
|
|
@@ -1669,7 +1729,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
|
|
|
|
</section>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<section aria-label="Captura rápida de lead">
|
|
|
|
|
<section aria-label="Captura rápida de lead" className="order-1">
|
|
|
|
|
<TituloSeccion>Captura rápida de lead</TituloSeccion>
|
|
|
|
|
<form
|
|
|
|
|
onSubmit={enviarCaptura}
|
|
|
|
|
|