feat(captura): fecha junto a checkboxes de Obra con semaforo verde/rojo
This commit is contained in:
@@ -1,13 +1,19 @@
|
||||
export const DEPARTAMENTOS = ['OBRA', 'VENTAS', 'ESCRITURACION', 'FINANZAS', 'POSTVENTA'] as const
|
||||
|
||||
export const CAMPOS_POR_DEPARTAMENTO: Record<string, { campo: string; tipo: string; label: string; options?: string[] }[]> = {
|
||||
export const CAMPOS_POR_DEPARTAMENTO: Record<string, { campo: string; tipo: string; label: string; options?: string[]; conFecha?: string }[]> = {
|
||||
OBRA: [
|
||||
{ campo: 'certificacion_gas', tipo: 'boolean', label: 'Certificación Gas' },
|
||||
{ campo: 'to_c4_fianza', tipo: 'boolean', label: 'TO y C4-Fianza' },
|
||||
{ campo: 'regimen_inscrito', tipo: 'boolean', label: 'Régimen Inscrito' },
|
||||
{ campo: 'no_oficial', tipo: 'boolean', label: 'No. Oficial' },
|
||||
{ campo: 'clave_catastral', tipo: 'boolean', label: 'Clave Catastral' },
|
||||
{ campo: 'listas_avaluo', tipo: 'boolean', label: 'Listas para Avaluo' },
|
||||
{ campo: 'certificacion_gas', tipo: 'boolean', label: 'Certificación Gas', conFecha: 'certificacion_gas_fecha' },
|
||||
{ campo: 'certificacion_gas_fecha', tipo: 'date', label: 'Fecha Certificación Gas' },
|
||||
{ campo: 'to_c4_fianza', tipo: 'boolean', label: 'TO y C4-Fianza', conFecha: 'to_c4_fianza_fecha' },
|
||||
{ campo: 'to_c4_fianza_fecha', tipo: 'date', label: 'Fecha TO y C4-Fianza' },
|
||||
{ campo: 'regimen_inscrito', tipo: 'boolean', label: 'Régimen Inscrito', conFecha: 'regimen_inscrito_fecha' },
|
||||
{ campo: 'regimen_inscrito_fecha', tipo: 'date', label: 'Fecha Régimen Inscrito' },
|
||||
{ campo: 'no_oficial', tipo: 'boolean', label: 'No. Oficial', conFecha: 'no_oficial_fecha' },
|
||||
{ campo: 'no_oficial_fecha', tipo: 'date', label: 'Fecha No. Oficial' },
|
||||
{ campo: 'clave_catastral', tipo: 'boolean', label: 'Clave Catastral', conFecha: 'clave_catastral_fecha' },
|
||||
{ campo: 'clave_catastral_fecha', tipo: 'date', label: 'Fecha Clave Catastral' },
|
||||
{ campo: 'listas_avaluo', tipo: 'boolean', label: 'Listas para Avaluo', conFecha: 'listas_avaluo_fecha' },
|
||||
{ campo: 'listas_avaluo_fecha', tipo: 'date', label: 'Fecha Listas para Avaluo' },
|
||||
{ campo: 'habitabilidad', tipo: 'date', label: 'Habitabilidad a partir de' },
|
||||
{ campo: 'comentarios_obra', tipo: 'text', label: 'Comentarios' },
|
||||
],
|
||||
|
||||
@@ -206,4 +206,49 @@ describe('TablaCaptura', () => {
|
||||
fireEvent.click(screen.getByText('Siguiente'))
|
||||
expect(screen.getByText(/Mostrando 51–60 de 60/)).toBeTruthy()
|
||||
})
|
||||
|
||||
it('boolean con conFecha: fecha + checkbox en una celda, semáforo y dirty del campo fecha', () => {
|
||||
const onGuardar = vi.fn()
|
||||
const camposEmparejados: CampoDef[] = [
|
||||
{ campo: 'certificacion_gas', tipo: 'boolean', label: 'Certificación Gas', conFecha: 'certificacion_gas_fecha' },
|
||||
{ campo: 'certificacion_gas_fecha', tipo: 'date', label: 'Fecha Certificación Gas' },
|
||||
]
|
||||
const datosEmparejados: DatosCaptura = {
|
||||
// Check activo → fecha en verde
|
||||
'409-1': { certificacion_gas: 'true', certificacion_gas_fecha: '2026-01-01' },
|
||||
// Fecha pasada sin check → fecha en rojo
|
||||
'409-2': { certificacion_gas: null, certificacion_gas_fecha: '2026-01-01' },
|
||||
}
|
||||
|
||||
const { container } = render(
|
||||
<TablaCaptura
|
||||
unidades={UNIDADES}
|
||||
campos={camposEmparejados}
|
||||
datos={datosEmparejados}
|
||||
onGuardar={onGuardar}
|
||||
guardando={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
// La columna standalone de fecha se oculta: solo Unidad + Certificación Gas
|
||||
expect(screen.getAllByRole('columnheader')).toHaveLength(2)
|
||||
expect(screen.queryByText('Fecha Certificación Gas')).toBeNull()
|
||||
|
||||
// Un date input y un checkbox por unidad
|
||||
const fechas = container.querySelectorAll('input[type="date"]')
|
||||
expect(fechas).toHaveLength(2)
|
||||
expect((fechas[0] as HTMLInputElement).value).toBe('2026-01-01')
|
||||
|
||||
// Semáforo: check activo → verde; fecha pasada sin check → rojo
|
||||
expect(fechas[0].className).toContain('text-green-600')
|
||||
expect(fechas[1].className).toContain('text-red-600')
|
||||
|
||||
// Cambiar la fecha marca dirty en el campo _fecha y se guarda en el bulk
|
||||
fireEvent.change(fechas[1], { target: { value: '2026-12-01' } })
|
||||
fireEvent.click(screen.getByText('Guardar cambios'))
|
||||
|
||||
expect(onGuardar).toHaveBeenCalledTimes(1)
|
||||
const dirty = onGuardar.mock.calls[0][0]
|
||||
expect(dirty['409-2']).toEqual({ certificacion_gas_fecha: '2026-12-01' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -24,6 +24,14 @@ function valorOriginal(datos: DatosCaptura, unidad: string, campo: string): stri
|
||||
return datos[unidad]?.[campo] ?? null
|
||||
}
|
||||
|
||||
/** Fecha de hoy en formato ISO local (YYYY-MM-DD) */
|
||||
function hoyISO(): string {
|
||||
const d = new Date()
|
||||
const mes = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const dia = String(d.getDate()).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${mes}-${dia}`
|
||||
}
|
||||
|
||||
// ─── Componente de celda ─────────────────────────────────────────────
|
||||
|
||||
interface CeldaProps {
|
||||
@@ -32,21 +40,53 @@ interface CeldaProps {
|
||||
options?: string[]
|
||||
onChange: (nuevo: string | null) => void
|
||||
dirty: boolean
|
||||
/** Campo de fecha emparejado (booleanos de Obra) */
|
||||
conFecha?: string
|
||||
valorFecha?: string | null
|
||||
dirtyFecha?: boolean
|
||||
onFechaChange?: (nuevo: string | null) => void
|
||||
}
|
||||
|
||||
function CeldaEditable({ tipo, valor, options, onChange, dirty }: CeldaProps) {
|
||||
function CeldaEditable({
|
||||
tipo,
|
||||
valor,
|
||||
options,
|
||||
onChange,
|
||||
dirty,
|
||||
conFecha,
|
||||
valorFecha,
|
||||
dirtyFecha,
|
||||
onFechaChange,
|
||||
}: CeldaProps) {
|
||||
const bordeDirty = dirty ? 'ring-2 ring-accent/40' : ''
|
||||
|
||||
if (tipo === 'boolean') {
|
||||
const checked = valor === 'true'
|
||||
// Semáforo del campo fecha emparejado (Obra): verde si el check está activo,
|
||||
// rojo si la fecha ya pasó sin activarse, neutro mientras no llegue la fecha.
|
||||
const colorFecha = checked
|
||||
? 'text-green-600 dark:text-green-400'
|
||||
: valorFecha && valorFecha < hoyISO()
|
||||
? 'text-red-600 dark:text-red-400'
|
||||
: 'text-[#111827] dark:text-[#E2E8F0]'
|
||||
const bordeFecha = dirtyFecha ? 'ring-2 ring-accent/40' : ''
|
||||
return (
|
||||
<label className="flex items-center justify-center">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
{conFecha && (
|
||||
<input
|
||||
type="date"
|
||||
value={valorFecha ?? ''}
|
||||
onChange={(e) => onFechaChange?.(e.target.value || null)}
|
||||
className={`w-28 border-0 bg-transparent px-1 py-0.5 text-xs focus:outline-none [color-scheme:light] dark:[color-scheme:dark] ${colorFecha} ${bordeFecha}`}
|
||||
/>
|
||||
)}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={valor === 'true'}
|
||||
checked={checked}
|
||||
onChange={(e) => onChange(e.target.checked ? 'true' : null)}
|
||||
className="h-4 w-4 rounded border-[#D1D5DB] text-accent focus:ring-accent/20 dark:border-[#334155]"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -133,6 +173,12 @@ export function TablaCaptura({
|
||||
(paginaActual + 1) * POR_PAGINA,
|
||||
)
|
||||
|
||||
/** Campos a renderizar: ocultar fechas emparejadas (viven dentro de su checkbox) */
|
||||
const camposVisibles = useMemo(
|
||||
() => campos.filter((c) => !campos.some((o) => o.conFecha === c.campo)),
|
||||
[campos],
|
||||
)
|
||||
|
||||
/** ¿Hay cambios pendientes? */
|
||||
const tieneDirty = useMemo(() => {
|
||||
for (const u of Object.values(dirty)) {
|
||||
@@ -212,7 +258,7 @@ export function TablaCaptura({
|
||||
<th className="sticky left-0 z-10 border-r border-[#E5E7EB] bg-[#F9FAFB] px-3 py-2.5 text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:border-[#334155] dark:bg-[#1E293B] dark:text-[#94A3B8]">
|
||||
Unidad
|
||||
</th>
|
||||
{campos.map((c) => (
|
||||
{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]"
|
||||
@@ -236,9 +282,10 @@ export function TablaCaptura({
|
||||
<td className="sticky left-0 z-10 border-r border-[#E5E7EB] bg-inherit px-3 py-2 text-xs font-semibold text-[#111827] dark:border-[#334155] dark:text-[#E2E8F0]">
|
||||
{unidad}
|
||||
</td>
|
||||
{campos.map((c) => {
|
||||
{camposVisibles.map((c) => {
|
||||
const val = valorOriginal(datos, unidad, c.campo)
|
||||
const isDirty = dirty[unidad]?.[c.campo] !== undefined
|
||||
const campoFecha = c.conFecha
|
||||
return (
|
||||
<td
|
||||
key={`${unidad}-${c.campo}`}
|
||||
@@ -250,6 +297,16 @@ export function TablaCaptura({
|
||||
options={c.options}
|
||||
onChange={(nuevo) => handleChange(unidad, c.campo, nuevo)}
|
||||
dirty={isDirty}
|
||||
conFecha={campoFecha}
|
||||
valorFecha={
|
||||
campoFecha ? valorOriginal(datos, unidad, campoFecha) : undefined
|
||||
}
|
||||
dirtyFecha={campoFecha ? dirty[unidad]?.[campoFecha] !== undefined : false}
|
||||
onFechaChange={
|
||||
campoFecha
|
||||
? (nuevo) => handleChange(unidad, campoFecha, nuevo)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
)
|
||||
|
||||
@@ -7,6 +7,8 @@ export interface CampoDef {
|
||||
tipo: string
|
||||
label: string
|
||||
options?: string[]
|
||||
/** Campo de fecha emparejado (solo booleanos de Obra) */
|
||||
conFecha?: string
|
||||
}
|
||||
|
||||
export interface ResumenDepartamento {
|
||||
@@ -25,12 +27,18 @@ export type DepartamentoId = (typeof DEPARTAMENTOS)[number]
|
||||
|
||||
export const CAMPOS_POR_DEPARTAMENTO: Record<DepartamentoId, CampoDef[]> = {
|
||||
OBRA: [
|
||||
{ campo: 'certificacion_gas', tipo: 'boolean', label: 'Certificación Gas' },
|
||||
{ campo: 'to_c4_fianza', tipo: 'boolean', label: 'TO y C4-Fianza' },
|
||||
{ campo: 'regimen_inscrito', tipo: 'boolean', label: 'Régimen Inscrito' },
|
||||
{ campo: 'no_oficial', tipo: 'boolean', label: 'No. Oficial' },
|
||||
{ campo: 'clave_catastral', tipo: 'boolean', label: 'Clave Catastral' },
|
||||
{ campo: 'listas_avaluo', tipo: 'boolean', label: 'Listas para Avaluo' },
|
||||
{ campo: 'certificacion_gas', tipo: 'boolean', label: 'Certificación Gas', conFecha: 'certificacion_gas_fecha' },
|
||||
{ campo: 'certificacion_gas_fecha', tipo: 'date', label: 'Fecha Certificación Gas' },
|
||||
{ campo: 'to_c4_fianza', tipo: 'boolean', label: 'TO y C4-Fianza', conFecha: 'to_c4_fianza_fecha' },
|
||||
{ campo: 'to_c4_fianza_fecha', tipo: 'date', label: 'Fecha TO y C4-Fianza' },
|
||||
{ campo: 'regimen_inscrito', tipo: 'boolean', label: 'Régimen Inscrito', conFecha: 'regimen_inscrito_fecha' },
|
||||
{ campo: 'regimen_inscrito_fecha', tipo: 'date', label: 'Fecha Régimen Inscrito' },
|
||||
{ campo: 'no_oficial', tipo: 'boolean', label: 'No. Oficial', conFecha: 'no_oficial_fecha' },
|
||||
{ campo: 'no_oficial_fecha', tipo: 'date', label: 'Fecha No. Oficial' },
|
||||
{ campo: 'clave_catastral', tipo: 'boolean', label: 'Clave Catastral', conFecha: 'clave_catastral_fecha' },
|
||||
{ campo: 'clave_catastral_fecha', tipo: 'date', label: 'Fecha Clave Catastral' },
|
||||
{ campo: 'listas_avaluo', tipo: 'boolean', label: 'Listas para Avaluo', conFecha: 'listas_avaluo_fecha' },
|
||||
{ campo: 'listas_avaluo_fecha', tipo: 'date', label: 'Fecha Listas para Avaluo' },
|
||||
{ campo: 'habitabilidad', tipo: 'date', label: 'Habitabilidad a partir de' },
|
||||
{ campo: 'comentarios_obra', tipo: 'text', label: 'Comentarios' },
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user