feat: Interesado en leads, lada MX/USA, orígenes FRASA y subcategoría de descarte
- Mis leads: botón Interesado (label sembrado en Monday, mueve a Leads Progreso); el botón Agenda cita ahora solo vive en Mis oportunidades - Descarte: además de la categoría, subcategoría del catálogo de Monday (workspace de referencia CRM FRASA); se guarda en la columna Subcategoria del tablero Motivos No Compra y el item queda en el grupo de su categoría - Teléfonos con selector de lada 🇲🇽 +52 / 🇺🇸 +1 en captura rápida (Mi día), Alta Afluencia y Alta de Cliente (tel. casa y celular); Monday escribe countryShortName MX/US y el número compacto (rechaza espacios); el dedup sigue comparando por los últimos 10 dígitos - Origen nuevo en captura rápida y afluencia: PASE, PROSPECCION ASESOR, ACTIVACION, FACEBOOK FRASA, REFERIDO, OTRO (sembrados en Leads.Origen y Oportunidades.Origen del Lead) - Tests: 461 API + 16 mi-dia + 116 alta-clientes, verde
This commit is contained in:
@@ -13,10 +13,14 @@ import {
|
||||
MOTIVOS_NO_COMPRA,
|
||||
ORIGENES_LEAD,
|
||||
SLOTS_DOCS_APARTADO,
|
||||
SUBCATEGORIAS_NO_COMPRA,
|
||||
componerTelefono,
|
||||
LADAS,
|
||||
textoDiasRestantes,
|
||||
urlAfluenciaParaOportunidad,
|
||||
urlCotizadorParaOportunidad,
|
||||
type ApartadoMiDia,
|
||||
type ClaveLada,
|
||||
type FaseApartado,
|
||||
type LeadMiDia,
|
||||
type MotivoNoCompra,
|
||||
@@ -57,7 +61,7 @@ const CLASE_INPUT =
|
||||
|
||||
const CLASE_LABEL = 'mb-2 block text-xs font-medium text-[#4B5563]'
|
||||
|
||||
/** Modal de descarte: motivo (dropdown CRM) + comentario opcional */
|
||||
/** Modal de descarte: motivo + subcategoría (catálogo Monday) + comentario opcional */
|
||||
function ModalDescartar({
|
||||
lead,
|
||||
onCancelar,
|
||||
@@ -66,11 +70,13 @@ function ModalDescartar({
|
||||
}: {
|
||||
lead: { id: string; nombre: string }
|
||||
onCancelar: () => void
|
||||
onConfirmar: (motivo: MotivoNoCompra, comentario: string) => void
|
||||
onConfirmar: (motivo: MotivoNoCompra, subcategoria: string | undefined, comentario: string) => void
|
||||
enviando: boolean
|
||||
}) {
|
||||
const [motivo, setMotivo] = useState<MotivoNoCompra>(MOTIVOS_NO_COMPRA[0])
|
||||
const [subcategoria, setSubcategoria] = useState('')
|
||||
const [comentario, setComentario] = useState('')
|
||||
const subcategorias = SUBCATEGORIAS_NO_COMPRA[motivo]
|
||||
return (
|
||||
<div className="fixed inset-0 z-10 flex items-center justify-center bg-black/40 px-4" role="dialog" aria-modal="true">
|
||||
<div className="w-full max-w-md rounded-2xl border border-[#E5E7EB] bg-white p-6 shadow-xl">
|
||||
@@ -80,7 +86,7 @@ function ModalDescartar({
|
||||
className="mt-6 space-y-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
onConfirmar(motivo, comentario.trim())
|
||||
onConfirmar(motivo, subcategoria === '' ? undefined : subcategoria, comentario.trim())
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
@@ -90,7 +96,10 @@ function ModalDescartar({
|
||||
<select
|
||||
id="motivo-descarte"
|
||||
value={motivo}
|
||||
onChange={(e) => setMotivo(e.target.value as MotivoNoCompra)}
|
||||
onChange={(e) => {
|
||||
setMotivo(e.target.value as MotivoNoCompra)
|
||||
setSubcategoria('') // las subcategorías dependen de la categoría
|
||||
}}
|
||||
className={CLASE_INPUT}
|
||||
>
|
||||
{MOTIVOS_NO_COMPRA.map((m) => (
|
||||
@@ -100,6 +109,24 @@ function ModalDescartar({
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="subcategoria-descarte" className={CLASE_LABEL}>
|
||||
Subcategoría
|
||||
</label>
|
||||
<select
|
||||
id="subcategoria-descarte"
|
||||
value={subcategoria}
|
||||
onChange={(e) => setSubcategoria(e.target.value)}
|
||||
className={CLASE_INPUT}
|
||||
>
|
||||
<option value="">— Selecciona —</option>
|
||||
{subcategorias.map((s) => (
|
||||
<option key={s} value={s}>
|
||||
{s}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="comentario-descarte" className={CLASE_LABEL}>
|
||||
Comentario (opcional)
|
||||
@@ -452,7 +479,8 @@ function TabsCrm({
|
||||
)
|
||||
}
|
||||
|
||||
/** Fila de lead con acciones rápidas (contactado / intento / cita / descartar) */
|
||||
/** Fila de lead con acciones rápidas (contactado / intento / interesado / descartar).
|
||||
* Agendar cita ya no está aquí: la cita se agenda desde Mis oportunidades. */
|
||||
function FilaLead({
|
||||
lead,
|
||||
onAccion,
|
||||
@@ -464,8 +492,6 @@ function FilaLead({
|
||||
onDescartar: () => void
|
||||
enviando: boolean
|
||||
}) {
|
||||
const [conCita, setConCita] = useState(false)
|
||||
const [fechaCita, setFechaCita] = useState('')
|
||||
return (
|
||||
<li className="rounded-xl border border-[#E5E7EB] bg-white p-4 shadow-sm">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
@@ -505,23 +531,9 @@ function FilaLead({
|
||||
<button type="button" disabled={enviando} onClick={() => onAccion('Intento de contacto')} className={CLASE_BOTON_ACCION}>
|
||||
Intento
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={enviando}
|
||||
onClick={() => (conCita ? onAccion('AGENDA CITA', fechaCita || undefined) : setConCita(true))}
|
||||
className={CLASE_BOTON_ACCION}
|
||||
>
|
||||
{conCita ? 'Confirmar cita' : 'Agenda cita'}
|
||||
<button type="button" disabled={enviando} onClick={() => onAccion('INTERESADO')} className={CLASE_BOTON_ACCION}>
|
||||
Interesado
|
||||
</button>
|
||||
{conCita && (
|
||||
<input
|
||||
type="date"
|
||||
aria-label="Fecha de la cita"
|
||||
value={fechaCita}
|
||||
onChange={(e) => setFechaCita(e.target.value)}
|
||||
className="rounded-lg border border-[#D1D5DB] bg-white px-2 py-1.5 text-xs transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
disabled={enviando}
|
||||
@@ -552,6 +564,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
||||
|
||||
// Captura rápida
|
||||
const [capNombre, setCapNombre] = useState('')
|
||||
const [capLada, setCapLada] = useState<ClaveLada>('MX')
|
||||
const [capTelefono, setCapTelefono] = useState('')
|
||||
const [capEmail, setCapEmail] = useState('')
|
||||
const [capOrigen, setCapOrigen] = useState<string>(ORIGENES_LEAD[0])
|
||||
@@ -612,9 +625,10 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
||||
setCapEnviando(true)
|
||||
setCapMensaje(null)
|
||||
try {
|
||||
const telefono = componerTelefono(capLada, capTelefono)
|
||||
const creado = await crearLead({
|
||||
nombre: capNombre.trim(),
|
||||
...(capTelefono.trim() !== '' ? { telefono: capTelefono.trim() } : {}),
|
||||
...(telefono !== '' ? { telefono } : {}),
|
||||
...(capEmail.trim() !== '' ? { email: capEmail.trim() } : {}),
|
||||
...(capOrigen !== '' ? { origen: capOrigen } : {}),
|
||||
...(capDesarrollo !== '' ? { desarrollo: capDesarrollo } : {}),
|
||||
@@ -965,13 +979,28 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
||||
<label htmlFor="cap-telefono" className={CLASE_LABEL}>
|
||||
Teléfono
|
||||
</label>
|
||||
<input
|
||||
id="cap-telefono"
|
||||
type="tel"
|
||||
value={capTelefono}
|
||||
onChange={(e) => setCapTelefono(e.target.value)}
|
||||
className={CLASE_INPUT}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
aria-label="Lada"
|
||||
value={capLada}
|
||||
onChange={(e) => setCapLada(e.target.value as ClaveLada)}
|
||||
className="w-28 shrink-0 rounded-lg border border-[#D1D5DB] bg-white px-2 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
|
||||
>
|
||||
{LADAS.map((l) => (
|
||||
<option key={l.clave} value={l.clave}>
|
||||
{l.etiqueta}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
id="cap-telefono"
|
||||
type="tel"
|
||||
placeholder="664 123 4567"
|
||||
value={capTelefono}
|
||||
onChange={(e) => setCapTelefono(e.target.value)}
|
||||
className={CLASE_INPUT}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="cap-email" className={CLASE_LABEL}>
|
||||
@@ -1092,11 +1121,11 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
||||
lead={descartando}
|
||||
enviando={enviando}
|
||||
onCancelar={() => setDescartando(null)}
|
||||
onConfirmar={(motivo, comentario) => {
|
||||
onConfirmar={(motivo, subcategoria, comentario) => {
|
||||
const lead = descartando
|
||||
setDescartando(null)
|
||||
void ejecutar(
|
||||
() => descartarLead(lead.id, motivo, comentario === '' ? undefined : comentario, lead.fuente),
|
||||
() => descartarLead(lead.id, motivo, comentario === '' ? undefined : comentario, lead.fuente, subcategoria),
|
||||
'No se pudo descartar el lead',
|
||||
)
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user