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:
@@ -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"
|
||||
|
||||
@@ -38,6 +38,22 @@ function paramCotizacion(): string | null {
|
||||
return new URLSearchParams(hash).get('cotizacion')
|
||||
}
|
||||
|
||||
/**
|
||||
* Lee ?afluencia=<oportunidadId> + prefill (nombre/telefono/email/desarrollo/
|
||||
* modelo) — botón "Afluencia" de Mis oportunidades en Mi día.
|
||||
*/
|
||||
function paramsAfluencia(): { oportunidadId: string; valores: Record<string, string> } | null {
|
||||
const qs = new URLSearchParams(window.location.search)
|
||||
const id = qs.get('afluencia')
|
||||
if (!id) return null
|
||||
const valores: Record<string, string> = {}
|
||||
for (const k of ['nombre', 'telefono', 'email', 'desarrollo', 'modelo', 'origen']) {
|
||||
const v = qs.get(k)
|
||||
if (v) valores[k] = v
|
||||
}
|
||||
return { oportunidadId: id, valores }
|
||||
}
|
||||
|
||||
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' })
|
||||
|
||||
@@ -104,9 +120,23 @@ export default function AltaClientePage() {
|
||||
const [ligaCopiada, setLigaCopiada] = useState(false)
|
||||
const [errorLiga, setErrorLiga] = useState('')
|
||||
|
||||
// Afluencia prellenada desde Mis oportunidades (Mi día): actualiza la
|
||||
// oportunidad existente en vez de crear una nueva
|
||||
const [afluenciaPrevia, setAfluenciaPrevia] = useState<{
|
||||
oportunidadId: string
|
||||
valores: Record<string, string>
|
||||
} | null>(null)
|
||||
|
||||
// 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
|
||||
// ?afluencia=<id> abre el formulario de afluencia prellenado; si no, la
|
||||
// entrada ofrece Alta Afluencia (captura ligera) y Alta de Cliente
|
||||
useEffect(() => {
|
||||
const afl = paramsAfluencia()
|
||||
if (afl) {
|
||||
setAfluenciaPrevia(afl)
|
||||
setPantalla('afluencia')
|
||||
return
|
||||
}
|
||||
const cotizacionId = paramCotizacion()
|
||||
if (cotizacionId) {
|
||||
obtenerCotizacion(cotizacionId)
|
||||
@@ -335,7 +365,12 @@ export default function AltaClientePage() {
|
||||
</p>
|
||||
<div className="mt-6 rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm">
|
||||
<AltaAfluenciaForm
|
||||
onVolver={() => setPantalla('entrada')}
|
||||
valoresIniciales={afluenciaPrevia?.valores}
|
||||
oportunidadId={afluenciaPrevia?.oportunidadId}
|
||||
onVolver={() => {
|
||||
setAfluenciaPrevia(null)
|
||||
setPantalla('entrada')
|
||||
}}
|
||||
onPasarAAltaCliente={empezarDesdeLead}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -248,6 +248,8 @@ export interface LeadCreado {
|
||||
oportunidadId: string | null
|
||||
/** 'capturista' = asignado a quien lo registró; 'round-robin' = entra al reparto */
|
||||
asignacion?: 'capturista' | 'round-robin'
|
||||
/** true cuando se actualizó una oportunidad existente (vino de Mis oportunidades) */
|
||||
actualizada?: boolean
|
||||
}
|
||||
|
||||
/** Resultado del alta de afluencia: distingue duplicado (409) de fallo de red */
|
||||
@@ -269,6 +271,8 @@ export async function crearAfluencia(payload: {
|
||||
origen?: string
|
||||
desarrollo?: string
|
||||
notas?: string
|
||||
/** Oportunidad de Monday a actualizar (botón "Afluencia" de Mi día) */
|
||||
oportunidadId?: string
|
||||
modelo?: string
|
||||
medioContacto?: string
|
||||
edad?: string
|
||||
|
||||
Reference in New Issue
Block a user