import type { Request, Response } from 'express' import { z } from 'zod' import { VOCABULARIO, type MotivoNoCompra } from '../config/vocabulario.js' import { prisma } from '../services/prisma.js' import { crmLocal, esIdLocal } from '../services/crm-local.service.js' import * as auditoria from '../services/auditoria.service.js' /** Categorías del dropdown "Motivo No Compra" (vocabulario CRM FRASA) */ export const MOTIVOS_NO_COMPRA = VOCABULARIO.MOTIVOS_NO_COMPRA /** * Subcategorías por categoría, tomadas del catálogo del tablero de referencia * "Motivos No Compra" del workspace CRM FRASA. NOTA: 'Mesualidad' y 'Cambo de * Planes' llevan las faltas de ortografía originales del catálogo del cliente. */ export const SUBCATEGORIAS_NO_COMPRA: Record = VOCABULARIO.SUBCATEGORIAS const perderSchema = z.object({ motivo: z.enum(MOTIVOS_NO_COMPRA), comentario: z.string().optional(), }) /** * Marca una oportunidad como perdida (etapa 'Perdido' + motivo/submotivo en la * fila local) y encola el DESCARTE. SIEMPRE local. */ export async function perderOportunidadEndpoint(req: Request, res: Response): Promise { const body = perderSchema.parse(req.body) const mondayItemId = String(req.params['mondayItemId']) await crmLocal.descartar(mondayItemId, body.motivo, { fuente: 'oportunidad', comentario: body.comentario, usuarioId: req.usuarioToken?.sub, sombra: !esIdLocal(mondayItemId), }) res.json({ mondayItemId, etapa: 'Perdido', motivo: body.motivo, motivoItemId: null }) auditoria.registrar(req.usuarioToken?.sub ?? 'sistema', 'PERDER_OPORTUNIDAD', 'oportunidad', mondayItemId, body.motivo) } // ---------- Cambio de etapa desde Mi día ---------- /** * Etapas del funnel que el asesor puede mover desde los botones de * "Mis oportunidades" (vocabulario canónico del CRM — el frontend las manda * tal cual). */ export const ETAPAS_OPORTUNIDAD_PERMITIDAS = VOCABULARIO.FASES_OPORTUNIDAD const cambiarEtapaSchema = z.object({ etapa: z.enum(ETAPAS_OPORTUNIDAD_PERMITIDAS), // En 'Agendo Cita' puede setear la fecha de la cita fecha: z.string().optional(), }) /** * Cambio de etapa local: guarda la etapa en la fila local y encola * CAMBIO_ETAPA. En 'Aparto' crea además el apartado local (crmLocal.apartar). * Devuelve el unidadCodigo de la oportunidad si existe (para bloquear la unidad). */ async function cambiarEtapaLocal( itemId: string, etapa: (typeof ETAPAS_OPORTUNIDAD_PERMITIDAS)[number], fecha: string | undefined, usuarioId: string | undefined, ): Promise { if (etapa === 'Aparto') { await crmLocal.apartar(itemId, usuarioId) // Leer la oportunidad local para obtener el unidadCodigo y bloquear la unidad const captura = await prisma.leadCaptura.findUnique({ where: { leadId: itemId } }) let unidadCodigo = '' if (captura?.datosJson) { try { const datos = JSON.parse(captura.datosJson) as Record if (typeof datos['unidadCodigo'] === 'string') unidadCodigo = datos['unidadCodigo'] } catch { /* datosJson inválido: se ignora */ } } return unidadCodigo.trim() !== '' ? unidadCodigo.trim() : null } await crmLocal.cambiarEstado(itemId, etapa, 'OPORTUNIDADES', { fecha, usuarioId, sombra: !esIdLocal(itemId), }) return null } /** POST /api/oportunidades/:itemId/etapa { etapa, fecha? } */ export async function cambiarEtapaOportunidadEndpoint(req: Request, res: Response): Promise { const body = cambiarEtapaSchema.parse(req.body) const itemId = String(req.params['itemId']) const unidadCodigo = await cambiarEtapaLocal(itemId, body.etapa, body.fecha, req.usuarioToken?.sub) // Bloquear la unidad del inventario local si la oportunidad tiene una asignada if (body.etapa === 'Aparto' && unidadCodigo !== null) { try { await prisma.unidad.updateMany({ where: { codigo: unidadCodigo }, data: { status: 'APARTADA' } }) } catch (err) { // eslint-disable-next-line no-console console.error(`No se pudo bloquear la unidad ${unidadCodigo} de la oportunidad ${itemId}:`, err) } } res.json({ itemId, etapa: body.etapa, ...(body.fecha !== undefined ? { fecha: body.fecha } : {}) }) auditoria.registrar(req.usuarioToken?.sub ?? 'sistema', 'CAMBIAR_ETAPA', 'oportunidad', itemId, body.etapa) }