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. Propietarioanalítica de clínicaenspecs/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.tspuro + 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-ESen etiquetas, importes, fechas y avisos; verificación concomprobar: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