From 5063ffe281c8fe7903c00a3b8e6f6a5bfb505950 Mon Sep 17 00:00:00 2001 From: Consult AS Technology Date: Tue, 8 Sep 2026 06:31:41 +0000 Subject: [PATCH] =?UTF-8?q?fix(apartados):=20rename=20firma=E2=86=92alta?= =?UTF-8?q?=20enkontrol=20+=20campo=20usuario=20+=20bloqueo=20unidad?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../controllers/apartados.controller.test.ts | 7 +-- .../src/controllers/apartados.controller.ts | 1 - .../src/controllers/clientes.controller.ts | 8 ++- .../controllers/oportunidades.controller.ts | 56 +++++++++++++++++-- api-gateway/src/routes/index.ts | 2 +- .../src/services/automatizaciones.service.ts | 2 +- .../src/services/grupos-estado.service.ts | 1 - .../src/services/mi-dia.service.test.ts | 4 +- api-gateway/src/services/mi-dia.service.ts | 1 - api-gateway/src/services/monday.service.ts | 2 +- .../src/forms/DatosGeneralesCampos.tsx | 3 + apps/alta-clientes/src/lib/schemas.ts | 1 + .../src/pages/AltaClientePage.tsx | 2 +- apps/alta-clientes/src/types/index.ts | 3 +- apps/mi-dia/src/lib/mi-dia.ts | 2 - apps/mi-dia/src/pages/MiDiaPage.tsx | 8 +-- 16 files changed, 76 insertions(+), 27 deletions(-) diff --git a/api-gateway/src/controllers/apartados.controller.test.ts b/api-gateway/src/controllers/apartados.controller.test.ts index f79f122..fd33b27 100644 --- a/api-gateway/src/controllers/apartados.controller.test.ts +++ b/api-gateway/src/controllers/apartados.controller.test.ts @@ -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', ]) }) diff --git a/api-gateway/src/controllers/apartados.controller.ts b/api-gateway/src/controllers/apartados.controller.ts index ab745ec..66fc416 100644 --- a/api-gateway/src/controllers/apartados.controller.ts +++ b/api-gateway/src/controllers/apartados.controller.ts @@ -26,7 +26,6 @@ export const FASES_APARTADO_PERMITIDAS = [ 'Entrego Documentacion', 'Cita Clientes', 'Contrato Enkontrol', - 'Firma Cliente (Venta)', 'Cancelacion Apartado', ] as const diff --git a/api-gateway/src/controllers/clientes.controller.ts b/api-gateway/src/controllers/clientes.controller.ts index 34f61f7..5c1aa6d 100644 --- a/api-gateway/src/controllers/clientes.controller.ts +++ b/api-gateway/src/controllers/clientes.controller.ts @@ -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 { +): Promise { 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 + 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 } : {}) }) diff --git a/api-gateway/src/routes/index.ts b/api-gateway/src/routes/index.ts index e0f33ed..34101c6 100644 --- a/api-gateway/src/routes/index.ts +++ b/api-gateway/src/routes/index.ts @@ -144,7 +144,7 @@ apiRouter.get('/cotizaciones/:id/publico', obtenerCotizacionPublica) apiRouter.get('/cotizaciones/:id', obtenerCotizacion) apiRouter.post('/clientes', crearCliente) -// Prefill del alta desde un apartado de Monday (botón "Firma Cliente (Venta)" +// Prefill del alta desde un apartado de Monday (botón "Alta Enkontrol" // de Mis apartados). Va ANTES de /clientes/:id para no colisionar. apiRouter.get('/altas/prefill-apartado/:itemId', prefillApartadoEndpoint) // Alta progresiva: guardado etapa por etapa del wizard (Contacto PROSPECTO → CLIENTE). diff --git a/api-gateway/src/services/automatizaciones.service.ts b/api-gateway/src/services/automatizaciones.service.ts index 59c96c6..7bff527 100644 --- a/api-gateway/src/services/automatizaciones.service.ts +++ b/api-gateway/src/services/automatizaciones.service.ts @@ -899,7 +899,7 @@ export class AutomatizacionesService { const apartados = await prisma.leadCaptura.findMany({ where: { board: 'APARTADO', - etapa: { notIn: ['Cancelacion Apartado', 'Firma Cliente (Venta)'] }, + etapa: { notIn: ['Cancelacion Apartado', 'Alta Enkontrol'] }, fechaLimite: { not: null, lte: limite, gte: ahora }, }, }) diff --git a/api-gateway/src/services/grupos-estado.service.ts b/api-gateway/src/services/grupos-estado.service.ts index 37900cc..e35d758 100644 --- a/api-gateway/src/services/grupos-estado.service.ts +++ b/api-gateway/src/services/grupos-estado.service.ts @@ -94,7 +94,6 @@ export const GRUPOS_POR_ESTADO: Record { apartado({ id: 'p1', fechaLimite: '2026-09-05' }), apartado({ id: 'p2', fechaLimite: '2026-09-01', estado: 'Cita Clientes' }), apartado({ id: 'p3', fechaLimite: '', estado: 'Alta Enkontrol' }), - apartado({ id: 'p4', estado: 'Firma Cliente (Venta)' }), // final: fuera + apartado({ id: 'p4', estado: 'Escriturado' }), // final: fuera apartado({ id: 'p5', estado: 'Cancelacion Apartado' }), // final: fuera apartado({ id: 'p6', vendedorIds: [OTRO_ID] }), // de otro: fuera ], @@ -254,7 +254,7 @@ describe('resumenMiDia (GET /api/mi-dia/resumen)', () => { }) it('esEstadoFinalApartado: venta y cancelación cierran el pipeline', () => { - expect(esEstadoFinalApartado('Firma Cliente (Venta)')).toBe(true) + expect(esEstadoFinalApartado('Alta Enkontrol')).toBe(false) expect(esEstadoFinalApartado('Cancelacion Apartado')).toBe(true) expect(esEstadoFinalApartado('Escriturado')).toBe(true) expect(esEstadoFinalApartado('Pago Apartado')).toBe(false) diff --git a/api-gateway/src/services/mi-dia.service.ts b/api-gateway/src/services/mi-dia.service.ts index 3ca0239..9459afc 100644 --- a/api-gateway/src/services/mi-dia.service.ts +++ b/api-gateway/src/services/mi-dia.service.ts @@ -29,7 +29,6 @@ export const ESTADOS_FINALES_APARTADO = [ 'CANCELADO', 'LIBERADO', 'FORMALIZADO', - 'FIRMA CLIENTE (VENTA)', // la venta se concreta: pasa a Escrituración ] const ROLES_CON_EQUIPO = ['coordinador', 'corporativo', 'admin'] diff --git a/api-gateway/src/services/monday.service.ts b/api-gateway/src/services/monday.service.ts index 10a2fa3..fd7bc48 100644 --- a/api-gateway/src/services/monday.service.ts +++ b/api-gateway/src/services/monday.service.ts @@ -736,7 +736,7 @@ export class MondayService { /** * Actualiza un apartado existente con los campos presentes (mismo builder de * valores que crearApartado) — usado cuando el alta de cliente arranca desde - * un apartado ya creado (botón "Firma Cliente (Venta)" de Mis apartados): + * un apartado ya creado (botón "Alta Enkontrol" de Mis apartados): * se enriquece el item en vez de crear uno duplicado. */ async actualizarApartado(itemId: string, datos: Partial): Promise { diff --git a/apps/alta-clientes/src/forms/DatosGeneralesCampos.tsx b/apps/alta-clientes/src/forms/DatosGeneralesCampos.tsx index a8dd172..9a64125 100644 --- a/apps/alta-clientes/src/forms/DatosGeneralesCampos.tsx +++ b/apps/alta-clientes/src/forms/DatosGeneralesCampos.tsx @@ -64,6 +64,9 @@ export default function DatosGeneralesCampos({ prefix, compacto = false }: Props className="sm:col-span-2" /> )} + {!compacto && ( + + )} ) } diff --git a/apps/alta-clientes/src/lib/schemas.ts b/apps/alta-clientes/src/lib/schemas.ts index cb18415..162f54c 100644 --- a/apps/alta-clientes/src/lib/schemas.ts +++ b/apps/alta-clientes/src/lib/schemas.ts @@ -68,6 +68,7 @@ export const datosGeneralesSchema = z tipoVivienda: z.enum(['PRESTADA', 'RENTADA', 'FAMILIAR', 'PROPIA', '']), tipoCompra: z.enum(['INVERSION', 'VIVIR_AHI', '']), actividadOcupacion: textoOpcional, + usuarioEnkontrol: textoOpcional, }) .superRefine((datos, ctx) => { // Régimen de bienes solo aplica (y se exige) cuando el estado civil es CASADO diff --git a/apps/alta-clientes/src/pages/AltaClientePage.tsx b/apps/alta-clientes/src/pages/AltaClientePage.tsx index 0a15f5c..f4da2b7 100644 --- a/apps/alta-clientes/src/pages/AltaClientePage.tsx +++ b/apps/alta-clientes/src/pages/AltaClientePage.tsx @@ -59,7 +59,7 @@ function paramsAfluencia(): { oportunidadId: string; valores: Record — botón "Firma Cliente (Venta)" de Mis apartados en Mi día */ +/** Lee ?apartado= — botón "Alta Enkontrol" de Mis apartados en Mi día */ function paramApartado(): string | null { return new URLSearchParams(window.location.search).get('apartado') } diff --git a/apps/alta-clientes/src/types/index.ts b/apps/alta-clientes/src/types/index.ts index 9389f86..f4be065 100644 --- a/apps/alta-clientes/src/types/index.ts +++ b/apps/alta-clientes/src/types/index.ts @@ -44,6 +44,7 @@ export interface DatosGenerales { tipoVivienda: TipoVivienda | '' tipoCompra: TipoCompra | '' actividadOcupacion: string + usuarioEnkontrol?: string } export interface DatosEmpleo { @@ -150,7 +151,7 @@ export interface AltaCliente { // un lead (null = captura manual). Passthrough hasta payloadJson del API. leadMondayId: string | null // ID del item del tablero Apartado de Monday cuando el alta arrancó desde un - // apartado existente (botón "Firma Cliente (Venta)" de Mis apartados): el + // apartado existente (botón "Alta Enkontrol" de Mis apartados): el // API lo LIGA y enriquece en vez de crear un apartado duplicado. apartadoMondayId: string | null // ID del Cliente creado por el guardado progresivo (POST /clientes/progreso/ diff --git a/apps/mi-dia/src/lib/mi-dia.ts b/apps/mi-dia/src/lib/mi-dia.ts index c79b6cd..3b42cfd 100644 --- a/apps/mi-dia/src/lib/mi-dia.ts +++ b/apps/mi-dia/src/lib/mi-dia.ts @@ -172,7 +172,6 @@ export const FASES_APARTADO = [ 'Entrego Documentacion', 'Cita Clientes', 'Contrato Enkontrol', - 'Firma Cliente (Venta)', 'Cancelacion Apartado', ] as const export type FaseApartado = (typeof FASES_APARTADO)[number] @@ -184,7 +183,6 @@ export const ETIQUETA_FASE_APARTADO: Record = { 'Entrego Documentacion': 'Entregó documentación', 'Cita Clientes': 'Cita cliente', 'Contrato Enkontrol': 'Contrato Enkontrol', - 'Firma Cliente (Venta)': 'Firma cliente (venta)', 'Cancelacion Apartado': 'Cancelación', } diff --git a/apps/mi-dia/src/pages/MiDiaPage.tsx b/apps/mi-dia/src/pages/MiDiaPage.tsx index b594610..37359c5 100644 --- a/apps/mi-dia/src/pages/MiDiaPage.tsx +++ b/apps/mi-dia/src/pages/MiDiaPage.tsx @@ -329,9 +329,9 @@ function FilaApartado({ type="button" disabled={enviando} onClick={() => { - // Firma Cliente (Venta): abre el alta de cliente prellenada con + // Alta Enkontrol: abre el alta de cliente prellenada con // todo lo recolectado del apartado (ligada, sin duplicar) - if (fase === 'Firma Cliente (Venta)') { + if (fase === 'Alta Enkontrol') { window.location.href = `/client/?apartado=${apartado.id}` return } @@ -340,14 +340,14 @@ function FilaApartado({ className={ fase === 'Cancelacion Apartado' ? 'rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 shadow-sm transition-all duration-100 ease-out hover:bg-red-50 disabled:opacity-50 dark:bg-[#0F172A] dark:border-red-800 dark:text-red-400' - : fase === 'Firma Cliente (Venta)' + : fase === 'Alta Enkontrol' ? 'rounded-lg bg-accent px-3 py-1.5 text-xs font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover disabled:opacity-50' : fase === apartado.estado ? 'rounded-lg border border-accent bg-accent/5 px-3 py-1.5 text-xs font-medium text-accent shadow-sm disabled:opacity-50' : CLASE_BOTON_ACCION } > - {fase === 'Firma Cliente (Venta)' ? 'Firma cliente (venta) →' : ETIQUETA_FASE_APARTADO[fase]} + {fase === 'Alta Enkontrol' ? 'Alta Enkontrol →' : ETIQUETA_FASE_APARTADO[fase]} ))}