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. Propietarioexperiencia del pacienteenspecs/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, tramo08/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 dequickstart.mdusan la ficha canónica y el ejemplosesión fisio08/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-ESen UI, errores y docs;comprobar:esen 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