feat: vínculo con usuario Monday en landing + alta prellenada desde leads
API: - GET /api/monday/usuarios: usuarios activos no-guest del workspace - PATCH /api/usuarios/me/monday: vincula/desvincula el mondayUserId del usuario autenticado (valida contra el workspace) - GET /api/leads/buscar?q=: búsqueda de leads por nombre/teléfono/email (sin acentos, orden updatedAt desc, límite 15) - aPublico/me ahora incluyen mondayUserId Landing (/): - Nueva sección 'Configuración de cuenta': login inline, selector del usuario de Monday, guardar/desvincular, cerrar sesión Alta de clientes (/client): - Búsqueda de leads de Monday con debounce en la pantalla de entrada; al elegir uno prellena nombre/email/celular y guarda leadMondayId - El picker (borradores + alta nueva + búsqueda de lead) es ahora siempre la pantalla de entrada Tests: api 294, landing 8, alta-clientes 96 — todos verdes
This commit is contained in:
@@ -35,6 +35,29 @@ const descartarSchema = z.object({
|
||||
comentario: z.string().trim().min(1).optional(),
|
||||
})
|
||||
|
||||
// ---------- Búsqueda de leads (prellenado del alta de clientes) ----------
|
||||
|
||||
const LIMITE_BUSQUEDA_LEADS = 15
|
||||
|
||||
/** Minúsculas y sin diacríticos, para comparar nombres/teléfonos con acentos */
|
||||
const sinAcentos = (s: string): string =>
|
||||
s
|
||||
.normalize('NFD')
|
||||
.replace(/[̀-ͯ]/g, '') // diacríticos U+0300–U+036F
|
||||
.toLowerCase()
|
||||
|
||||
/**
|
||||
* Filtra leads por q en nombre/teléfono/email (case-insensitive, sin acentos),
|
||||
* ordena por actualización reciente y limita a 15 resultados.
|
||||
*/
|
||||
export function buscarLeads(leads: LeadMonday[], q: string): LeadMonday[] {
|
||||
const needle = sinAcentos(q)
|
||||
return leads
|
||||
.filter((l) => [l.nombre, l.telefono, l.email].some((campo) => sinAcentos(campo).includes(needle)))
|
||||
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
||||
.slice(0, LIMITE_BUSQUEDA_LEADS)
|
||||
}
|
||||
|
||||
type MondayLeads = Pick<
|
||||
MondayService,
|
||||
| 'cambiarValorColumna'
|
||||
@@ -144,3 +167,23 @@ export async function descartarLeadEndpoint(req: Request, res: Response): Promis
|
||||
const motivoItemId = await descartarLead(mondayService, itemId, body.motivo, body.comentario)
|
||||
res.json({ itemId, estado: 'DESCARTADO', motivo: body.motivo, motivoItemId })
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/leads/buscar?q=texto — autocompletado para prellenar el alta de
|
||||
* clientes desde un lead. q mínimo 2 caracteres; devuelve máximo 15.
|
||||
*/
|
||||
export async function buscarLeadsEndpoint(req: Request, res: Response): Promise<void> {
|
||||
const q = String(req.query['q'] ?? '').trim()
|
||||
if (q.length < 2) throw new AppError(400, 'La búsqueda requiere al menos 2 caracteres')
|
||||
const encontrados = buscarLeads(await mondayService.getLeads(), q)
|
||||
res.json(
|
||||
encontrados.map((l) => ({
|
||||
id: l.id,
|
||||
nombre: l.nombre,
|
||||
telefono: l.telefono,
|
||||
email: l.email,
|
||||
estado: l.estado,
|
||||
origen: l.origen,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user