feat(captura): Escrituracion con 3 pares fecha+checkbox y columna Desviacion en dias

This commit is contained in:
2026-09-12 08:03:44 +00:00
parent d2e97f33f5
commit b2d3287c31
6 changed files with 155 additions and 48 deletions

View File

@@ -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(
<TablaCaptura
unidades={UNIDADES}
campos={camposEscrituracion}
datos={datosEscrituracion}
onGuardar={onGuardar}
guardando={false}
/>,
)
// 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')
})
})

View File

@@ -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
</th>
{camposVisibles.map((c) => (
<th
key={c.campo}
className="px-3 py-2.5 text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]"
title={c.label}
>
{c.label}
</th>
<Fragment key={c.campo}>
<th
className="px-3 py-2.5 text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]"
title={c.label}
>
{c.label}
</th>
{tieneDesviacion && c.campo === 'entrega_reprogramada' && (
<th
className="px-3 py-2.5 text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]"
title="Días entre la entrega programada y la reprogramada"
>
Desviación
</th>
)}
</Fragment>
))}
</tr>
</thead>
@@ -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 (
<td
key={`${unidad}-${c.campo}`}
className="px-1 py-0.5"
>
<CeldaEditable
tipo={c.tipo}
valor={val}
options={c.options}
onChange={(nuevo) => 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
}
/>
</td>
<Fragment key={`${unidad}-${c.campo}`}>
<td className="px-1 py-0.5">
<CeldaEditable
tipo={c.tipo}
valor={val}
options={c.options}
onChange={(nuevo) => 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
}
/>
</td>
{esReprogramada && (
<td className="px-1 py-0.5 text-center text-xs text-[#111827] dark:text-[#E2E8F0]">
{diasDesviacion(dirty, datos, unidad) ?? '—'}
</td>
)}
</Fragment>
)
})}
</tr>

View File

@@ -55,6 +55,12 @@ export const CAMPOS_POR_DEPARTAMENTO: Record<DepartamentoId, CampoDef[]> = {
{ 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' },