feat: mis cobranzas, recordatorios de pago y agente IA con acciones

- PlanPago.vendedorId + filtro ?mios=1; toggle Mis planes/Todos en /pay
- Motor de recordatorios: próximos a vencer (3d) y vencidos (re-cada 7d),
  canal Monday Actividad + stub WhatsApp listo para WA CRM, log persistente,
  cron diario 9:30, run manual con dry-run, historial en /api/cobranza/recordatorios
- Agente IA: memoria por usuario (conversaciones aisladas) y tool calling:
  cotizar_unidad (motor real por desarrollo) y buscar_unidad (inventario Monday)
- Tests: api 261, cobranza 75, agente-ia 13
This commit is contained in:
2026-08-28 08:07:12 +00:00
parent 59944668a8
commit aa27e89c75
18 changed files with 1437 additions and 17 deletions

View File

@@ -14,7 +14,8 @@ import {
YAxis,
} from 'recharts'
import type { StatusPlan } from '../types'
import { listarPlanes, type ResultadoPlanes } from '../services/repository'
import { listarPlanes, miosPorDefecto, type ResultadoPlanes } from '../services/repository'
import { getSesion } from '../services/auth'
import {
alCorriente,
avancePlan,
@@ -55,11 +56,17 @@ export default function DashboardPage() {
const paramOrden = searchParams.get('sort')
const orden: ColumnaOrden | null = esColumnaOrden(paramOrden) ? paramOrden : null
const dir: 'asc' | 'desc' = searchParams.get('dir') === 'desc' ? 'desc' : 'asc'
// Toggle "Mis planes / Todos": persiste en la URL (?vista=mios|todos); sin
// param, los vendedores arrancan en "Mis planes" y el resto en "Todos"
const paramVista = searchParams.get('vista')
const mios = paramVista !== null ? paramVista === 'mios' : miosPorDefecto(getSesion()?.usuario.rol)
const hoy = useMemo(() => new Date(), [])
useEffect(() => {
let activo = true
listarPlanes()
setResultado(null)
setErrorCarga(null)
listarPlanes(undefined, mios)
.then((r) => {
if (activo) setResultado(r)
})
@@ -69,7 +76,7 @@ export default function DashboardPage() {
return () => {
activo = false
}
}, [])
}, [mios])
const planes = useMemo(() => resultado?.planes ?? [], [resultado])
@@ -287,6 +294,29 @@ export default function DashboardPage() {
{/* Filtros */}
<div className="flex flex-wrap items-center gap-4">
{/* Toggle Mis planes / Todos (?mios=1 filtra en el API por el vendedor del token) */}
<div className="flex border border-neutral-300" role="group" aria-label="Alcance de planes">
<button
type="button"
onClick={() => actualizarParams({ vista: mios ? null : 'mios' })}
aria-pressed={mios}
className={`px-4 py-2.5 text-xs font-bold uppercase tracking-widest transition-colors duration-150 ${
mios ? 'bg-accent text-white' : 'bg-white text-neutral-600 hover:text-neutral-900'
}`}
>
Mis planes
</button>
<button
type="button"
onClick={() => actualizarParams({ vista: mios ? 'todos' : null })}
aria-pressed={!mios}
className={`px-4 py-2.5 text-xs font-bold uppercase tracking-widest transition-colors duration-150 ${
mios ? 'bg-white text-neutral-600 hover:text-neutral-900' : 'bg-accent text-white'
}`}
>
Todos
</button>
</div>
<label htmlFor="filtro-busqueda" className="sr-only">
Buscar por cliente o unidad
</label>