feat(mejoras): 22 mejoras UX en todos los módulos

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
This commit is contained in:
2026-09-11 12:48:47 +00:00
parent 89d95eb53b
commit 1ba7aa49da
28 changed files with 518 additions and 160 deletions

View File

@@ -389,10 +389,6 @@ function FilaApartado({
window.open(`/client/?apartado=${apartado.id}`, '_blank')
return
}
if (fase === 'Cancelacion Apartado') {
onFase(fase)
return
}
onFase(fase)
}}
className={
@@ -976,6 +972,20 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
fecha: (a) => a.fechaLimite,
})
: []
// Escrituración: apartados en fases activas posteriores a Firma Cliente
// (Cancelo y Escriturado son terminales). Usa los filtros PROPIOS del tab
// para que el conteo del tab refleje la lista filtrada de escrituración.
const enEscrituracion = resumen
? (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('AUTORIZADO') || e.includes('X ESCRITURAR')
})
: []
const escrituracionFiltrados = aplicarFiltros(enEscrituracion, filtrosPorTab.escrituracion, {
texto: (a) => a.nombre,
estado: (a) => a.estado,
fecha: (a) => a.fechaLimite,
})
const hoy = hoyLocal()
const iniciales = nombreVendedor
@@ -1095,7 +1105,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
conteos={{
leads: resumen.misLeads.filter((l) => l.fuente === 'lead').length,
oportunidades: resumen.misOportunidades.items.length,
apartados: resumen.misApartados.length, escrituracion: resumen.misApartados.length, tablero: 0, calendario: 0,
apartados: resumen.misApartados.length, escrituracion: escrituracionFiltrados.length, tablero: 0, calendario: 0,
}}
onCambiar={setTabCrm}
vistaKanban={vistaKanban}
@@ -1211,7 +1221,15 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
items={apartadosFiltrados}
columnas={columnasApartados}
obtenerColumna={obtenerColumnaApartado}
onCambiarEstado={(id, estado) => void ejecutar(() => cambiarEstadoApartado(id, estado), 'No se pudo actualizar')}
onCambiarEstado={(id, estado) => {
if (estado === 'Cancelacion Apartado') {
// Pedir motivo antes de cambiar (ModalDescarteGenerico)
const a = apartadosFiltrados.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-2">
@@ -1224,6 +1242,11 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
enviando={enviando}
onVerDetalle={() => setLeadSeleccionado(a)}
onFase={(fase) => {
if (fase === 'Cancelacion Apartado') {
// Pedir motivo antes de cambiar (ModalDescarteGenerico)
setApartadoCancelando({ id: a.id, nombre: a.nombre })
return
}
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')
@@ -1245,33 +1268,16 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
<p className="text-sm text-[#64748B] dark:text-[#94A3B8] mb-4">
Seguimiento de escrituración de apartados.
</p>
{(() => {
// 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 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('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 ? (
{escrituracionFiltrados.length === 0 ? (
<EstadoVacio mensaje="No hay apartados en escrituración" />
) : vistaKanban ? (
<KanbanView
items={apartadosEscrituracion}
items={escrituracionFiltrados}
columnas={columnasEscrituracion}
obtenerColumna={obtenerColumnaEscrituracion}
onCambiarEstado={(id, estado) => {
if (estado === 'Cancelo') {
const a = apartadosEscrituracion.find((x) => x.id === id)
const a = escrituracionFiltrados.find((x) => x.id === id)
if (a) setApartadoCancelando({ id: a.id, nombre: a.nombre })
return
}
@@ -1280,7 +1286,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
/>
) : (
<ul className="space-y-3">
{apartadosEscrituracion.map((apartado) => (
{escrituracionFiltrados.map((apartado) => (
<li key={apartado.id} className="rounded-xl border border-[#E5E7EB] bg-white p-4 shadow-sm dark:border-[#1E293B] dark:bg-[#1E293B]">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
@@ -1314,7 +1320,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
</li>
))}
</ul>
)})()}
)}
</section>
)}
@@ -1325,7 +1331,9 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
{tabCrm === 'calendario' && (
<section aria-label="Calendario">
<CalendarioCitas
actividades={resumen.misActividadesHoy}
// Todas las actividades abiertas del usuario (no solo las de hoy);
// los resúmenes cacheados previos pueden no traer el campo nuevo.
actividades={resumen.todasMisActividades ?? resumen.misActividadesHoy}
oportunidades={resumen.misOportunidades.items}
onVerDetalle={(op) => setLeadSeleccionado(op)}
/>
@@ -1673,8 +1681,11 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
'modeloInteres' in leadSeleccionado
? (fechaHora) => {
const op = leadSeleccionado
// Si la op ya pasó por Agendo Cita (Cotizador/Aparto) NO se
// retrocede la etapa: solo se actualiza la fecha de la cita.
const etapa = op.etapa === 'Cotizador' || op.etapa === 'Aparto' ? op.etapa : 'Agendo Cita'
return ejecutar(
() => cambiarEtapaOportunidad(op.id, 'Agendo Cita', fechaHora),
() => cambiarEtapaOportunidad(op.id, etapa, fechaHora),
'No se pudo guardar la cita',
)
}