diff --git a/api-gateway/src/controllers/leads.controller.test.ts b/api-gateway/src/controllers/leads.controller.test.ts index fac98d0..2e7d375 100644 --- a/api-gateway/src/controllers/leads.controller.test.ts +++ b/api-gateway/src/controllers/leads.controller.test.ts @@ -10,7 +10,7 @@ import { import type { LeadMonday } from '../services/monday.service.js' /** Monday mockeado: registra escrituras y devuelve catálogos controlados */ -function mondayMock(opts: { labelsOrigen?: string[]; duplicado?: LeadMonday | null } = {}) { +function mondayMock(opts: { labelsOrigen?: string[]; labelsDesarrollo?: string[]; duplicado?: LeadMonday | null } = {}) { return { cambios: [] as { boardId: string; itemId: string; columna: string; valor: unknown }[], motivos: [] as { nombre: string; categoria: string; descripcion?: string }[], @@ -25,7 +25,10 @@ function mondayMock(opts: { labelsOrigen?: string[]; duplicado?: LeadMonday | nu async buscarLeadPorTelefono(_telefono: string) { return opts.duplicado ?? null }, - async labelsDeColumna(_boardId: string, _titulo: string) { + async labelsDeColumna(_boardId: string, titulo: string) { + if (titulo === 'Desarrollo') { + return opts.labelsDesarrollo ?? ['ATELIER', 'DIAGONAL', 'ESTANZA', 'SENDA', 'SOLANA'] + } return opts.labelsOrigen ?? ['WhatsApp', 'Web', 'Recomendado', 'Facebook', 'Otro'] }, async obtenerNombreItem(_itemId: string) { @@ -46,10 +49,36 @@ describe('crearLead (POST /api/leads)', () => { const r = await crearLead(monday, { ...datosBase, origen: 'Facebook', notas: 'Vio el volante' }) expect(r).toEqual({ id: 'lead-nuevo-1' }) expect(monday.leadsCreados).toEqual([ - { nombre: 'Lead Prueba', telefono: '4771234567', email: undefined, origenLabel: 'Facebook', notas: 'Vio el volante' }, + { + nombre: 'Lead Prueba', + telefono: '4771234567', + email: undefined, + origenLabel: 'Facebook', + desarrolloLabel: undefined, + notas: 'Vio el volante', + }, ]) }) + it('pasa email y desarrollo al item de Monday cuando vienen en la captura', async () => { + const monday = mondayMock() + const r = await crearLead(monday, { + ...datosBase, + email: 'lead@correo.com', + desarrollo: 'SENDA', + }) + expect(r).toEqual({ id: 'lead-nuevo-1' }) + expect(monday.leadsCreados[0]).toMatchObject({ email: 'lead@correo.com', desarrolloLabel: 'SENDA' }) + }) + + it('rechaza un desarrollo que no sea label real de la columna Desarrollo', async () => { + const monday = mondayMock() + await expect(crearLead(monday, { ...datosBase, desarrollo: 'MONTEBELLO' })).rejects.toThrow( + /Desarrollo inválido/, + ) + expect(monday.leadsCreados).toEqual([]) + }) + it('duplicado por teléfono: no crea nada y devuelve el lead existente (endpoint → 409)', async () => { const existente: LeadMonday = { id: 'lead-viejo-9', diff --git a/api-gateway/src/controllers/leads.controller.ts b/api-gateway/src/controllers/leads.controller.ts index 6bc7b81..4101a0e 100644 --- a/api-gateway/src/controllers/leads.controller.ts +++ b/api-gateway/src/controllers/leads.controller.ts @@ -19,6 +19,7 @@ const crearLeadSchema = z.object({ telefono: z.string().trim().min(1).optional(), email: z.string().trim().email('Email inválido').optional(), origen: z.string().trim().min(1).optional(), + desarrollo: z.string().trim().min(1).optional(), notas: z.string().trim().min(1).optional(), }) @@ -93,11 +94,19 @@ export async function crearLead( } } + if (datos.desarrollo !== undefined) { + const labels = await monday.labelsDeColumna(boardLeads, 'Desarrollo') + if (!labels.includes(datos.desarrollo)) { + throw new AppError(400, `Desarrollo inválido: "${datos.desarrollo}". Labels válidos: ${labels.join(', ')}`) + } + } + const id = await monday.crearLead({ nombre: datos.nombre, telefono: datos.telefono, email: datos.email, origenLabel: datos.origen, + desarrolloLabel: datos.desarrollo, notas: datos.notas, }) return { id } diff --git a/api-gateway/src/services/monday.service.ts b/api-gateway/src/services/monday.service.ts index 08965c8..117a0aa 100644 --- a/api-gateway/src/services/monday.service.ts +++ b/api-gateway/src/services/monday.service.ts @@ -792,6 +792,7 @@ export class MondayService { Teléfono: datos.telefono, Email: datos.email, Origen: datos.origenLabel, + Desarrollo: datos.desarrolloLabel, Notas: datos.notas, }) return this.crearItem(boardId, datos.nombre, columnValues) @@ -985,6 +986,7 @@ export interface DatosLead { telefono?: string | undefined email?: string | undefined origenLabel?: string | undefined // label real de la columna status 'Origen' del board Leads + desarrolloLabel?: string | undefined // label real de la columna status 'Desarrollo' del board Leads notas?: string | undefined } diff --git a/apps/mi-dia/src/lib/mi-dia.test.ts b/apps/mi-dia/src/lib/mi-dia.test.ts index 6a5281f..ef2fc73 100644 --- a/apps/mi-dia/src/lib/mi-dia.test.ts +++ b/apps/mi-dia/src/lib/mi-dia.test.ts @@ -4,6 +4,7 @@ import { esRolConEquipo, formatoFechaCorta, hoyLocal, + DESARROLLOS_LEAD, MOTIVOS_NO_COMPRA, ORIGENES_LEAD, textoDiasRestantes, @@ -14,6 +15,10 @@ describe('catálogos', () => { expect([...ORIGENES_LEAD]).toEqual(['WhatsApp', 'Web', 'Recomendado', 'Facebook', 'Otro']) }) + it('desarrollos = labels reales de la columna Desarrollo del board Leads', () => { + expect([...DESARROLLOS_LEAD]).toEqual(['ATELIER', 'DIAGONAL', 'ESTANZA', 'SENDA', 'SOLANA']) + }) + it('motivos de descarte = vocabulario CRM (igual que el API)', () => { expect(MOTIVOS_NO_COMPRA).toContain('Precio') expect(MOTIVOS_NO_COMPRA).toContain('Timing') diff --git a/apps/mi-dia/src/lib/mi-dia.ts b/apps/mi-dia/src/lib/mi-dia.ts index 08e842b..722abb8 100644 --- a/apps/mi-dia/src/lib/mi-dia.ts +++ b/apps/mi-dia/src/lib/mi-dia.ts @@ -53,6 +53,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 reales de la columna status 'Desarrollo' del board Leads (verificados en Monday) +export const DESARROLLOS_LEAD = ['ATELIER', 'DIAGONAL', 'ESTANZA', 'SENDA', 'SOLANA'] as const + // Categorías del dropdown Motivo No Compra (vocabulario CRM FRASA, igual que el API) export const MOTIVOS_NO_COMPRA = [ 'Precio', diff --git a/apps/mi-dia/src/pages/MiDiaPage.tsx b/apps/mi-dia/src/pages/MiDiaPage.tsx index cb43b3a..4e78e1e 100644 --- a/apps/mi-dia/src/pages/MiDiaPage.tsx +++ b/apps/mi-dia/src/pages/MiDiaPage.tsx @@ -5,6 +5,7 @@ import { esRolConEquipo, formatoFechaCorta, hoyLocal, + DESARROLLOS_LEAD, MOTIVOS_NO_COMPRA, ORIGENES_LEAD, textoDiasRestantes, @@ -186,7 +187,9 @@ export default function MiDiaPage() { // Captura rápida const [capNombre, setCapNombre] = useState('') const [capTelefono, setCapTelefono] = useState('') + const [capEmail, setCapEmail] = useState('') const [capOrigen, setCapOrigen] = useState(ORIGENES_LEAD[0]) + const [capDesarrollo, setCapDesarrollo] = useState('') const [capNotas, setCapNotas] = useState('') const [capEnviando, setCapEnviando] = useState(false) const [capMensaje, setCapMensaje] = useState<{ tipo: 'ok' | 'error'; texto: string } | null>(null) @@ -229,12 +232,16 @@ export default function MiDiaPage() { await crearLead({ nombre: capNombre.trim(), ...(capTelefono.trim() !== '' ? { telefono: capTelefono.trim() } : {}), + ...(capEmail.trim() !== '' ? { email: capEmail.trim() } : {}), ...(capOrigen !== '' ? { origen: capOrigen } : {}), + ...(capDesarrollo !== '' ? { desarrollo: capDesarrollo } : {}), ...(capNotas.trim() !== '' ? { notas: capNotas.trim() } : {}), }) setCapMensaje({ tipo: 'ok', texto: 'Lead registrado, se asignará por round-robin' }) setCapNombre('') setCapTelefono('') + setCapEmail('') + setCapDesarrollo('') setCapNotas('') } catch (err) { setCapMensaje({ tipo: 'error', texto: err instanceof Error ? err.message : 'No se pudo registrar el lead' }) @@ -388,6 +395,18 @@ export default function MiDiaPage() { className="w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none" /> +
+ + setCapEmail(e.target.value)} + className="w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none" + /> +
+
+ + +