Implementation Plan: Panel de analítica de CitaClara (004)

Branch: 004-panel-analitica | Date: 2026-09-30 | Spec: specs/004-panel-analitica/spec.md

Input: Feature specification from specs/004-panel-analitica/spec.md (incl. clarificaciones 2026-09-29/30, US1–US5, FR-001…FR-010, SC-001…SC-007, Assumptions S-08)

Note: Este plan decide stack y diseño (la constitución no los impone). Toda la UI y textos en español de España.

Summary

Página de solo lectura /analitica (misma clave que agenda) con 4 gráficos SVG propios: tasa de no asistencia por profesional (US1/P1), ingresos por servicio al céntimo con precio congelado (US2/P1), ocupación semanal agregada con denominador vigente de la 006 (US3/P2) y evolución de 8 semanas recalculada en cada carga (US4/P2), más acceso con la misma clave y garantía de cero escritura (US5/P3). Dominio puro en lib/analitica.ts sobre lib/ventanas.ts (006) + lib/dinero.ts; prerrequisito bloqueante: migración S-05 Cita.precioCongeladoCentimos (propiedad 001, ausente en código) + escritura al reservar + semilla. Sin dependencias nuevas, sin filtros/exportaciones (FR-010).

Technical Context

Language/Version: TypeScript 5.6 strict (incl. noUncheckedIndexedAccess), Node.js 22 LTS (igual que 001/006/007; sin cambio de stack por Constitución — contexto y límites).

Primary Dependencies: Next.js 15 (App Router Server Components + Route Handlers, cookies()), React 19, Prisma 6, date-fns-tz + Intl es-ES Europe/Madrid (vía lib/tiempo.ts/lib/ventanas.ts), zod solo si valida query (sin query en v1). Nada nuevo que instalar (gráficos SVG inline, sin recharts/chart.js por R5).

Storage: PostgreSQL 16 vía Prisma. NINGUNA tabla nueva (FR-006/S-07). Único cambio persistente: columna Cita.precioCongeladoCentimos Int NOT NULL (enmienda S-05 propiedad de la 001) + escritura en alta + semilla con retrolleno. Lecturas: una foto coherente por carga (clinicaId + inicio ∈ ventana, force-dynamic, sin caché entre clínicas).

Testing: Vitest unit (puras + oráculos semilla v1 en ventana de referencia) + integración (2 clínicas, diff cero-escritura, ventana móvil con hoy inyectado) + contrato (GET /api/analitica: 200/401/405, forma es-ES) + comprobar:es + grep jerga + contraste. Playwright existente para 1440/390 sin desplazamiento; SC-001 (<10 s) es protocolo manual, no aserción auto.

Target Platform: Mismo monolito web Next.js 15 (Node 22, Docker + Postgres 16). Responsive 1440 px (recepción) y 390 px (móvil), letra ≥ 16 px, contraste texto ≥ 4,5:1 / gráfico ≥ 3:1.

Project Type: Web application monolítica (Route Handler = backend fino, Server Component = UI, lib/ = dominio puro compartido).

Performance Goals: Una lectura de ventana (~500 citas) + agregación O(n) en memoria por carga; p95 <200 ms en local heredado de la 001 (sin objetivo nuevo; no bloqueante). 4 SVG estáticos sin JS cliente.

Constraints: Español de España en todo texto visible (VIII), formatos 5.040,00 €, DD/MM/AAAA, S-1…S-8, jornada 09:00–20:00, Lun–Vie (II); céntimos enteros, mitad-arriba si hubiera fracción (FR-007); semanas derivadas, nunca constantes (FR-008); 4 gráficos con eje + valores, cero jerga prohibida (FR-009); ventana recalculada en cada carga, curso y futuras fuera (FR-005); todo acotado a clinicaId, fuga = bloqueante (FR-001); cero escritura en consulta (FR-006); fuera FR-010 (filtros, comparativas, exportación, recordatorios, pagos, predicciones, escrituras).

Scale/Scope: 1–N clínicas; ~40 pacientes y ~500 citas por clínica (semilla v1: 409 + 82); 8 semanas cerradas + semana en curso excluida + 2 futuras; 3 profesionales y 4 servicios por clínica en demo.

Constitution Check

GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.

  • I. Spec First: todo comportamiento (FR-001…FR-010, US1–US5, SC-001…SC-007) vive en spec.md; este plan no añade comportamiento. Propietario analítica de clínica en specs/MAPA.md; enmienda S-05 acordada con propietario 001 (MAPA, 001 FR-006).
  • II. Exactitud numérica y temporal: céntimos enteros + redondeo mitad-arriba + ejemplos límite en research/data-model/contracts (FR-007); tiempo delegado a 006 (capturarAhora, semanasAnalitica, denominadorOcupacion, Europe/Madrid, etiquetas derivadas FR-008); oráculos versionados S-08 (ventana 28/09/2026).
  • III. Cero solapes: esta feature no escribe en agenda (solo lee y agrega); ningún cambio a RN1/RN2; la escritura nueva (precio congelado al reservar) va en la transacción de alta existente sin tocar el predicado de exclusión; suite antisolape en verde como regresión.
  • IV. Simplicidad / cero alcance fantasma: lib/analitica.ts puro + página/API finas + 1 columna + SVG propio; cero dependencias, cero tablas, cero endpoints de escritura, cero filtros/exportaciones/predicciones (FR-010). Cada firma traza a un FR (contracts §Trazabilidad).
  • V. Datos reproducibles: semilla Eleva v1 reutilizada (SEMILLA_PRNG=1); oráculos citan historia reproducible; fuera de la ventana de referencia los tests cuadran contra la historia real (SC-003), sin datos ad hoc.
  • VI. Tests con la spec: matriz test↔FR/SC en contracts + quickstart QS-1…QC-7; suite en verde como puerta de merge; cada tasa/ocupación/importe/fila tiene test trazable.
  • VII. Interfaz clara y moderna: 4 gráficos con eje + valores, lenguaje de clínica sin jerga, letra/contraste/responsive verificables (SC-007); sin formación ni manual (SC-001).
  • VIII. Español de España: es-ES en etiquetas, importes, fechas y avisos; verificación con comprobar:es + grep de jerga.

Gates: PASS — sin violaciones que justificar. Complexity Tracking queda vacío.

Project Structure

Documentation (this feature)

specs/004-panel-analitica/
├── plan.md              # This file
├── research.md          # Phase 0 output
├── data-model.md        # Phase 1 output
├── quickstart.md        # Phase 1 output
├── contracts/           # Phase 1 output
│   └── analitica.md     # GET /api/analitica + lib/analitica.ts + página
└── tasks.md             # Phase 2 output (/speckit.tasks — NOT created here)

Source Code (repository root)

lib/
├── analitica.ts            # NUEVO: tasaNoAsistencia + ocupacionSemanal + ingresosPorServicio + evolucionSemanal + calcularAnalitica (puras, cero escritura)
├── ventanas.ts             # EXISTENTE (006): capturarAhora + semanasAnalitica + denominadorOcupacion + etiquetarJornada (se reutiliza, no se redefine)
├── tiempo.ts               # EXISTENTE: ZONA_HORARIA, formatearDia (se reutiliza)
├── dinero.ts               # EXISTENTE: sumarCentimos + formatearEuros (se reutiliza)
├── validacion.ts           # EXISTENTE: AVISO_SIN_ACCESO (se reutiliza; sin esquema nuevo en v1)
└── auth.ts / sesion.ts     # EXISTENTE: exigirClinica / clinicaDeLaSesion (misma clave que agenda)

prisma/
├── schema.prisma           # + Cita.precioCongeladoCentimos Int (S-05, propiedad 001)
└── seed.ts                 # + congelar precio por cita al crear

app/
├── (panel)/layout.tsx      # + enlace «Analítica»
├── (panel)/analitica/page.tsx  # NUEVA: Server Component force-dynamic, 4 gráficos
└── api/analitica/route.ts  # NUEVA: GET solo lectura (401/405), misma foto que la página

components/
└── grafico-barras.tsx      # NUEVO: barras/líneas SVG accesible reutilizado ×4

tests/
├── unit/test_analitica.test.ts                 # NUEVO: puras + oráculos + céntimo + congelado inmóvil + vacíos
├── integration/test_analitica_aislamiento.test.ts  # NUEVO: 2 clínicas + diff cero-escritura + ventana móvil
└── contract/test_analitica_get.test.ts         # NUEVO: 200/401/405 + forma es-ES + 4 gráficos

scripts/comprobar-es.ts    # + catálogo 004 (sin jerga, es-ES)

Structure Decision: monolito existente, sin directorios nuevos salvo specs/004-panel-analitica/contracts/ y app/(panel)/analitica/ + app/api/analitica/. Dominio puro en lib/ (testeable sin BD/navegador); rutas y página como fachadas finas sobre el mismo cálculo (una sola definición).

Complexity Tracking

Fill ONLY if Constitution Check has violations that must be justified

Violation Why Needed Simpler Alternative Rejected Because
— — —