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) connode --versionynpm ls date-fns-tzen 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.tsyapp/(panel)/agenda/page.tsxcomo plantilla de/api/analiticay/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 Intal modeloCitaenprisma/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.sqlconALTER TABLE "Cita" ADD COLUMN "precioCongeladoCentimos" INTEGER NOT NULL DEFAULT 0+ retrollenoUPDATE "Cita" SET "precioCongeladoCentimos" = (SELECT "precioCentimos" FROM "Servicio" WHERE "Servicio"."id" = "Cita"."servicioId")y ejecutarnpx prisma migrate dev+npx prisma generate - T006 Escribir
precioCongeladoCentimosal reservar enapp/api/citas/route.ts(dentro de la transacción de alta existente, junto afin = inicio + duracionMin: leerservicio.precioCentimosvigente y copiarlo; nunca recalcular después; sin tocar el predicado RN1) - T007 Congelar precio por cita en
prisma/seed.ts(cadaprisma.cita.createincluyeprecioCongeladoCentimos: servicio.precioCentimosde su servicio; con semilla v1 coincide con oráculos 40/50/35/45 €) - T008 [P] Crear esqueleto
lib/analitica.tscon tiposCitaAnalitica,ProfesionalAnalitica,ServicioAnaliticay firmas vacías detasaNoAsistencia,ocupacionSemanal,ingresosPorServicio,evolucionSemanal,calcularAnaliticasegúncontracts/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,reservadafutura 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/analiticaentests/contract/test_analitica_get.test.ts(clave válida ⇒ 200 contasas[]por profesional; sin clave ⇒ 401SIN_ACCESOes-ES; formatasa/texto/partes)
Implementation for User Story 1
- T011 [US1] Implementar
tasaNoAsistencia(citas)enlib/analitica.ts(no_asistidas / (completada+no_asistida+cancelada)en ventana,reservadafuera, null ⇒ «—», redondeo a 1 decimal es-ES) - T012 [US1] Implementar
GET /api/analitica/route.tsmínimo viable (soloexigirClinica+capturarAhora+semanasAnalitica+ lecturaclinicaId + inicio ∈ ventana+tasaNoAsistenciapor 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: solocompletadasuma congelado, resto 0 €, céntimos exactos sin flotantes, servicio sin completadas ⇒ «0,00 €», cambio deservicio.precioCentimosno mueve ingresos ya cerrados; secuencial con T009: mismo fichero)
Implementation for User Story 2
- T014 [US2] Implementar
ingresosPorServicio(citas)enlib/analitica.ts(Σ precioCongeladoCentimosdecompletadaconsumarCentimos, resto 0 €; incluye serviciosactivo=false; prohibido leerservicio.precioCentimosvigente) y exponeringresos{porServicio,totalCentimos,totalTexto con formatearEuros}enapp/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 realesfin−iniciode completadas, resto 0 min, denominador víadenominadorOcupacionvigente nunca literal, jornada reconfigurada cambia denominador y se declara)
Implementation for User Story 3
- T016 [US3] Implementar
ocupacionSemanal(citas, denominadorMin)enlib/analitica.tsy cableardenominadorOcupacion(jornadaInicio, jornadaFin, diasLaborables, 8)+etiquetarJornadavigentes de la clínica enapp/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 etiquetasS-N: DD/MM/AAAA–DD/MM/AAAAderivadas desemanasAnalitica, curso fuera, futuras fuera, suma = historia; ventana móvil conhoyinyectado ⇒ filas distintas pero coherentes)
Implementation for User Story 4
- T018 [US4] Implementar
evolucionSemanal(citas, semanas)+calcularAnalitica(entrada)foto coherente enlib/analitica.ts(unahorapor cálculo,sinDesenlaceaparte) y exponerevolucion[]+ventana{semanas, generadaCon}enapp/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 Componentforce-dynamic:clinicaDeLaSesionoredirect('/acceso'), una lecturaclinicaId + ventana,calcularAnalitica, 4grafico-barrascon 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 accesiblerole="img"+title, letra ≥ 16 px, responsiveviewBoxsin scroll a 390 px, paleta con contraste verificado, cero jerga prohibida) - T023 [US5] Añadir enlace «Analítica» en
app/(panel)/layout.tsxy rechazar escrituras enapp/api/analitica/route.ts(POST/PATCH/PUT/DELETE ⇒ 405 «Esta página es de solo lectura»; el GET nunca escribe) - T024 [US5] Auditar
whereporclinicaIdenapp/api/analitica/route.tsyapp/(panel)/analitica/page.tsx(citas + profesionales + servicios filtrados; profesionales/servicios incluyenactivo=false; ningúngroupBy/findManysin 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_tiempomásnpm run typechecken 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.tscon catálogo 004 y ejecutarnpm 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 estilosfont-size ≥ 16 px; SC-001 manual con cronómetro <10 s) y ejecutarnpm run lint+npm run format:checkcorrigiendo 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/analiticamínimo +tasaNoAsistenciacomo patrón) — testeable sola sumando congelados - User Story 3 (P2): Needs Foundational +
denominadorOcupacionvigente (006) — testeable sola reconfigurando jornada - User Story 4 (P2): Needs US1–US3 (
semanasAnalitica+ foto coherente) — testeable sola conhoyinyectado - 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.tsantes queapp/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)
- Complete Phase 1: Setup (T001–T003)
- Complete Phase 2: Foundational (T004–T008, S-05 + semilla + esqueleto)
- Complete Phase 3: User Story 1 (T009–T012, tasas + API mínima)
- STOP and VALIDATE:
npm run test -- test_analitica test_analitica_getpara tasas + QS-1; oráculos 7,3/12,6/10,6 % en ventana de referencia - Deploy/demo if ready (tasa como argumento comercial mínimo)
Incremental Delivery
- Setup + Foundational → base migrada y sembrada
- Add US1 → tasas → validar (MVP)
- Add US2 → ingresos al céntimo inmóviles → validar QS-3 (SC-002)
- Add US3 → ocupación agregada con jornada declarada → validar QS-2
- Add US4 → evolución 8 filas coherente → validar QS-4 (SC-003)
- Add US5 → página con 4 gráficos + aislamiento + cero escritura → validar QC-6/QC-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:
- Ambos completan Setup + Foundational juntos
- Dev A: US1 (T009–T012) → US2 (T013–T014) → US4 (T017–T018)
- Dev B: US3 (T015–T016) → US5 (T019–T024) tras API mínima de Dev A
- 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).tses defecto