Data Model — 002 Portal del paciente
Fuente: spec.md (FR-001…FR-016). Propietario de las tablas: 001 (esta feature no crea ni migra nada). Zona Europe/Madrid, idioma es-ES, sin importes en v1 (FR-013).
Sin tablas nuevas ni migraciones (principio IV + FR-015/FR-016)
Reutiliza tal cual Paciente (id, nombre, telefono, email), Cita (id, pacienteId, profesionalId, servicioId, inicio, fin, estado), Profesional (nombre), Servicio (nombre) y Clinica (telefono). No hay ALTER TABLE, no hay backfill, no hay cuentas ni tokens persistidos (la sesión vive en memoria según 007 FR-005).
Entidades lógicas (no persistentes)
Clasificación (orden vinculante, proyección de lectura)
ahora = capturarAhora() una vez por operación (006 FR-001), todo truncado a minuto (006 FR-002)
para cada cita de la ficha (WHERE pacienteId):
si estado == 'reservada' y inicio <= ahora < fin → enCurso
si estado == 'reservada' y inicio > ahora → proximas
en otro caso → anteriores
(completada/cancelada/no_asistida siempre; reservada con fin <= ahora también)
orden: enCurso asc por inicio · proximas asc por inicio · anteriores desc por inicio
etiqueta: enCurso→«en curso»; proximas→«reservada»; anteriores→«completada»/«cancelada»/«no asistida»/«reservada»(fin pasado)
Bordes: inicio == ahora → en curso (no es pasado RN2 pero tampoco tiene 24 h de antelación: no cancelable); fin == ahora → anteriores (el intervalo es [inicio, fin)); inicio − ahora == 24 h 00 min → cancelable, 23 h 59 min → no (006 FR-005). Una reservada con fin pasado sigue siendo reservada en BD (001): solo se proyecta en «Anteriores».
Cancelación (escritura delegada, sin transición propia)
Solo ejecutarCancelarCita({ pacienteIdVerificado de la sesión, citaId, origen: 'portal' }) (005 FR-001). La UI exige confirmación expresa previa (¿Seguro que quieres cancelar esta cita? Se liberará tu hueco); visualizar (GET) nunca escribe (005 FR-003). Efecto: reservada → cancelada + tramo [inicio, fin) libre para recepción (FR-009, RN1). Rechazos con el catálogo único de la 005 (YA_CANCELADA, ESTADO_FINAL, CITA_AJENA, PLAZO_VENCIDO, ENLACE_INVALIDO), con teléfono de la clínica en los avisos.
Diagrama (texto)
Acceso: credenciales --resolverPacientePorCredenciales (007)--> ok(pacienteId)
--> crearSesionPortal (cookie 30min) --> GET /api/portal/citas WHERE pacienteId
--> clasificarCitasDePortal (lib/portal.ts) --> enCurso/proximas/anteriores
Cancelar: confirmar --> PATCH /api/portal/citas/[id]/cancelar (sesion -> pacienteId)
--> ejecutarCancelarCita-origen-portal (005) --> cancelada + tramo libre
Cerrar: DELETE /api/portal/sesion --> revoca + limpia cookie --> reidentificacion
Reglas que esta feature NO toca
Normalización/duplicados/sesión/enlace (007), «ahora»/bordes/ventanas (006), transición/catálogo/atomicidad/traza (005), alta/desenlace/antisolape (001). Definir cualquiera localmente es defecto.