fix(mi-dia): 5 bugs críticos + 8 mejoras UX
Bugs críticos: descartar, kanban drag&drop, escrituración, fases API, export filtros UX: tel clicable, WhatsApp, filtros por tab, tareas, calendario, estado, notas, navegación
This commit is contained in:
@@ -10,10 +10,13 @@ import {
|
||||
formatoFechaCorta,
|
||||
hoyLocal,
|
||||
DESARROLLOS_LEAD,
|
||||
ESTADOS_LEAD,
|
||||
ETIQUETA_FASE_APARTADO,
|
||||
ETIQUETA_FASE_ESCRITURACION,
|
||||
ETIQUETA_FASE_OPORTUNIDAD,
|
||||
FASES_APARTADO,
|
||||
FASES_ESCRITURACION,
|
||||
FASES_OPORTUNIDAD,
|
||||
ORIGENES_LEAD,
|
||||
SLOTS_DOCS_APARTADO,
|
||||
componerTelefono,
|
||||
@@ -21,6 +24,7 @@ import {
|
||||
textoDiasRestantes,
|
||||
urlAfluenciaParaOportunidad,
|
||||
urlCotizadorParaOportunidad,
|
||||
urlWhatsApp,
|
||||
type ApartadoMiDia,
|
||||
type ClaveLada,
|
||||
type FaseApartado,
|
||||
@@ -44,7 +48,7 @@ import {
|
||||
import { guardarResumen, cargarResumenLocal } from '../services/offline-db'
|
||||
import { suscribirPush } from '../services/push'
|
||||
import { exportarLeads, exportarOportunidades, exportarApartados } from '../services/exportar'
|
||||
import { FiltrosAvanzados, aplicarFiltros, type FiltrosCRM } from '../components/FiltrosAvanzados'
|
||||
import { FiltrosAvanzados, aplicarFiltros, FILTROS_VACIOS, type FiltrosCRM } from '../components/FiltrosAvanzados'
|
||||
import { getSesion } from '../services/auth'
|
||||
import DashboardVendedor from '../components/DashboardVendedor'
|
||||
import ScoreBadge, { calcularScore } from '../components/ScoreBadge'
|
||||
@@ -107,6 +111,35 @@ const CLASE_INPUT =
|
||||
|
||||
const CLASE_LABEL = 'mb-2 block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]'
|
||||
|
||||
/** Motivos de no compra: valores EXACTOS que valida el API (z.enum MOTIVOS_NO_COMPRA) */
|
||||
const MOTIVOS_NO_COMPRA = ['Precio', 'Producto', 'Ubicacion', 'Financiamiento', 'Competencia', 'Cliente', 'Timing'] as const
|
||||
|
||||
/** Teléfono clicable (tel:) + botón de WhatsApp con saludo FRASA prellenado */
|
||||
function EnlacesContacto({ nombre, telefono }: { nombre: string; telefono: string }) {
|
||||
const vendedor = getSesion()?.usuario.nombre ?? ''
|
||||
if (telefono.trim() === '') return <span>Sin teléfono</span>
|
||||
return (
|
||||
<>
|
||||
<a
|
||||
href={`tel:${telefono.replace(/\s+/g, '')}`}
|
||||
className="text-accent hover:underline"
|
||||
>
|
||||
{telefono}
|
||||
</a>
|
||||
<a
|
||||
href={urlWhatsApp(nombre, telefono, vendedor)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={`Enviar WhatsApp a ${nombre}`}
|
||||
aria-label={`Enviar WhatsApp a ${nombre}`}
|
||||
className="ml-1.5 hover:underline"
|
||||
>
|
||||
💬
|
||||
</a>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/** Fila de oportunidad con botones de fase del funnel (Agendo Cita lleva fecha) */
|
||||
function FilaOportunidad({
|
||||
@@ -146,7 +179,7 @@ function FilaOportunidad({
|
||||
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-[#64748B] dark:text-[#94A3B8]">
|
||||
{op.telefono || 'Sin teléfono'}
|
||||
<EnlacesContacto nombre={op.nombre} telefono={op.telefono} />
|
||||
{op.desarrollo ? ` · ${op.desarrollo}` : ''}
|
||||
{op.modeloInteres ? ` · ${op.modeloInteres}` : ''}
|
||||
{op.unidadCodigo ? ` · ${op.unidadCodigo}` : ''}
|
||||
@@ -269,8 +302,8 @@ function ModalDescarteGenerico({
|
||||
onChange={(e) => setMotivo(e.target.value)}
|
||||
className="w-full rounded-lg border border-[#D1D5DB] bg-white px-3 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] py-2.5 text-sm"
|
||||
>
|
||||
{['Timing', 'Cliente', 'Competencia', 'Financiamiento', 'Ubicación', 'Producto', 'Precio'].map((m) => (
|
||||
<option key={m} value={m}>{m}</option>
|
||||
{MOTIVOS_NO_COMPRA.map((m) => (
|
||||
<option key={m} value={m}>{m === 'Ubicacion' ? 'Ubicación' : m}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
@@ -351,9 +384,10 @@ function FilaApartado({
|
||||
disabled={enviando}
|
||||
onClick={() => {
|
||||
// Alta Enkontrol: abre el alta de cliente prellenada con
|
||||
// todo lo recolectado del apartado (ligada, sin duplicar)
|
||||
// todo lo recolectado del apartado (ligada, sin duplicar).
|
||||
// En pestaña nueva para no perder el estado de Mi día.
|
||||
if (fase === 'Alta Enkontrol') {
|
||||
window.location.href = `/client/?apartado=${apartado.id}`
|
||||
window.open(`/client/?apartado=${apartado.id}`, '_blank')
|
||||
return
|
||||
}
|
||||
if (fase === 'Cancelacion Apartado') {
|
||||
@@ -598,7 +632,7 @@ function FilaLead({
|
||||
</span>
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-[#64748B] dark:text-[#94A3B8]">
|
||||
{lead.telefono || 'Sin teléfono'}
|
||||
<EnlacesContacto nombre={lead.nombre} telefono={lead.telefono} />
|
||||
{lead.desarrollo ? ` · ${lead.desarrollo}` : ''}
|
||||
{lead.origen ? ` · ${lead.origen}` : ''}
|
||||
</p>
|
||||
@@ -662,87 +696,113 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
||||
const [tabCrm, setTabCrm] = useState<TabCrm>('leads')
|
||||
const [leadSeleccionado, setLeadSeleccionado] = useState<LeadMiDia | OportunidadMiDia | ApartadoMiDia | null>(null)
|
||||
const [apartadoCancelando, setApartadoCancelando] = useState<{ id: string; nombre: string } | null>(null)
|
||||
const [filtros, setFiltros] = useState<FiltrosCRM>({ texto: '', desarrollo: '', origen: '', fechaDesde: '', fechaHasta: '' })
|
||||
// Filtros independientes por tab: cada pestaña conserva su propia búsqueda
|
||||
const [filtrosPorTab, setFiltrosPorTab] = useState<Record<TabCrm, FiltrosCRM>>({
|
||||
leads: FILTROS_VACIOS,
|
||||
oportunidades: FILTROS_VACIOS,
|
||||
apartados: FILTROS_VACIOS,
|
||||
escrituracion: FILTROS_VACIOS,
|
||||
tablero: FILTROS_VACIOS,
|
||||
calendario: FILTROS_VACIOS,
|
||||
})
|
||||
const filtros = filtrosPorTab[tabCrm]
|
||||
const actualizarFiltros = (f: FiltrosCRM) => setFiltrosPorTab((prev) => ({ ...prev, [tabCrm]: f }))
|
||||
// Estados posibles por tab: alimenta el select "Estado" de FiltrosAvanzados
|
||||
const estadosPorTab: Partial<Record<TabCrm, readonly string[]>> = {
|
||||
leads: ESTADOS_LEAD,
|
||||
oportunidades: FASES_OPORTUNIDAD,
|
||||
apartados: FASES_APARTADO,
|
||||
escrituracion: FASES_ESCRITURACION,
|
||||
}
|
||||
const [offline, setOffline] = useState(!navigator.onLine)
|
||||
const [offlineFecha, setOfflineFecha] = useState<string | null>(null)
|
||||
|
||||
// Kanban toggle
|
||||
const [vistaKanban, setVistaKanban] = useState(false)
|
||||
// Configuración de columnas por pestaña
|
||||
// Configuración de columnas por pestaña. Los IDs son los valores EXACTOS que
|
||||
// valida el API (z.enum en los controllers). NUEVO/DESCARTADO/PERDIDO son
|
||||
// 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: 'INTERESADO', titulo: 'Interesado', color: 'border-t-yellow-500' },
|
||||
{ id: 'AGENDO', titulo: 'Agendo', color: 'border-t-green-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' },
|
||||
]
|
||||
const columnasOportunidades = [
|
||||
{ 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: 'APARTO', titulo: 'Apartó', color: 'border-t-emerald-600' },
|
||||
{ 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: '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' },
|
||||
]
|
||||
const columnasApartados = [
|
||||
{ id: 'PAGO APARTADO', titulo: 'Pago Apartado', color: 'border-t-blue-500' },
|
||||
{ id: 'ALTA ENKONTROL', titulo: 'Alta Enkontrol', color: 'border-t-yellow-500' },
|
||||
{ id: 'ENTREGO DOCUMENTACION', titulo: 'Entregó Docs', color: 'border-t-orange-500' },
|
||||
{ id: 'CITA CLIENTES', titulo: 'Cita Clientes', color: 'border-t-green-500' },
|
||||
{ id: 'CONTRATO ENKONTROL', titulo: 'Contrato Enkontrol', color: 'border-t-emerald-600' },
|
||||
{ id: 'FIRMA CLIENTE', titulo: 'Firma Cliente', color: 'border-t-accent' },
|
||||
{ id: 'CANCELACION APARTADO', titulo: 'Cancelación', color: 'border-t-red-500' },
|
||||
{ id: 'Pago Apartado', titulo: 'Pago Apartado', color: 'border-t-blue-500' },
|
||||
{ id: 'Alta Enkontrol', titulo: 'Alta Enkontrol', color: 'border-t-yellow-500' },
|
||||
{ id: 'Entrego Documentacion', titulo: 'Entregó Docs', color: 'border-t-orange-500' },
|
||||
{ id: 'Cita Clientes', titulo: 'Cita Clientes', color: 'border-t-green-500' },
|
||||
{ id: 'Contrato Enkontrol', titulo: 'Contrato Enkontrol', color: 'border-t-emerald-600' },
|
||||
{ id: 'Firma Cliente', titulo: 'Firma Cliente', color: 'border-t-accent' },
|
||||
{ id: 'Cancelacion Apartado', titulo: 'Cancelación', color: 'border-t-red-500' },
|
||||
]
|
||||
const columnasEscrituracion = [
|
||||
{ id: 'SEGUIMIENTO', titulo: 'Seguimiento', color: 'border-t-gray-400' },
|
||||
{ id: 'VENTA', titulo: 'Venta', color: 'border-t-gray-400' },
|
||||
{ id: 'SEGUIMIENTO', titulo: 'Seguimiento', color: 'border-t-blue-500' },
|
||||
{ id: 'INTEGRACION', titulo: 'Integración', color: 'border-t-yellow-500' },
|
||||
{ id: 'X AUTORIZAR', titulo: 'X Autorizar', color: 'border-t-orange-500' },
|
||||
{ id: 'AUTORIZADO', titulo: 'Autorizado', color: 'border-t-green-500' },
|
||||
{ id: 'X ESCRITURAR', titulo: 'X Escriturar', color: 'border-t-emerald-500' },
|
||||
{ id: 'ESCRITURADO', titulo: 'Escriturado', color: 'border-t-emerald-600' },
|
||||
{ id: 'TITULADO', titulo: 'Titulado', color: 'border-t-purple-600' },
|
||||
{ id: 'CANCELO', titulo: 'Canceló', color: 'border-t-red-500' },
|
||||
{ id: 'Venta', titulo: 'Venta', color: 'border-t-gray-400' },
|
||||
{ id: 'Seguimiento', titulo: 'Seguimiento', color: 'border-t-blue-500' },
|
||||
{ id: 'Integracion', titulo: 'Integración', color: 'border-t-yellow-500' },
|
||||
{ id: 'X Autorizar', titulo: 'X Autorizar', color: 'border-t-orange-500' },
|
||||
{ id: 'Autorizado', titulo: 'Autorizado', color: 'border-t-green-500' },
|
||||
{ id: 'X Escriturar', titulo: 'X Escriturar', color: 'border-t-emerald-500' },
|
||||
{ id: 'Escriturado', titulo: 'Escriturado', color: 'border-t-emerald-600' },
|
||||
{ id: 'Titulado', titulo: 'Titulado', color: 'border-t-purple-600' },
|
||||
{ id: 'Cancelo', titulo: 'Canceló', color: 'border-t-red-500' },
|
||||
]
|
||||
const obtenerColumnaLead = (lead: LeadMiDia) => {
|
||||
const e = (lead.estado ?? '').toUpperCase()
|
||||
if (e.includes('DESCART')) return 'DESCARTADO'
|
||||
if (e.includes('AGENDO') || e.includes('CITA')) return 'AGENDO'
|
||||
if (e.includes('AGENDO') || e.includes('CITA')) return 'AGENDA CITA'
|
||||
if (e.includes('CALIFIC')) return 'CALIFICADO'
|
||||
if (e.includes('INTENTO')) return 'Intento de contacto'
|
||||
if (e.includes('INTERES')) return 'INTERESADO'
|
||||
if (e.includes('CONTACT')) return 'CONTACTADO'
|
||||
return 'NUEVO'
|
||||
}
|
||||
const obtenerColumnaOp = (op: OportunidadMiDia) => {
|
||||
const e = (op.etapa ?? '').toUpperCase()
|
||||
if (e.includes('APARTO')) return 'APARTO'
|
||||
if (e.includes('PERDIDO') || e.includes('PERDI')) return 'PERDIDO'
|
||||
if (e.includes('COTIZ')) return 'COTIZADOR'
|
||||
if (e.includes('AGENDO') || e.includes('CITA')) return 'AGENDO CITA'
|
||||
return 'AFLUENCIA'
|
||||
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'
|
||||
return 'Afluencia'
|
||||
}
|
||||
const obtenerColumnaApartado = (a: ApartadoMiDia) => {
|
||||
const e = (a.estado ?? '').toUpperCase()
|
||||
if (e.includes('PAGO')) return 'PAGO APARTADO'
|
||||
if (e.includes('ALTA') && e.includes('ENKONTROL')) return 'ALTA ENKONTROL'
|
||||
if (e.includes('ENTREG') || e.includes('DOC')) return 'ENTREGO DOCUMENTACION'
|
||||
if (e.includes('CITA')) return 'CITA CLIENTES'
|
||||
if (e.includes('CONTRATO')) return 'CONTRATO ENKONTROL'
|
||||
if (e.includes('FIRMA')) return 'FIRMA CLIENTE'
|
||||
if (e.includes('CANCEL')) return 'CANCELACION APARTADO'
|
||||
return 'PAGO APARTADO'
|
||||
if (e.includes('PAGO')) return 'Pago Apartado'
|
||||
if (e.includes('ALTA') && e.includes('ENKONTROL')) return 'Alta Enkontrol'
|
||||
if (e.includes('ENTREG') || e.includes('DOC')) return 'Entrego Documentacion'
|
||||
if (e.includes('CITA')) return 'Cita Clientes'
|
||||
if (e.includes('CONTRATO')) return 'Contrato Enkontrol'
|
||||
if (e.includes('FIRMA')) return 'Firma Cliente'
|
||||
if (e.includes('CANCEL')) return 'Cancelacion Apartado'
|
||||
return 'Pago Apartado'
|
||||
}
|
||||
const obtenerColumnaEscrituracion = (a: ApartadoMiDia) => {
|
||||
const e = (a.estado ?? '').toUpperCase()
|
||||
if (e.includes('VENTA')) return 'VENTA'
|
||||
if (e.includes('SEGUIMIENTO')) return 'SEGUIMIENTO'
|
||||
if (e.includes('INTEGRACION')) return 'INTEGRACION'
|
||||
if (e.includes('X AUTORIZAR')) return 'X AUTORIZAR'
|
||||
if (e.includes('AUTORIZADO')) return 'AUTORIZADO'
|
||||
if (e.includes('X ESCRITURAR')) return 'X ESCRITURAR'
|
||||
if (e.includes('ESCRITURADO')) return 'ESCRITURADO'
|
||||
if (e.includes('TITULADO')) return 'TITULADO'
|
||||
if (e.includes('CANCEL')) return 'CANCELO'
|
||||
return 'VENTA'
|
||||
if (e.includes('VENTA')) return 'Venta'
|
||||
if (e.includes('SEGUIMIENTO')) return 'Seguimiento'
|
||||
if (e.includes('INTEGRACION')) return 'Integracion'
|
||||
if (e.includes('X AUTORIZAR')) return 'X Autorizar'
|
||||
if (e.includes('AUTORIZADO')) return 'Autorizado'
|
||||
if (e.includes('X ESCRITURAR')) return 'X Escriturar'
|
||||
if (e.includes('ESCRITURADO')) return 'Escriturado'
|
||||
if (e.includes('TITULADO')) return 'Titulado'
|
||||
if (e.includes('CANCEL')) return 'Cancelo'
|
||||
return 'Venta'
|
||||
}
|
||||
|
||||
|
||||
@@ -854,7 +914,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
||||
} catch {
|
||||
toast.error('No se pudo actualizar la etapa en Monday; el cotizador se abrirá de todos modos.')
|
||||
}
|
||||
window.location.href = urlCotizadorParaOportunidad(op)
|
||||
window.open(urlCotizadorParaOportunidad(op), '_blank')
|
||||
}
|
||||
|
||||
async function enviarCaptura(e: FormEvent) {
|
||||
@@ -893,6 +953,32 @@ 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)
|
||||
const leadsFiltrados = resumen
|
||||
? aplicarFiltros(resumen.misLeads.filter((l) => l.fuente === 'lead'), filtros, {
|
||||
texto: (l) => `${l.nombre} ${l.telefono} ${l.email}`,
|
||||
origen: (l) => l.origen,
|
||||
estado: (l) => l.estado,
|
||||
fecha: (l) => l.updatedAt.slice(0, 10),
|
||||
})
|
||||
: []
|
||||
const opsFiltradas = resumen
|
||||
? aplicarFiltros(resumen.misOportunidades.items, filtros, {
|
||||
texto: (o) => `${o.nombre} ${o.telefono} ${o.email}`,
|
||||
desarrollo: (o) => o.desarrollo,
|
||||
estado: (o) => o.etapa,
|
||||
fecha: (o) => o.updatedAt.slice(0, 10),
|
||||
})
|
||||
: []
|
||||
const apartadosFiltrados = resumen
|
||||
? aplicarFiltros(resumen.misApartados, filtros, {
|
||||
texto: (a) => a.nombre,
|
||||
estado: (a) => a.estado,
|
||||
fecha: (a) => a.fechaLimite,
|
||||
})
|
||||
: []
|
||||
|
||||
const hoy = hoyLocal()
|
||||
const iniciales = nombreVendedor
|
||||
.split(' ')
|
||||
@@ -1028,14 +1114,14 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
||||
{tabCrm !== 'tablero' && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1">
|
||||
<FiltrosAvanzados valor={filtros} onChange={setFiltros} />
|
||||
<FiltrosAvanzados valor={filtros} onChange={actualizarFiltros} estados={estadosPorTab[tabCrm]} />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (tabCrm === 'leads') exportarLeads(resumen.misLeads.filter((l) => l.fuente === 'lead'))
|
||||
else if (tabCrm === 'oportunidades') exportarOportunidades(resumen.misOportunidades.items)
|
||||
else if (tabCrm === 'apartados') exportarApartados(resumen.misApartados)
|
||||
if (tabCrm === 'leads') exportarLeads(leadsFiltrados)
|
||||
else if (tabCrm === 'oportunidades') exportarOportunidades(opsFiltradas)
|
||||
else if (tabCrm === 'apartados') exportarApartados(apartadosFiltrados)
|
||||
}}
|
||||
className="shrink-0 rounded-lg border border-[#D1D5DB] bg-white px-3 py-2 text-xs font-medium text-[#374151] shadow-sm transition-all hover:bg-[#F9FAFB] dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#CBD5E1] dark:hover:bg-[#1E293B]"
|
||||
>
|
||||
@@ -1044,74 +1130,66 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tabCrm === 'leads' && (() => {
|
||||
const leadsFiltrados = aplicarFiltros(
|
||||
resumen.misLeads.filter((l) => l.fuente === 'lead'),
|
||||
filtros,
|
||||
{
|
||||
texto: (l) => `${l.nombre} ${l.telefono} ${l.email}`,
|
||||
origen: (l) => l.origen,
|
||||
fecha: (l) => l.updatedAt.slice(0, 10),
|
||||
},
|
||||
)
|
||||
return (
|
||||
<>
|
||||
{tabCrm === 'leads' && (
|
||||
<>
|
||||
{leadsFiltrados.length === 0 ? (
|
||||
<EstadoVacio mensaje="Sin leads asignados" />
|
||||
) : vistaKanban ? (
|
||||
<KanbanView
|
||||
items={leadsFiltrados}
|
||||
columnas={columnasLeads}
|
||||
obtenerColumna={obtenerColumnaLead}
|
||||
onCambiarEstado={(id, estado) => {
|
||||
if (estado === 'DESCARTADO') {
|
||||
const lead = resumen.misLeads.find((l) => l.id === id)
|
||||
if (lead) setDescartando(lead)
|
||||
return
|
||||
}
|
||||
if (estado === 'NUEVO') return
|
||||
void ejecutar(() => cambiarEstadoLead(id, estado, undefined, 'lead'), 'No se pudo actualizar')
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{leadsFiltrados.map((lead) => (
|
||||
<FilaLead
|
||||
key={`${lead.fuente}-${lead.id}`}
|
||||
lead={lead}
|
||||
enviando={enviando}
|
||||
onAccion={(estado, fecha) => void ejecutar(() => cambiarEstadoLead(lead.id, estado, fecha, lead.fuente), 'No se pudo actualizar el lead')}
|
||||
onConvertir={() => void convertir(lead)}
|
||||
onDescartar={() => setDescartando(lead)}
|
||||
onVerDetalle={() => setLeadSeleccionado(lead)}
|
||||
acciones={acciones}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{leadsFiltrados.length === 0 ? (
|
||||
<EstadoVacio mensaje="Sin leads asignados" />
|
||||
) : vistaKanban ? (
|
||||
<KanbanView
|
||||
items={leadsFiltrados}
|
||||
columnas={columnasLeads}
|
||||
obtenerColumna={obtenerColumnaLead}
|
||||
onCambiarEstado={(id, estado) => void ejecutar(() => cambiarEstadoLead(id, estado, 'lead'), 'No se pudo actualizar')}
|
||||
/>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{leadsFiltrados.map((lead) => (
|
||||
<FilaLead
|
||||
key={`${lead.fuente}-${lead.id}`}
|
||||
lead={lead}
|
||||
enviando={enviando}
|
||||
onAccion={(estado, fecha) => void ejecutar(() => cambiarEstadoLead(lead.id, estado, fecha, lead.fuente), 'No se pudo actualizar el lead')}
|
||||
onConvertir={() => void convertir(lead)}
|
||||
onDescartar={() => setDescartando(lead)}
|
||||
onVerDetalle={() => setLeadSeleccionado(lead)}
|
||||
acciones={acciones}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
|
||||
{tabCrm === 'oportunidades' && (() => {
|
||||
const opsFiltradas = aplicarFiltros(
|
||||
resumen.misOportunidades.items,
|
||||
filtros,
|
||||
{
|
||||
texto: (o) => `${o.nombre} ${o.telefono} ${o.email}`,
|
||||
desarrollo: (o) => o.desarrollo,
|
||||
fecha: (o) => o.updatedAt.slice(0, 10),
|
||||
},
|
||||
)
|
||||
return (
|
||||
<>
|
||||
|
||||
{opsFiltradas.length === 0 ? (
|
||||
<EstadoVacio mensaje="Sin oportunidades asignadas" />
|
||||
) : vistaKanban ? (
|
||||
<KanbanView
|
||||
items={opsFiltradas}
|
||||
columnas={columnasOportunidades}
|
||||
obtenerColumna={obtenerColumnaOp}
|
||||
onCambiarEstado={(id, estado) => void ejecutar(() => cambiarEtapaOportunidad(id, estado), 'No se pudo actualizar')}
|
||||
/>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{opsFiltradas.map((op) => (
|
||||
<FilaOportunidad
|
||||
{tabCrm === 'oportunidades' && (
|
||||
<>
|
||||
{opsFiltradas.length === 0 ? (
|
||||
<EstadoVacio mensaje="Sin oportunidades asignadas" />
|
||||
) : vistaKanban ? (
|
||||
<KanbanView
|
||||
items={opsFiltradas}
|
||||
columnas={columnasOportunidades}
|
||||
obtenerColumna={obtenerColumnaOp}
|
||||
onCambiarEstado={(id, estado) => {
|
||||
if (estado === 'PERDIDO') {
|
||||
const op = resumen.misOportunidades.items.find((o) => o.id === id)
|
||||
if (op) setDescartando({ id: op.id, nombre: op.nombre, fuente: 'oportunidad' })
|
||||
return
|
||||
}
|
||||
void ejecutar(() => cambiarEtapaOportunidad(id, estado), 'No se pudo actualizar')
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{opsFiltradas.map((op) => (
|
||||
<FilaOportunidad
|
||||
key={op.id}
|
||||
op={op}
|
||||
enviando={enviando}
|
||||
@@ -1120,7 +1198,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
||||
}
|
||||
onCotizar={() => void cotizarOportunidad(op)}
|
||||
onAfluencia={() => {
|
||||
window.location.href = urlAfluenciaParaOportunidad(op)
|
||||
window.open(urlAfluenciaParaOportunidad(op), '_blank')
|
||||
}}
|
||||
onDescartar={() => setDescartando({ id: op.id, nombre: op.nombre, fuente: 'oportunidad' })}
|
||||
onVerDetalle={() => setLeadSeleccionado(op)}
|
||||
@@ -1129,57 +1207,45 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</>
|
||||
)}
|
||||
|
||||
{tabCrm === 'apartados' && (() => {
|
||||
const apartadosFiltrados = aplicarFiltros(
|
||||
resumen.misApartados,
|
||||
filtros,
|
||||
{
|
||||
texto: (a) => a.nombre,
|
||||
fecha: (a) => a.fechaLimite,
|
||||
},
|
||||
)
|
||||
return (
|
||||
<>
|
||||
|
||||
{apartadosFiltrados.length === 0 ? (
|
||||
<EstadoVacio mensaje="Sin apartados asignados" />
|
||||
) : vistaKanban ? (
|
||||
<KanbanView
|
||||
items={apartadosFiltrados}
|
||||
columnas={columnasApartados}
|
||||
obtenerColumna={obtenerColumnaApartado}
|
||||
onCambiarEstado={(id, estado) => void ejecutar(() => cambiarEstadoApartado(id, estado), 'No se pudo actualizar')}
|
||||
/>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{apartadosFiltrados.map((a) => (
|
||||
<li
|
||||
key={a.id}
|
||||
>
|
||||
<FilaApartado
|
||||
apartado={a}
|
||||
enviando={enviando}
|
||||
onVerDetalle={() => setLeadSeleccionado(a)}
|
||||
onFase={(fase) => {
|
||||
if (fase === 'Firma Cliente') {
|
||||
// Cambiar etapa AÚNQUE luego cambie a pestaña Escrituración
|
||||
void ejecutar(() => cambiarEstadoApartado(a.id, fase).then(() => setTabCrm('escrituracion')), 'No se pudo actualizar el apartado')
|
||||
return
|
||||
}
|
||||
void ejecutar(() => cambiarEstadoApartado(a.id, fase), 'No se pudo actualizar el apartado')
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
{tabCrm === 'apartados' && (
|
||||
<>
|
||||
{apartadosFiltrados.length === 0 ? (
|
||||
<EstadoVacio mensaje="Sin apartados asignados" />
|
||||
) : vistaKanban ? (
|
||||
<KanbanView
|
||||
items={apartadosFiltrados}
|
||||
columnas={columnasApartados}
|
||||
obtenerColumna={obtenerColumnaApartado}
|
||||
onCambiarEstado={(id, estado) => void ejecutar(() => cambiarEstadoApartado(id, estado), 'No se pudo actualizar')}
|
||||
/>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{apartadosFiltrados.map((a) => (
|
||||
<li
|
||||
key={a.id}
|
||||
>
|
||||
<FilaApartado
|
||||
apartado={a}
|
||||
enviando={enviando}
|
||||
onVerDetalle={() => setLeadSeleccionado(a)}
|
||||
onFase={(fase) => {
|
||||
if (fase === 'Firma Cliente') {
|
||||
// Cambiar etapa AÚNQUE luego cambie a pestaña Escrituración
|
||||
void ejecutar(() => cambiarEstadoApartado(a.id, fase).then(() => setTabCrm('escrituracion')), 'No se pudo actualizar el apartado')
|
||||
return
|
||||
}
|
||||
void ejecutar(() => cambiarEstadoApartado(a.id, fase), 'No se pudo actualizar el apartado')
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{tabCrm === 'escrituracion' && (
|
||||
<section aria-label="Escrituración">
|
||||
@@ -1191,18 +1257,34 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
||||
// Filtrar: solo apartados en fases de escrituración o Firma Cliente
|
||||
// Solo fases activas (Cancelo y Escriturado son terminales)
|
||||
// Fases activas de escrituración (excluye Cancelo y Escriturado que son terminales)
|
||||
const apartadosEscrituracion = (resumen.todosApartados || resumen.misApartados || []).filter((a) => {
|
||||
const enEscrituracion = (resumen.todosApartados || resumen.misApartados || []).filter((a) => {
|
||||
const e = (a.estado ?? '').toUpperCase()
|
||||
return e.includes('FIRMA') || e.includes('VENTA') || e.includes('SEGUIMIENTO') || e.includes('INTEGRACION') || e.includes('X AUTORIZAR') || e.includes('X ESCRITURAR')
|
||||
return e.includes('FIRMA') || e.includes('VENTA') || e.includes('SEGUIMIENTO') || e.includes('INTEGRACION') || e.includes('X AUTORIZAR') || e.includes('AUTORIZADO') || e.includes('X ESCRITURAR')
|
||||
})
|
||||
const apartadosEscrituracion = aplicarFiltros(
|
||||
enEscrituracion,
|
||||
filtros,
|
||||
{
|
||||
texto: (a) => a.nombre,
|
||||
estado: (a) => a.estado,
|
||||
fecha: (a) => a.fechaLimite,
|
||||
},
|
||||
)
|
||||
return apartadosEscrituracion.length === 0 ? (
|
||||
<EstadoVacio mensaje="No hay apartados en escrituración" />
|
||||
) : vistaKanban ? (
|
||||
<KanbanView
|
||||
items={resumen.misApartados}
|
||||
items={apartadosEscrituracion}
|
||||
columnas={columnasEscrituracion}
|
||||
obtenerColumna={obtenerColumnaEscrituracion}
|
||||
onCambiarEstado={(id, estado) => void ejecutar(() => cambiarEstadoApartado(id, estado), 'No se pudo actualizar')}
|
||||
onCambiarEstado={(id, estado) => {
|
||||
if (estado === 'Cancelo') {
|
||||
const a = apartadosEscrituracion.find((x) => x.id === id)
|
||||
if (a) setApartadoCancelando({ id: a.id, nombre: a.nombre })
|
||||
return
|
||||
}
|
||||
void ejecutar(() => cambiarEstadoApartado(id, estado), 'No se pudo actualizar')
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ul className="space-y-3">
|
||||
@@ -1253,6 +1335,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
||||
<CalendarioCitas
|
||||
actividades={resumen.misActividadesHoy}
|
||||
oportunidades={resumen.misOportunidades.items}
|
||||
onVerDetalle={(op) => setLeadSeleccionado(op)}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
@@ -1552,15 +1635,22 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
||||
)}
|
||||
</main>
|
||||
|
||||
{descartando && (() => {
|
||||
const lead = descartando
|
||||
setDescartando(null)
|
||||
void ejecutar(
|
||||
() => descartarLead(lead.id, '', undefined, lead.fuente, undefined),
|
||||
'No se pudo descartar el lead',
|
||||
)
|
||||
return null
|
||||
})()}
|
||||
{descartando && (
|
||||
<ModalDescarteGenerico
|
||||
item={descartando}
|
||||
titulo={descartando.fuente === 'oportunidad' ? 'Descartar oportunidad' : 'Descartar lead'}
|
||||
enviando={enviando}
|
||||
onCancelar={() => setDescartando(null)}
|
||||
onConfirmar={(motivo, subcategoria, comentario) => {
|
||||
const item = descartando
|
||||
setDescartando(null)
|
||||
void ejecutar(
|
||||
() => descartarLead(item.id, motivo, comentario, item.fuente, subcategoria),
|
||||
'No se pudo descartar el lead',
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user