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:
@@ -48,7 +48,7 @@ describe('AltaAfluenciaForm', () => {
|
||||
fetchMock.mockResolvedValueOnce(respuesta({ id: 'l-1', estado: 'NUEVO', oportunidadId: 'op-1' }))
|
||||
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />)
|
||||
llenarRequeridos()
|
||||
fireEvent.change(screen.getByLabelText(/¿Cómo se enteró de nosotros\?/), { target: { value: 'Web' } })
|
||||
fireEvent.change(screen.getByLabelText(/¿Cómo se enteró de nosotros\?/), { target: { value: 'PASE' } })
|
||||
fireEvent.change(screen.getByLabelText(/Desarrollo de interés/), { target: { value: 'SENDA' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
|
||||
|
||||
@@ -59,8 +59,8 @@ describe('AltaAfluenciaForm', () => {
|
||||
expect(url).toContain('/afluencias')
|
||||
expect(JSON.parse(String(init.body))).toMatchObject({
|
||||
nombre: 'Ana Demo',
|
||||
telefono: '6641234567',
|
||||
origen: 'Web',
|
||||
telefono: '+52 6641234567',
|
||||
origen: 'PASE',
|
||||
desarrollo: 'SENDA',
|
||||
})
|
||||
})
|
||||
@@ -158,7 +158,7 @@ describe('AltaAfluenciaForm', () => {
|
||||
await screen.findByRole('status')
|
||||
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]
|
||||
const cuerpo = JSON.parse(String(init.body)) as Record<string, unknown>
|
||||
expect(cuerpo).toEqual({ nombre: 'Ana Demo', telefono: '6641234567' })
|
||||
expect(cuerpo).toEqual({ nombre: 'Ana Demo', telefono: '+52 6641234567' })
|
||||
})
|
||||
|
||||
it('con 409 muestra el duplicado con el nombre del lead existente', async () => {
|
||||
@@ -218,7 +218,7 @@ describe('AltaAfluenciaForm', () => {
|
||||
expect(onPasar).toHaveBeenCalledWith({
|
||||
id: 'op-1',
|
||||
nombre: 'Ana Demo',
|
||||
telefono: '6641234567',
|
||||
telefono: '+52 6641234567',
|
||||
email: 'ana@correo.com',
|
||||
estado: 'Afluencia',
|
||||
origen: '',
|
||||
|
||||
@@ -64,6 +64,9 @@ const FORM_INICIAL = {
|
||||
*/
|
||||
export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valoresIniciales, oportunidadId }: Props) {
|
||||
const [form, setForm] = useState({ ...FORM_INICIAL, ...valoresIniciales })
|
||||
const [lada, setLada] = useState<'MX' | 'US'>(
|
||||
(valoresIniciales?.telefono ?? '').trim().startsWith('+1') ? 'US' : 'MX',
|
||||
)
|
||||
const [errores, setErrores] = useState<Errores>({})
|
||||
const [enviando, setEnviando] = useState(false)
|
||||
const [leadCreado, setLeadCreado] = useState<LeadResumen | null>(null)
|
||||
@@ -89,6 +92,11 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valor
|
||||
* lead existe entre las opciones del formulario.
|
||||
*/
|
||||
const prellenarDesdeLead = (lead: LeadResumen) => {
|
||||
// Teléfono con prefijo internacional: separa lada y número para el selector
|
||||
const tel = lead.telefono.trim()
|
||||
const esUsa = tel.startsWith('+1')
|
||||
const numero = tel.startsWith('+1') ? tel.slice(2).trim() : tel.startsWith('+52') ? tel.slice(3).trim() : tel
|
||||
setLada(esUsa ? 'US' : 'MX')
|
||||
setForm((prev) => {
|
||||
const desarrollo =
|
||||
lead.desarrollo !== undefined && (DESARROLLOS_LEAD as readonly string[]).includes(lead.desarrollo)
|
||||
@@ -102,7 +110,7 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valor
|
||||
return {
|
||||
...prev,
|
||||
nombre: lead.nombre,
|
||||
telefono: lead.telefono,
|
||||
telefono: numero,
|
||||
email: lead.email,
|
||||
origen: (ORIGENES_LEAD as readonly string[]).includes(lead.origen) ? lead.origen : prev.origen,
|
||||
desarrollo,
|
||||
@@ -129,9 +137,13 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valor
|
||||
setAviso('')
|
||||
if (!validar()) return
|
||||
setEnviando(true)
|
||||
// Teléfono con lada: "<prefijo> <número>" (si ya trae +, se respeta tal cual)
|
||||
const numeroTel = form.telefono.trim()
|
||||
const telefono =
|
||||
numeroTel === '' ? '' : numeroTel.startsWith('+') ? numeroTel : `${lada === 'US' ? '+1' : '+52'} ${numeroTel}`
|
||||
const res = await crearAfluencia({
|
||||
nombre: form.nombre.trim(),
|
||||
telefono: form.telefono.trim(),
|
||||
telefono,
|
||||
...(oportunidadId !== undefined ? { oportunidadId } : {}),
|
||||
...(form.email.trim() ? { email: form.email.trim() } : {}),
|
||||
...(form.origen ? { origen: form.origen } : {}),
|
||||
@@ -154,7 +166,7 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valor
|
||||
setLeadCreado({
|
||||
id: res.lead.id,
|
||||
nombre: form.nombre.trim(),
|
||||
telefono: form.telefono.trim(),
|
||||
telefono,
|
||||
email: form.email.trim(),
|
||||
estado: res.lead.estado,
|
||||
origen: form.origen,
|
||||
@@ -170,6 +182,7 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valor
|
||||
|
||||
const capturarOtro = () => {
|
||||
setForm(FORM_INICIAL)
|
||||
setLada('MX')
|
||||
setErrores({})
|
||||
setAviso('')
|
||||
setLeadCreado(null)
|
||||
@@ -251,19 +264,31 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valor
|
||||
{errores.nombre && <p className="mt-1 text-xs text-red-700">{errores.nombre}</p>}
|
||||
</label>
|
||||
|
||||
<label htmlFor="afluencia-telefono" className="block">
|
||||
<span className={labelClase}>
|
||||
<div>
|
||||
<label htmlFor="afluencia-telefono" 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' : ''}`}
|
||||
/>
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
aria-label="Lada del teléfono"
|
||||
value={lada}
|
||||
onChange={(e) => setLada(e.target.value as 'MX' | 'US')}
|
||||
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"
|
||||
>
|
||||
<option value="MX">🇲🇽 +52</option>
|
||||
<option value="US">🇺🇸 +1</option>
|
||||
</select>
|
||||
<input
|
||||
id="afluencia-telefono"
|
||||
type="tel"
|
||||
placeholder="664 123 4567"
|
||||
value={form.telefono}
|
||||
onChange={cambiar('telefono')}
|
||||
className={`${inputClase} ${errores.telefono ? 'border-red-400' : ''}`}
|
||||
/>
|
||||
</div>
|
||||
{errores.telefono && <p className="mt-1 text-xs text-red-700">{errores.telefono}</p>}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label htmlFor="afluencia-email" className="block">
|
||||
<span className={labelClase}>Correo</span>
|
||||
|
||||
@@ -89,6 +89,7 @@ interface CampoSelectProps extends CampoBaseProps {
|
||||
opciones: { valor: string; etiqueta: string }[]
|
||||
}
|
||||
|
||||
/** CampoSelect (dropdown) conectado a react-hook-form */
|
||||
export function CampoSelect({ nombre, etiqueta, opciones, requerido, className }: CampoSelectProps) {
|
||||
const { register, formState: { errors } } = useFormContext()
|
||||
return (
|
||||
@@ -106,3 +107,50 @@ export function CampoSelect({ nombre, etiqueta, opciones, requerido, className }
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Teléfono con selector de lada (México +52 / USA +1). El campo del formulario
|
||||
* guarda el valor compuesto "<prefijo> <número>" (p. ej. "+52 6641234567");
|
||||
* el dedup del API compara por los últimos 10 dígitos, así que no afecta.
|
||||
*/
|
||||
export function CampoTelefono({ nombre, etiqueta, requerido, className }: CampoBaseProps) {
|
||||
const { watch, setValue, formState: { errors } } = useFormContext()
|
||||
const valor = ((watch(nombre) as string) ?? '').trim()
|
||||
const lada = valor.startsWith('+1') ? '+1' : '+52'
|
||||
const numero = valor.startsWith('+1') ? valor.slice(2).trim() : valor.startsWith('+52') ? valor.slice(3).trim() : valor
|
||||
|
||||
const actualizar = (nuevaLada: string, nuevoNumero: string) => {
|
||||
const limpio = nuevoNumero.trim()
|
||||
// Vacío se guarda vacío (sin prefijo huérfano); con número, compuesto
|
||||
setValue(nombre, limpio === '' ? '' : `${nuevaLada} ${limpio}`, { shouldValidate: true })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className ?? ''}>
|
||||
<label htmlFor={nombre}>
|
||||
<Etiqueta etiqueta={etiqueta} requerido={requerido} />
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
id={`${nombre}-lada`}
|
||||
aria-label="Lada"
|
||||
value={lada}
|
||||
onChange={(e) => actualizar(e.target.value, numero)}
|
||||
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"
|
||||
>
|
||||
<option value="+52">🇲🇽 +52</option>
|
||||
<option value="+1">🇺🇸 +1</option>
|
||||
</select>
|
||||
<input
|
||||
id={nombre}
|
||||
type="tel"
|
||||
placeholder="664 123 4567"
|
||||
value={numero}
|
||||
onChange={(e) => actualizar(lada, e.target.value)}
|
||||
className={claseInput(nombre, errors)}
|
||||
/>
|
||||
</div>
|
||||
<MensajeError nombre={nombre} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -93,8 +93,9 @@ export const SLOTS_DOCUMENTOS: { slot: SlotDocumento; etiqueta: string }[] = [
|
||||
|
||||
export const TAMANO_MAX_DOCUMENTO = 10 * 1024 * 1024 // 10 MB
|
||||
|
||||
// Catálogos del tablero Leads de Monday (labels exactos, verificados ago-2026)
|
||||
export const ORIGENES_LEAD = ['WhatsApp', 'Web', 'Recomendado', 'Facebook', 'Otro'] as const
|
||||
// Catálogos de Monday (labels exactos). Origen: vocabulario FRASA sembrado en
|
||||
// los tableros Leads y Oportunidades el 2026-09-04 (reemplaza al set anterior)
|
||||
export const ORIGENES_LEAD = ['PASE', 'PROSPECCION ASESOR', 'ACTIVACION', 'FACEBOOK FRASA', 'REFERIDO', 'OTRO'] as const
|
||||
export const DESARROLLOS_LEAD = ['ATELIER', 'DIAGONAL', 'ESTANZA', 'SENDA', 'SOLANA'] as const
|
||||
|
||||
// Label de la columna status 'Califica' del tablero Leads (captura de afluencia).
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
import { CampoTexto, CampoSelect } from '../components/Campos'
|
||||
import { CampoTexto, CampoSelect, CampoTelefono } from '../components/Campos'
|
||||
import SectionCard from '../components/SectionCard'
|
||||
import { compararConDatos } from '../lib/curp'
|
||||
import { ESTADOS_CIVILES, REGIMENES_BIENES, TIPOS_VIVIENDA, TIPOS_COMPRA } from '../data/constantes'
|
||||
@@ -52,8 +52,8 @@ export default function DatosGeneralesCampos({ prefix, compacto = false }: Props
|
||||
<CampoTexto nombre={p('peso')} etiqueta="Peso (kg)" />
|
||||
<CampoTexto nombre={p('estatura')} etiqueta="Estatura (m)" />
|
||||
<CampoTexto nombre={p('email')} etiqueta="Email" tipo="email" requerido={!compacto} />
|
||||
<CampoTexto nombre={p('telCasa')} etiqueta="Tel. casa" />
|
||||
<CampoTexto nombre={p('celular')} etiqueta="Celular" requerido={!compacto} />
|
||||
<CampoTelefono nombre={p('telCasa')} etiqueta="Tel. casa" />
|
||||
<CampoTelefono nombre={p('celular')} etiqueta="Celular" requerido={!compacto} />
|
||||
<CampoTexto nombre={p('viviendaAnios')} etiqueta="Años en vivienda" />
|
||||
<CampoSelect nombre={p('tipoVivienda')} etiqueta="Tipo de vivienda" opciones={TIPOS_VIVIENDA} />
|
||||
<CampoSelect nombre={p('tipoCompra')} etiqueta="Tipo de compra" opciones={TIPOS_COMPRA} />
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
} from './mi-dia'
|
||||
|
||||
describe('catálogos', () => {
|
||||
it('orígenes = labels reales de la columna Origen del board Leads', () => {
|
||||
expect([...ORIGENES_LEAD]).toEqual(['WhatsApp', 'Web', 'Recomendado', 'Facebook', 'Otro'])
|
||||
it('orígenes = vocabulario FRASA sembrado en Monday (Leads y Oportunidades)', () => {
|
||||
expect([...ORIGENES_LEAD]).toEqual(['PASE', 'PROSPECCION ASESOR', 'ACTIVACION', 'FACEBOOK FRASA', 'REFERIDO', 'OTRO'])
|
||||
})
|
||||
|
||||
it('desarrollos = labels reales de la columna Desarrollo del board Leads', () => {
|
||||
|
||||
@@ -100,8 +100,9 @@ export interface ResumenMiDia {
|
||||
}
|
||||
}
|
||||
|
||||
// Labels reales de la columna status 'Origen' del board Leads (verificados en Monday)
|
||||
export const ORIGENES_LEAD = ['WhatsApp', 'Web', 'Recomendado', 'Facebook', 'Otro'] as const
|
||||
// Labels de la columna 'Origen' (board Leads) / 'Origen del Lead' (Oportunidades),
|
||||
// vocabulario FRASA verificado y sembrado en Monday el 2026-09-04
|
||||
export const ORIGENES_LEAD = ['PASE', 'PROSPECCION ASESOR', 'ACTIVACION', 'FACEBOOK FRASA', 'REFERIDO', 'OTRO'] as const
|
||||
|
||||
// Labels reales de la columna status 'Desarrollo' del board Leads (verificados en Monday)
|
||||
export const DESARROLLOS_LEAD = ['ATELIER', 'DIAGONAL', 'ESTANZA', 'SENDA', 'SOLANA'] as const
|
||||
@@ -119,6 +120,39 @@ export const MOTIVOS_NO_COMPRA = [
|
||||
|
||||
export type MotivoNoCompra = (typeof MOTIVOS_NO_COMPRA)[number]
|
||||
|
||||
// Subcategorías por categoría del tablero Motivos No Compra (catálogo del
|
||||
// workspace de referencia CRM FRASA, sembrado en Frasa-CAS el 2026-09-04).
|
||||
// Espejo de SUBCATEGORIAS_NO_COMPRA del API (mantener sincronizados).
|
||||
export const SUBCATEGORIAS_NO_COMPRA: Record<MotivoNoCompra, string[]> = {
|
||||
Precio: ['Precio Total', 'Enganche', 'Mesualidad'],
|
||||
Producto: ['Distribucion', 'Tamaño', 'Recamaras', 'Baños', 'Amenidades', 'Acabados', 'Estacionamiento', 'Seguridad', 'Inventario'],
|
||||
Ubicacion: ['Zona', 'Accesos', 'Distancia', 'Servicios', 'Seguridad', 'Escuela', 'Trabajo'],
|
||||
Financiamiento: ['No Califica', 'Credito Insuficiente', 'Tasa de interes', 'Mensualidad Alta', 'Enganche Alto', 'Forma de Pago', 'Infonavit Insuficiente', 'No tiene liquidez'],
|
||||
Competencia: ['Precio', 'Producto', 'Ubicacion', 'Promocion', 'Financiamiento'],
|
||||
Cliente: ['Cambo de Planes', 'Decision Familiar', 'No estaba Listo', 'Emergencia Personal'],
|
||||
Timing: ['Compra Posterior', 'Esperando Credito', 'Esperando Venta de Propiedad', 'Esperando Proyecto'],
|
||||
}
|
||||
|
||||
// LADAs soportadas en la captura de teléfonos (México default, USA opcional)
|
||||
export const LADAS = [
|
||||
{ clave: 'MX', etiqueta: '🇲🇽 +52', prefijo: '+52' },
|
||||
{ clave: 'US', etiqueta: '🇺🇸 +1', prefijo: '+1' },
|
||||
] as const
|
||||
|
||||
export type ClaveLada = (typeof LADAS)[number]['clave']
|
||||
|
||||
/**
|
||||
* Compone el teléfono a guardar: "<prefijo> <numero>" (p. ej. "+52 4771234567").
|
||||
* Si el número ya trae prefijo internacional, se respeta tal cual.
|
||||
*/
|
||||
export function componerTelefono(lada: ClaveLada, numero: string): string {
|
||||
const limpio = numero.trim()
|
||||
if (limpio === '') return ''
|
||||
if (limpio.startsWith('+')) return limpio
|
||||
const prefijo = LADAS.find((l) => l.clave === lada)?.prefijo ?? '+52'
|
||||
return `${prefijo} ${limpio}`
|
||||
}
|
||||
|
||||
// Fases del funnel de Oportunidades (labels REALES del board; el API las valida tal cual)
|
||||
export const FASES_OPORTUNIDAD = ['Agendo Cita', 'Afluencia', 'Cotizador', 'Aparto', 'No Asistio', 'Abandono'] as const
|
||||
export type FaseOportunidad = (typeof FASES_OPORTUNIDAD)[number]
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
}}
|
||||
|
||||
@@ -53,12 +53,13 @@ export function cambiarEstadoLead(itemId: string, estado: string, fecha?: string
|
||||
})
|
||||
}
|
||||
|
||||
export function descartarLead(itemId: string, motivo: string, comentario?: string, fuente?: 'lead' | 'oportunidad'): Promise<unknown> {
|
||||
export function descartarLead(itemId: string, motivo: string, comentario?: string, fuente?: 'lead' | 'oportunidad', subcategoria?: string): Promise<unknown> {
|
||||
return fetchJson(`/leads/${itemId}/descartar`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
motivo,
|
||||
...(subcategoria !== undefined && subcategoria !== '' ? { subcategoria } : {}),
|
||||
...(comentario !== undefined && comentario !== '' ? { comentario } : {}),
|
||||
...(fuente !== undefined ? { fuente } : {}),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user