Implementation Plan: Núcleo de agenda de CitaClara (001)

Branch: 001-nucleo-agenda | Date: 2026-09-29 | Spec: specs/001-nucleo-agenda/spec.md

Input: Feature specification from specs/001-nucleo-agenda/spec.md + user input: «Selecciona un stack tecnológico moderno que permita crear una aplicación con una apariencia profesional y adaptada al estilo del año 2026».

Note: Este plan decide stack y formatos (la constitución no los impone). Toda la UI y textos en español de España.

Summary

Construir el núcleo de agenda (ver → crear → cerrar + acceso con clave + semilla Eleva) como monolito web full-stack Next.js 15 (React 19, TypeScript estricto) + Tailwind CSS 4 + shadcn/ui, con Prisma 6 + PostgreSQL 16 como única persistencia. RN1 cero solapes se garantiza en dos capas: restricción de exclusión Postgres (EXCLUDE USING gist sobre rango temporal por profesional para citas vigentes) + validación de aplicación en transacción serializable; RN2 sin pasado con un único now() de BD. Dinero en céntimos enteros, tiempo en timestamptz mostrado en Europe/Madrid con formato es-ES. Semilla determinista versionada con seedVersion=1. Tests Vitest + Playwright con prueba antisolape concurrente obligatoria en verde antes de merge.

Technical Context

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

Primary Dependencies: Next.js 15 (App Router, Route Handlers, Server Components), React 19, Tailwind CSS 4, shadcn/ui sobre Radix UI, Prisma 6, date-fns-tz + Intl (es-ES, EUR, Europe/Madrid), zod para validación, bcryptjs (hash clave panel, aunque v1 sea compartida) o argon2.

Storage: PostgreSQL 16 (única BD). Esquema Prisma con tablas Clinica, Profesional, Servicio, Paciente, Cita. Restricción de exclusión nativa para RN1 + índices (profesionalId, inicio). Migraciones Prisma versionadas. Semilla SQL/TS determinista (prisma/seed.ts, seedVersion=1).

Testing: Vitest (unit + integración de Route Handlers + concurrente antisolape), Playwright (e2e: agenda día en portátil 1440px y móvil 390px, alta <60s, desenlace <15s), Intl assertions para 40,00 € y 29/09/2026, 10:00–10:45.

Target Platform: Web responsive desplegada como contenedor Node 22 (Docker + Compose con Postgres 16). Navegadores modernos 2026 (Chrome/Edge/Firefox/Safari últimas 2 versiones). Sin app nativa en la 001.

Project Type: web-application monolítica (frontend + backend en el mismo Next.js; sin backend//frontend/ separados).

Performance Goals: API agenda del día objetivo p95 <200 ms en local con ~1000 citas (objetivo no bloqueante, sin tarea de medición en la 001); render agenda distinguible en <10 s sin formación —medición manual— (SC-001); alta válida <60 s y desenlace <15 s —medición manual— (SC-004); 100 % rechazo de solapes y de inicios en pasado (SC-002/SC-003).

Constraints: Español de España en todo texto visible y error (VIII); importes en céntimos enteros con redondeo mitad-hacia-arriba y formato 40,00 € (II); fechas Europe/Madrid con día/mes/año + hora/minutos (II); jornada configurable por clínica por defecto 09:00–20:00; auth v1: clave compartida hasheada, sin bloqueo ni límite de intentos (deuda consciente); prohibido eliminar fichas con citas; nombre de servicio único por clínica; fuera de alcance: acceso paciente, recordatorios, analítica, pagos (IV).

Scale/Scope: 1–N clínicas; 2–5 profesionales por clínica (3 en semilla); ~40 pacientes; 10 semanas de citas (~500–1200 filas); decenas de reservas concurrentes como peor caso. Un solo despliegue basta para la 001.

Constitution Check

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

  • I. Spec First: todo comportamiento (FR-001…FR-020 + 5 clarificaciones 2026-09-29) vive en spec.md; este plan no añade comportamiento, solo stack y diseño. Propietario pendiente en specs/MAPA.md (crear si no existe en /speckit.tasks).
  • II. Exactitud numérica y temporal: céntimos enteros + redondeo documentado en plan/research/data-model; timestamptz + Europe/Madrid + ejemplos límite 40,00 € / 29/09/2026, 10:00–10:45. Sin creative accounting.
  • III. Cero solapes: exclusión Postgres + test concurrente obligatorio (Vitest Promise.all doble reserva mismo hueco → máximo 1). Solape = defecto crítico bloqueante.
  • IV. Simplicidad / cero alcance fantasma: monolito Next.js (no microservicios, no Redis/colas, no Auth provider, no i18n multi-idioma, no analítica/pagos/recordatorios). Cada tabla/campo/endpoint traza a un FR.
  • V. Datos reproducibles: seedVersion=1, generador PRNG con semilla fija (p. ej. mulberry32), historia 8+2 semanas relativa a fecha de carga, tolerancia ±2 documentada, regeneración bit a bit.
  • VI. Tests con la spec: matriz trazable test↔regla (SC-001…SC-006 ↔ FR/RN); suite en verde + antisolape en verde como condición de merge.
  • VII. Interfaz clara y moderna: Tailwind 4 + shadcn/ui, lenguaje de clínica pequeña, contraste WCAG AA, responsive portátil+móvil verificado con Playwright, sin jerga técnica.
  • VIII. Español de España: es-ES en UI, errores, seed y docs; validación de teléfono/email españoles; € pospuesto con coma decimal.

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

Justificaciones IV (adiciones trazadas, no alcance fantasma): (1) bandera activo en Profesional/Servicio implementa el «MUST NOT eliminar» de FR-003 sin borrado físico; (2) GET /api/semilla es auxiliar de verificabilidad de FR-019/SC-005 exigido por Constitución V, no funcionalidad de clínica; (3) códigos FUERA_JORNADA/DIA_PARTIDO aplican FR-013 (tramo íntegro en jornada/día).

Project Structure

Documentation (this feature)

specs/001-nucleo-agenda/
├── 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)
│   ├── agenda-api.md    # Route Handlers REST + esquemas zod
│   └── ui-agenda.md     # Contrato UI agenda del día (props/estados)
└── tasks.md             # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)

Source Code (repository root)

citaclara/
├── app/
│   ├── (panel)/
│   │   ├── agenda/            # vista día por profesional (US1)
│   │   ├── citas/nueva/       # alta sin solapes ni pasado (US2)
│   │   └── acceso/            # clave de clínica (US4)
│   ├── api/
│   │   ├── agenda/route.ts    # GET ?profesionalId&fecha
│   │   ├── citas/route.ts     # POST crear (RN1+RN2)
│   │   ├── citas/[id]/estado/route.ts  # PATCH desenlace (US3)
│   │   ├── fichas/            # profesionales/servicios/pacientes CRUD básico
│   │   └── auth/panel/route.ts
│   ├── layout.tsx
│   └── globals.css            # Tailwind 4 + tokens shadcn
├── components/ui/             # shadcn (button, dialog, calendar, toast…)
├── lib/
│   ├── dinero.ts              # céntimos ↔ «40,00 €»
│   ├── tiempo.ts              # Europe/Madrid ↔ «29/09/2026, 10:00»
│   ├── validacion.ts          # zod: teléfono/email ES, jornada, unicidad servicio
│   ├── agenda.ts              # cálculo fin, contigüidad, solape minuto común
│   └── auth.ts                # clave panel v1 (hash + compare, sin bloqueo)
├── prisma/
│   ├── schema.prisma
│   ├── migrations/
│   └── seed.ts                # Clínica Eleva + seedVersion=1
├── tests/
│   ├── unit/                  # dinero, tiempo, solape, estados
│   ├── contract/              # zod/route schemas ↔ FR
│   └── integration/           # antisolape concurrente, RN2, semilla
├── e2e/                       # Playwright: agenda, alta, desenlace, acceso
├── docker-compose.yml         # app + postgres:16
└── Dockerfile

Structure Decision: Monolito Next.js 15 App Router en citaclara/ (repo sdd-course-udemy como contenedor; la app vive en tema5/citaclara/). Sin split backend/frontend: los Route Handlers son el backend y los Server Components la UI. Prisma es la única capa de acceso a datos (sin Repository pattern extra — simplicidad IV).

Complexity Tracking

Fill ONLY if Constitution Check has violations that must be justified

Violation Why Needed Simpler Alternative Rejected Because
— — —