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

@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import Burbuja from '../components/Burbuja'
import DarkModeToggle from '../components/DarkModeToggle'
import toast from "react-hot-toast"
@@ -10,25 +10,20 @@ import {
type ConversacionResumen,
} from '../services/api'
import { cargarSesion, guardarSesion, limpiarSesion } from '../lib/sesion'
import { preguntasSugeridas } from '../lib/vocabulario'
import type { Mensaje } from '../types'
let contadorLocal = 0
const idLocal = () => `local-${++contadorLocal}`
const PREGUNTAS_SUGERIDAS = [
'¿Qué incluye la propuesta del cotizador DIAGONAL?',
'¿Cuáles son los pasos del proceso de venta?',
'¿Qué políticas de Frasa debo conocer?',
'¿Qué modelos hay en el catálogo DIAGONAL?',
]
export default function ChatPage() {
const [sessionId, setSessionId] = useState<string | null>(null)
const [mensajes, setMensajes] = useState<Mensaje[]>([])
const [entrada, setEntrada] = useState('')
const [escribiendo, setEscribiendo] = useState(false)
const [sinConexion, setSinConexion] = useState(false)
const [error, setError] = useState<string | null>(null)
// Preguntas sugeridas del día: rotan por desarrollo (VOCABULARIO.DESARROLLOS)
const preguntasSugeridasHoy = useMemo(() => preguntasSugeridas(), [])
// Texto del último envío fallido (habilita "Reintentar" junto al input)
const [pendiente, setPendiente] = useState<string | null>(null)
// Panel de conversaciones previas
@@ -70,7 +65,7 @@ export default function ChatPage() {
if (esErrorDeRed(err)) {
setSinConexion(true)
} else {
toast.error('Error al enviar el mensaje'); setError(err instanceof Error ? err.message : 'Error al enviar el mensaje')
toast.error('Error al enviar el mensaje')
}
setPendiente(texto) // el mensaje del usuario se queda visible; puede reintentar
} finally {
@@ -82,7 +77,6 @@ export default function ChatPage() {
const texto = entrada.trim()
if (!texto || escribiendo) return
setEntrada('')
setError(null)
setMensajes((prev) => [...prev, { id: idLocal(), rol: 'user', contenido: texto, fuentes: [] }])
await transmitir(texto)
}
@@ -90,7 +84,6 @@ export default function ChatPage() {
/** Reenvía el último mensaje que falló (sin duplicar la burbuja del usuario) */
async function reintentar() {
if (!pendiente || escribiendo) return
setError(null)
setSinConexion(false)
await transmitir(pendiente)
}
@@ -99,15 +92,14 @@ export default function ChatPage() {
limpiarSesion()
setSessionId(null)
setMensajes([])
setError(null)
setSinConexion(false)
setPendiente(null)
}
/** Abre el panel y carga (una vez) el listado de conversaciones del usuario */
/** Abre el panel y recarga SIEMPRE el listado de conversaciones del usuario */
function abrirPanel() {
setPanelAbierto(true)
if (conversaciones !== null || cargandoConversaciones) return
if (cargandoConversaciones) return
setCargandoConversaciones(true)
listarConversaciones()
.then(setConversaciones)
@@ -123,7 +115,6 @@ export default function ChatPage() {
guardarSesion(id)
setMensajes(historial)
setPendiente(null)
setError(null)
setSinConexion(false)
setPanelAbierto(false)
} catch {
@@ -132,7 +123,7 @@ export default function ChatPage() {
}
return (
<div className="flex min-h-screen flex-col bg-[#F8FAFC] text-[#111827] dark:text-[#E2E8F0] dark:bg-[#0F172A] dark:text-[#E2E8F0]">
<div className="flex min-h-screen flex-col bg-[#F8FAFC] text-[#111827] dark:text-[#E2E8F0] dark:bg-[#0F172A]">
<header className="sticky top-0 z-10 border-b border-[#E5E7EB] bg-white/95 backdrop-blur dark:border-[#1E293B] dark:bg-[#1E293B]/95">
<div className="mx-auto flex h-14 w-full max-w-3xl items-center justify-between px-4 sm:px-6">
<div className="flex items-center gap-3">
@@ -176,11 +167,6 @@ export default function ChatPage() {
nuevo.
</p>
)}
{error && (
<p className="mb-6 rounded-lg bg-red-50 px-4 py-3 text-xs font-medium text-red-700">
{error}
</p>
)}
{mensajes.length === 0 && !escribiendo ? (
<div className="flex flex-1 flex-col items-center justify-center">
@@ -193,7 +179,7 @@ export default function ChatPage() {
venta o las políticas de Frasa.
</p>
<div className="mt-6 grid gap-2 sm:grid-cols-2">
{PREGUNTAS_SUGERIDAS.map((pregunta) => (
{preguntasSugeridasHoy.map((pregunta) => (
<button
key={pregunta}
type="button"