From b2d3287c31bbde6918cf58cb892c246793c0527c Mon Sep 17 00:00:00 2001 From: Consult AS Technology Date: Sat, 12 Sep 2026 08:03:44 +0000 Subject: [PATCH] feat(captura): Escrituracion con 3 pares fecha+checkbox y columna Desviacion en dias --- api-gateway/src/config/campos-departamento.ts | 6 + .../captura-departamento.service.test.ts | 18 +++ .../services/captura-departamento.service.ts | 26 +++-- .../src/components/TablaCaptura.test.tsx | 43 ++++++++ apps/landing/src/components/TablaCaptura.tsx | 104 ++++++++++++------ apps/landing/src/services/captura.ts | 6 + 6 files changed, 155 insertions(+), 48 deletions(-) diff --git a/api-gateway/src/config/campos-departamento.ts b/api-gateway/src/config/campos-departamento.ts index 819789a..19ac3c6 100644 --- a/api-gateway/src/config/campos-departamento.ts +++ b/api-gateway/src/config/campos-departamento.ts @@ -30,6 +30,12 @@ export const CAMPOS_POR_DEPARTAMENTO: Record { ).rejects.toThrow('solo puede modificarlas un administrador') expect(prismaMock.$transaction).not.toHaveBeenCalled() }) + + it('ESCRITURACION: no-admin no puede modificar una fecha ya establecida', async () => { + prismaMock.capturaDepartamento.findMany.mockResolvedValue([ + { unidadCodigo: 'U-101', campo: 'entrega_programada_fecha', valor: '2026-10-01' }, + ]) + await expect( + guardarCampo( + 'SENDA', + 'U-101', + 'ESCRITURACION', + 'entrega_programada_fecha', + '2026-10-15', + 'user-1', + 'escrituracion', + ), + ).rejects.toThrow('solo puede modificarlas un administrador') + expect(prismaMock.capturaDepartamento.upsert).not.toHaveBeenCalled() + }) }) describe('resumenAvance', () => { diff --git a/api-gateway/src/services/captura-departamento.service.ts b/api-gateway/src/services/captura-departamento.service.ts index 87379f4..8f24f52 100644 --- a/api-gateway/src/services/captura-departamento.service.ts +++ b/api-gateway/src/services/captura-departamento.service.ts @@ -2,22 +2,24 @@ import { prisma } from './prisma.js' import { CAMPOS_POR_DEPARTAMENTO, DEPARTAMENTOS } from '../config/campos-departamento.js' import { AppError } from '../utils/errors.js' -/** Fechas de Obra emparejadas a checkboxes: tras establecerse solo las cambia un admin */ -const CAMPOS_FECHA_OBRA = new Set( - (CAMPOS_POR_DEPARTAMENTO.OBRA ?? []) - .filter((c) => c.conFecha) - .map((c) => c.conFecha as string), -) +/** Fechas emparejadas a checkboxes por departamento: tras establecerse solo las cambia un admin */ +const CAMPOS_FECHA_BLOQUEADOS: Record> = {} +for (const [dept, campos] of Object.entries(CAMPOS_POR_DEPARTAMENTO)) { + const fechas = campos.filter((c) => c.conFecha).map((c) => c.conFecha as string) + if (fechas.length > 0) CAMPOS_FECHA_BLOQUEADOS[dept] = new Set(fechas) +} -/** Bloquear modificación de fechas de Obra ya establecidas (solo admin) */ -async function validarFechasObra( +/** Bloquear modificación de fechas ya establecidas (solo admin) */ +async function validarFechasBloqueadas( desarrollo: string, departamento: string, registros: { unidad: string; campo: string; valor: string | null }[], rol: string, ): Promise { - if (rol === 'admin' || departamento !== 'OBRA') return - const candidatos = registros.filter((r) => CAMPOS_FECHA_OBRA.has(r.campo)) + if (rol === 'admin') return + const bloqueados = CAMPOS_FECHA_BLOQUEADOS[departamento] + if (!bloqueados) return + const candidatos = registros.filter((r) => bloqueados.has(r.campo)) if (candidatos.length === 0) return const existentes = await prisma.capturaDepartamento.findMany({ @@ -106,7 +108,7 @@ export async function guardarCampo( usuarioId: string, rol: string, ): Promise { - await validarFechasObra(desarrollo, departamento, [{ unidad: unidadCodigo, campo, valor }], rol) + await validarFechasBloqueadas(desarrollo, departamento, [{ unidad: unidadCodigo, campo, valor }], rol) const tipo = CAMPOS_POR_DEPARTAMENTO[departamento]?.find((c) => c.campo === campo)?.tipo ?? 'text' @@ -133,7 +135,7 @@ export async function guardarBulk( usuarioId: string, rol: string, ): Promise { - await validarFechasObra(desarrollo, departamento, registros, rol) + await validarFechasBloqueadas(desarrollo, departamento, registros, rol) const tipoPorCampo = new Map( (CAMPOS_POR_DEPARTAMENTO[departamento] ?? []).map((c) => [c.campo, c.tipo]), diff --git a/apps/landing/src/components/TablaCaptura.test.tsx b/apps/landing/src/components/TablaCaptura.test.tsx index 48a1c3a..6256564 100644 --- a/apps/landing/src/components/TablaCaptura.test.tsx +++ b/apps/landing/src/components/TablaCaptura.test.tsx @@ -319,4 +319,47 @@ describe('TablaCaptura', () => { expect((fechas2[0] as HTMLInputElement).disabled).toBe(false) expect((fechas2[1] as HTMLInputElement).disabled).toBe(false) }) + + it('ESCRITURACION: pares fecha+checkbox y columna Desviación calculada', () => { + const onGuardar = vi.fn() + const camposEscrituracion: CampoDef[] = [ + { campo: 'entrega_programada', tipo: 'boolean', label: 'Entrega Programada', conFecha: 'entrega_programada_fecha' }, + { campo: 'entrega_programada_fecha', tipo: 'date', label: 'Fecha Programada Entrega Vivienda' }, + { campo: 'entrega_reprogramada', tipo: 'boolean', label: 'Entrega Reprogramada', conFecha: 'entrega_reprogramada_fecha' }, + { campo: 'entrega_reprogramada_fecha', tipo: 'date', label: 'Fecha Reprogramada Entrega Vivienda' }, + ] + const datosEscrituracion: DatosCaptura = { + // 15 días de desviación + '409-1': { entrega_programada: 'true', entrega_programada_fecha: '2026-01-01', entrega_reprogramada: 'true', entrega_reprogramada_fecha: '2026-01-16' }, + // Falta reprogramada → desviación '—' + '409-2': { entrega_programada: null, entrega_programada_fecha: '2026-02-01', entrega_reprogramada: null, entrega_reprogramada_fecha: null }, + } + + const { container } = render( + , + ) + + // Columnas: Unidad + 2 checkboxes emparejados + Desviación (fechas ocultas) + expect(screen.getAllByRole('columnheader')).toHaveLength(4) + expect(screen.getByText('Desviación')).toBeTruthy() + + // Desviación: 15 días en la primera unidad, '—' en la segunda + const celdas1 = container.querySelectorAll('tbody tr')[0]?.querySelectorAll('td') + expect(celdas1?.[celdas1.length - 1]?.textContent).toBe('15') + const celdas2 = container.querySelectorAll('tbody tr')[1]?.querySelectorAll('td') + expect(celdas2?.[celdas2.length - 1]?.textContent).toBe('—') + + // Editar la reprogramada recalcula la desviación al vuelo + const fechas = container.querySelectorAll('input[type="date"]') + expect(fechas).toHaveLength(4) // 2 fechas por unidad + fireEvent.change(fechas[3], { target: { value: '2026-02-21' } }) + const celdas2b = container.querySelectorAll('tbody tr')[1]?.querySelectorAll('td') + expect(celdas2b?.[celdas2b.length - 1]?.textContent).toBe('20') + }) }) diff --git a/apps/landing/src/components/TablaCaptura.tsx b/apps/landing/src/components/TablaCaptura.tsx index 7b8109c..50b64dc 100644 --- a/apps/landing/src/components/TablaCaptura.tsx +++ b/apps/landing/src/components/TablaCaptura.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState, useCallback } from 'react' +import { Fragment, useMemo, useState, useCallback } from 'react' import type { CampoDef, DatosCaptura } from '../services/captura' // ─── Tipos ────────────────────────────────────────────────────────── @@ -46,6 +46,16 @@ function hoyISO(): string { return `${d.getFullYear()}-${mes}-${dia}` } +/** Días de desviación entre entrega programada y reprogramada (null si falta una fecha) */ +function diasDesviacion(dirty: DirtyMap, datos: DatosCaptura, unidad: string): number | null { + const prog = valorEfectivo(dirty, datos, unidad, 'entrega_programada_fecha') + const reprog = valorEfectivo(dirty, datos, unidad, 'entrega_reprogramada_fecha') + if (!prog || !reprog) return null + return Math.round( + (new Date(reprog).getTime() - new Date(prog).getTime()) / 86_400_000, + ) +} + // ─── Componente de celda ───────────────────────────────────────────── interface CeldaProps { @@ -203,6 +213,14 @@ export function TablaCaptura({ [campos], ) + /** Columna virtual Desviación: activa si el depto tiene las dos fechas de entrega */ + const tieneDesviacion = useMemo( + () => + campos.some((c) => c.campo === 'entrega_programada_fecha') && + campos.some((c) => c.campo === 'entrega_reprogramada_fecha'), + [campos], + ) + /** ¿Hay cambios pendientes? */ const tieneDirty = useMemo(() => { for (const u of Object.values(dirty)) { @@ -283,13 +301,22 @@ export function TablaCaptura({ Unidad {camposVisibles.map((c) => ( - - {c.label} - + + + {c.label} + + {tieneDesviacion && c.campo === 'entrega_reprogramada' && ( + + Desviación + + )} + ))} @@ -310,35 +337,40 @@ export function TablaCaptura({ const val = valorEfectivo(dirty, datos, unidad, c.campo) const isDirty = dirty[unidad]?.[c.campo] !== undefined const campoFecha = c.conFecha + const esReprogramada = tieneDesviacion && c.campo === 'entrega_reprogramada' return ( - - handleChange(unidad, c.campo, nuevo)} - dirty={isDirty} - conFecha={campoFecha} - valorFecha={ - campoFecha ? valorEfectivo(dirty, datos, unidad, campoFecha) : undefined - } - dirtyFecha={campoFecha ? dirty[unidad]?.[campoFecha] !== undefined : false} - fechaBloqueada={ - campoFecha - ? !puedeEditarFechas && - valorOriginal(datos, unidad, campoFecha) != null - : false - } - onFechaChange={ - campoFecha - ? (nuevo) => handleChange(unidad, campoFecha, nuevo) - : undefined - } - /> - + + + handleChange(unidad, c.campo, nuevo)} + dirty={isDirty} + conFecha={campoFecha} + valorFecha={ + campoFecha ? valorEfectivo(dirty, datos, unidad, campoFecha) : undefined + } + dirtyFecha={campoFecha ? dirty[unidad]?.[campoFecha] !== undefined : false} + fechaBloqueada={ + campoFecha + ? !puedeEditarFechas && + valorOriginal(datos, unidad, campoFecha) != null + : false + } + onFechaChange={ + campoFecha + ? (nuevo) => handleChange(unidad, campoFecha, nuevo) + : undefined + } + /> + + {esReprogramada && ( + + {diasDesviacion(dirty, datos, unidad) ?? '—'} + + )} + ) })} diff --git a/apps/landing/src/services/captura.ts b/apps/landing/src/services/captura.ts index ce544c1..8964322 100644 --- a/apps/landing/src/services/captura.ts +++ b/apps/landing/src/services/captura.ts @@ -55,6 +55,12 @@ export const CAMPOS_POR_DEPARTAMENTO: Record = { { campo: 'precio_venta', tipo: 'number', label: 'Precio Venta' }, ], ESCRITURACION: [ + { campo: 'inicio_obra', tipo: 'boolean', label: 'Inicio Obra', conFecha: 'inicio_obra_fecha' }, + { campo: 'inicio_obra_fecha', tipo: 'date', label: 'Fecha Inicio Obra' }, + { campo: 'entrega_programada', tipo: 'boolean', label: 'Entrega Programada', conFecha: 'entrega_programada_fecha' }, + { campo: 'entrega_programada_fecha', tipo: 'date', label: 'Fecha Programada Entrega Vivienda' }, + { campo: 'entrega_reprogramada', tipo: 'boolean', label: 'Entrega Reprogramada', conFecha: 'entrega_reprogramada_fecha' }, + { campo: 'entrega_reprogramada_fecha', tipo: 'date', label: 'Fecha Reprogramada Entrega Vivienda' }, { campo: 'fecha_autorizacion', tipo: 'date', label: 'Fecha Autorización' }, { campo: 'fecha_firma', tipo: 'date', label: 'Fecha Firma' }, { campo: 'notaria', tipo: 'text', label: 'Notaría' },