feat(mi-dia): botón Afluencia abre el formulario prellenado y actualiza la oportunidad

- Mis oportunidades: 'Afluencia →' navega a /client/?afluencia=<id> con
  nombre, contacto, desarrollo y modelo prellenados
- Alta Afluencia: con oportunidadId el envío ACTUALIZA la oportunidad en
  Monday (perfil completo + etapa Afluencia) en vez de crear duplicado;
  sin oportunidadId el flujo de captura nueva no cambia
- monday.service: actualizarOportunidadAfluencia (mismo builder de la
  creación + name); moverAGrupoSegunEstado tras actualizar
- Tests: 457 API + 16 mi-dia + 116 alta-clientes, verde
This commit is contained in:
2026-09-04 22:53:21 +00:00
parent cbd50e2066
commit 5d7c6f58cd
9 changed files with 266 additions and 22 deletions

View File

@@ -18,6 +18,10 @@ interface Props {
/** Conversión directa: tras registrar la afluencia, abrir el alta completa
* prellenada con los datos recién capturados (sin rebuscar el lead) */
onPasarAAltaCliente: (lead: LeadResumen) => void
/** Prefill desde Mis oportunidades (Mi día): valores iniciales del formulario */
valoresIniciales?: Partial<typeof FORM_INICIAL>
/** Oportunidad de Monday a ACTUALIZAR al enviar (sin duplicar el item) */
oportunidadId?: string
}
const emailSchema = z.string().trim().email()
@@ -58,8 +62,8 @@ const FORM_INICIAL = {
* lead en Monday y su oportunidad en etapa Afluencia. El alta completa de
* cliente sigue siendo el wizard del picker.
*/
export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente }: Props) {
const [form, setForm] = useState(FORM_INICIAL)
export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valoresIniciales, oportunidadId }: Props) {
const [form, setForm] = useState({ ...FORM_INICIAL, ...valoresIniciales })
const [errores, setErrores] = useState<Errores>({})
const [enviando, setEnviando] = useState(false)
const [leadCreado, setLeadCreado] = useState<LeadResumen | null>(null)
@@ -128,6 +132,7 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente }: Pro
const res = await crearAfluencia({
nombre: form.nombre.trim(),
telefono: form.telefono.trim(),
...(oportunidadId !== undefined ? { oportunidadId } : {}),
...(form.email.trim() ? { email: form.email.trim() } : {}),
...(form.origen ? { origen: form.origen } : {}),
...(form.desarrollo ? { desarrollo: form.desarrollo } : {}),
@@ -178,9 +183,11 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente }: Pro
role="status"
className="rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm font-medium text-green-700"
>
{asignacion === 'capturista'
? 'Registrado en afluencia — asignado a ti'
: 'Registrado en afluencia — se asignará por round-robin'}
{oportunidadId !== undefined
? 'Afluencia actualizada — el perfil completo quedó guardado en la oportunidad'
: asignacion === 'capturista'
? 'Registrado en afluencia — asignado a ti'
: 'Registrado en afluencia — se asignará por round-robin'}
</p>
<div className="mt-6 flex flex-wrap gap-3">
<button
@@ -190,13 +197,15 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente }: Pro
>
Pasar a Alta de Cliente
</button>
<button
type="button"
onClick={capturarOtro}
className="rounded-lg border border-accent bg-white px-5 py-2.5 text-sm font-medium text-accent shadow-sm transition-all duration-100 ease-out hover:bg-accent hover:text-white"
>
Capturar otro
</button>
{oportunidadId === undefined && (
<button
type="button"
onClick={capturarOtro}
className="rounded-lg border border-accent bg-white px-5 py-2.5 text-sm font-medium text-accent shadow-sm transition-all duration-100 ease-out hover:bg-accent hover:text-white"
>
Capturar otro
</button>
)}
<button
type="button"
onClick={onVolver}
@@ -211,12 +220,22 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente }: Pro
return (
<form onSubmit={(e) => void enviar(e)} noValidate>
<div className="mb-6 border-b border-[#E5E7EB] pb-6">
<BusquedaLead onSeleccion={prellenarDesdeLead} />
<p className="mt-2 text-xs text-[#64748B]">
Si el interesado ya está en Monday (lead u oportunidad), sus datos se prellenan solos.
{oportunidadId !== undefined ? (
<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"
>
Estás completando la afluencia de {form.nombre || 'esta oportunidad'} — al enviar se actualiza su
oportunidad en Monday (no se duplica).
</p>
</div>
) : (
<div className="mb-6 border-b border-[#E5E7EB] pb-6">
<BusquedaLead onSeleccion={prellenarDesdeLead} />
<p className="mt-2 text-xs text-[#64748B]">
Si el interesado ya está en Monday (lead u oportunidad), sus datos se prellenan solos.
</p>
</div>
)}
<div className="grid gap-6 sm:grid-cols-2">
<label htmlFor="afluencia-nombre" className="block sm:col-span-2">
<span className={labelClase}>
@@ -478,7 +497,7 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente }: Pro
disabled={enviando}
className="rounded-lg bg-accent px-5 py-2.5 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover disabled:opacity-50"
>
{enviando ? 'Registrando…' : 'Registrar en afluencia'}
{enviando ? 'Registrando…' : oportunidadId !== undefined ? 'Actualizar afluencia' : 'Registrar en afluencia'}
</button>
<button
type="button"