Skip to content

Sidebar derecha — Actividad Global (Calendario)

Última actualización: 06-01-2026 09:01 PM

Este módulo implementa un calendario global en la barra lateral derecha para visualizar actividad por día agregada a través de todos los proyectos.

La UI:

  • Muestra 12 meses como bloques independientes.
  • Cada mes es una grilla de 7 columnas (D–S).
  • Cada celda (día) muestra un número: horas/día (máximo 1 decimal).
  • El color de cada celda es un degradado verde según intensidad (relativa al máximo del año seleccionado).
  • Hace auto-scroll al mes actual (si el año seleccionado es el actual) o al primer mes con actividad (si no).

  • Agregación global: suma minutos por fecha YYYY-MM-DD desde todos los retos de todos los proyectos.
  • Selector de año (derivado automáticamente de las fechas presentes).
  • Render por meses (12 secciones) con encabezado del mes.
  • Grilla del mes con padding inicial/final para alinear semanas.
  • Número dentro de cada celda: horas con 1 decimal (redondeo).
  • Colores en verdes: 0 neutro; 4 niveles de verde.
  • Auto-focus/auto-scroll al mes relevante.
  • No filtra/selecciona retos al hacer click en un día (mejora opcional).
  • No tiene tooltip custom (solo title HTML).
  • No tiene botón “Ir a hoy” (fácil de agregar, opcional).
  • No diferencia “solo done” vs “todos” (opcional si se decide).
  • No persiste preferencias de UI del calendario (año/scroll/mes seleccionado). Por ahora, la única UI persistida en pAVANTI es el colapsado de grupos (local pweb_proyectos_ui_v1 y export/import v2 vía ui.collapsedByProyecto).

El componente recibe:

  • proyectos: Proyecto[]

Esto es intencional: la actividad es global, no depende del proyecto seleccionado.

  • Bloques mensuales con grilla y celdas.
  • Cada celda:
    • valor numérico horas/día,
    • color por intensidad,
    • ring/foco para “hoy” cuando aplica.

La actividad se basa en:

  • NodoReto.fecha (esperado: YYYY-MM-DD, o string que empiece por eso).
  • NodoReto.minutos (número >= 0).

Reglas:

  • Si fecha es inválida → se ignora esa entrada.
  • Si minutos no es número o es negativo → se normaliza a 0 y se ignora si queda 0.
  • Se suman minutos por día: YYYY-MM-DD -> minutos_totales.

La agregación global vive en lib/actividad.ts con:

  • actividadPorDia(proyecto)
  • actividadGlobalPorDia(proyectos)
  • yearsDesdeActividad(actividad)
  • normalizarFechaISO(fecha)

Este diseño mantiene la lógica:

  • sin React,
  • testable,
  • reutilizable si mañana se construyen reportes, export, gráficos, etc.

5) Diseño del calendario (render mensual)

Section titled “5) Diseño del calendario (render mensual)”

Cada mes:

  • encabezado (nombre del mes + año),
  • fila de días de semana: D L M X J V S,
  • grilla grid-cols-7 donde:
    • se insertan celdas vacías antes del día 1 (padding inicial),
    • luego celdas de 1..N,
    • luego celdas vacías al final para completar la última semana.

Motivo: esto asegura que el layout sea “calendario real” y se lea bien por meses.

Se construyen fechas usando “mediodía local” (12:00) para evitar errores típicos por cambios de hora (DST) en algunos entornos.

  • Se evita instanciar a las 00:00 donde pueden ocurrir saltos/offset raros.

6) Número dentro de cada celda (horas/día)

Section titled “6) Número dentro de cada celda (horas/día)”
  • horas = minutos / 60
  • redondeo a 1 decimal: Math.round(horas * 10) / 10
  • presentación:
    • si es entero: "3"
    • si no: "3.5"
  • La UI es densa por naturaleza (muchas celdas).
  • Se usa tipografía pequeña y tabular-nums para alineación.
  • Si el “0” visualmente estorba, es un cambio trivial:
    • mostrar vacío cuando minutos === 0.
    • (Pero hoy se mantiene consistente mostrando 0.)

  • Solo verde (preferencia estética + consistencia).
  • 5 estados:
    • 0: neutro (gris/oscuro)
    • 1..4: verde con intensidad creciente.

La intensidad se calcula relativa al máximo del año seleccionado:

  • maxYear = max(minutos_por_dia_en_year)
  • ratio = minutosDia / maxYear
  • thresholds:
    • <= 0.25 → nivel 1
    • <= 0.50 → nivel 2
    • <= 0.75 → nivel 3
    • 0.75 → nivel 4

Ventaja: escala automática. Riesgo: si hay un outlier enorme en un solo día, comprime el resto. Si eso pasa, alternativa (opcional):

  • usar percentiles o escala logarítmica.

Cuando cambia el año (o al montar):

  • Si year === currentYear → enfoca el mes actual.
  • Si year !== currentYear → enfoca el primer mes con actividad (si existe), si no enero.
  • Se guardan referencias a cada <section> de mes (monthRefs[0..11]).
  • Se ejecuta scrollIntoView({ block: "start", behavior: "smooth" }).
  • Se usa un guard (scrolledYearRef) para no re-disparar el scroll repetidamente.

  • Agregación global: O(total de nodos/reto en todos los proyectos).
  • Render:
    • 12 meses,
    • ~ (35 a 42) celdas por mes (dependiendo padding),
    • total ~ 420–504 celdas/año.

Esto es perfectamente razonable para un sidebar.

Se recomienda (y se aplica) useMemo para:

  • actividad global,
  • años disponibles,
  • meses ya construidos,
  • máximo anual.

  • fecha con tiempo (YYYY-MM-DDTHH:mm...) → se recorta a 10 chars.
  • fechas inválidas: 2025-02-31 → se ignora.
  • minutos undefined/string → se normaliza.
  • año sin actividad: todo neutro, mensaje “no hay actividad”.
  • importaciones: si el import trae fechas fuera de rango (1970..3000), se ignoran.

  1. Botón “Ir a hoy”
  • útil cuando el usuario se pierde en scroll.
  • se implementa reusando scrollIntoView hacia el mes actual.
  1. Click en día → filtrar/centrar retos por fecha
  • el calendario se vuelve una herramienta de navegación real.
  • requiere un contrato hacia TablaProyectos (p.ej. selectedDate en estado global UI).
  1. Modo “Solo done”
  • togglear si se suma actividad de todos los retos o solo completados.
  1. Mostrar número solo si > 0
  • reduce ruido visual.
  1. Tooltip bonito
  • reemplazar title por popover accesible (sin bloquear scroll).
  1. Escala robusta
  • percentiles o log para evitar outliers.

  • Mantener lib/actividad.ts como fuente de verdad:
    • no duplicar agregación en UI.
  • Validar que normalizarFechaISO siga siendo conservador.
  • No introducir dependencias “laterales” entre dominios:
    • este módulo vive en proyectos/ y consume proyectos/lib.

  • UI:
    • /src/proyectos/components/SidebarDerechaProyectos.tsx
  • Lógica:
    • /src/proyectos/lib/actividad.ts
  • Orquestación:
    • /src/proyectos/components/ProyectosApp.tsx (pasa proyectos al sidebar derecho)