Files
FRASA-CAS/apps/alta-clientes/src/components/AltaAfluenciaForm.tsx
Consult AS Technology 599dce2629 style: reskin NovaSpark en las 7 apps
Del look editorial (verde #1B4332, esquinas a 0, botones en mayusculas)
al sistema NovaSpark: primario azul #2563EB, detalle naranja #F97316,
fondo #F8FAFC con tarjetas blancas rounded-xl + shadow-sm, chips
rounded-full semanticos, focus ring azul, transiciones 100-200ms.
Incluye headers de tabla en los PDFs (cotizador y estado de cuenta).
2026-09-03 02:35:34 +00:00

451 lines
15 KiB
TypeScript

import { useState, type FormEvent } from 'react'
import { z } from 'zod'
import { crearLead, type LeadResumen } from '../services/api'
import {
DESARROLLOS_LEAD,
ESTADOS_CIVILES,
MODELOS_POR_PROYECTO,
OPCIONES_CALIFICA,
ORIGENES_LEAD,
TIPOS_CREDITO,
TIPOS_ECONOMIA,
TIPOS_EMPLEO,
} from '../data/constantes'
interface Props {
onVolver: () => void
/** 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
}
const emailSchema = z.string().trim().email()
const inputClase =
'w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20'
const labelClase = 'mb-1 block text-xs font-medium text-[#4B5563]'
interface Errores {
nombre?: string
telefono?: string
email?: string
}
const FORM_INICIAL = {
nombre: '',
telefono: '',
email: '',
origen: '',
desarrollo: '',
modelo: '',
medioContacto: '',
edad: '',
estadoCivil: '',
tipoEmpleo: '',
ocupacion: '',
rangoSalarial: '',
credito: '',
tipoEconomia: '',
califica: '',
loQuierePara: '',
notas: '',
}
/**
* Captura ligera del interesado al entrar al embudo (POST /leads): crea el
* 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)
const [errores, setErrores] = useState<Errores>({})
const [enviando, setEnviando] = useState(false)
const [leadCreado, setLeadCreado] = useState<LeadResumen | null>(null)
const [aviso, setAviso] = useState('')
const cambiar = (campo: keyof typeof FORM_INICIAL) => (e: { target: { value: string } }) => {
setForm((prev) => ({ ...prev, [campo]: e.target.value }))
setErrores((prev) => ({ ...prev, [campo]: undefined }))
}
// Al cambiar de desarrollo se resetea el modelo (las opciones dependen de él)
const cambiarDesarrollo = (e: { target: { value: string } }) => {
setForm((prev) => ({ ...prev, desarrollo: e.target.value, modelo: '' }))
}
const modelosDelDesarrollo =
MODELOS_POR_PROYECTO[form.desarrollo as keyof typeof MODELOS_POR_PROYECTO] ?? []
const validar = (): boolean => {
const nuevos: Errores = {}
if (!form.nombre.trim()) nuevos.nombre = 'El nombre es obligatorio'
if (!form.telefono.trim()) nuevos.telefono = 'El teléfono es obligatorio'
if (form.email.trim() && !emailSchema.safeParse(form.email).success) {
nuevos.email = 'Correo electrónico inválido'
}
setErrores(nuevos)
return Object.keys(nuevos).length === 0
}
const enviar = async (e: FormEvent) => {
e.preventDefault()
setAviso('')
if (!validar()) return
setEnviando(true)
const res = await crearLead({
nombre: form.nombre.trim(),
telefono: form.telefono.trim(),
...(form.email.trim() ? { email: form.email.trim() } : {}),
...(form.origen ? { origen: form.origen } : {}),
...(form.desarrollo ? { desarrollo: form.desarrollo } : {}),
...(form.modelo ? { modelo: form.modelo } : {}),
...(form.medioContacto.trim() ? { medioContacto: form.medioContacto.trim() } : {}),
...(form.edad.trim() ? { edad: form.edad.trim() } : {}),
...(form.estadoCivil ? { estadoCivil: form.estadoCivil } : {}),
...(form.tipoEmpleo ? { tipoEmpleo: form.tipoEmpleo } : {}),
...(form.ocupacion.trim() ? { ocupacion: form.ocupacion.trim() } : {}),
...(form.rangoSalarial.trim() ? { rangoSalarial: form.rangoSalarial.trim() } : {}),
...(form.credito ? { credito: form.credito } : {}),
...(form.tipoEconomia ? { tipoEconomia: form.tipoEconomia } : {}),
...(form.califica ? { califica: form.califica } : {}),
...(form.loQuierePara.trim() ? { loQuierePara: form.loQuierePara.trim() } : {}),
...(form.notas.trim() ? { notas: form.notas.trim() } : {}),
})
setEnviando(false)
if (res.ok) {
setLeadCreado({
id: res.lead.id,
nombre: form.nombre.trim(),
telefono: form.telefono.trim(),
email: form.email.trim(),
estado: res.lead.estado,
origen: form.origen,
})
} else if (res.motivo === 'duplicado') {
setAviso(`Ya existe un lead con ese teléfono: ${res.nombre}.`)
} else {
setAviso('No se pudo registrar el lead. Revisa tu conexión e intenta de nuevo.')
}
}
const capturarOtro = () => {
setForm(FORM_INICIAL)
setErrores({})
setAviso('')
setLeadCreado(null)
}
if (leadCreado !== null) {
return (
<div className="max-w-md">
<p
role="status"
className="rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm font-medium text-green-700"
>
Registrado en afluencia — se asignará por round-robin
</p>
<div className="mt-6 flex flex-wrap gap-3">
<button
type="button"
onClick={() => onPasarAAltaCliente(leadCreado)}
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"
>
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>
<button
type="button"
onClick={onVolver}
className="rounded-lg border border-[#D1D5DB] bg-white px-5 py-2.5 text-sm font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
>
Volver
</button>
</div>
</div>
)
}
return (
<form onSubmit={(e) => void enviar(e)} noValidate className="max-w-2xl">
<div className="grid gap-6 sm:grid-cols-2">
<label htmlFor="afluencia-nombre" className="block sm:col-span-2">
<span className={labelClase}>
Nombre<span className="text-red-700"> *</span>
</span>
<input
id="afluencia-nombre"
type="text"
value={form.nombre}
onChange={cambiar('nombre')}
className={`${inputClase} ${errores.nombre ? 'border-red-400' : ''}`}
/>
{errores.nombre && <p className="mt-1 text-xs text-red-700">{errores.nombre}</p>}
</label>
<label htmlFor="afluencia-telefono" className="block">
<span className={labelClase}>
Teléfono<span className="text-red-700"> *</span>
</span>
<input
id="afluencia-telefono"
type="tel"
value={form.telefono}
onChange={cambiar('telefono')}
className={`${inputClase} ${errores.telefono ? 'border-red-400' : ''}`}
/>
{errores.telefono && <p className="mt-1 text-xs text-red-700">{errores.telefono}</p>}
</label>
<label htmlFor="afluencia-email" className="block">
<span className={labelClase}>Correo</span>
<input
id="afluencia-email"
type="email"
value={form.email}
onChange={cambiar('email')}
className={`${inputClase} ${errores.email ? 'border-red-400' : ''}`}
/>
{errores.email && <p className="mt-1 text-xs text-red-700">{errores.email}</p>}
</label>
<label htmlFor="afluencia-desarrollo" className="block">
<span className={labelClase}>Desarrollo de interés</span>
<select
id="afluencia-desarrollo"
value={form.desarrollo}
onChange={cambiarDesarrollo}
className={inputClase}
>
<option value="">Sin especificar</option>
{DESARROLLOS_LEAD.map((d) => (
<option key={d} value={d}>
{d}
</option>
))}
</select>
</label>
<label htmlFor="afluencia-modelo" className="block">
<span className={labelClase}>Departamento o casa de interés</span>
<select
id="afluencia-modelo"
value={form.modelo}
onChange={cambiar('modelo')}
disabled={form.desarrollo === ''}
className={`${inputClase} disabled:bg-[#F1F5F9] disabled:text-[#94A3B8]`}
>
<option value="">
{form.desarrollo === '' ? 'Selecciona primero el desarrollo' : '— Selecciona —'}
</option>
{modelosDelDesarrollo.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
</label>
<label htmlFor="afluencia-medio-contacto" className="block">
<span className={labelClase}>Medio de contacto</span>
<input
id="afluencia-medio-contacto"
type="text"
value={form.medioContacto}
onChange={cambiar('medioContacto')}
className={inputClase}
/>
</label>
<label htmlFor="afluencia-origen" className="block">
<span className={labelClase}>¿Cómo se enteró de nosotros?</span>
<select
id="afluencia-origen"
value={form.origen}
onChange={cambiar('origen')}
className={inputClase}
>
<option value="">— Selecciona —</option>
{ORIGENES_LEAD.map((o) => (
<option key={o} value={o}>
{o}
</option>
))}
</select>
</label>
<label htmlFor="afluencia-edad" className="block">
<span className={labelClase}>Edad</span>
<input
id="afluencia-edad"
type="number"
min={15}
max={120}
value={form.edad}
onChange={cambiar('edad')}
className={inputClase}
/>
</label>
<label htmlFor="afluencia-estado-civil" className="block">
<span className={labelClase}>Estado civil</span>
<select
id="afluencia-estado-civil"
value={form.estadoCivil}
onChange={cambiar('estadoCivil')}
className={inputClase}
>
<option value="">— Selecciona —</option>
{ESTADOS_CIVILES.map((e) => (
<option key={e.valor} value={e.etiqueta}>
{e.etiqueta}
</option>
))}
</select>
</label>
<label htmlFor="afluencia-tipo-empleo" className="block">
<span className={labelClase}>Tipo de empleo</span>
<select
id="afluencia-tipo-empleo"
value={form.tipoEmpleo}
onChange={cambiar('tipoEmpleo')}
className={inputClase}
>
<option value="">— Selecciona —</option>
{TIPOS_EMPLEO.map((t) => (
<option key={t.valor} value={t.etiqueta}>
{t.etiqueta}
</option>
))}
</select>
</label>
<label htmlFor="afluencia-ocupacion" className="block">
<span className={labelClase}>Ocupación</span>
<input
id="afluencia-ocupacion"
type="text"
value={form.ocupacion}
onChange={cambiar('ocupacion')}
className={inputClase}
/>
</label>
<label htmlFor="afluencia-rango-salarial" className="block">
<span className={labelClase}>Rango salarial</span>
<input
id="afluencia-rango-salarial"
type="text"
value={form.rangoSalarial}
onChange={cambiar('rangoSalarial')}
className={inputClase}
/>
</label>
<label htmlFor="afluencia-credito" className="block">
<span className={labelClase}>Crédito</span>
<select
id="afluencia-credito"
value={form.credito}
onChange={cambiar('credito')}
className={inputClase}
>
<option value="">— Selecciona —</option>
{TIPOS_CREDITO.map((c) => (
<option key={c.valor} value={c.etiqueta}>
{c.etiqueta}
</option>
))}
</select>
</label>
<label htmlFor="afluencia-tipo-economia" className="block">
<span className={labelClase}>Tipo de economía</span>
<select
id="afluencia-tipo-economia"
value={form.tipoEconomia}
onChange={cambiar('tipoEconomia')}
className={inputClase}
>
<option value="">— Selecciona —</option>
{TIPOS_ECONOMIA.map((t) => (
<option key={t.valor} value={t.etiqueta}>
{t.etiqueta}
</option>
))}
</select>
</label>
<label htmlFor="afluencia-califica" className="block">
<span className={labelClase}>Califica</span>
<select
id="afluencia-califica"
value={form.califica}
onChange={cambiar('califica')}
className={inputClase}
>
<option value="">— Selecciona —</option>
{OPCIONES_CALIFICA.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
</label>
<label htmlFor="afluencia-lo-quiere-para" className="block">
<span className={labelClase}>Lo quiere para</span>
<input
id="afluencia-lo-quiere-para"
type="text"
value={form.loQuierePara}
onChange={cambiar('loQuierePara')}
className={inputClase}
/>
</label>
<label htmlFor="afluencia-notas" className="block sm:col-span-2">
<span className={labelClase}>Notas</span>
<input
id="afluencia-notas"
type="text"
value={form.notas}
onChange={cambiar('notas')}
className={inputClase}
/>
</label>
</div>
{aviso && (
<p role="alert" className="mt-6 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-xs text-red-700">
{aviso}
</p>
)}
<div className="mt-8 flex gap-3">
<button
type="submit"
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'}
</button>
<button
type="button"
onClick={onVolver}
className="rounded-lg border border-[#D1D5DB] bg-white px-5 py-2.5 text-sm font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
>
Volver
</button>
</div>
</form>
)
}