feat: conversión lead→oportunidad (Agendo) y alta ligada a apartado

- Mis leads: botón 'Agendo →' convierte el lead en oportunidad (etapa
  Agendo Cita, con sus datos, asesor = vendedor del lead o quien
  convierte), marca el lead Calificado y guarda el mapping
  LeadOportunidad; 409 si ya fue convertido
- Mis oportunidades: se quitan No Asistio y Abandono (redundantes con
  Descartar); quedan Cotizar, Afluencia, Agendo Cita y Aparto
- Mis apartados: 'Firma cliente (venta) →' abre el alta de cliente con
  TODO lo recolectado prellenado (ficha del apartado + contacto +
  oportunidad + cotización vinculada vía GET /altas/prefill-apartado/:id)
- El alta con apartadoMondayId se LIGA al apartado existente y lo
  enriquece (curp, teléfono, email, precio final, enganche, forma de
  pago, unidad) en vez de crear un duplicado
- monday.service: actualizarApartado + ApartadoMonday con curp, forma de
  pago, enganche y monto apartado
- Tests: 470 API + 120 alta-clientes + 16 mi-dia, verde
This commit is contained in:
2026-09-05 00:33:53 +00:00
parent a4575bb948
commit 5b348d21e5
14 changed files with 654 additions and 17 deletions

View File

@@ -21,11 +21,13 @@ import {
listarBorradores,
obtenerBorrador,
obtenerCotizacion,
obtenerPrefillApartado,
subirDocumentos,
type BorradorResumen,
type LeadResumen,
} from '../services/api'
import { cotizacionAPrellenado } from '../lib/cotizacionAlta'
import { apartadoAPrellenado } from '../lib/apartadoAlta'
import { leadAPrellenado } from '../lib/leadAlta'
import BusquedaLead from '../components/BusquedaLead'
import AltaAfluenciaForm from '../components/AltaAfluenciaForm'
@@ -54,6 +56,11 @@ function paramsAfluencia(): { oportunidadId: string; valores: Record<string, str
return { oportunidadId: id, valores }
}
/** Lee ?apartado=<itemId> — botón "Firma Cliente (Venta)" de Mis apartados en Mi día */
function paramApartado(): string | null {
return new URLSearchParams(window.location.search).get('apartado')
}
const formatoFechaHora = (iso: string): string =>
new Date(iso).toLocaleString('es-MX', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })
@@ -127,10 +134,25 @@ export default function AltaClientePage() {
valores: Record<string, string>
} | null>(null)
// Al entrar: ?cotizacion=<id> prellena el alta y salta directo al wizard;
// ?afluencia=<id> abre el formulario de afluencia prellenado; si no, la
// entrada ofrece Alta Afluencia (captura ligera) y Alta de Cliente
// Al entrar: ?apartado=<id> prellena el alta con todo lo recolectado del
// apartado y salta al wizard ligado a ese item; ?afluencia=<id> abre el
// formulario de afluencia prellenado; ?cotizacion=<id> prellena el alta;
// si no, la entrada ofrece Alta Afluencia (captura ligera) y Alta de Cliente
useEffect(() => {
const apartadoId = paramApartado()
if (apartadoId) {
obtenerPrefillApartado(apartadoId)
.then((pre) => {
const p = apartadoAPrellenado(pre)
actualizar('datosGenerales', { ...datos.datosGenerales, ...p.datosGenerales })
actualizar('viviendaCredito', { ...datos.viviendaCredito, ...p.viviendaCredito })
actualizar('apartadoMondayId', apartadoId)
if (p.leadMondayId) actualizar('leadMondayId', p.leadMondayId)
})
.catch(() => setErrorCarga('No se pudo cargar el apartado para prellenar el alta.'))
.finally(() => setPantalla('wizard'))
return
}
const afl = paramsAfluencia()
if (afl) {
setAfluenciaPrevia(afl)
@@ -487,6 +509,11 @@ export default function AltaClientePage() {
{errorCarga}
</p>
)}
{datos.apartadoMondayId && (
<p role="note" className="mb-6 rounded-lg border border-blue-200 bg-blue-50 px-4 py-3 text-xs font-medium text-blue-800">
Alta ligada al apartado del cliente en Monday — al completarla se actualiza ese item (no se duplica).
</p>
)}
{paso === 0 && (
<TarjetaPaso>