feat(alta): separar en Alta Afluencia y Alta de Cliente

- Nueva pantalla de entrada en /client con dos opciones:
  · Alta Afluencia: captura ligera (nombre, teléfono, email, origen,
    desarrollo de interés, notas) → POST /api/leads (lead + oportunidad
    Afluencia + round-robin, todo ya existente en el API)
  · Alta de Cliente: formato completo actual (picker + wizard intactos)
- Manejo de duplicado por teléfono (409 con nombre del lead existente)
- ?cotizacion=<id> sigue saltando directo al wizard
- Tests: 107 verdes (11 nuevos); smoke en vivo OK y limpio
This commit is contained in:
2026-09-01 20:24:26 +00:00
parent 98fedbd8d3
commit 2b87bf66a7
6 changed files with 515 additions and 7 deletions

View File

@@ -24,6 +24,7 @@ import {
import { cotizacionAPrellenado } from '../lib/cotizacionAlta'
import { leadAPrellenado } from '../lib/leadAlta'
import BusquedaLead from '../components/BusquedaLead'
import AltaAfluenciaForm from '../components/AltaAfluenciaForm'
/** Lee ?cotizacion=<id> del query string o del hash (#cotizacion=<id>) */
function paramCotizacion(): string | null {
@@ -55,7 +56,7 @@ export default function AltaClientePage() {
const [errorValidacion, setErrorValidacion] = useState<string>('')
// ---------- Borradores ----------
type Pantalla = 'cargando' | 'picker' | 'wizard'
type Pantalla = 'cargando' | 'entrada' | 'picker' | 'afluencia' | 'wizard'
const [pantalla, setPantalla] = useState<Pantalla>('cargando')
const [borradores, setBorradores] = useState<BorradorResumen[]>([])
const [borradorId, setBorradorId] = useState<string | null>(null)
@@ -68,7 +69,8 @@ export default function AltaClientePage() {
const [ligaCopiada, setLigaCopiada] = useState(false)
const [errorLiga, setErrorLiga] = useState('')
// Al entrar: ?cotizacion=<id> prellena el alta; si no, ofrece borradores
// Al entrar: ?cotizacion=<id> prellena el alta y salta directo al wizard;
// si no, la entrada ofrece Alta Afluencia (captura ligera) y Alta de Cliente
useEffect(() => {
const cotizacionId = paramCotizacion()
if (cotizacionId) {
@@ -85,11 +87,11 @@ export default function AltaClientePage() {
listarBorradores()
.then((lista) => {
setBorradores(lista)
// El picker es siempre la entrada: además de los borradores ofrece
// "Empezar nuevo" y la búsqueda de leads de Monday.
setPantalla('picker')
setPantalla('entrada')
})
.catch(() => setPantalla('wizard')) // sin conexión: el wizard funciona igual
// Sin conexión: la entrada no depende de los borradores y conserva la
// opción de afluencia (el alta de cliente funcionará cuando vuelva la red)
.catch(() => setPantalla('entrada'))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@@ -196,6 +198,59 @@ export default function AltaClientePage() {
)
}
if (pantalla === 'entrada') {
return (
<div className="min-h-screen bg-white">
<header className="border-b border-neutral-200 px-4 py-6">
<div className="mx-auto flex max-w-6xl items-baseline gap-3">
<span className="text-sm font-bold uppercase tracking-widest">FRASA</span>
<h1 className="text-2xl font-light tracking-wide">Alta de Clientes</h1>
</div>
</header>
<main className="mx-auto max-w-6xl px-4 py-16">
<div className="grid gap-6 sm:grid-cols-2">
<button
type="button"
onClick={() => setPantalla('afluencia')}
className="border border-neutral-200 bg-white p-8 text-left transition-colors duration-150 hover:border-accent"
>
<h2 className="text-lg font-light tracking-wide text-neutral-900">Alta Afluencia</h2>
<p className="mt-2 text-xs font-light text-neutral-500">
Captura rápida del interesado; entra al embudo de ventas.
</p>
</button>
<button
type="button"
onClick={() => setPantalla('picker')}
className="border border-neutral-200 bg-white p-8 text-left transition-colors duration-150 hover:border-accent"
>
<h2 className="text-lg font-light tracking-wide text-neutral-900">Alta de Cliente</h2>
<p className="mt-2 text-xs font-light text-neutral-500">
Formato completo para quien aparta.
</p>
</button>
</div>
</main>
</div>
)
}
if (pantalla === 'afluencia') {
return (
<div className="min-h-screen bg-white">
<header className="border-b border-neutral-200 px-4 py-6">
<div className="mx-auto flex max-w-6xl items-baseline gap-3">
<span className="text-sm font-bold uppercase tracking-widest">FRASA</span>
<h1 className="text-2xl font-light tracking-wide">Alta Afluencia</h1>
</div>
</header>
<main className="mx-auto max-w-6xl px-4 py-16">
<AltaAfluenciaForm onVolver={() => setPantalla('entrada')} />
</main>
</div>
)
}
if (pantalla === 'picker') {
return (
<div className="min-h-screen bg-white">