Tasks: Panel de analítica de CitaClara (004)

Input: Design documents from specs/004-panel-analitica/ Prerequisites: plan.md, spec.md, research.md, data-model.md, contracts/analitica.md, quickstart.md Tests: YES — Vitest unit + integración + contrato por Constitución VI y plan.md (cada FR/SC con test trazable). Playwright existente para responsive; SC-001 (<10 s) es protocolo manual. Organization: Tasks grouped by user story. Cada historia es un incremento independiente y testeable.

Format: [ID] [P?] [Story] Description

  • [P]: Can run in parallel (different files, no dependencies)
  • [Story]: Which user story this task belongs to (e.g., US1, US2, US3)
  • Include exact file paths in descriptions

Path Conventions

Monolito existente: lib/ (dominio puro), prisma/ (migración + semilla), app/(panel)/ + app/api/ (fachadas finas), components/ (SVG), tests/unit|integration|contract. Sin backend//frontend/ nuevos (plan.md Structure Decision). FR-010 prohíbe filtros, comparativas, exportación y escrituras.


Phase 1: Setup (Shared Infrastructure)

Purpose: Toolchain verificado e inventario de la base reutilizable sin instalar nada

  • T001 Verificar toolchain Node 22 + dependencias instaladas (date-fns-tz, Prisma 6, Vitest) con node --version y npm ls date-fns-tz en repo root
  • T002 [P] Inventariar reutilizables en lib/ventanas.ts (capturarAhora, semanasAnalitica, denominadorOcupacion, etiquetarJornada), lib/tiempo.ts (formatearDia), lib/dinero.ts (sumarCentimos, formatearEuros) y confirmar cero dependencias nuevas (plan.md: SVG propio, sin recharts)
  • T003 [P] Inventariar auth y patrones de fachada en lib/auth.ts (exigirClinica), lib/sesion.ts (clinicaDeLaSesion), app/api/agenda/route.ts y app/(panel)/agenda/page.tsx como plantilla de /api/analitica y /analitica

Phase 2: Foundational (Blocking Prerequisites)

Purpose: Migración S-05 + escritura del precio congelado + semilla que TODAS las historias necesitan (sin ella FR-004/SC-002 es imposible)

⚠️ CRITICAL: No user story work can begin until this phase is complete

  • T004 Añadir columna precioCongeladoCentimos Int al modelo Cita en prisma/schema.prisma (enmienda S-05 propiedad de la 001, 001 FR-006; copia inmutable al reservar; la 004 solo la lee)
  • T005 Crear migración SQL prisma/migrations/XXXX_add_precio_congelado_cita/migration.sql con ALTER TABLE "Cita" ADD COLUMN "precioCongeladoCentimos" INTEGER NOT NULL DEFAULT 0 + retrolleno UPDATE "Cita" SET "precioCongeladoCentimos" = (SELECT "precioCentimos" FROM "Servicio" WHERE "Servicio"."id" = "Cita"."servicioId") y ejecutar npx prisma migrate dev + npx prisma generate
  • T006 Escribir precioCongeladoCentimos al reservar en app/api/citas/route.ts (dentro de la transacción de alta existente, junto a fin = inicio + duracionMin: leer servicio.precioCentimos vigente y copiarlo; nunca recalcular después; sin tocar el predicado RN1)
  • T007 Congelar precio por cita en prisma/seed.ts (cada prisma.cita.create incluye precioCongeladoCentimos: servicio.precioCentimos de su servicio; con semilla v1 coincide con oráculos 40/50/35/45 €)
  • T008 [P] Crear esqueleto lib/analitica.ts con tipos CitaAnalitica, ProfesionalAnalitica, ServicioAnalitica y firmas vacías de tasaNoAsistencia, ocupacionSemanal, ingresosPorServicio, evolucionSemanal, calcularAnalitica según contracts/analitica.md (cero escritura, cero Prisma en este módulo)

Checkpoint: Foundation ready — npx prisma migrate status en verde, POST /api/citas congela precio, semilla v1 con congelados; las historias pueden empezar


Phase 3: User Story 1 — Tasa de no asistencia por profesional (Priority: P1) 🎯 MVP

Goal: Barras por profesional con no_asistidas / total(con canceladas) en la ventana + tasa de la clínica, «7,3 %» con coma, «—» sin datos (FR-002)

Independent Test: Semilla v1 en ventana 28/09/2026 ⇒ María 11/150 = 7,3 %, Jorge 16/127 = 12,6 %, Lucía 14/132 = 10,6 %, clínica 41/409 = 10,0 %; regenerar en la misma semana ⇒ bit a bit (QS-1; US1-AS1…AS3)

Tests for User Story 1

NOTE: Write these tests FIRST, ensure they FAIL before implementation

  • T009 [P] [US1] Tests unitarios de tasa en tests/unit/test_analitica.test.ts (QS-1: historia sintética con canceladas dentro del denominador, reservada futura y sin desenlace fuera y aparte, sin citas ⇒ «—» no 0 %, un decimal con coma «7,3 %»; oráculos semilla v1 solo cuando la ventana coincide)
  • T010 [P] [US1] Test de contrato GET /api/analitica en tests/contract/test_analitica_get.test.ts (clave válida ⇒ 200 con tasas[] por profesional; sin clave ⇒ 401 SIN_ACCESO es-ES; forma tasa/texto/partes)

Implementation for User Story 1

  • T011 [US1] Implementar tasaNoAsistencia(citas) en lib/analitica.ts (no_asistidas / (completada+no_asistida+cancelada) en ventana, reservada fuera, null ⇒ «—», redondeo a 1 decimal es-ES)
  • T012 [US1] Implementar GET /api/analitica/route.ts mínimo viable (solo exigirClinica + capturarAhora + semanasAnalitica + lectura clinicaId + inicio ∈ ventana + tasaNoAsistencia por profesional de la clínica incl. activo=false; sin escritura; otros agregados pueden ir vacíos tras este hito)

Checkpoint: US1 funciona sola — npm run test -- test_analitica test_analitica_get en verde para tasas; la página aún puede no existir


Phase 4: User Story 2 — Ingresos por servicio al céntimo (Priority: P1)

Goal: Barras por servicio con Σ precioCongeladoCentimos de completada, formato «5.040,00 €», total 14.355,00 €, inmóvil ante cambio de tarifa (FR-004/FR-007)

Independent Test: Semilla v1 ⇒ 5.040,00 € (126) + 4.900,00 € (98) + 2.030,00 € (58) + 2.385,00 € (53) = 14.355,00 €; cancelada/no_asistida = 0,00 €; cambiar tarifa y recargar ⇒ sin movimiento (QS-3; US2-AS1…AS3)

Tests for User Story 2

  • T013 [P] [US2] Tests unitarios de ingresos en tests/unit/test_analitica.test.ts (QS-3: solo completada suma congelado, resto 0 €, céntimos exactos sin flotantes, servicio sin completadas ⇒ «0,00 €», cambio de servicio.precioCentimos no mueve ingresos ya cerrados; secuencial con T009: mismo fichero)

Implementation for User Story 2

  • T014 [US2] Implementar ingresosPorServicio(citas) en lib/analitica.ts (Σ precioCongeladoCentimos de completada con sumarCentimos, resto 0 €; incluye servicios activo=false; prohibido leer servicio.precioCentimos vigente) y exponer ingresos{porServicio,totalCentimos,totalTexto con formatearEuros} en app/api/analitica/route.ts (secuencial con T012: mismo fichero)

Checkpoint: US1 + US2 en verde — ingresos cuadran al céntimo (SC-002) y son inmóviles ante tarifa


Phase 5: User Story 3 — Ocupación semanal por profesional (Priority: P2)

Goal: UN número por profesional = min(completadas) / denominadorOcupacion(vigente), con jornada declarada (FR-003/FR-008)

Independent Test: Semilla v1 + jornada 09:00–20:00 ⇒ María 6.615/26.400 = 25,1 %, Jorge 4.935/26.400 = 18,7 %, Lucía 4.125/26.400 = 15,6 % con «09:00–20:00» visible; cancelada/no_asistida = 0 min (QS-2; US3-AS1…AS3)

Tests for User Story 3

  • T015 [P] [US3] Tests unitarios de ocupación en tests/unit/test_analitica.test.ts (QS-2: minutos reales fin−inicio de completadas, resto 0 min, denominador vía denominadorOcupacion vigente nunca literal, jornada reconfigurada cambia denominador y se declara)

Implementation for User Story 3

  • T016 [US3] Implementar ocupacionSemanal(citas, denominadorMin) en lib/analitica.ts y cablear denominadorOcupacion(jornadaInicio, jornadaFin, diasLaborables, 8) + etiquetarJornada vigentes de la clínica en app/api/analitica/route.ts (prohibido hardcodear 26.400; agregar como nº de 8 semanas, no serie semanal)

Checkpoint: US1–US3 en verde — tasas, ingresos y ocupación cuadran con oráculos en ventana de referencia


Phase 6: User Story 4 — Evolución de las últimas 8 semanas (Priority: P2)

Goal: 8 filas S-8…S-1 con citas/completadas/no_asistidas/canceladas/ingresos, etiquetas derivadas, curso y futuras fuera (FR-005/FR-008)

Independent Test: Ventana 28/09/2026 ⇒ tabla exacta US4-AS1; suma = 409 e 14.355,00 €; 82 futuras fuera (QS-4; US4-AS1…AS3)

Tests for User Story 4

  • T017 [P] [US4] Tests unitarios de evolución en tests/unit/test_analitica.test.ts (QS-4: 8 filas con etiquetas S-N: DD/MM/AAAA–DD/MM/AAAA derivadas de semanasAnalitica, curso fuera, futuras fuera, suma = historia; ventana móvil con hoy inyectado ⇒ filas distintas pero coherentes)

Implementation for User Story 4

  • T018 [US4] Implementar evolucionSemanal(citas, semanas) + calcularAnalitica(entrada) foto coherente en lib/analitica.ts (un ahora por cálculo, sinDesenlace aparte) y exponer evolucion[] + ventana{semanas, generadaCon} en app/api/analitica/route.ts

Checkpoint: US1–US4 en verde — npm run test -- test_analitica reproduce oráculos en ventana de referencia y cuadra con historia real fuera de ella (SC-003)


Phase 7: User Story 5 — Entrar con la misma clave y solo mirar (Priority: P3)

Goal: Misma clave que agenda, aviso genérico sin clave, cero escritura, aislamiento total por clinicaId (FR-001/FR-006/FR-010)

Independent Test: Clave correcta ⇒ entra y ve gráficos; incorrecta ⇒ aviso es-ES sin revelar claves; navegar + recargar ⇒ diff BD = 0; 2 clínicas ⇒ 100 % propio / 0 % ajeno (QS QC-6/QC-7; US5-AS1…AS4)

Tests for User Story 5

  • T019 [P] [US5] Tests de aislamiento + solo lectura en tests/integration/test_analitica_aislamiento.test.ts (QC-6: clínica B con datos propios ⇒ calcularAnalitica/API de A con 0 % de B; QC-7: volcar BD antes/después de leer ⇒ cero escrituras; métodos POST/PATCH/DELETE ⇒ 405)
  • T020 [P] [US5] Test de página y acceso en tests/contract/test_analitica_get.test.ts (sin sesión en /analitica ⇒ redirect /acceso; contrato ya cubre 401 API en T010; aviso genérico es-ES)

Implementation for User Story 5

  • T021 [US5] Crear página app/(panel)/analitica/page.tsx (Server Component force-dynamic: clinicaDeLaSesion o redirect('/acceso'), una lectura clinicaId + ventana, calcularAnalitica, 4 grafico-barras con eje + valores, estados «Aún no hay datos suficientes»/«—»/«0,00 €», sin formularios ni botones que muten)
  • T022 [P] [US5] Crear componente components/grafico-barras.tsx (SVG inline accesible role="img" + title, letra ≥ 16 px, responsive viewBox sin scroll a 390 px, paleta con contraste verificado, cero jerga prohibida)
  • T023 [US5] Añadir enlace «Analítica» en app/(panel)/layout.tsx y rechazar escrituras en app/api/analitica/route.ts (POST/PATCH/PUT/DELETE ⇒ 405 «Esta página es de solo lectura»; el GET nunca escribe)
  • T024 [US5] Auditar where por clinicaId en app/api/analitica/route.ts y app/(panel)/analitica/page.tsx (citas + profesionales + servicios filtrados; profesionales/servicios incluyen activo=false; ningún groupBy/findMany sin filtro de clínica aunque hoy solo haya una)

Checkpoint: Toda la feature funciona — página visible con la misma clave, cero escritura, cero fuga entre clínicas


Phase 8: Polish & Cross-Cutting Concerns

Purpose: Puertas de calidad SC-002…SC-007 + Constitución II/VI/VII/VIII

  • T025 Ejecutar validación quickstart QS-1…QC-7 completa y npm run test -- test_analitica test_analitica_aislamiento test_analitica_get test_antisolape test_ventanas_tiempo más npm run typecheck en repo root (suite en verde incl. regresión antisolape; SC-002 céntimo, SC-003 determinismo misma ventana, SC-004 cero escritura, SC-006 aislamiento)
  • T026 [P] Ampliar scripts/comprobar-es.ts con catálogo 004 y ejecutar npm run comprobar:es (100 % es-ES, formatos «5.040,00 €», «DD/MM/AAAA», «S-1…S-8») + grep -rn "dataset\|serie\|KPI\|coeficiente\|métrica\|agregado" --include="*.tsx" --include="*.ts" "app/(panel)/analitica" "components/grafico-barras.tsx" "lib/analitica.ts" ⇒ cero hits (SC-007 jerga)
  • T027 [P] Verificar contraste (texto ≥ 4,5:1, gráfico ≥ 3:1) y responsive 1440/390 sin desplazamiento en /analitica (revisión visual registrada + test de estilos font-size ≥ 16 px; SC-001 manual con cronómetro <10 s) y ejecutar npm run lint + npm run format:check corrigiendo desviaciones
  • T028 Actualizar specs/MAPA.md (004 → «en revisión» durante la implementación y a «implementada» con suites en verde) sin cambiar propietarios

Dependencies & Execution Order

Phase Dependencies

  • Setup (Phase 1): No dependencies — can start immediately
  • Foundational (Phase 2): Depends on Setup — BLOCKS all user stories (S-05 + semilla + esqueleto parametrizan US1–US4)
  • User Stories (Phase 3+): All depend on Foundational completion
    • US1 (P1) → US2 (P1, necesita API mínima de US1) → US3 (P2, necesita denominador vigente) → US4 (P2, necesita semanas + foto coherente) → US5 (P3, necesita los 4 agregados para la página)
    • Con un solo desarrollador: orden P1 → P1 → P2 → P2 → P3
  • Polish (Phase 8): Depends on all desired user stories being complete

User Story Dependencies

  • User Story 1 (P1): Can start after Foundational — no dependencies on other stories
  • User Story 2 (P1): Needs US1 (GET /api/analitica mínimo + tasaNoAsistencia como patrón) — testeable sola sumando congelados
  • User Story 3 (P2): Needs Foundational + denominadorOcupacion vigente (006) — testeable sola reconfigurando jornada
  • User Story 4 (P2): Needs US1–US3 (semanasAnalitica + foto coherente) — testeable sola con hoy inyectado
  • User Story 5 (P3): Needs US1–US4 (los 4 agregados para dibujar la página) — testeable sola con 2 clínicas y diff

Within Each User Story

  • Tests MUST be written and FAIL before implementation (T009/T010 antes de T011; T013 antes de T014; T015 antes de T016; T017 antes de T018; T019/T020 antes de T021)
  • Puras antes que cableado (lib/analitica.ts antes que app/api/analitica/route.ts / página)
  • Core implementation before integration (API mínima T012 antes de ingresos/ocupación/evolución)
  • Story complete before moving to next priority

Parallel Opportunities

  • T002 + T003 (inventarios ventanas/dinero y auth/patrones) en paralelo
  • T008 (esqueleto puro) en paralelo con T004–T007 (migración + alta + semilla, distintos ficheros)
  • T009 + T010 (unit tasa + contrato GET) en paralelo (distintos ficheros); T013 → T015 → T017 secuencial (mismo tests/unit/test_analitica.test.ts, coordinar por bloque)
  • T011 → T014 → T016 → T018 secuencial (mismo lib/analitica.ts + app/api/analitica/route.ts, coordinar por función)
  • T019 + T020 (integración aislamiento + contrato página) en paralelo; T022 (SVG) en paralelo con T021 (página, distinto fichero)
  • T026 + T027 (es/jerga/contraste + lint/format) en paralelo tras T025

Parallel Example: User Story 1

# Launch all tests for User Story 1 together:
Task: "Tests unitarios de tasa en tests/unit/test_analitica.test.ts (QS-1)"
Task: "Test de contrato GET /api/analitica en tests/contract/test_analitica_get.test.ts (401/200 tasas)"

Parallel Example: User Story 5

# Launch page + chart component together (different files):
Task: "Crear página app/(panel)/analitica/page.tsx (Server Component solo lectura)"
Task: "Crear componente components/grafico-barras.tsx (SVG accesible)"

Implementation Strategy

MVP First (User Story 1 Only)

  1. Complete Phase 1: Setup (T001–T003)
  2. Complete Phase 2: Foundational (T004–T008, S-05 + semilla + esqueleto)
  3. Complete Phase 3: User Story 1 (T009–T012, tasas + API mínima)
  4. STOP and VALIDATE: npm run test -- test_analitica test_analitica_get para tasas + QS-1; oráculos 7,3/12,6/10,6 % en ventana de referencia
  5. Deploy/demo if ready (tasa como argumento comercial mínimo)

Incremental Delivery

  1. Setup + Foundational → base migrada y sembrada
  2. Add US1 → tasas → validar (MVP)
  3. Add US2 → ingresos al céntimo inmóviles → validar QS-3 (SC-002)
  4. Add US3 → ocupación agregada con jornada declarada → validar QS-2
  5. Add US4 → evolución 8 filas coherente → validar QS-4 (SC-003)
  6. Add US5 → página con 4 gráficos + aislamiento + cero escritura → validar QC-6/QC-7
  7. Polish → es/jerga/contraste/responsive + typecheck/lint → merge

Parallel Team Strategy

Un solo agente por spec (Constitución: un agente, una spec). Dentro de la 004, con dos desarrolladores tras Foundational:

  1. Ambos completan Setup + Foundational juntos
  2. Dev A: US1 (T009–T012) → US2 (T013–T014) → US4 (T017–T018)
  3. Dev B: US3 (T015–T016) → US5 (T019–T024) tras API mínima de Dev A
  4. Juntos: Polish (T025–T028)

Notes

  • [P] tasks = different files, no dependencies
  • [Story] label maps task to specific user story for traceability (US1 ↔ FR-002; US2 ↔ FR-004/FR-007; US3 ↔ FR-003/FR-008; US4 ↔ FR-005/FR-008; US5 ↔ FR-001/FR-006/FR-009/FR-010)
  • Cada historia es independientemente completable y testeable (oráculos semilla v1 solo en ventana 28/09/2026; fuera de ella cuadrar contra historia real, S-08)
  • Verify tests fail before implementing
  • Commit after each task or logical group
  • FR-010: filtros, comparativas, exportación, recordatorios, pagos, predicciones o escrituras ⇒ alcance fantasma, no se implementan
  • 006 FR-010: redefinir «ahora», zona, bordes o ventanas fuera de lib/(tiempo|ventanas).ts es defecto