1263 lines
34 KiB
Markdown
1263 lines
34 KiB
Markdown
# 🏗️ ARQUITECTURA DE BASE DE DATOS Y GESTIÓN DE CONTENIDO - EMERGES TES
|
||
|
||
**Versión:** 1.0
|
||
**Fecha:** 2025-01-XX
|
||
**Estado:** Especificación Técnica - Sin Implementación
|
||
**Autor:** Arquitecto Senior de Software Sanitario
|
||
|
||
---
|
||
|
||
## 🎯 PROPÓSITO DEL DOCUMENTO
|
||
|
||
Este documento define **LA ARQUITECTURA COMPLETA** del sistema de base de datos y gestión de contenido para EMERGES TES. Especifica:
|
||
|
||
- ✅ Modelo de datos PostgreSQL (tablas, relaciones, índices)
|
||
- ✅ API REST mínima (endpoints, contratos)
|
||
- ✅ Flujo de sincronización offline-first
|
||
- ✅ Panel administrador (alcance, funcionalidades)
|
||
- ✅ Límites de modificación (qué NO tocar)
|
||
- ✅ Roadmap de implementación por fases
|
||
|
||
**Este sistema permite modificar contenido clínico sin tocar código React de la app operativa.**
|
||
|
||
---
|
||
|
||
## 1️⃣ PRINCIPIOS ARQUITECTÓNICOS
|
||
|
||
### Principio 1: Separación Estricta
|
||
|
||
```
|
||
┌─────────────────────────────────────┐
|
||
│ APP OPERATIVA (PWA - React) │
|
||
│ - Solo LEE contenido (pull-only) │
|
||
│ - Funciona 100% offline │
|
||
│ - NO modifica contenido │
|
||
│ - Service Worker intacto │
|
||
└─────────────────────────────────────┘
|
||
│
|
||
│ GET /api/content/*
|
||
│ (solo lectura)
|
||
▼
|
||
┌─────────────────────────────────────┐
|
||
│ BACKEND (Node.js + PostgreSQL) │
|
||
│ - Almacena contenido versionado │
|
||
│ - API REST simple │
|
||
│ - Validación clínica │
|
||
│ - Historial completo │
|
||
└─────────────────────────────────────┘
|
||
▲
|
||
│ PUT/POST /api/content/*
|
||
│ (escritura con auth)
|
||
│
|
||
┌─────────────────────────────────────┐
|
||
│ PANEL ADMINISTRADOR (Web App) │
|
||
│ - Edita contenido │
|
||
│ - Valida clínicamente │
|
||
│ - Gestiona versiones │
|
||
│ - App separada │
|
||
└─────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
### Principio 2: Offline-First Garantizado
|
||
|
||
**La app operativa:**
|
||
- ✅ Cachea contenido localmente (IndexedDB)
|
||
- ✅ Funciona sin red usando cache
|
||
- ✅ Sincroniza en background cuando hay red
|
||
- ✅ Nunca falla por falta de conexión
|
||
|
||
**El backend:**
|
||
- ✅ Siempre disponible (alta disponibilidad)
|
||
- ✅ Versionado permite rollback
|
||
- ✅ API idempotente y estable
|
||
|
||
---
|
||
|
||
### Principio 3: Validación Clínica Obligatoria
|
||
|
||
**Todo contenido clínico:**
|
||
- ✅ Debe pasar validación antes de ser público
|
||
- ✅ Solo contenido `validated` es servido a la app
|
||
- ✅ Historial completo de validaciones
|
||
- ✅ Trazabilidad de quién validó y cuándo
|
||
|
||
---
|
||
|
||
## 2️⃣ MODELO DE DATOS POSTGRESQL
|
||
|
||
### 2.1 Esquema General
|
||
|
||
```sql
|
||
-- Esquema principal
|
||
CREATE SCHEMA IF NOT EXISTS emerges_content;
|
||
|
||
-- Extensión para UUIDs
|
||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||
|
||
-- Extensión para JSONB (ya incluida en PostgreSQL)
|
||
```
|
||
|
||
---
|
||
|
||
### 2.2 Tabla: `content_items` (Contenido Principal)
|
||
|
||
**Propósito:** Almacena todos los tipos de contenido (protocolos, guías, manual, fármacos)
|
||
|
||
```sql
|
||
CREATE TABLE emerges_content.content_items (
|
||
-- Identificación (inmutable)
|
||
id VARCHAR(100) PRIMARY KEY, -- "rcp-adulto-svb", "adrenalina", etc.
|
||
type VARCHAR(50) NOT NULL, -- 'protocol' | 'guide' | 'manual' | 'drug'
|
||
level VARCHAR(50) NOT NULL, -- 'operativo' | 'formativo' | 'referencia'
|
||
|
||
-- Contenido (editable)
|
||
title VARCHAR(500) NOT NULL, -- Título del contenido
|
||
short_title VARCHAR(200), -- Título corto (opcional)
|
||
content JSONB NOT NULL, -- Contenido estructurado (varía por tipo)
|
||
content_markdown TEXT, -- Contenido Markdown (para guías/manual)
|
||
|
||
-- Metadatos de contenido
|
||
category VARCHAR(100), -- Categoría (ej: 'soporte_vital', 'cardiovascular')
|
||
subcategory VARCHAR(100), -- Subcategoría (ej: 'rcp', 'anafilaxia')
|
||
priority VARCHAR(20), -- 'critico' | 'alto' | 'medio' | 'bajo'
|
||
age_group VARCHAR(20), -- 'adulto' | 'pediatrico' | 'neonatal' | 'todos'
|
||
|
||
-- Versionado
|
||
version INTEGER NOT NULL DEFAULT 1, -- Versión actual
|
||
latest_version INTEGER NOT NULL DEFAULT 1, -- Última versión (para queries rápidas)
|
||
|
||
-- Estado y validación
|
||
status VARCHAR(20) NOT NULL DEFAULT 'draft', -- 'draft' | 'pending' | 'validated' | 'rejected' | 'archived'
|
||
validated_by VARCHAR(100), -- ID del validador clínico
|
||
validated_at TIMESTAMP WITH TIME ZONE, -- Fecha de validación
|
||
clinical_source VARCHAR(200), -- 'ERC 2021', 'AHA 2020', 'SEMES 2022', etc.
|
||
quality_score INTEGER, -- 0-100 (opcional)
|
||
|
||
-- Revisión
|
||
reviewed_by VARCHAR(100), -- ID del revisor
|
||
reviewed_at TIMESTAMP WITH TIME ZONE, -- Fecha de revisión
|
||
|
||
-- Auditoría
|
||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||
created_by VARCHAR(100) NOT NULL, -- ID del creador
|
||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||
updated_by VARCHAR(100) NOT NULL, -- ID del último editor
|
||
|
||
-- Constraints
|
||
CONSTRAINT chk_type CHECK (type IN ('protocol', 'guide', 'manual', 'drug')),
|
||
CONSTRAINT chk_level CHECK (level IN ('operativo', 'formativo', 'referencia')),
|
||
CONSTRAINT chk_status CHECK (status IN ('draft', 'pending', 'validated', 'rejected', 'archived')),
|
||
CONSTRAINT chk_priority CHECK (priority IN ('critico', 'alto', 'medio', 'bajo') OR priority IS NULL),
|
||
CONSTRAINT chk_quality_score CHECK (quality_score IS NULL OR (quality_score >= 0 AND quality_score <= 100))
|
||
);
|
||
|
||
-- Índices para performance
|
||
CREATE INDEX idx_content_items_type ON emerges_content.content_items(type);
|
||
CREATE INDEX idx_content_items_level ON emerges_content.content_items(level);
|
||
CREATE INDEX idx_content_items_status ON emerges_content.content_items(status);
|
||
CREATE INDEX idx_content_items_category ON emerges_content.content_items(category);
|
||
CREATE INDEX idx_content_items_validated_at ON emerges_content.content_items(validated_at);
|
||
CREATE INDEX idx_content_items_updated_at ON emerges_content.content_items(updated_at);
|
||
|
||
-- Índice GIN para búsqueda en JSONB
|
||
CREATE INDEX idx_content_items_content_gin ON emerges_content.content_items USING GIN (content);
|
||
|
||
-- Índice para búsqueda full-text en título
|
||
CREATE INDEX idx_content_items_title_fts ON emerges_content.content_items USING GIN (to_tsvector('spanish', title));
|
||
```
|
||
|
||
---
|
||
|
||
### 2.3 Tabla: `content_versions` (Historial de Versiones)
|
||
|
||
**Propósito:** Almacena todas las versiones históricas de cada contenido
|
||
|
||
```sql
|
||
CREATE TABLE emerges_content.content_versions (
|
||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||
content_id VARCHAR(100) NOT NULL, -- FK a content_items.id
|
||
version INTEGER NOT NULL, -- Número de versión
|
||
|
||
-- Snapshot del contenido en esta versión
|
||
content JSONB NOT NULL,
|
||
content_markdown TEXT,
|
||
title VARCHAR(500) NOT NULL,
|
||
|
||
-- Metadatos de la versión
|
||
status VARCHAR(20) NOT NULL,
|
||
validated_by VARCHAR(100),
|
||
validated_at TIMESTAMP WITH TIME ZONE,
|
||
clinical_source VARCHAR(200),
|
||
|
||
-- Cambios en esta versión
|
||
change_summary TEXT, -- Resumen de cambios
|
||
changed_fields TEXT[], -- Array de campos modificados
|
||
|
||
-- Auditoría
|
||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||
created_by VARCHAR(100) NOT NULL,
|
||
|
||
-- Constraints
|
||
CONSTRAINT fk_content_versions_content_id
|
||
FOREIGN KEY (content_id)
|
||
REFERENCES emerges_content.content_items(id)
|
||
ON DELETE CASCADE,
|
||
CONSTRAINT chk_version_status CHECK (status IN ('draft', 'pending', 'validated', 'rejected', 'archived')),
|
||
CONSTRAINT uq_content_versions_content_version UNIQUE (content_id, version)
|
||
);
|
||
|
||
-- Índices
|
||
CREATE INDEX idx_content_versions_content_id ON emerges_content.content_versions(content_id);
|
||
CREATE INDEX idx_content_versions_version ON emerges_content.content_versions(version);
|
||
CREATE INDEX idx_content_versions_created_at ON emerges_content.content_versions(created_at);
|
||
```
|
||
|
||
---
|
||
|
||
### 2.4 Tabla: `content_relations` (Relaciones entre Contenidos)
|
||
|
||
**Propósito:** Define relaciones entre protocolos, guías y manual
|
||
|
||
```sql
|
||
CREATE TABLE emerges_content.content_relations (
|
||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||
source_id VARCHAR(100) NOT NULL, -- ID del contenido origen
|
||
target_id VARCHAR(100) NOT NULL, -- ID del contenido destino
|
||
relation_type VARCHAR(50) NOT NULL, -- 'has_guide' | 'has_manual' | 'references' | 'related_to'
|
||
|
||
-- Metadatos
|
||
is_primary BOOLEAN DEFAULT false, -- Si es relación principal
|
||
order_index INTEGER, -- Orden si hay múltiples relaciones
|
||
|
||
-- Auditoría
|
||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||
created_by VARCHAR(100) NOT NULL,
|
||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||
|
||
-- Constraints
|
||
CONSTRAINT fk_content_relations_source
|
||
FOREIGN KEY (source_id)
|
||
REFERENCES emerges_content.content_items(id)
|
||
ON DELETE CASCADE,
|
||
CONSTRAINT fk_content_relations_target
|
||
FOREIGN KEY (target_id)
|
||
REFERENCES emerges_content.content_items(id)
|
||
ON DELETE CASCADE,
|
||
CONSTRAINT chk_relation_type CHECK (relation_type IN ('has_guide', 'has_manual', 'references', 'related_to')),
|
||
CONSTRAINT uq_content_relations_source_target_type UNIQUE (source_id, target_id, relation_type)
|
||
);
|
||
|
||
-- Índices
|
||
CREATE INDEX idx_content_relations_source ON emerges_content.content_relations(source_id);
|
||
CREATE INDEX idx_content_relations_target ON emerges_content.content_relations(target_id);
|
||
CREATE INDEX idx_content_relations_type ON emerges_content.content_relations(relation_type);
|
||
```
|
||
|
||
---
|
||
|
||
### 2.5 Tabla: `content_change_log` (Log de Cambios)
|
||
|
||
**Propósito:** Registro detallado de todos los cambios realizados
|
||
|
||
```sql
|
||
CREATE TABLE emerges_content.content_change_log (
|
||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||
content_id VARCHAR(100) NOT NULL,
|
||
version INTEGER NOT NULL,
|
||
|
||
-- Cambio
|
||
action VARCHAR(20) NOT NULL, -- 'create' | 'update' | 'delete' | 'validate' | 'reject' | 'archive'
|
||
field_path VARCHAR(200), -- Ruta del campo (ej: 'steps[0]', 'adultDose')
|
||
old_value JSONB, -- Valor anterior
|
||
new_value JSONB, -- Valor nuevo
|
||
|
||
-- Contexto
|
||
change_reason TEXT, -- Razón del cambio
|
||
comment TEXT, -- Comentario del editor
|
||
|
||
-- Auditoría
|
||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||
created_by VARCHAR(100) NOT NULL,
|
||
ip_address INET, -- IP del usuario (para auditoría)
|
||
user_agent TEXT, -- User agent (para auditoría)
|
||
|
||
-- Constraints
|
||
CONSTRAINT fk_content_change_log_content
|
||
FOREIGN KEY (content_id)
|
||
REFERENCES emerges_content.content_items(id)
|
||
ON DELETE CASCADE,
|
||
CONSTRAINT chk_change_log_action CHECK (action IN ('create', 'update', 'delete', 'validate', 'reject', 'archive'))
|
||
);
|
||
|
||
-- Índices
|
||
CREATE INDEX idx_content_change_log_content_id ON emerges_content.content_change_log(content_id);
|
||
CREATE INDEX idx_content_change_log_version ON emerges_content.content_change_log(version);
|
||
CREATE INDEX idx_content_change_log_created_at ON emerges_content.content_change_log(created_at);
|
||
CREATE INDEX idx_content_change_log_created_by ON emerges_content.content_change_log(created_by);
|
||
```
|
||
|
||
---
|
||
|
||
### 2.6 Tabla: `users` (Usuarios del Panel)
|
||
|
||
**Propósito:** Gestión de usuarios y roles para el panel administrador
|
||
|
||
```sql
|
||
CREATE TABLE emerges_content.users (
|
||
id VARCHAR(100) PRIMARY KEY, -- ID único (ej: "dr-juan-perez")
|
||
email VARCHAR(255) UNIQUE NOT NULL,
|
||
name VARCHAR(200) NOT NULL,
|
||
role VARCHAR(50) NOT NULL, -- 'admin_clinico' | 'editor_docente' | 'editor_clinico' | 'revisor'
|
||
|
||
-- Autenticación
|
||
password_hash VARCHAR(255) NOT NULL, -- Hash bcrypt
|
||
salt VARCHAR(255) NOT NULL,
|
||
|
||
-- Estado
|
||
is_active BOOLEAN DEFAULT true,
|
||
last_login TIMESTAMP WITH TIME ZONE,
|
||
|
||
-- Metadatos
|
||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||
created_by VARCHAR(100),
|
||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||
|
||
-- Constraints
|
||
CONSTRAINT chk_user_role CHECK (role IN ('admin_clinico', 'editor_docente', 'editor_clinico', 'revisor'))
|
||
);
|
||
|
||
-- Índices
|
||
CREATE INDEX idx_users_email ON emerges_content.users(email);
|
||
CREATE INDEX idx_users_role ON emerges_content.users(role);
|
||
CREATE INDEX idx_users_active ON emerges_content.users(is_active);
|
||
```
|
||
|
||
---
|
||
|
||
### 2.7 Tabla: `content_sync_status` (Estado de Sincronización)
|
||
|
||
**Propósito:** Trackear qué versiones de contenido han sido sincronizadas con las apps
|
||
|
||
```sql
|
||
CREATE TABLE emerges_content.content_sync_status (
|
||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||
content_id VARCHAR(100) NOT NULL,
|
||
version INTEGER NOT NULL,
|
||
|
||
-- Sincronización
|
||
synced_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||
sync_count INTEGER DEFAULT 1, -- Número de veces sincronizado
|
||
|
||
-- Constraints
|
||
CONSTRAINT fk_content_sync_status_content
|
||
FOREIGN KEY (content_id)
|
||
REFERENCES emerges_content.content_items(id)
|
||
ON DELETE CASCADE,
|
||
CONSTRAINT uq_content_sync_status_content_version UNIQUE (content_id, version)
|
||
);
|
||
|
||
-- Índices
|
||
CREATE INDEX idx_content_sync_status_content_id ON emerges_content.content_sync_status(content_id);
|
||
CREATE INDEX idx_content_sync_status_synced_at ON emerges_content.content_sync_status(synced_at);
|
||
```
|
||
|
||
---
|
||
|
||
### 2.8 Vistas Útiles
|
||
|
||
```sql
|
||
-- Vista: Contenido validado (solo para lectura pública)
|
||
CREATE VIEW emerges_content.v_validated_content AS
|
||
SELECT
|
||
id,
|
||
type,
|
||
level,
|
||
title,
|
||
short_title,
|
||
content,
|
||
content_markdown,
|
||
category,
|
||
subcategory,
|
||
priority,
|
||
age_group,
|
||
version,
|
||
validated_at,
|
||
clinical_source,
|
||
updated_at
|
||
FROM emerges_content.content_items
|
||
WHERE status = 'validated'
|
||
ORDER BY updated_at DESC;
|
||
|
||
-- Vista: Contenido pendiente de validación (para panel admin)
|
||
CREATE VIEW emerges_content.v_pending_validation AS
|
||
SELECT
|
||
ci.*,
|
||
u.name as created_by_name,
|
||
u.email as created_by_email
|
||
FROM emerges_content.content_items ci
|
||
LEFT JOIN emerges_content.users u ON ci.created_by = u.id
|
||
WHERE ci.status = 'pending'
|
||
ORDER BY ci.updated_at ASC;
|
||
```
|
||
|
||
---
|
||
|
||
## 3️⃣ API REST MÍNIMA
|
||
|
||
### 3.1 Principios de Diseño
|
||
|
||
**La API es:**
|
||
- ✅ RESTful simple
|
||
- ✅ Solo lectura para la app operativa
|
||
- ✅ Escritura solo con autenticación
|
||
- ✅ Versionada (v1)
|
||
- ✅ Documentada (OpenAPI/Swagger)
|
||
|
||
---
|
||
|
||
### 3.2 Endpoints Públicos (App Operativa)
|
||
|
||
#### GET `/api/v1/content/protocols`
|
||
|
||
**Propósito:** Obtener todos los protocolos validados
|
||
|
||
**Query params:**
|
||
- `level` (opcional): Filtrar por nivel ('operativo')
|
||
- `category` (opcional): Filtrar por categoría
|
||
- `status` (opcional): Por defecto 'validated'
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"data": [
|
||
{
|
||
"id": "rcp-adulto-svb",
|
||
"type": "protocol",
|
||
"level": "operativo",
|
||
"title": "RCP Adulto - Soporte Vital Básico",
|
||
"shortTitle": "RCP Adulto SVB",
|
||
"content": {
|
||
"steps": [...],
|
||
"warnings": [...],
|
||
"keyPoints": [...],
|
||
"equipment": [...],
|
||
"drugs": [...]
|
||
},
|
||
"version": 3,
|
||
"updatedAt": "2025-01-15T10:00:00Z"
|
||
}
|
||
],
|
||
"meta": {
|
||
"total": 5,
|
||
"version": "v1"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### GET `/api/v1/content/protocols/:id`
|
||
|
||
**Propósito:** Obtener un protocolo específico
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"data": {
|
||
"id": "rcp-adulto-svb",
|
||
"type": "protocol",
|
||
"level": "operativo",
|
||
"title": "RCP Adulto - Soporte Vital Básico",
|
||
"content": {...},
|
||
"version": 3,
|
||
"validatedAt": "2025-01-15T10:00:00Z",
|
||
"clinicalSource": "ERC 2021"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### GET `/api/v1/content/drugs`
|
||
|
||
**Propósito:** Obtener todos los fármacos validados
|
||
|
||
**Response:** Similar a protocols
|
||
|
||
---
|
||
|
||
#### GET `/api/v1/content/drugs/:id`
|
||
|
||
**Propósito:** Obtener un fármaco específico
|
||
|
||
---
|
||
|
||
#### GET `/api/v1/content/guides/:id/sections/:sectionNumber`
|
||
|
||
**Propósito:** Obtener una sección específica de una guía
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"data": {
|
||
"guideId": "rcp-adulto-svb",
|
||
"sectionNumber": 1,
|
||
"title": "Introducción y Contexto",
|
||
"contentMarkdown": "# SECCIÓN 1: Introducción...",
|
||
"version": 2,
|
||
"updatedAt": "2025-01-10T08:00:00Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### GET `/api/v1/content/manual/blocks/:blockId`
|
||
|
||
**Propósito:** Obtener un bloque del manual
|
||
|
||
---
|
||
|
||
#### GET `/api/v1/content/relations/:id`
|
||
|
||
**Propósito:** Obtener relaciones de un contenido
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"data": {
|
||
"sourceId": "rcp-adulto-svb",
|
||
"relations": [
|
||
{
|
||
"targetId": "rcp-adulto-svb",
|
||
"targetType": "guide",
|
||
"relationType": "has_guide",
|
||
"isPrimary": true
|
||
},
|
||
{
|
||
"targetId": "bloque-04-1",
|
||
"targetType": "manual",
|
||
"relationType": "has_manual",
|
||
"isPrimary": true
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### GET `/api/v1/content/versions/:id`
|
||
|
||
**Propósito:** Obtener historial de versiones de un contenido
|
||
|
||
**Query params:**
|
||
- `limit` (opcional): Número de versiones (default: 10)
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"data": {
|
||
"contentId": "rcp-adulto-svb",
|
||
"currentVersion": 3,
|
||
"versions": [
|
||
{
|
||
"version": 3,
|
||
"status": "validated",
|
||
"validatedAt": "2025-01-15T10:00:00Z",
|
||
"validatedBy": "dr-juan-perez",
|
||
"createdAt": "2025-01-15T09:00:00Z"
|
||
},
|
||
{
|
||
"version": 2,
|
||
"status": "validated",
|
||
"validatedAt": "2025-01-10T08:00:00Z",
|
||
"createdAt": "2025-01-10T07:00:00Z"
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### GET `/api/v1/sync/check`
|
||
|
||
**Propósito:** Verificar si hay actualizaciones disponibles
|
||
|
||
**Query params:**
|
||
- `lastSync` (requerido): Timestamp ISO 8601 de última sincronización
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"data": {
|
||
"hasUpdates": true,
|
||
"updatedItems": [
|
||
{
|
||
"id": "rcp-adulto-svb",
|
||
"type": "protocol",
|
||
"version": 3,
|
||
"updatedAt": "2025-01-15T10:00:00Z"
|
||
}
|
||
],
|
||
"serverTime": "2025-01-20T12:00:00Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3.3 Endpoints Protegidos (Panel Admin)
|
||
|
||
#### POST `/api/v1/auth/login`
|
||
|
||
**Body:**
|
||
```json
|
||
{
|
||
"email": "admin@emergestes.es",
|
||
"password": "***"
|
||
}
|
||
```
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"data": {
|
||
"token": "jwt-token-here",
|
||
"user": {
|
||
"id": "admin-001",
|
||
"name": "Dr. Juan Pérez",
|
||
"role": "admin_clinico"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### PUT `/api/v1/content/protocols/:id`
|
||
|
||
**Headers:**
|
||
- `Authorization: Bearer {jwt-token}`
|
||
|
||
**Body:**
|
||
```json
|
||
{
|
||
"title": "RCP Adulto - Soporte Vital Básico (Actualizado)",
|
||
"content": {
|
||
"steps": [...],
|
||
"warnings": [...]
|
||
},
|
||
"changeReason": "Actualización según ERC 2021"
|
||
}
|
||
```
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"data": {
|
||
"id": "rcp-adulto-svb",
|
||
"version": 4,
|
||
"status": "draft",
|
||
"updatedAt": "2025-01-20T12:00:00Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### POST `/api/v1/content/protocols/:id/validate`
|
||
|
||
**Propósito:** Solicitar validación clínica
|
||
|
||
**Body:**
|
||
```json
|
||
{
|
||
"clinicalSource": "ERC 2021",
|
||
"comment": "Validado según guías ERC 2021"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### POST `/api/v1/content/protocols/:id/approve`
|
||
|
||
**Propósito:** Aprobar contenido (solo admin_clinico)
|
||
|
||
**Body:**
|
||
```json
|
||
{
|
||
"qualityScore": 95,
|
||
"comment": "Contenido validado y aprobado"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### POST `/api/v1/content/protocols/:id/reject`
|
||
|
||
**Propósito:** Rechazar contenido (solo admin_clinico)
|
||
|
||
**Body:**
|
||
```json
|
||
{
|
||
"reason": "Dosis incorrecta según última guía",
|
||
"comment": "Revisar dosis de adrenalina"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### GET `/api/v1/admin/content/pending`
|
||
|
||
**Propósito:** Obtener contenido pendiente de validación
|
||
|
||
---
|
||
|
||
#### GET `/api/v1/admin/audit/log`
|
||
|
||
**Propósito:** Obtener log de auditoría
|
||
|
||
**Query params:**
|
||
- `contentId` (opcional)
|
||
- `userId` (opcional)
|
||
- `action` (opcional)
|
||
- `from` (opcional): Fecha inicio
|
||
- `to` (opcional): Fecha fin
|
||
|
||
---
|
||
|
||
## 4️⃣ FLUJO DE SINCRONIZACIÓN OFFLINE-FIRST
|
||
|
||
### 4.1 Arquitectura de Sincronización
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ APP OPERATIVA (PWA) │
|
||
│ │
|
||
│ 1. Al iniciar: │
|
||
│ - Cargar cache local (IndexedDB) │
|
||
│ - Verificar actualizaciones │
|
||
│ │
|
||
│ 2. Si hay red: │
|
||
│ - GET /api/v1/sync/check │
|
||
│ - Si hay updates: │
|
||
│ * Descargar contenido nuevo │
|
||
│ * Actualizar cache local │
|
||
│ * Notificar usuario (opcional) │
|
||
│ │
|
||
│ 3. Si NO hay red: │
|
||
│ - Usar cache local │
|
||
│ - Funcionar normalmente │
|
||
│ │
|
||
│ 4. En background (si hay red): │
|
||
│ - Verificar actualizaciones │
|
||
│ - Sincronizar silenciosamente │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
### 4.2 Estrategia de Cache Local
|
||
|
||
**Almacenamiento:** IndexedDB (navegador)
|
||
|
||
**Estructura de cache:**
|
||
```typescript
|
||
interface CachedContent {
|
||
id: string;
|
||
type: 'protocol' | 'drug' | 'guide' | 'manual';
|
||
version: number;
|
||
content: any;
|
||
cachedAt: string; // ISO 8601
|
||
expiresAt: string; // ISO 8601 (cachedAt + 7 días)
|
||
lastSync: string; // ISO 8601
|
||
}
|
||
```
|
||
|
||
**Reglas de cache:**
|
||
1. **Cache inicial:** Contenido embebido en build (fallback)
|
||
2. **Cache dinámico:** Descargado desde API y guardado en IndexedDB
|
||
3. **Expiración:** 7 días (configurable)
|
||
4. **Invalidación:** Por versión o por tiempo
|
||
5. **Prioridad:** Cache local > API > Fallback embebido
|
||
|
||
---
|
||
|
||
### 4.3 Flujo Detallado
|
||
|
||
#### Paso 1: Inicio de la App
|
||
|
||
```typescript
|
||
// Pseudocódigo conceptual
|
||
async function initializeApp() {
|
||
// 1. Cargar cache local
|
||
const cachedContent = await loadFromIndexedDB();
|
||
|
||
// 2. Si hay cache válido, usarlo inmediatamente
|
||
if (cachedContent && !isExpired(cachedContent)) {
|
||
useContent(cachedContent);
|
||
} else {
|
||
// 3. Usar fallback embebido
|
||
useContent(embeddedContent);
|
||
}
|
||
|
||
// 4. Intentar sincronizar en background
|
||
if (navigator.onLine) {
|
||
syncInBackground();
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### Paso 2: Sincronización en Background
|
||
|
||
```typescript
|
||
async function syncInBackground() {
|
||
try {
|
||
// 1. Verificar actualizaciones
|
||
const lastSync = getLastSyncTimestamp();
|
||
const response = await fetch(`/api/v1/sync/check?lastSync=${lastSync}`);
|
||
const { hasUpdates, updatedItems } = await response.json();
|
||
|
||
if (hasUpdates) {
|
||
// 2. Descargar contenido actualizado
|
||
for (const item of updatedItems) {
|
||
const content = await fetchContent(item.id, item.type);
|
||
await saveToIndexedDB(content);
|
||
}
|
||
|
||
// 3. Actualizar timestamp de sincronización
|
||
updateLastSyncTimestamp();
|
||
|
||
// 4. Notificar usuario (opcional, no intrusivo)
|
||
showUpdateNotification();
|
||
}
|
||
} catch (error) {
|
||
// Silencioso: no interrumpir uso de la app
|
||
console.error('Sync failed:', error);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### Paso 3: Uso Offline
|
||
|
||
```typescript
|
||
async function getContent(id: string, type: string) {
|
||
// 1. Intentar cache local
|
||
const cached = await getFromIndexedDB(id, type);
|
||
if (cached && !isExpired(cached)) {
|
||
return cached.content;
|
||
}
|
||
|
||
// 2. Si hay red, intentar API
|
||
if (navigator.onLine) {
|
||
try {
|
||
const content = await fetchFromAPI(id, type);
|
||
await saveToIndexedDB(content);
|
||
return content;
|
||
} catch (error) {
|
||
// Continuar con fallback
|
||
}
|
||
}
|
||
|
||
// 3. Fallback embebido (nunca falla)
|
||
return getEmbeddedContent(id, type);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4.4 Versionado de Contenido
|
||
|
||
**Estrategia:**
|
||
- Cada contenido tiene `version` incremental
|
||
- App cachea `version` junto con contenido
|
||
- Al sincronizar, compara versiones
|
||
- Solo descarga si `version` remota > `version` local
|
||
|
||
**Ventaja:** Minimiza transferencia de datos
|
||
|
||
---
|
||
|
||
## 5️⃣ PANEL ADMINISTRADOR
|
||
|
||
### 5.1 Alcance y Rol
|
||
|
||
**El panel administrador:**
|
||
- ✅ Es una aplicación web separada (no parte de la PWA)
|
||
- ✅ Permite editar TODO el contenido clínico
|
||
- ✅ Gestiona validación clínica
|
||
- ✅ Proporciona auditoría completa
|
||
- ✅ NO modifica código React de la app operativa
|
||
|
||
**El panel administrador NO:**
|
||
- ❌ Modifica componentes React
|
||
- ❌ Cambia rutas de la app
|
||
- ❌ Afecta al Service Worker
|
||
- ❌ Requiere redeploy de la app operativa
|
||
|
||
---
|
||
|
||
### 5.2 Funcionalidades Principales
|
||
|
||
#### 5.2.1 Gestión de Contenido
|
||
|
||
**Editor de Protocolos:**
|
||
- Formulario para editar: título, pasos, advertencias, puntos clave, material, fármacos
|
||
- Validación en tiempo real
|
||
- Preview del protocolo
|
||
- Guardar como borrador o solicitar validación
|
||
|
||
**Editor de Fármacos:**
|
||
- Formulario para editar: dosis, vías, indicaciones, contraindicaciones, notas
|
||
- Validación de formato de dosis
|
||
- Preview del fármaco
|
||
- Guardar como borrador o solicitar validación
|
||
|
||
**Editor de Guías:**
|
||
- Editor Markdown con preview
|
||
- Navegación entre secciones (1-8)
|
||
- Validación de estructura
|
||
- Guardar secciones individuales
|
||
|
||
**Editor de Manual:**
|
||
- Editor Markdown avanzado
|
||
- Navegación por bloques
|
||
- Búsqueda y reemplazo
|
||
- Validación de referencias cruzadas
|
||
|
||
---
|
||
|
||
#### 5.2.2 Validación Clínica
|
||
|
||
**Panel de Validación:**
|
||
- Lista de contenido pendiente
|
||
- Vista previa del contenido
|
||
- Campos para completar:
|
||
- Fuente clínica (ERC, AHA, SEMES, etc.)
|
||
- Puntuación de calidad (0-100)
|
||
- Comentarios
|
||
- Botones: Aprobar / Rechazar
|
||
|
||
**Flujo:**
|
||
1. Editor crea/modifica contenido → Estado: `draft`
|
||
2. Editor solicita validación → Estado: `pending`
|
||
3. Validador revisa → Aprobar → Estado: `validated` (público)
|
||
4. Validador revisa → Rechazar → Estado: `rejected` (vuelve a `draft`)
|
||
|
||
---
|
||
|
||
#### 5.2.3 Auditoría
|
||
|
||
**Log de Cambios:**
|
||
- Historial completo de todos los cambios
|
||
- Filtros: por contenido, usuario, fecha, acción
|
||
- Vista de diferencias entre versiones
|
||
- Exportación de logs
|
||
|
||
**Trazabilidad:**
|
||
- Quién hizo qué cambio y cuándo
|
||
- IP y user agent (para seguridad)
|
||
- Rollback a versiones anteriores
|
||
|
||
---
|
||
|
||
### 5.3 Tecnología del Panel
|
||
|
||
**Stack propuesto:**
|
||
- **Frontend:** React + TypeScript (consistente con app principal)
|
||
- **UI:** shadcn/ui (consistente con app principal)
|
||
- **Editor Markdown:** react-markdown-editor-lite
|
||
- **Validación:** Zod (TypeScript-first)
|
||
- **Autenticación:** JWT tokens
|
||
- **Estado:** React Query (para cache de API)
|
||
|
||
**Estructura:**
|
||
```
|
||
admin-panel/
|
||
├── src/
|
||
│ ├── pages/
|
||
│ │ ├── ProtocolsEditor.tsx
|
||
│ │ ├── DrugsEditor.tsx
|
||
│ │ ├── GuidesEditor.tsx
|
||
│ │ ├── ManualEditor.tsx
|
||
│ │ ├── Validation.tsx
|
||
│ │ └── Audit.tsx
|
||
│ ├── components/
|
||
│ │ ├── ContentEditor/
|
||
│ │ ├── Validation/
|
||
│ │ └── VersionHistory/
|
||
│ └── lib/
|
||
│ ├── api.ts
|
||
│ └── validation.ts
|
||
```
|
||
|
||
---
|
||
|
||
## 6️⃣ QUÉ NO TOCAR DE LA APP EXISTENTE
|
||
|
||
### 6.1 Componentes React
|
||
|
||
**NO modificar:**
|
||
- ❌ `src/pages/RCP.tsx`, `Ictus.tsx`, `ViaAerea.tsx`, `Shock.tsx`
|
||
- ❌ `src/components/procedures/`
|
||
- ❌ `src/components/drugs/`
|
||
- ❌ Estructura visual y lógica de UI
|
||
|
||
**SÍ permitido:**
|
||
- ✅ Modificar funciones de carga de datos (de estático a dinámico)
|
||
- ✅ Añadir lógica de cache local
|
||
- ✅ Añadir sincronización en background
|
||
|
||
---
|
||
|
||
### 6.2 Service Worker
|
||
|
||
**NO modificar:**
|
||
- ❌ `public/sw.js`
|
||
- ❌ Estrategia de cache
|
||
- ❌ Lógica de actualización
|
||
|
||
**Razón:** El Service Worker es crítico para funcionamiento offline. Cualquier cambio puede romper la app en emergencias.
|
||
|
||
---
|
||
|
||
### 6.3 Rutas y Navegación
|
||
|
||
**NO modificar:**
|
||
- ❌ Rutas existentes (`/rcp`, `/ictus`, `/via-aerea`, etc.)
|
||
- ❌ Estructura de navegación
|
||
- ❌ Deep links
|
||
|
||
**Razón:** Las rutas pueden estar bookmarked o compartidas. Cambiarlas rompe enlaces.
|
||
|
||
---
|
||
|
||
### 6.4 Estructura de Datos TypeScript
|
||
|
||
**NO eliminar:**
|
||
- ❌ `src/data/procedures.ts` (mantener como fallback)
|
||
- ❌ `src/data/drugs.ts` (mantener como fallback)
|
||
- ❌ `src/data/guides-index.ts` (mantener como fallback)
|
||
|
||
**SÍ permitido:**
|
||
- ✅ Modificar para cargar desde API con fallback
|
||
- ✅ Añadir lógica de cache
|
||
|
||
---
|
||
|
||
### 6.5 Build y Deploy
|
||
|
||
**NO cambiar:**
|
||
- ❌ Proceso de build (Vite)
|
||
- ❌ Estructura de archivos públicos
|
||
- ❌ Configuración de PWA
|
||
|
||
**Razón:** Cambios en build pueden afectar funcionamiento offline.
|
||
|
||
---
|
||
|
||
## 7️⃣ ROADMAP DE IMPLEMENTACIÓN
|
||
|
||
### FASE 1: Infraestructura Base (4-5 semanas)
|
||
|
||
**Objetivos:**
|
||
- Configurar PostgreSQL
|
||
- Crear esquema de base de datos
|
||
- Implementar API REST básica
|
||
- Migrar contenido inicial
|
||
|
||
**Tareas:**
|
||
- [ ] Instalar y configurar PostgreSQL
|
||
- [ ] Ejecutar scripts de creación de esquema
|
||
- [ ] Crear backend Node.js básico (Express)
|
||
- [ ] Implementar endpoints GET (solo lectura)
|
||
- [ ] Script de migración: TypeScript → PostgreSQL
|
||
- [ ] Validar que contenido migrado es correcto
|
||
|
||
**Criterios de éxito:**
|
||
- ✅ Base de datos funcionando
|
||
- ✅ API responde correctamente
|
||
- ✅ Contenido migrado sin pérdidas
|
||
- ✅ App operativa sigue funcionando igual
|
||
|
||
---
|
||
|
||
### FASE 2: Sincronización Offline (4-5 semanas)
|
||
|
||
**Objetivos:**
|
||
- Implementar carga dinámica en app
|
||
- Sistema de cache local (IndexedDB)
|
||
- Sincronización en background
|
||
- Fallback robusto
|
||
|
||
**Tareas:**
|
||
- [ ] Crear `src/lib/content-loader.ts`
|
||
- [ ] Crear `src/lib/content-cache.ts` (IndexedDB)
|
||
- [ ] Modificar `src/data/procedures.ts` para cargar dinámicamente
|
||
- [ ] Modificar `src/data/drugs.ts` para cargar dinámicamente
|
||
- [ ] Implementar sincronización en background
|
||
- [ ] Testing exhaustivo offline/online
|
||
- [ ] Validar que fallback funciona
|
||
|
||
**Criterios de éxito:**
|
||
- ✅ App carga contenido desde API
|
||
- ✅ Cache local funciona
|
||
- ✅ App funciona 100% offline
|
||
- ✅ Sincronización transparente
|
||
- ✅ Sin regresiones
|
||
|
||
---
|
||
|
||
### FASE 3: Panel Admin MVP (6-8 semanas)
|
||
|
||
**Objetivos:**
|
||
- Crear panel administrador básico
|
||
- Editor de protocolos y fármacos
|
||
- Autenticación y autorización
|
||
- Guardado en base de datos
|
||
|
||
**Tareas:**
|
||
- [ ] Crear proyecto `admin-panel/`
|
||
- [ ] Implementar autenticación JWT
|
||
- [ ] Crear editor de protocolos
|
||
- [ ] Crear editor de fármacos
|
||
- [ ] Implementar endpoints PUT/POST
|
||
- [ ] Sistema de versionado automático
|
||
- [ ] Testing del panel
|
||
|
||
**Criterios de éxito:**
|
||
- ✅ Panel permite editar contenido
|
||
- ✅ Cambios se guardan en BD
|
||
- ✅ Versiones se incrementan
|
||
- ✅ App consume contenido actualizado
|
||
- ✅ Autenticación funciona
|
||
|
||
---
|
||
|
||
### FASE 4: Validación Clínica (4-5 semanas)
|
||
|
||
**Objetivos:**
|
||
- Sistema de roles y permisos
|
||
- Flujo de validación clínica
|
||
- Panel de validación
|
||
- Metadatos de validación
|
||
|
||
**Tareas:**
|
||
- [ ] Implementar RBAC
|
||
- [ ] Crear panel de validación
|
||
- [ ] Endpoints de aprobación/rechazo
|
||
- [ ] Campos de metadatos clínicos
|
||
- [ ] Notificaciones de cambios de estado
|
||
- [ ] Testing con usuarios reales
|
||
|
||
**Criterios de éxito:**
|
||
- ✅ Roles funcionan correctamente
|
||
- ✅ Flujo de validación completo
|
||
- ✅ Solo contenido validado es público
|
||
- ✅ Metadatos se guardan correctamente
|
||
|
||
---
|
||
|
||
### FASE 5: Editores Completos (6-8 semanas)
|
||
|
||
**Objetivos:**
|
||
- Editor de guías formativas
|
||
- Editor de manual completo
|
||
- Funcionalidades avanzadas
|
||
- Migración completa
|
||
|
||
**Tareas:**
|
||
- [ ] Editor de guías (Markdown)
|
||
- [ ] Editor de manual (Markdown avanzado)
|
||
- [ ] Comparación de versiones (diff)
|
||
- [ ] Rollback a versiones anteriores
|
||
- [ ] Migrar todas las guías
|
||
- [ ] Migrar todo el manual
|
||
|
||
**Criterios de éxito:**
|
||
- ✅ Todos los editores funcionan
|
||
- ✅ Todo el contenido migrado
|
||
- ✅ Funcionalidades avanzadas disponibles
|
||
|
||
---
|
||
|
||
### FASE 6: Auditoría y Optimización (4-5 semanas)
|
||
|
||
**Objetivos:**
|
||
- Sistema de auditoría completo
|
||
- Optimización de performance
|
||
- Documentación
|
||
- Mejoras de UX
|
||
|
||
**Tareas:**
|
||
- [ ] Panel de auditoría
|
||
- [ ] Exportación de logs
|
||
- [ ] Optimización de queries
|
||
- [ ] CDN para contenido estático
|
||
- [ ] Documentación completa
|
||
- [ ] Testing de carga
|
||
|
||
**Criterios de éxito:**
|
||
- ✅ Auditoría completa funcional
|
||
- ✅ Performance optimizada
|
||
- ✅ Documentación completa
|
||
|
||
---
|
||
|
||
## 📊 RESUMEN EJECUTIVO
|
||
|
||
### Arquitectura
|
||
|
||
- **Base de datos:** PostgreSQL con esquema `emerges_content`
|
||
- **Backend:** Node.js + Express + API REST
|
||
- **Panel Admin:** React + TypeScript (app separada)
|
||
- **Sincronización:** Offline-first con IndexedDB
|
||
|
||
### Modelo de Datos
|
||
|
||
- **7 tablas principales:** content_items, content_versions, content_relations, content_change_log, users, content_sync_status
|
||
- **Versionado completo:** Historial de todas las versiones
|
||
- **Validación clínica:** Estados y metadatos de validación
|
||
- **Auditoría:** Log completo de cambios
|
||
|
||
### API REST
|
||
|
||
- **Endpoints públicos:** Solo lectura (app operativa)
|
||
- **Endpoints protegidos:** Escritura con autenticación (panel admin)
|
||
- **Versionado:** v1
|
||
- **Documentación:** OpenAPI/Swagger
|
||
|
||
### Sincronización
|
||
|
||
- **Estrategia:** Cache local > API > Fallback embebido
|
||
- **Offline-first:** App funciona sin red
|
||
- **Background sync:** Sincronización transparente
|
||
- **Versionado:** Solo descarga si hay actualizaciones
|
||
|
||
### Panel Admin
|
||
|
||
- **Alcance:** Edición completa de contenido clínico
|
||
- **Validación:** Flujo de aprobación/rechazo
|
||
- **Auditoría:** Historial completo
|
||
- **Separado:** No modifica app operativa
|
||
|
||
### Roadmap
|
||
|
||
- **6 fases:** 28-36 semanas total
|
||
- **Progresivo:** Cada fase independiente
|
||
- **Validado:** Testing continuo
|
||
- **Sin disrupciones:** App funciona en todo momento
|
||
|
||
---
|
||
|
||
**Este sistema permite modificar contenido clínico sin tocar código React, manteniendo funcionamiento offline-first y validación clínica obligatoria.**
|
||
|