Implementation Plan: Portal del paciente de CitaClara (002)

Branch: 002-portal-paciente | Date: 2026-10-01 | Spec: specs/002-portal-paciente/spec.md

Input: Feature specification from specs/002-portal-paciente/spec.md + clarificaciones 2026-09-29 (Q1/Q2/Q3=A) y 2026-09-30 (Q4–Q8=A) + specs consumidas 001/005/006/007.

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 personal del paciente (ver en tres secciones + cancelar con 24 h) sobre el monolito Next.js 15 existente, sin migración: lib/portal.ts (proyección pura enCurso/proximas/anteriores), GET /api/portal/citas (lectura con sesión 007 + renovación deslizante) y PATCH /api/portal/citas/[id]/cancelar (fachada que deriva pacienteId de la sesión y llama a ejecutarCancelarCita origen portal de la 005), más UI app/portal/acceso + app/portal/mis-citas móvil primero con Boton shadcn. Tiempo (006), identidad/sesión (007) y transición/catálogo (005) se reutilizan sin redefinir; ningún importe visible en v1 (FR-013 como guarda).

Technical Context

Language/Version: TypeScript 5.6 strict (incl. noUncheckedIndexedAccess), Node.js 22 LTS.

Primary Dependencies: Next.js 15 (App Router, Route Handlers, Server Components, cookies()), React 19, Tailwind CSS 4, shadcn/ui (Boton), Prisma 6, zod, date-fns-tz + Intl (es-ES, Europe/Madrid).

Storage: PostgreSQL 16 (única BD). Sin tablas nuevas y sin migración (FR-009/FR-015/FR-016): se leen Paciente, Cita (+profesional.nombre, servicio.nombre) y Clinica.telefono de la 001; la sesión sigue en memoria según 007 FR-005.

Testing: Vitest (unit: partición/orden/bordes/etiquetas sin importes; integración: acceso incorrecto/duplicado sin fugas, lectura solo propia + renovación, cancelación en plazo/fuera/doble/caducada) + Playwright (e2e/portal-paciente.spec.ts: acceso → secciones → confirmar → Anteriores, 390 px y 1440 px, Cerrar sesión).

Target Platform: Web responsive (contenedor Node 22 + Postgres 16). Navegadores modernos 2026. Sin app nativa.

Project Type: web-application monolítica (Route Handlers = backend, Server Components + islas de cliente = UI; lib/ = dominio).

Performance Goals: Lectura del portal p95 <200 ms en local (decenas de citas por ficha); cancelación interactiva p95 <200 ms (objetivo no bloqueante); encontrar la próxima cita <30 s y cancelar <60 s en protocolo manual (SC-001/SC-002, no aserciones automáticas de tiempo).

Constraints: Español de España en todo texto y aviso (VIII) con teléfono de la clínica (001 FR-001); sin importes visibles (FR-013); tramo DD/MM/AAAA, HH:MM–HH:MM y confirmación ¿Seguro que quieres cancelar esta cita? Se liberará tu hueco (FR-002/FR-006/FR-012); antelación inicio − ahora ≥ 24 h borde incluido a minuto (006 FR-005); GET nunca escribe (005 FR-003); sesión 30 min deslizante + cierre instantáneo, nada en localStorage (007 FR-005); fuera de alcance: reservar/mover/pagar/recordar (FR-015).

Scale/Scope: 1–N clínicas; ~40 pacientes y ~500–1200 citas como en la 001; peor caso decenas de sesiones y doble clic concurrente sobre la misma cita.

Constitution Check

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

  • I. Spec First: todo comportamiento (FR-001…FR-016 + 8 clarificaciones) vive en spec.md; el plan no añade comportamiento. Propietario experiencia del paciente en specs/MAPA.md; no se tocan tablas de la 001 (sin enmienda).
  • II. Exactitud numérica y temporal: sin dinero visible en v1 (FR-013 como guarda; si volviera, precio congelado 001 FR-006 con 40,00 €); tiempo delegado a 006 (capturarAhora, borde 24 h incluido, minuto, Europe/Madrid, tramo 08/10/2026, 10:00–10:45).
  • III. Cero solapes: el portal no crea citas (no puede solapar); cancelar libera tramo y los tests verifican reutilización por recepción + suite antisolape 001 en verde para merge.
  • IV. Simplicidad / cero alcance fantasma: sin tablas, sin migración, sin Auth nueva, sin i18n, sin pagos/reservas/recordatorios; cada fichero/endpoint traza a un FR (ver justificaciones abajo).
  • V. Datos reproducibles: semilla Eleva existente (seedVersion=1); escenarios de quickstart.md usan la ficha canónica y el ejemplo sesión fisio 08/10/2026 (re-datado si cambia la fecha de carga, FR-014).
  • VI. Tests con la spec: matriz test↔FR/SC en quickstart.md; suite + antisolape + e2e portal en verde como puerta de merge.
  • VII. Interfaz clara y moderna: Tailwind + Boton, lenguaje de clínica, contraste heredado, responsive 390/1440 verificado con Playwright, sin jerga.
  • VIII. Español de España: es-ES en UI, errores y docs; comprobar:es en verde; cero importes que formatear en v1.

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

Justificaciones IV (adiciones trazadas, no alcance fantasma): (1) lib/portal.ts implementa FR-001/FR-002 como proyección pura (sin él cada consumidor clasificaría por su cuenta); (2) GET /api/portal/citas implementa FR-003/FR-005 como lectura con sesión (el POST /api/portal/sesion de la 007 solo resuelve identidad, no lista citas); (3) PATCH /api/portal/citas/[id]/cancelar implementa FR-006…FR-009 como fachada que deriva pacienteId de la sesión (usar el PATCH genérico con pacienteId del cliente permitiría suplantar fichas); (4) páginas app/portal/* + components/portal-cita.tsx implementan FR-002/FR-006/FR-010 (sin UI no hay SC-001/SC-002/SC-006).

Project Structure

Documentation (this feature)

specs/002-portal-paciente/
├── plan.md              # This file (/speckit.plan command output)
├── research.md          # Phase 0 output (/speckit.plan command)
├── data-model.md        # Phase 1 output (/speckit.plan command)
├── quickstart.md        # Phase 1 output (/speckit.plan command)
├── contracts/           # Phase 1 output (/speckit.plan command)
│   └── portal.md        # lib/portal + GET/PATCH portal + UI (FR-001…FR-016)
└── tasks.md             # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)

Source Code (repository root)

tema5/citaclara/
├── app/
│   ├── portal/
│   │   ├── page.tsx                 # redirige a acceso/mis-citas según sesión (US3)
│   │   ├── acceso/page.tsx          # formulario correo+teléfono → POST /api/portal/sesion (US3)
│   │   └── mis-citas/page.tsx       # Server Component: 3 secciones + Cerrar sesión (US1)
│   └── api/portal/
│       ├── sesion/route.ts          # existente 007 (sin cambios; acceso/cierre)
│       ├── citas/route.ts           # GET lectura clasificada con renovación (US1/US3)
│       └── citas/[id]/cancelar/route.ts  # PATCH fachada sesión→cancelarCita portal (US2)
├── components/
│   └── portal-cita.tsx              # tarjeta + confirmación + Cancelar/aviso teléfono (US1/US2)
├── lib/
│   ├── portal.ts                    # clasificarCitasDePortal + tipos (FR-001/FR-002)
│   ├── validacion.ts                # + ESQUEMA_CONFIRMAR_CANCELACION_PORTAL
│   ├── cancelacion.ts               # sin cambios (consume pacienteId verificado)
│   ├── identidad.ts                 # sin cambios (resolución/avisos únicos)
│   ├── sesion-paciente.ts           # sin cambios (verificar/renovar/cerrar)
│   ├── ventanas.ts                  # sin cambios (capturarAhora/cancelacionEnPlazo)
│   └── tiempo.ts                    # sin cambios (formatearTramo)
├── tests/
│   ├── unit/test_portal.test.ts
│   └── integration/test_portal_paciente.test.ts
├── e2e/
│   └── portal-paciente.spec.ts      # acceso→secciones→cancelar→Anteriores (390+1440)
└── scripts/comprobar-es.ts          # + textos del portal (si aplica)

Structure Decision: Monolito Next.js existente (misma estructura que 001/005/007). El dominio de vista vive en lib/portal.ts (puro, sin BD) para que API y UI compartan la partición; las rutas son fachadas finas con sesión; la UI son Server Components + una isla de cliente para confirmar/cancelar. Prisma es la única capa de datos (sin Repository extra — simplicidad IV).

Complexity Tracking

Fill ONLY if Constitution Check has violations that must be justified

Violation Why Needed Simpler Alternative Rejected Because
— — —