feat(afluencia): el campo de interes muestra los modelos del desarrollo

'Al departamento o casa de interes' ahora es un select dependiente:
al elegir desarrollo se cargan sus modelos (MODELOS_POR_PROYECTO) y al
cambiar de desarrollo se resetea. En Monday la columna 'Tipo de
Propiedad' se renombro a 'Modelo de Interes' con los 16 modelos como
labels del status.
This commit is contained in:
2026-09-02 01:48:45 +00:00
parent 52667df041
commit 57029bdd3d
7 changed files with 49 additions and 25 deletions

View File

@@ -105,7 +105,7 @@ describe('crearLead (POST /api/leads)', () => {
const monday = mondayMock() const monday = mondayMock()
const r = await crearLead(monday, { const r = await crearLead(monday, {
...datosBase, ...datosBase,
tipoPropiedad: 'Casa', modelo: 'HIMALAYA',
medioContacto: 'WhatsApp', medioContacto: 'WhatsApp',
edad: 35, edad: 35,
estadoCivil: 'Casado(a)', estadoCivil: 'Casado(a)',
@@ -119,7 +119,7 @@ describe('crearLead (POST /api/leads)', () => {
}) })
expect(r).toEqual({ id: 'lead-nuevo-1', oportunidadId: null }) expect(r).toEqual({ id: 'lead-nuevo-1', oportunidadId: null })
expect(monday.leadsCreados[0]).toMatchObject({ expect(monday.leadsCreados[0]).toMatchObject({
tipoPropiedadLabel: 'Casa', modeloLabel: 'HIMALAYA',
medioContacto: 'WhatsApp', medioContacto: 'WhatsApp',
edad: 35, edad: 35,
estadoCivilLabel: 'Casado(a)', estadoCivilLabel: 'Casado(a)',

View File

@@ -24,7 +24,7 @@ const crearLeadSchema = z.object({
notas: z.string().trim().min(1).optional(), notas: z.string().trim().min(1).optional(),
// Campos de calificación de la afluencia (opcionales; los labels fijos del // Campos de calificación de la afluencia (opcionales; los labels fijos del
// front coinciden con los sembrados en Monday, no se validan contra el board) // front coinciden con los sembrados en Monday, no se validan contra el board)
tipoPropiedad: z.string().trim().min(1).optional(), modelo: z.string().trim().min(1).optional(),
medioContacto: z.string().trim().min(1).optional(), medioContacto: z.string().trim().min(1).optional(),
edad: z.coerce.number().int().min(15).max(120).optional(), edad: z.coerce.number().int().min(15).max(120).optional(),
estadoCivil: z.string().trim().min(1).optional(), estadoCivil: z.string().trim().min(1).optional(),
@@ -125,7 +125,7 @@ export async function crearLead(
origenLabel: datos.origen, origenLabel: datos.origen,
desarrolloLabel: datos.desarrollo, desarrolloLabel: datos.desarrollo,
notas: datos.notas, notas: datos.notas,
tipoPropiedadLabel: datos.tipoPropiedad, modeloLabel: datos.modelo,
medioContacto: datos.medioContacto, medioContacto: datos.medioContacto,
edad: datos.edad, edad: datos.edad,
estadoCivilLabel: datos.estadoCivil, estadoCivilLabel: datos.estadoCivil,

View File

@@ -882,7 +882,7 @@ export class MondayService {
Origen: datos.origenLabel, Origen: datos.origenLabel,
Desarrollo: datos.desarrolloLabel, Desarrollo: datos.desarrolloLabel,
Notas: datos.notas, Notas: datos.notas,
'Tipo de Propiedad': datos.tipoPropiedadLabel, 'Modelo de Interés': datos.modeloLabel,
'Medio de Contacto': datos.medioContacto, 'Medio de Contacto': datos.medioContacto,
Edad: datos.edad, Edad: datos.edad,
'Estado Civil': datos.estadoCivilLabel, 'Estado Civil': datos.estadoCivilLabel,
@@ -1095,7 +1095,7 @@ export interface DatosLead {
desarrolloLabel?: string | undefined // label real de la columna status 'Desarrollo' del board Leads desarrolloLabel?: string | undefined // label real de la columna status 'Desarrollo' del board Leads
notas?: string | undefined notas?: string | undefined
// Campos de calificación del interesado (labels reales de las columnas status del board Leads) // Campos de calificación del interesado (labels reales de las columnas status del board Leads)
tipoPropiedadLabel?: string | undefined modeloLabel?: string | undefined // label de la columna status 'Modelo de Interés' (modelo según desarrollo)
medioContacto?: string | undefined medioContacto?: string | undefined
edad?: number | undefined edad?: number | undefined
estadoCivilLabel?: string | undefined estadoCivilLabel?: string | undefined

View File

@@ -71,7 +71,19 @@ describe('AltaAfluenciaForm', () => {
const opcionesDe = (label: RegExp) => const opcionesDe = (label: RegExp) =>
Array.from((screen.getByLabelText(label) as HTMLSelectElement).options).map((o) => o.value) Array.from((screen.getByLabelText(label) as HTMLSelectElement).options).map((o) => o.value)
expect(opcionesDe(/Departamento o casa de interés/)).toEqual(['', 'Casa', 'Departamento']) // El select de modelo arranca deshabilitado hasta elegir desarrollo
const selectModelo = screen.getByLabelText(/Departamento o casa de interés/) as HTMLSelectElement
expect(selectModelo.disabled).toBe(true)
fireEvent.change(screen.getByLabelText(/Desarrollo de interés/), { target: { value: 'SENDA' } })
expect(selectModelo.disabled).toBe(false)
expect(opcionesDe(/Departamento o casa de interés/)).toEqual([
'',
'HIMALAYA',
'SANTIAGO',
'ESMERALDA PA',
'ESMERALDA PB',
'SANTA CRUZ',
])
expect(opcionesDe(/^Estado civil/)).toEqual(['', 'Soltero(a)', 'Casado(a)', 'Unión libre', 'Divorciado(a)', 'Viudo(a)']) expect(opcionesDe(/^Estado civil/)).toEqual(['', 'Soltero(a)', 'Casado(a)', 'Unión libre', 'Divorciado(a)', 'Viudo(a)'])
expect(opcionesDe(/Tipo de empleo/)).toEqual(['', 'Asalariado', 'Independiente']) expect(opcionesDe(/Tipo de empleo/)).toEqual(['', 'Asalariado', 'Independiente'])
expect(opcionesDe(/^Crédito/)).toEqual([ expect(opcionesDe(/^Crédito/)).toEqual([
@@ -93,7 +105,8 @@ describe('AltaAfluenciaForm', () => {
fetchMock.mockResolvedValueOnce(respuesta({ id: 'l-1', estado: 'NUEVO', oportunidadId: 'op-1' })) fetchMock.mockResolvedValueOnce(respuesta({ id: 'l-1', estado: 'NUEVO', oportunidadId: 'op-1' }))
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />) render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />)
llenarRequeridos() llenarRequeridos()
fireEvent.change(screen.getByLabelText(/Departamento o casa de interés/), { target: { value: 'Casa' } }) fireEvent.change(screen.getByLabelText(/Desarrollo de interés/), { target: { value: 'SENDA' } })
fireEvent.change(screen.getByLabelText(/Departamento o casa de interés/), { target: { value: 'HIMALAYA' } })
fireEvent.change(screen.getByLabelText(/Medio de contacto/), { target: { value: 'Llamada' } }) fireEvent.change(screen.getByLabelText(/Medio de contacto/), { target: { value: 'Llamada' } })
fireEvent.change(screen.getByLabelText(/^Edad/), { target: { value: '35' } }) fireEvent.change(screen.getByLabelText(/^Edad/), { target: { value: '35' } })
fireEvent.change(screen.getByLabelText(/^Estado civil/), { target: { value: 'Casado(a)' } }) fireEvent.change(screen.getByLabelText(/^Estado civil/), { target: { value: 'Casado(a)' } })
@@ -109,7 +122,7 @@ describe('AltaAfluenciaForm', () => {
await screen.findByRole('status') await screen.findByRole('status')
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit] const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]
expect(JSON.parse(String(init.body))).toMatchObject({ expect(JSON.parse(String(init.body))).toMatchObject({
tipoPropiedad: 'Casa', modelo: 'HIMALAYA',
medioContacto: 'Llamada', medioContacto: 'Llamada',
edad: '35', edad: '35',
estadoCivil: 'Casado(a)', estadoCivil: 'Casado(a)',

View File

@@ -4,12 +4,12 @@ import { crearLead, type LeadResumen } from '../services/api'
import { import {
DESARROLLOS_LEAD, DESARROLLOS_LEAD,
ESTADOS_CIVILES, ESTADOS_CIVILES,
MODELOS_POR_PROYECTO,
OPCIONES_CALIFICA, OPCIONES_CALIFICA,
ORIGENES_LEAD, ORIGENES_LEAD,
TIPOS_CREDITO, TIPOS_CREDITO,
TIPOS_ECONOMIA, TIPOS_ECONOMIA,
TIPOS_EMPLEO, TIPOS_EMPLEO,
TIPOS_PROPIEDAD_LEAD,
} from '../data/constantes' } from '../data/constantes'
interface Props { interface Props {
@@ -38,7 +38,7 @@ const FORM_INICIAL = {
email: '', email: '',
origen: '', origen: '',
desarrollo: '', desarrollo: '',
tipoPropiedad: '', modelo: '',
medioContacto: '', medioContacto: '',
edad: '', edad: '',
estadoCivil: '', estadoCivil: '',
@@ -69,6 +69,14 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente }: Pro
setErrores((prev) => ({ ...prev, [campo]: undefined })) 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 validar = (): boolean => {
const nuevos: Errores = {} const nuevos: Errores = {}
if (!form.nombre.trim()) nuevos.nombre = 'El nombre es obligatorio' if (!form.nombre.trim()) nuevos.nombre = 'El nombre es obligatorio'
@@ -91,7 +99,7 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente }: Pro
...(form.email.trim() ? { email: form.email.trim() } : {}), ...(form.email.trim() ? { email: form.email.trim() } : {}),
...(form.origen ? { origen: form.origen } : {}), ...(form.origen ? { origen: form.origen } : {}),
...(form.desarrollo ? { desarrollo: form.desarrollo } : {}), ...(form.desarrollo ? { desarrollo: form.desarrollo } : {}),
...(form.tipoPropiedad ? { tipoPropiedad: form.tipoPropiedad } : {}), ...(form.modelo ? { modelo: form.modelo } : {}),
...(form.medioContacto.trim() ? { medioContacto: form.medioContacto.trim() } : {}), ...(form.medioContacto.trim() ? { medioContacto: form.medioContacto.trim() } : {}),
...(form.edad.trim() ? { edad: form.edad.trim() } : {}), ...(form.edad.trim() ? { edad: form.edad.trim() } : {}),
...(form.estadoCivil ? { estadoCivil: form.estadoCivil } : {}), ...(form.estadoCivil ? { estadoCivil: form.estadoCivil } : {}),
@@ -212,7 +220,7 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente }: Pro
<select <select
id="afluencia-desarrollo" id="afluencia-desarrollo"
value={form.desarrollo} value={form.desarrollo}
onChange={cambiar('desarrollo')} onChange={cambiarDesarrollo}
className={inputClase} className={inputClase}
> >
<option value="">Sin especificar</option> <option value="">Sin especificar</option>
@@ -224,18 +232,21 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente }: Pro
</select> </select>
</label> </label>
<label htmlFor="afluencia-tipo-propiedad" className="block"> <label htmlFor="afluencia-modelo" className="block">
<span className={labelClase}>Departamento o casa de interés</span> <span className={labelClase}>Departamento o casa de interés</span>
<select <select
id="afluencia-tipo-propiedad" id="afluencia-modelo"
value={form.tipoPropiedad} value={form.modelo}
onChange={cambiar('tipoPropiedad')} onChange={cambiar('modelo')}
className={inputClase} disabled={form.desarrollo === ''}
className={`${inputClase} disabled:bg-neutral-100 disabled:text-neutral-400`}
> >
<option value="">— Selecciona —</option> <option value="">
{TIPOS_PROPIEDAD_LEAD.map((t) => ( {form.desarrollo === '' ? 'Selecciona primero el desarrollo' : '— Selecciona —'}
<option key={t} value={t}> </option>
{t} {modelosDelDesarrollo.map((m) => (
<option key={m} value={m}>
{m}
</option> </option>
))} ))}
</select> </select>

View File

@@ -97,8 +97,8 @@ export const TAMANO_MAX_DOCUMENTO = 10 * 1024 * 1024 // 10 MB
export const ORIGENES_LEAD = ['WhatsApp', 'Web', 'Recomendado', 'Facebook', 'Otro'] as const export const ORIGENES_LEAD = ['WhatsApp', 'Web', 'Recomendado', 'Facebook', 'Otro'] as const
export const DESARROLLOS_LEAD = ['ATELIER', 'DIAGONAL', 'ESTANZA', 'SENDA', 'SOLANA'] as const export const DESARROLLOS_LEAD = ['ATELIER', 'DIAGONAL', 'ESTANZA', 'SENDA', 'SOLANA'] as const
// Labels de las columnas status nuevas del tablero Leads (captura de afluencia) // Label de la columna status 'Califica' del tablero Leads (captura de afluencia).
export const TIPOS_PROPIEDAD_LEAD = ['Casa', 'Departamento'] as const // El modelo de interés sale de MODELOS_POR_PROYECTO según el desarrollo elegido.
export const OPCIONES_CALIFICA = ['Sí', 'No', 'Por confirmar'] as const export const OPCIONES_CALIFICA = ['Sí', 'No', 'Por confirmar'] as const
export const ASESOR_DEFAULT = 'ESMERALDA BECERRA DOMINGUEZ' export const ASESOR_DEFAULT = 'ESMERALDA BECERRA DOMINGUEZ'

View File

@@ -259,7 +259,7 @@ export async function crearLead(payload: {
origen?: string origen?: string
desarrollo?: string desarrollo?: string
notas?: string notas?: string
tipoPropiedad?: string modelo?: string
medioContacto?: string medioContacto?: string
edad?: string edad?: string
estadoCivil?: string estadoCivil?: string