fix(apartados): rename firma→alta enkontrol + campo usuario + bloqueo unidad

- Firma Cliente (Venta) renombrada a Alta Enkontrol (mismo redirect a alta cliente)
- Campo 'Usuario Enkontrol' añadido en Datos Generales del alta cliente
- Unidad se bloquea (APARTADA) al pasar cliente a etapa Aparto
- Backend y frontend actualizados
This commit is contained in:
2026-09-08 06:31:41 +00:00
parent f297aabc1a
commit 5063ffe281
16 changed files with 76 additions and 27 deletions

View File

@@ -74,10 +74,10 @@ describe('cambiarEstadoApartado (POST /api/apartados/:id/estado)', () => {
expect(monday.movimientos).toEqual([])
})
it('Firma Cliente (Venta) mueve el item a "Clientes Activos"', async () => {
it('Alta Enkontrol se queda en "Nuevos Clientes" (grupo topics)', async () => {
const monday = mondayMock()
await cambiarEstadoApartado(monday, 'apt-2', 'Firma Cliente (Venta)')
expect(monday.movimientos).toEqual([{ itemId: 'apt-2', grupo: 'group_mm6sx3rn' }])
await cambiarEstadoApartado(monday, 'apt-2', 'Alta Enkontrol')
expect(monday.movimientos).toEqual([])
})
it('Cancelacion Apartado mueve el item a "Historico"', async () => {
@@ -93,7 +93,6 @@ describe('cambiarEstadoApartado (POST /api/apartados/:id/estado)', () => {
'Entrego Documentacion',
'Cita Clientes',
'Contrato Enkontrol',
'Firma Cliente (Venta)',
'Cancelacion Apartado',
])
})

View File

@@ -26,7 +26,6 @@ export const FASES_APARTADO_PERMITIDAS = [
'Entrego Documentacion',
'Cita Clientes',
'Contrato Enkontrol',
'Firma Cliente (Venta)',
'Cancelacion Apartado',
] as const

View File

@@ -32,6 +32,7 @@ export const crearClienteSchema = z
estadoCivil: z.string().optional(),
tipoVivienda: z.string().optional(),
tipoCompra: z.string().optional(),
usuarioEnkontrol: z.string().optional(),
})
.passthrough(),
empleo: z.object({ tipoEmpleo: z.string().optional() }).passthrough().optional(),
@@ -312,7 +313,7 @@ async function encolarReplicasPendientesCliente(
/**
* Apartado Monday del alta: con apartadoMondayId (alta arrancada desde un
* apartado existente, botón "Firma Cliente (Venta)" de Mis apartados) se
* apartado existente, botón "Alta Enkontrol" de Mis apartados) se
* LIGA y enriquece ese item; sin él, se crea uno nuevo como siempre.
*/
async function apartadoDeAlta(
@@ -444,6 +445,7 @@ export const iniciarProgresoSchema = z
estadoCivil: z.string().optional(),
tipoVivienda: z.string().optional(),
tipoCompra: z.string().optional(),
usuarioEnkontrol: z.string().optional(),
})
.passthrough(),
leadMondayId: z.string().optional(),
@@ -767,8 +769,8 @@ export async function listarDocumentos(req: Request, res: Response): Promise<voi
/**
* GET /api/altas/prefill-apartado/:itemId — todo lo ya recolectado de un
* apartado de Monday para prellenar el alta de cliente (botón "Firma Cliente
* (Venta)" de Mis apartados): ficha del apartado, contacto relacionado
* apartado de Monday para prellenar el alta de cliente (botón "Alta Enkontrol"
* de Mis apartados): ficha del apartado, contacto relacionado
* (perfil completo), oportunidad relacionada (perfil de la afluencia) y la
* cotización más reciente vinculada. Las relaciones se arman por teléfono
* (últimos 10 dígitos) o email.

View File

@@ -7,6 +7,7 @@ import { automatizacionesService } from '../services/automatizaciones.service.js
import { saludMonday } from '../services/salud-monday.service.js'
import { crmLocal, esIdLocal } from '../services/crm-local.service.js'
import { esFalloMonday } from '../services/sync-monday.service.js'
import { prisma } from '../services/prisma.js'
import * as auditoria from '../services/auditoria.service.js'
/** Categorías del dropdown "Motivo No Compra" (vocabulario CRM FRASA) */
@@ -154,22 +155,33 @@ export async function cambiarEtapaOportunidad(
* Cambio de etapa en modo local: guarda la etapa en la fila local y encola
* CAMBIO_ETAPA. En 'Aparto' crea además el apartado local (crmLocal.apartar),
* el equivalente al asegurarApartado del flujo Monday.
* 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<void> {
): Promise<string | null> {
if (etapa === 'Aparto') {
await crmLocal.apartar(itemId, usuarioId)
return
// 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<string, unknown>
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? } */
@@ -178,13 +190,31 @@ export async function cambiarEtapaOportunidadEndpoint(req: Request, res: Respons
const itemId = String(req.params['itemId'])
const usuarioSub = req.usuarioToken?.sub
if (esIdLocal(itemId) || !saludMonday.estaDisponible()) {
await cambiarEtapaLocal(itemId, body.etapa, body.fecha, usuarioSub)
const unidadCodigo = await cambiarEtapaLocal(itemId, body.etapa, body.fecha, usuarioSub)
// Bloquear la unidad del inventario si la oportunidad tiene una asignada
if (body.etapa === 'Aparto' && unidadCodigo !== null) {
try {
await mondayService.actualizarDisponibilidadUnidad(unidadCodigo, 'APARTADA')
} catch (err) {
// eslint-disable-next-line no-console
console.error(`No se pudo bloquear la unidad ${unidadCodigo} de la oportunidad ${itemId}:`, err)
}
}
} else {
try {
await cambiarEtapaOportunidad(mondayService, itemId, body.etapa, body.fecha)
} catch (err) {
if (!esFalloMonday(err)) throw err
await cambiarEtapaLocal(itemId, body.etapa, body.fecha, usuarioSub)
const unidadCodigo = await cambiarEtapaLocal(itemId, body.etapa, body.fecha, usuarioSub)
// Bloquear la unidad del inventario si la oportunidad tiene una asignada
if (body.etapa === 'Aparto' && unidadCodigo !== null) {
try {
await mondayService.actualizarDisponibilidadUnidad(unidadCodigo, 'APARTADA')
} catch (err2) {
// eslint-disable-next-line no-console
console.error(`No se pudo bloquear la unidad ${unidadCodigo} de la oportunidad ${itemId}:`, err2)
}
}
res.json({ itemId, etapa: body.etapa, ...(body.fecha !== undefined ? { fecha: body.fecha } : {}) })
return
}
@@ -199,6 +229,24 @@ export async function cambiarEtapaOportunidadEndpoint(req: Request, res: Respons
// eslint-disable-next-line no-console
console.error(`No se pudo crear el apartado de la oportunidad ${itemId}:`, err)
}
// Bloquear la unidad del inventario (colocar en APARTADA)
try {
const boardOportunidades = config.monday.boards.oportunidades
const svc = mondayService as MondayService
const item = await svc.getItemConGrupo(itemId)
if (item) {
const colUnidad = await svc.columnaIdPorTitulo(boardOportunidades, 'Unidad')
const unidadCodigo = colUnidad
? (item.column_values.find((cv) => cv.id === colUnidad)?.text ?? '').trim()
: ''
if (unidadCodigo !== '') {
await svc.actualizarDisponibilidadUnidad(unidadCodigo, 'APARTADA')
}
}
} catch (err) {
// eslint-disable-next-line no-console
console.error(`No se pudo bloquear la unidad de la oportunidad ${itemId}:`, err)
}
}
}
res.json({ itemId, etapa: body.etapa, ...(body.fecha !== undefined ? { fecha: body.fecha } : {}) })