feat(9-mejoras): limpieza leads + admin CRUD + calendario + CRM360 FRASA
1. Leads limpiados (207 eliminados, 18 apartados conservados) 2. Round-robin desactivado (config round_robin_habilitado=false) 3. Admin puede borrar/reasignar/mover leads (DELETE/PATCH/POST) 4. Calendario: fechaCita guardada y pasada correctamente 5. Avisos Monday eliminados (BannerModoLocal, ChipPendienteSync, vinculación) 6. Contactos filtrados por usuario asignado 7. Modal bitácora: notas persisten entre fases 8. Fases oportunidades: Agendo→Afluencia→Cotizar→Aparto 9. Toast de nota eliminado en cambios de status 10. Branding: CRM360 FRASA + logo en todas las apps 11. 7 apps construidas
This commit is contained in:
@@ -13,16 +13,17 @@ import { NotFoundError } from '../utils/errors.js'
|
||||
* (nombre, teléfono, email, estado), lo más reciente primero, máx 100.
|
||||
* Monday caído (o 502) → lista local (Cliente + LeadCaptura), mismo shape.
|
||||
*/
|
||||
export async function listarContactosEndpoint(_req: Request, res: Response): Promise<void> {
|
||||
export async function listarContactosEndpoint(req: Request, res: Response): Promise<void> {
|
||||
const usuarioId = req.usuarioToken?.sub
|
||||
if (!saludMonday.estaDisponible()) {
|
||||
res.json(await listarContactosLocal())
|
||||
res.json(await listarContactosLocal(usuarioId))
|
||||
return
|
||||
}
|
||||
try {
|
||||
res.json(await mondayService.getContactos())
|
||||
} catch (err) {
|
||||
if (!esFalloMonday(err)) throw err
|
||||
res.json(await listarContactosLocal())
|
||||
res.json(await listarContactosLocal(usuarioId))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -798,6 +798,81 @@ export async function convertirLeadEndpoint(req: Request, res: Response): Promis
|
||||
}
|
||||
}
|
||||
|
||||
// ── Admin CRUD de leads ────────────────────────────────────────────────
|
||||
|
||||
/** DELETE /api/leads/:itemId — eliminar lead (solo admin) */
|
||||
export async function eliminarLeadEndpoint(req: Request, res: Response): Promise<void> {
|
||||
const itemId = String(req.params['itemId'])
|
||||
const row = await prisma.leadCaptura.findUnique({ where: { leadId: itemId } })
|
||||
// Eliminar de Monday si es item remoto y Monday está disponible
|
||||
if (row && row.mondayItemId && !esIdLocal(itemId) && saludMonday.estaDisponible()) {
|
||||
try {
|
||||
await mondayService.eliminarItem(row.mondayItemId)
|
||||
} catch (err) {
|
||||
if (!esFalloMonday(err)) throw err
|
||||
}
|
||||
}
|
||||
// Eliminar la fila local (si existe)
|
||||
if (row) {
|
||||
await prisma.leadCaptura.delete({ where: { leadId: itemId } }).catch(() => {})
|
||||
}
|
||||
res.json({ eliminado: true, itemId })
|
||||
auditoria.registrar(req.usuarioToken?.sub ?? 'sistema', 'ELIMINAR_LEAD', 'lead', itemId)
|
||||
}
|
||||
|
||||
const asignarLeadSchema = z.object({
|
||||
usuarioId: z.string().trim().min(1, 'usuarioId es obligatorio'),
|
||||
})
|
||||
|
||||
/** PATCH /api/leads/:itemId/asignar — reasignar lead a otro vendedor (solo admin) */
|
||||
export async function asignarLeadEndpoint(req: Request, res: Response): Promise<void> {
|
||||
const itemId = String(req.params['itemId'])
|
||||
const { usuarioId } = asignarLeadSchema.parse(req.body)
|
||||
const usuario = await prisma.usuario.findUnique({
|
||||
where: { id: usuarioId },
|
||||
select: { mondayUserId: true, nombre: true },
|
||||
})
|
||||
if (!usuario) throw new NotFoundError('Usuario no encontrado')
|
||||
// Actualizar LeadCaptura
|
||||
await prisma.leadCaptura.updateMany({
|
||||
where: { leadId: itemId },
|
||||
data: { usuarioId },
|
||||
})
|
||||
// En Monday: determinar si es lead u oportunidad
|
||||
const row = await prisma.leadCaptura.findUnique({ where: { leadId: itemId } })
|
||||
const board: 'leads' | 'oportunidades' = row?.board === 'OPORTUNIDADES' ? 'oportunidades' : 'leads'
|
||||
if (usuario.mondayUserId != null && saludMonday.estaDisponible() && !esIdLocal(itemId)) {
|
||||
try {
|
||||
await automatizacionesService.asignarACapturista(itemId, row?.nombreLead ?? '', usuario.mondayUserId, board)
|
||||
} catch (err) {
|
||||
if (!esFalloMonday(err)) throw err
|
||||
await encolarSyncSeguro('ASIGNAR_PERSONA', { board, mondayUserId: usuario.mondayUserId }, itemId)
|
||||
}
|
||||
} else if (usuario.mondayUserId != null) {
|
||||
await encolarSyncSeguro('ASIGNAR_PERSONA', { board, mondayUserId: usuario.mondayUserId }, itemId)
|
||||
}
|
||||
res.json({ itemId, usuarioId, nombre: usuario.nombre })
|
||||
auditoria.registrar(req.usuarioToken?.sub ?? 'sistema', 'ASIGNAR_LEAD', 'lead', itemId, usuarioId)
|
||||
}
|
||||
|
||||
const moverLeadSchema = z.object({
|
||||
board: z.enum(['LEADS', 'OPORTUNIDADES']),
|
||||
})
|
||||
|
||||
/** POST /api/leads/:itemId/mover — mover lead a otro tablero (solo admin) */
|
||||
export async function moverLeadEndpoint(req: Request, res: Response): Promise<void> {
|
||||
const itemId = String(req.params['itemId'])
|
||||
const { board } = moverLeadSchema.parse(req.body)
|
||||
const row = await prisma.leadCaptura.findUnique({ where: { leadId: itemId } })
|
||||
if (!row) throw new NotFoundError('Lead no encontrado')
|
||||
await prisma.leadCaptura.update({
|
||||
where: { leadId: itemId },
|
||||
data: { board },
|
||||
})
|
||||
res.json({ itemId, board })
|
||||
auditoria.registrar(req.usuarioToken?.sub ?? 'sistema', 'MOVER_LEAD', 'lead', itemId, board)
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/leads/buscar?q=texto — autocompletado para prellenar el alta de
|
||||
* clientes. Busca en AMBOS tableros (Leads + Oportunidades); q mínimo 2
|
||||
|
||||
Reference in New Issue
Block a user