1359 lines
39 KiB
Markdown
1359 lines
39 KiB
Markdown
|
|
# 🏗️ PLAN TÉCNICO EJECUTABLE - SISTEMA DE GESTIÓN DE CONTENIDO
|
||
|
|
|
||
|
|
**Fecha:** 2025-01-06
|
||
|
|
**Arquitecto:** Tech Lead + Arquitecto de Sistema
|
||
|
|
**Versión:** 1.0
|
||
|
|
**Estado:** Diseño - Sin Implementación
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📋 ÍNDICE
|
||
|
|
|
||
|
|
1. [FASE 4 — BASE DE CONTENIDO](#fase-4--base-de-contenido)
|
||
|
|
2. [FASE 2 — INTEGRACIÓN ESTRUCTURAL](#fase-2--integración-estructural)
|
||
|
|
3. [FASE 3 — VALIDACIÓN Y ESCALADO](#fase-3--validación-y-escalado)
|
||
|
|
4. [PANEL DE ADMINISTRACIÓN](#panel-de-administración)
|
||
|
|
5. [ROADMAP DE IMPLEMENTACIÓN](#roadmap-de-implementación)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
# FASE 4 — BASE DE CONTENIDO
|
||
|
|
|
||
|
|
## 4.1 Modelo de Datos Canónico
|
||
|
|
|
||
|
|
### A. Principios de Diseño
|
||
|
|
|
||
|
|
1. **Desacoplamiento Total**
|
||
|
|
- Modelo independiente de `procedures.ts` y `drugs.ts`
|
||
|
|
- Compatible con contenido local (fallback)
|
||
|
|
- Extensible sin romper producción
|
||
|
|
|
||
|
|
2. **Neutralidad de Almacenamiento**
|
||
|
|
- Modelo JSON-serializable
|
||
|
|
- Compatible con SQL, NoSQL, JSON local
|
||
|
|
- Versionado integrado
|
||
|
|
|
||
|
|
3. **Asociación de Recursos**
|
||
|
|
- Imágenes y vídeos como entidades separadas
|
||
|
|
- Relaciones many-to-many
|
||
|
|
- Metadatos completos
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### B. Modelo de Datos Base
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// ============================================
|
||
|
|
// CORE: Base Content Item
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
interface BaseContentItem {
|
||
|
|
// Identificación
|
||
|
|
id: string; // ID estable (inmutable, ej: "rcp-adulto-svb")
|
||
|
|
slug: string; // Slug para URLs (ej: "rcp-adulto-svb")
|
||
|
|
type: ContentType; // 'protocol' | 'guide' | 'manual' | 'drug' | 'checklist'
|
||
|
|
level: ContentLevel; // 'operativo' | 'formativo' | 'referencia'
|
||
|
|
|
||
|
|
// Metadatos básicos
|
||
|
|
title: string;
|
||
|
|
shortTitle?: string;
|
||
|
|
description?: string;
|
||
|
|
icon?: string;
|
||
|
|
|
||
|
|
// Prioridad y clasificación
|
||
|
|
priority: Priority; // 'critica' | 'alta' | 'media' | 'baja'
|
||
|
|
category?: string; // Categoría temática
|
||
|
|
tags: string[]; // Tags para búsqueda
|
||
|
|
|
||
|
|
// Estado y validación
|
||
|
|
status: ContentStatus; // 'draft' | 'in_review' | 'approved' | 'published' | 'archived'
|
||
|
|
validatedBy?: string; // ID del validador clínico
|
||
|
|
validatedAt?: string; // ISO timestamp
|
||
|
|
clinicalSource?: string; // Fuente clínica (ERC, SEMES, etc.)
|
||
|
|
|
||
|
|
// Versionado
|
||
|
|
version: number; // Versión actual
|
||
|
|
latestVersion: number; // Última versión disponible
|
||
|
|
versionHistory?: string[]; // IDs de versiones anteriores
|
||
|
|
|
||
|
|
// Auditoría
|
||
|
|
createdBy?: string;
|
||
|
|
createdAt: string; // ISO timestamp
|
||
|
|
updatedBy?: string;
|
||
|
|
updatedAt: string; // ISO timestamp
|
||
|
|
|
||
|
|
// Relaciones
|
||
|
|
relations?: ContentRelations;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// RECURSOS VISUALES (Imágenes y Vídeos)
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
interface MediaResource {
|
||
|
|
id: string; // ID único del recurso
|
||
|
|
type: 'image' | 'video';
|
||
|
|
|
||
|
|
// Archivo
|
||
|
|
filename: string; // Nombre del archivo (ej: "rcp_posicion_manos_adulto.png")
|
||
|
|
path: string; // Ruta relativa (ej: "/assets/infografias/rcp/...")
|
||
|
|
url?: string; // URL completa (si está en CDN)
|
||
|
|
|
||
|
|
// Metadatos
|
||
|
|
alt: string; // Texto alternativo (accesibilidad)
|
||
|
|
caption?: string; // Caption opcional
|
||
|
|
title?: string; // Título del recurso
|
||
|
|
|
||
|
|
// Clasificación
|
||
|
|
block?: string; // Bloque temático (ej: "bloque-0-fundamentos")
|
||
|
|
chapter?: string; // Capítulo relacionado
|
||
|
|
tags: string[]; // Tags para búsqueda
|
||
|
|
|
||
|
|
// Prioridad y uso
|
||
|
|
priority: Priority; // Prioridad del recurso
|
||
|
|
usageType: ContentLevel[]; // ['operativo', 'formativo'] - dónde se usa
|
||
|
|
|
||
|
|
// Dimensiones (para imágenes)
|
||
|
|
width?: number;
|
||
|
|
height?: number;
|
||
|
|
format?: string; // 'png' | 'svg' | 'jpg' | 'webp'
|
||
|
|
|
||
|
|
// Duración (para vídeos)
|
||
|
|
duration?: number; // Duración en segundos
|
||
|
|
format?: string; // 'mp4' | 'webm'
|
||
|
|
|
||
|
|
// Fuente
|
||
|
|
source?: string; // Fuente del recurso
|
||
|
|
attribution?: string; // Atribución si es necesario
|
||
|
|
|
||
|
|
// Estado
|
||
|
|
status: 'draft' | 'approved' | 'published';
|
||
|
|
|
||
|
|
// Auditoría
|
||
|
|
uploadedBy?: string;
|
||
|
|
uploadedAt: string;
|
||
|
|
updatedAt: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// ASOCIACIÓN RECURSOS ⇄ CONTENIDO
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
interface ContentResourceAssociation {
|
||
|
|
id: string;
|
||
|
|
contentId: string; // ID del contenido (protocolo, guía, etc.)
|
||
|
|
resourceId: string; // ID del recurso (imagen/vídeo)
|
||
|
|
|
||
|
|
// Contexto de asociación
|
||
|
|
context: {
|
||
|
|
section?: string; // Sección específica (ej: "paso-3", "seccion-4")
|
||
|
|
position?: number; // Posición en el contenido (orden)
|
||
|
|
placement?: 'inline' | 'before' | 'after' | 'modal'; // Dónde se muestra
|
||
|
|
caption?: string; // Caption específico para este contexto
|
||
|
|
};
|
||
|
|
|
||
|
|
// Metadatos
|
||
|
|
isRequired: boolean; // Si es obligatorio para el contenido
|
||
|
|
priority: Priority; // Prioridad de esta asociación
|
||
|
|
|
||
|
|
createdAt: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// PROTOCOLO (Extendido)
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
interface ProtocolContent {
|
||
|
|
// Pasos operativos
|
||
|
|
steps: ProtocolStep[];
|
||
|
|
|
||
|
|
// Checklist integrado
|
||
|
|
checklist?: {
|
||
|
|
enabled: boolean;
|
||
|
|
title?: string;
|
||
|
|
items: ChecklistItem[];
|
||
|
|
};
|
||
|
|
|
||
|
|
// Dosis inline
|
||
|
|
inlineDoses?: InlineDose[];
|
||
|
|
|
||
|
|
// Herramientas de contexto
|
||
|
|
contextTools?: ContextTool[];
|
||
|
|
|
||
|
|
// Fuentes clínicas
|
||
|
|
clinicalSources?: ClinicalSource[];
|
||
|
|
|
||
|
|
// Campos existentes (compatibilidad)
|
||
|
|
warnings: string[];
|
||
|
|
keyPoints?: string[];
|
||
|
|
equipment?: string[];
|
||
|
|
drugs?: string[];
|
||
|
|
}
|
||
|
|
|
||
|
|
interface Protocol extends BaseContentItem {
|
||
|
|
type: 'protocol';
|
||
|
|
level: 'operativo';
|
||
|
|
content: ProtocolContent;
|
||
|
|
|
||
|
|
// Campos específicos
|
||
|
|
category: Category;
|
||
|
|
subcategory?: string;
|
||
|
|
ageGroup: AgeGroup;
|
||
|
|
|
||
|
|
// Recursos asociados
|
||
|
|
resources?: {
|
||
|
|
images?: string[]; // IDs de MediaResource
|
||
|
|
videos?: string[]; // IDs de MediaResource
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// GUÍA FORMATIVA (Extendido)
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
interface GuideSection {
|
||
|
|
numero: number; // 1-8
|
||
|
|
titulo: string;
|
||
|
|
markdown: string; // Contenido Markdown
|
||
|
|
estimatedTime?: string; // Tiempo estimado de lectura
|
||
|
|
|
||
|
|
// Recursos por sección
|
||
|
|
resources?: {
|
||
|
|
images?: string[]; // IDs de MediaResource
|
||
|
|
videos?: string[]; // IDs de MediaResource
|
||
|
|
links?: Array<{ title: string; url: string }>;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
interface GuideContent {
|
||
|
|
sections: GuideSection[]; // Siempre 8 secciones
|
||
|
|
relations: {
|
||
|
|
protocolId?: string; // Protocolo operativo relacionado
|
||
|
|
manualChapterIds?: string[]; // Capítulos de manual relacionados
|
||
|
|
};
|
||
|
|
metadata?: {
|
||
|
|
learningObjectives?: string[];
|
||
|
|
prerequisites?: string[];
|
||
|
|
targetAudience?: string[];
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
interface Guide extends BaseContentItem {
|
||
|
|
type: 'guide';
|
||
|
|
level: 'formativo';
|
||
|
|
content: GuideContent;
|
||
|
|
|
||
|
|
// Recursos globales de la guía
|
||
|
|
resources?: {
|
||
|
|
images?: string[]; // IDs de MediaResource
|
||
|
|
videos?: string[]; // IDs de MediaResource
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// CAPÍTULO DE MANUAL (Extendido)
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
interface ManualChapterContent {
|
||
|
|
markdown: string;
|
||
|
|
tags: string[];
|
||
|
|
relatedProtocols?: string[]; // IDs de protocolos relacionados
|
||
|
|
relatedGuides?: string[]; // IDs de guías relacionadas
|
||
|
|
references?: ClinicalSource[];
|
||
|
|
}
|
||
|
|
|
||
|
|
interface ManualChapter extends BaseContentItem {
|
||
|
|
type: 'manual';
|
||
|
|
level: 'referencia';
|
||
|
|
content: ManualChapterContent;
|
||
|
|
|
||
|
|
// Estructura jerárquica
|
||
|
|
block: string; // Bloque del manual (ej: "BLOQUE_01")
|
||
|
|
section?: string; // Sección dentro del bloque
|
||
|
|
order: number; // Orden dentro del bloque
|
||
|
|
|
||
|
|
// Recursos asociados
|
||
|
|
resources?: {
|
||
|
|
images?: string[]; // IDs de MediaResource
|
||
|
|
videos?: string[]; // IDs de MediaResource
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// FÁRMACO (Extendido)
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
interface DrugContent {
|
||
|
|
genericName: string;
|
||
|
|
tradeName: string;
|
||
|
|
category: string;
|
||
|
|
presentation: string;
|
||
|
|
|
||
|
|
adultDose: string;
|
||
|
|
pediatricDose?: string;
|
||
|
|
routes: AdministrationRoute[];
|
||
|
|
dilution?: string;
|
||
|
|
|
||
|
|
indications: string[];
|
||
|
|
contraindications: string[];
|
||
|
|
sideEffects?: string[];
|
||
|
|
antidote?: string;
|
||
|
|
|
||
|
|
notes?: string[];
|
||
|
|
criticalPoints?: string[];
|
||
|
|
source?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface Drug extends BaseContentItem {
|
||
|
|
type: 'drug';
|
||
|
|
level: 'referencia';
|
||
|
|
content: DrugContent;
|
||
|
|
|
||
|
|
// Recursos asociados (infografías de administración)
|
||
|
|
resources?: {
|
||
|
|
images?: string[]; // IDs de MediaResource (esquemas de administración)
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// RELACIONES ENTRE CONTENIDO
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
interface ContentRelations {
|
||
|
|
// Relaciones bidireccionales
|
||
|
|
protocols?: string[]; // IDs de protocolos relacionados
|
||
|
|
guides?: string[]; // IDs de guías relacionadas
|
||
|
|
manuals?: string[]; // IDs de capítulos de manual relacionados
|
||
|
|
drugs?: string[]; // IDs de fármacos relacionados
|
||
|
|
checklists?: string[]; // IDs de checklists relacionadas
|
||
|
|
|
||
|
|
// Relaciones específicas
|
||
|
|
parentContent?: string; // ID del contenido padre (si aplica)
|
||
|
|
childContent?: string[]; // IDs de contenidos hijos (si aplica)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// CONTENT PACK (Para distribución)
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
interface ContentPack {
|
||
|
|
version: string; // Versión semántica (ej: "1.2.3")
|
||
|
|
timestamp: string; // ISO timestamp
|
||
|
|
hash: string; // Hash SHA-256 del contenido
|
||
|
|
|
||
|
|
// Contenido
|
||
|
|
protocols?: Protocol[];
|
||
|
|
guides?: Guide[];
|
||
|
|
manuals?: ManualChapter[];
|
||
|
|
drugs?: Drug[];
|
||
|
|
checklists?: ChecklistReusable[];
|
||
|
|
|
||
|
|
// Recursos (opcional, puede estar en CDN)
|
||
|
|
resources?: MediaResource[];
|
||
|
|
|
||
|
|
// Metadatos
|
||
|
|
publishedBy?: string;
|
||
|
|
publishedAt?: string;
|
||
|
|
clinicalSource?: string;
|
||
|
|
notes?: string;
|
||
|
|
|
||
|
|
// Compatibilidad
|
||
|
|
minAppVersion?: string; // Versión mínima de app requerida
|
||
|
|
maxAppVersion?: string; // Versión máxima de app compatible
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// TIPOS AUXILIARES
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
type ContentType = 'protocol' | 'guide' | 'manual' | 'drug' | 'checklist';
|
||
|
|
type ContentLevel = 'operativo' | 'formativo' | 'referencia';
|
||
|
|
type ContentStatus = 'draft' | 'in_review' | 'approved' | 'published' | 'archived';
|
||
|
|
type Priority = 'critica' | 'alta' | 'media' | 'baja';
|
||
|
|
type Category = 'soporte_vital' | 'patologias' | 'escena';
|
||
|
|
type AgeGroup = 'adulto' | 'pediatrico' | 'neonatal' | 'todos';
|
||
|
|
type AdministrationRoute = 'IV' | 'IM' | 'SC' | 'IO' | 'Nebulizado' | 'SL' | 'Rectal' | 'Nasal';
|
||
|
|
|
||
|
|
interface ProtocolStep {
|
||
|
|
order: number;
|
||
|
|
text: string;
|
||
|
|
critical?: boolean;
|
||
|
|
equipment?: string[];
|
||
|
|
timeEstimate?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface ChecklistItem {
|
||
|
|
id: string;
|
||
|
|
text: string;
|
||
|
|
order: number;
|
||
|
|
critical?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface InlineDose {
|
||
|
|
drugId: string;
|
||
|
|
drugName: string;
|
||
|
|
adultDose: string;
|
||
|
|
pediatricDose?: string;
|
||
|
|
route: AdministrationRoute;
|
||
|
|
timing?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface ContextTool {
|
||
|
|
id: string;
|
||
|
|
name: string;
|
||
|
|
type: 'calculator' | 'algorithm' | 'reference';
|
||
|
|
url?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface ClinicalSource {
|
||
|
|
organization: string; // ERC, SEMES, AHA, etc.
|
||
|
|
guideline: string;
|
||
|
|
year: number;
|
||
|
|
url?: string;
|
||
|
|
section?: string;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4.2 Base de Datos Recomendada
|
||
|
|
|
||
|
|
### A. Opción Principal: **Supabase**
|
||
|
|
|
||
|
|
#### Justificación:
|
||
|
|
|
||
|
|
1. **Offline-First Compatible**
|
||
|
|
- ✅ PostgreSQL con replicación local
|
||
|
|
- ✅ Supabase Realtime para sincronización
|
||
|
|
- ✅ Supabase Storage para recursos multimedia
|
||
|
|
- ✅ Cache local con IndexedDB
|
||
|
|
|
||
|
|
2. **Editable desde Panel Admin**
|
||
|
|
- ✅ API REST automática
|
||
|
|
- ✅ Dashboard web integrado
|
||
|
|
- ✅ Autenticación y RBAC incluidos
|
||
|
|
- ✅ Editor visual de datos (opcional)
|
||
|
|
|
||
|
|
3. **Sin Tocar App en Producción**
|
||
|
|
- ✅ Content Pack como JSON descargable
|
||
|
|
- ✅ App consume pack, no DB directamente
|
||
|
|
- ✅ Actualizaciones sin deploy de app
|
||
|
|
- ✅ Versionado automático
|
||
|
|
|
||
|
|
4. **Fácil para Sanitarios No Técnicos**
|
||
|
|
- ✅ Interfaz web intuitiva
|
||
|
|
- ✅ Editor de Markdown integrado
|
||
|
|
- ✅ Subida de imágenes/vídeos drag-and-drop
|
||
|
|
- ✅ Validación de datos en UI
|
||
|
|
|
||
|
|
5. **Control de Versiones**
|
||
|
|
- ✅ Historial de cambios automático
|
||
|
|
- ✅ Rollback de versiones
|
||
|
|
- ✅ Auditoría completa
|
||
|
|
- ✅ Git-like para contenido
|
||
|
|
|
||
|
|
#### Arquitectura Propuesta:
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────┐
|
||
|
|
│ SUPABASE (Cloud) │
|
||
|
|
├─────────────────────────────────────────┤
|
||
|
|
│ PostgreSQL Database │
|
||
|
|
│ ├── content_items │
|
||
|
|
│ ├── media_resources │
|
||
|
|
│ ├── content_resource_associations │
|
||
|
|
│ ├── content_versions │
|
||
|
|
│ └── audit_logs │
|
||
|
|
│ │
|
||
|
|
│ Storage (S3-compatible) │
|
||
|
|
│ ├── images/ │
|
||
|
|
│ └── videos/ │
|
||
|
|
│ │
|
||
|
|
│ Realtime (WebSockets) │
|
||
|
|
│ └── content_updates │
|
||
|
|
└─────────────────────────────────────────┘
|
||
|
|
│
|
||
|
|
│ (Content Pack JSON)
|
||
|
|
▼
|
||
|
|
┌─────────────────────────────────────────┐
|
||
|
|
│ ADMIN PANEL (React) │
|
||
|
|
│ └── Edita contenido → Supabase │
|
||
|
|
└─────────────────────────────────────────┘
|
||
|
|
│
|
||
|
|
│ (Publica Content Pack)
|
||
|
|
▼
|
||
|
|
┌─────────────────────────────────────────┐
|
||
|
|
│ APP TES (PWA) │
|
||
|
|
│ └── Descarga pack → Cache local │
|
||
|
|
│ └── Funciona 100% offline │
|
||
|
|
└─────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Esquema de Base de Datos:
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- Tabla principal de contenido
|
||
|
|
CREATE TABLE content_items (
|
||
|
|
id TEXT PRIMARY KEY,
|
||
|
|
slug TEXT UNIQUE NOT NULL,
|
||
|
|
type TEXT NOT NULL, -- 'protocol' | 'guide' | 'manual' | 'drug' | 'checklist'
|
||
|
|
level TEXT NOT NULL, -- 'operativo' | 'formativo' | 'referencia'
|
||
|
|
title TEXT NOT NULL,
|
||
|
|
short_title TEXT,
|
||
|
|
description TEXT,
|
||
|
|
icon TEXT,
|
||
|
|
priority TEXT NOT NULL, -- 'critica' | 'alta' | 'media' | 'baja'
|
||
|
|
category TEXT,
|
||
|
|
tags TEXT[],
|
||
|
|
status TEXT NOT NULL, -- 'draft' | 'in_review' | 'approved' | 'published' | 'archived'
|
||
|
|
validated_by TEXT,
|
||
|
|
validated_at TIMESTAMPTZ,
|
||
|
|
clinical_source TEXT,
|
||
|
|
version INTEGER NOT NULL DEFAULT 1,
|
||
|
|
latest_version INTEGER NOT NULL DEFAULT 1,
|
||
|
|
created_by TEXT,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
|
|
updated_by TEXT,
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
|
|
content JSONB NOT NULL, -- Contenido específico del tipo
|
||
|
|
relations JSONB, -- Relaciones con otros contenidos
|
||
|
|
resources JSONB -- IDs de recursos asociados
|
||
|
|
);
|
||
|
|
|
||
|
|
-- Tabla de recursos multimedia
|
||
|
|
CREATE TABLE media_resources (
|
||
|
|
id TEXT PRIMARY KEY,
|
||
|
|
type TEXT NOT NULL, -- 'image' | 'video'
|
||
|
|
filename TEXT NOT NULL,
|
||
|
|
path TEXT NOT NULL,
|
||
|
|
url TEXT,
|
||
|
|
alt TEXT NOT NULL,
|
||
|
|
caption TEXT,
|
||
|
|
title TEXT,
|
||
|
|
block TEXT,
|
||
|
|
chapter TEXT,
|
||
|
|
tags TEXT[],
|
||
|
|
priority TEXT NOT NULL,
|
||
|
|
usage_type TEXT[], -- ['operativo', 'formativo']
|
||
|
|
width INTEGER,
|
||
|
|
height INTEGER,
|
||
|
|
format TEXT,
|
||
|
|
duration INTEGER, -- Para vídeos (segundos)
|
||
|
|
source TEXT,
|
||
|
|
attribution TEXT,
|
||
|
|
status TEXT NOT NULL,
|
||
|
|
uploaded_by TEXT,
|
||
|
|
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
|
|
);
|
||
|
|
|
||
|
|
-- Tabla de asociaciones contenido ⇄ recursos
|
||
|
|
CREATE TABLE content_resource_associations (
|
||
|
|
id TEXT PRIMARY KEY,
|
||
|
|
content_id TEXT NOT NULL REFERENCES content_items(id),
|
||
|
|
resource_id TEXT NOT NULL REFERENCES media_resources(id),
|
||
|
|
context JSONB NOT NULL, -- { section, position, placement, caption }
|
||
|
|
is_required BOOLEAN NOT NULL DEFAULT false,
|
||
|
|
priority TEXT NOT NULL,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
|
|
UNIQUE(content_id, resource_id, context->>'section')
|
||
|
|
);
|
||
|
|
|
||
|
|
-- Tabla de versiones
|
||
|
|
CREATE TABLE content_versions (
|
||
|
|
version_id TEXT PRIMARY KEY,
|
||
|
|
content_id TEXT NOT NULL REFERENCES content_items(id),
|
||
|
|
version INTEGER NOT NULL,
|
||
|
|
content JSONB NOT NULL,
|
||
|
|
status TEXT NOT NULL,
|
||
|
|
change_summary TEXT,
|
||
|
|
created_by TEXT NOT NULL,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
|
|
validated_by TEXT,
|
||
|
|
validated_at TIMESTAMPTZ,
|
||
|
|
UNIQUE(content_id, version)
|
||
|
|
);
|
||
|
|
|
||
|
|
-- Tabla de auditoría
|
||
|
|
CREATE TABLE audit_logs (
|
||
|
|
log_id TEXT PRIMARY KEY,
|
||
|
|
content_item_id TEXT REFERENCES content_items(id),
|
||
|
|
version_id TEXT REFERENCES content_versions(version_id),
|
||
|
|
user_id TEXT NOT NULL,
|
||
|
|
action TEXT NOT NULL, -- 'create' | 'update' | 'delete' | 'validate' | 'approve' | 'publish' | 'revert'
|
||
|
|
details JSONB,
|
||
|
|
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
|
|
);
|
||
|
|
|
||
|
|
-- Índices para búsqueda
|
||
|
|
CREATE INDEX idx_content_items_type ON content_items(type);
|
||
|
|
CREATE INDEX idx_content_items_level ON content_items(level);
|
||
|
|
CREATE INDEX idx_content_items_status ON content_items(status);
|
||
|
|
CREATE INDEX idx_content_items_tags ON content_items USING GIN(tags);
|
||
|
|
CREATE INDEX idx_media_resources_type ON media_resources(type);
|
||
|
|
CREATE INDEX idx_media_resources_usage_type ON media_resources USING GIN(usage_type);
|
||
|
|
CREATE INDEX idx_content_resource_associations_content ON content_resource_associations(content_id);
|
||
|
|
CREATE INDEX idx_content_resource_associations_resource ON content_resource_associations(resource_id);
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Ventajas de Supabase:
|
||
|
|
|
||
|
|
- ✅ **Gratis hasta 500MB** (suficiente para MVP)
|
||
|
|
- ✅ **PostgreSQL completo** (relaciones, transacciones, triggers)
|
||
|
|
- ✅ **Storage integrado** (imágenes/vídeos)
|
||
|
|
- ✅ **Realtime opcional** (notificaciones de actualizaciones)
|
||
|
|
- ✅ **API REST automática** (sin código backend)
|
||
|
|
- ✅ **Autenticación incluida** (JWT, RBAC)
|
||
|
|
- ✅ **Dashboard web** (edición visual)
|
||
|
|
- ✅ **Migraciones** (control de versiones de schema)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### B. Opción Alternativa: **PocketBase**
|
||
|
|
|
||
|
|
#### Justificación:
|
||
|
|
|
||
|
|
1. **Offline-First Compatible**
|
||
|
|
- ✅ SQLite embebido (archivo local)
|
||
|
|
- ✅ Sincronización opcional
|
||
|
|
- ✅ API REST automática
|
||
|
|
|
||
|
|
2. **Editable desde Panel Admin**
|
||
|
|
- ✅ Panel admin integrado
|
||
|
|
- ✅ Editor de datos visual
|
||
|
|
- ✅ Autenticación incluida
|
||
|
|
|
||
|
|
3. **Sin Tocar App en Producción**
|
||
|
|
- ✅ Export a JSON
|
||
|
|
- ✅ Content Pack como archivo
|
||
|
|
|
||
|
|
4. **Fácil para Sanitarios**
|
||
|
|
- ✅ Interfaz simple
|
||
|
|
- ✅ Subida de archivos
|
||
|
|
|
||
|
|
5. **Control de Versiones**
|
||
|
|
- ⚠️ Manual (export/import JSON)
|
||
|
|
- ⚠️ Sin historial automático
|
||
|
|
|
||
|
|
#### Ventajas de PocketBase:
|
||
|
|
|
||
|
|
- ✅ **Self-hosted** (control total)
|
||
|
|
- ✅ **SQLite** (sin servidor de BD)
|
||
|
|
- ✅ **Ligero** (single binary)
|
||
|
|
- ✅ **Gratis y open-source**
|
||
|
|
|
||
|
|
#### Desventajas vs Supabase:
|
||
|
|
|
||
|
|
- ❌ Sin historial automático
|
||
|
|
- ❌ Sin realtime
|
||
|
|
- ❌ Storage menos robusto
|
||
|
|
- ❌ Requiere hosting propio
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4.3 Estructura de Archivos (JSON Local - Fallback)
|
||
|
|
|
||
|
|
Si se opta por JSON local versionado (alternativa simple):
|
||
|
|
|
||
|
|
```
|
||
|
|
content/
|
||
|
|
├── packs/
|
||
|
|
│ ├── pack-v1.0.0.json
|
||
|
|
│ ├── pack-v1.1.0.json
|
||
|
|
│ └── pack-v1.2.0.json
|
||
|
|
├── resources/
|
||
|
|
│ ├── images/
|
||
|
|
│ │ ├── rcp_posicion_manos_adulto.png
|
||
|
|
│ │ └── ...
|
||
|
|
│ └── videos/
|
||
|
|
│ ├── rcp_adulto_svb.mp4
|
||
|
|
│ └── ...
|
||
|
|
└── metadata/
|
||
|
|
├── content-index.json
|
||
|
|
└── resources-index.json
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
# FASE 2 — INTEGRACIÓN ESTRUCTURAL
|
||
|
|
|
||
|
|
## 2.1 Capa de Lectura (Content Adapter)
|
||
|
|
|
||
|
|
### A. Arquitectura de Adaptadores
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// ============================================
|
||
|
|
// INTERFACE: Content Adapter
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
interface ContentAdapter {
|
||
|
|
// Lectura de contenido
|
||
|
|
getProtocol(id: string): Promise<Protocol | null>;
|
||
|
|
getGuide(id: string): Promise<Guide | null>;
|
||
|
|
getManualChapter(id: string): Promise<ManualChapter | null>;
|
||
|
|
getDrug(id: string): Promise<Drug | null>;
|
||
|
|
|
||
|
|
// Lectura de recursos
|
||
|
|
getResource(id: string): Promise<MediaResource | null>;
|
||
|
|
getResourcesForContent(contentId: string, type?: 'image' | 'video'): Promise<MediaResource[]>;
|
||
|
|
|
||
|
|
// Búsqueda
|
||
|
|
searchContent(query: string, filters?: ContentFilters): Promise<BaseContentItem[]>;
|
||
|
|
|
||
|
|
// Content Pack
|
||
|
|
getLatestPack(): Promise<ContentPack | null>;
|
||
|
|
getPackVersion(version: string): Promise<ContentPack | null>;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// IMPLEMENTACIÓN: Local Adapter (Fallback)
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
class LocalContentAdapter implements ContentAdapter {
|
||
|
|
// Lee de src/data/ (procedures.ts, drugs.ts, etc.)
|
||
|
|
// Compatible con código actual
|
||
|
|
// Sin cambios en producción
|
||
|
|
|
||
|
|
async getProtocol(id: string): Promise<Protocol | null> {
|
||
|
|
// 1. Intentar desde content pack (si existe)
|
||
|
|
const pack = await this.getLatestPack();
|
||
|
|
if (pack?.protocols) {
|
||
|
|
const protocol = pack.protocols.find(p => p.id === id);
|
||
|
|
if (protocol) return protocol;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. Fallback a local
|
||
|
|
const { getProcedureById } = await import('@/data/procedures');
|
||
|
|
const localProtocol = getProcedureById(id);
|
||
|
|
if (!localProtocol) return null;
|
||
|
|
|
||
|
|
// 3. Convertir formato local → formato canónico
|
||
|
|
return this.convertLocalProtocolToCanonical(localProtocol);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ... otros métodos
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// IMPLEMENTACIÓN: Supabase Adapter (Futuro)
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
class SupabaseContentAdapter implements ContentAdapter {
|
||
|
|
private supabase: SupabaseClient;
|
||
|
|
|
||
|
|
async getProtocol(id: string): Promise<Protocol | null> {
|
||
|
|
// 1. Intentar desde cache local (IndexedDB)
|
||
|
|
const cached = await this.getFromCache('protocol', id);
|
||
|
|
if (cached) return cached;
|
||
|
|
|
||
|
|
// 2. Obtener desde Supabase
|
||
|
|
const { data, error } = await this.supabase
|
||
|
|
.from('content_items')
|
||
|
|
.select('*')
|
||
|
|
.eq('id', id)
|
||
|
|
.eq('type', 'protocol')
|
||
|
|
.eq('status', 'published')
|
||
|
|
.single();
|
||
|
|
|
||
|
|
if (error || !data) {
|
||
|
|
// 3. Fallback a local
|
||
|
|
return await this.fallbackToLocal(id);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 4. Cachear localmente
|
||
|
|
await this.cacheContent('protocol', id, data);
|
||
|
|
|
||
|
|
return data as Protocol;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ... otros métodos
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// FACTORY: Selección de Adapter
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
class ContentAdapterFactory {
|
||
|
|
static create(): ContentAdapter {
|
||
|
|
// Detectar si hay content pack disponible
|
||
|
|
const hasPack = this.checkForContentPack();
|
||
|
|
|
||
|
|
if (hasPack) {
|
||
|
|
// Usar adapter con soporte de pack
|
||
|
|
return new PackContentAdapter();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fallback a local
|
||
|
|
return new LocalContentAdapter();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### B. Modo Fallback
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// ============================================
|
||
|
|
// ESTRATEGIA DE FALLBACK
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
class FallbackStrategy {
|
||
|
|
// 1. Intentar Content Pack (si existe)
|
||
|
|
// 2. Si falla, usar contenido local (procedures.ts, drugs.ts)
|
||
|
|
// 3. Si falla, mostrar error controlado
|
||
|
|
|
||
|
|
async getContentWithFallback<T>(
|
||
|
|
type: ContentType,
|
||
|
|
id: string,
|
||
|
|
localGetter: () => T | null
|
||
|
|
): Promise<T | null> {
|
||
|
|
try {
|
||
|
|
// Intentar desde pack
|
||
|
|
const packContent = await this.getFromPack(type, id);
|
||
|
|
if (packContent) return packContent;
|
||
|
|
} catch (error) {
|
||
|
|
console.warn(`Error obteniendo ${type} ${id} desde pack:`, error);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fallback a local
|
||
|
|
try {
|
||
|
|
const localContent = localGetter();
|
||
|
|
if (localContent) {
|
||
|
|
// Convertir formato local → canónico
|
||
|
|
return this.convertLocalToCanonical(type, localContent);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error(`Error obteniendo ${type} ${id} desde local:`, error);
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2.2 Enlaces Bidireccionales
|
||
|
|
|
||
|
|
### A. Sistema de Navegación
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// ============================================
|
||
|
|
// NAVEGACIÓN BIDIRECCIONAL
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
interface ContentNavigation {
|
||
|
|
// Desde Protocolo
|
||
|
|
protocolToGuide?: string; // ID de guía formativa relacionada
|
||
|
|
protocolToManual?: string[]; // IDs de capítulos de manual relacionados
|
||
|
|
|
||
|
|
// Desde Guía
|
||
|
|
guideToProtocol?: string; // ID de protocolo operativo relacionado
|
||
|
|
guideToManual?: string[]; // IDs de capítulos de manual relacionados
|
||
|
|
|
||
|
|
// Desde Manual
|
||
|
|
manualToProtocols?: string[]; // IDs de protocolos relacionados
|
||
|
|
manualToGuides?: string[]; // IDs de guías relacionadas
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// COMPONENTE: Navigation Links
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
function ContentNavigationLinks({ contentId, type }: { contentId: string; type: ContentType }) {
|
||
|
|
const navigation = useContentNavigation(contentId, type);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="content-navigation">
|
||
|
|
{navigation.protocolToGuide && (
|
||
|
|
<Link to={`/guia-refuerzo/${navigation.protocolToGuide}`}>
|
||
|
|
📚 Ver Guía de Refuerzo
|
||
|
|
</Link>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{navigation.guideToProtocol && (
|
||
|
|
<Link to={`/escena?tab=${navigation.guideToProtocol}`}>
|
||
|
|
🔵 Ver Protocolo Operativo
|
||
|
|
</Link>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{navigation.manualToProtocols && navigation.manualToProtocols.map(protocolId => (
|
||
|
|
<Link key={protocolId} to={`/escena?tab=${protocolId}`}>
|
||
|
|
🔵 Ver Protocolo {protocolId}
|
||
|
|
</Link>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### B. Mapeo Existente (Sin Cambios)
|
||
|
|
|
||
|
|
El archivo `src/data/protocol-guide-manual-mapping.ts` ya existe y funciona. Se extiende:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Extender sin romper
|
||
|
|
interface ExtendedMapping extends ProtocolGuideManualMapping {
|
||
|
|
// Añadir recursos
|
||
|
|
resources?: {
|
||
|
|
images?: string[];
|
||
|
|
videos?: string[];
|
||
|
|
};
|
||
|
|
|
||
|
|
// Añadir prioridades
|
||
|
|
priority?: Priority;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
# FASE 3 — VALIDACIÓN Y ESCALADO
|
||
|
|
|
||
|
|
## 3.1 Validación con TES Reales
|
||
|
|
|
||
|
|
### A. Sistema de Validación
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// ============================================
|
||
|
|
// VALIDACIÓN DE CONTENIDO
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
interface ContentValidation {
|
||
|
|
contentId: string;
|
||
|
|
validatorId: string; // ID del TES validador
|
||
|
|
validatorName: string;
|
||
|
|
validatorRole: string; // 'tes' | 'medico' | 'formador'
|
||
|
|
validatorOrganization?: string;
|
||
|
|
|
||
|
|
status: 'pending' | 'approved' | 'rejected' | 'needs_revision';
|
||
|
|
comments?: string;
|
||
|
|
suggestions?: string[];
|
||
|
|
|
||
|
|
validatedAt?: string;
|
||
|
|
expiresAt?: string; // Fecha de expiración de validación
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// FLUJO DE VALIDACIÓN
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
enum ValidationFlow {
|
||
|
|
// 1. Editor crea contenido → status: 'draft'
|
||
|
|
DRAFT = 'draft',
|
||
|
|
|
||
|
|
// 2. Editor solicita validación → status: 'in_review'
|
||
|
|
IN_REVIEW = 'in_review',
|
||
|
|
|
||
|
|
// 3. Validador TES revisa → status: 'approved' | 'needs_revision'
|
||
|
|
APPROVED = 'approved',
|
||
|
|
NEEDS_REVISION = 'needs_revision',
|
||
|
|
|
||
|
|
// 4. Publicación → status: 'published'
|
||
|
|
PUBLISHED = 'published',
|
||
|
|
|
||
|
|
// 5. Obsoleto → status: 'archived'
|
||
|
|
ARCHIVED = 'archived'
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### B. Feedback desde la App
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// ============================================
|
||
|
|
// FEEDBACK DESDE APP
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
interface UserFeedback {
|
||
|
|
id: string;
|
||
|
|
userId?: string; // Opcional (si hay usuarios)
|
||
|
|
contentId: string;
|
||
|
|
contentType: ContentType;
|
||
|
|
|
||
|
|
type: 'error' | 'suggestion' | 'question' | 'praise';
|
||
|
|
message: string;
|
||
|
|
screenshot?: string; // URL de screenshot (opcional)
|
||
|
|
|
||
|
|
context?: {
|
||
|
|
section?: string;
|
||
|
|
step?: number;
|
||
|
|
resourceId?: string;
|
||
|
|
};
|
||
|
|
|
||
|
|
createdAt: string;
|
||
|
|
status: 'pending' | 'reviewed' | 'resolved';
|
||
|
|
}
|
||
|
|
|
||
|
|
// Componente en app
|
||
|
|
function FeedbackButton({ contentId, contentType }: Props) {
|
||
|
|
const [showFeedback, setShowFeedback] = useState(false);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
<Button onClick={() => setShowFeedback(true)}>
|
||
|
|
💬 Enviar Feedback
|
||
|
|
</Button>
|
||
|
|
|
||
|
|
{showFeedback && (
|
||
|
|
<FeedbackModal
|
||
|
|
contentId={contentId}
|
||
|
|
contentType={contentType}
|
||
|
|
onClose={() => setShowFeedback(false)}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3.2 Estados de Contenido
|
||
|
|
|
||
|
|
### A. Estados y Transiciones
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────┐
|
||
|
|
│ DRAFT │ ← Creado por editor
|
||
|
|
└────┬────┘
|
||
|
|
│ Solicita validación
|
||
|
|
▼
|
||
|
|
┌─────────────┐
|
||
|
|
│ IN_REVIEW │ ← En revisión por TES
|
||
|
|
└────┬────────┘
|
||
|
|
│
|
||
|
|
├─→ APPROVED ──→ PUBLISHED
|
||
|
|
│
|
||
|
|
└─→ NEEDS_REVISION ──→ DRAFT
|
||
|
|
|
||
|
|
PUBLISHED ──→ ARCHIVED (obsoleto)
|
||
|
|
```
|
||
|
|
|
||
|
|
### B. Marcado de Contenido
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface ContentStatusBadge {
|
||
|
|
status: ContentStatus;
|
||
|
|
validatedBy?: string;
|
||
|
|
validatedAt?: string;
|
||
|
|
expiresAt?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Componente visual
|
||
|
|
function ContentStatusBadge({ status, validatedBy }: Props) {
|
||
|
|
const badgeConfig = {
|
||
|
|
draft: { label: 'Borrador', color: 'gray' },
|
||
|
|
in_review: { label: 'En Revisión', color: 'yellow' },
|
||
|
|
approved: { label: 'Validado', color: 'green' },
|
||
|
|
published: { label: 'Publicado', color: 'blue' },
|
||
|
|
archived: { label: 'Obsoleto', color: 'red' }
|
||
|
|
};
|
||
|
|
|
||
|
|
const config = badgeConfig[status];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Badge color={config.color}>
|
||
|
|
{config.label}
|
||
|
|
{validatedBy && ` por ${validatedBy}`}
|
||
|
|
</Badge>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3.3 Mejoras sin Romper Producción
|
||
|
|
|
||
|
|
### A. Versionado Semántico
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface ContentVersion {
|
||
|
|
version: string; // Semántica: "1.2.3"
|
||
|
|
breaking: boolean; // Si rompe compatibilidad
|
||
|
|
changelog: string[]; // Lista de cambios
|
||
|
|
minAppVersion?: string; // Versión mínima de app requerida
|
||
|
|
}
|
||
|
|
|
||
|
|
// Estrategia:
|
||
|
|
// - Versión mayor (1.x.x): Cambios breaking
|
||
|
|
// - Versión menor (x.2.x): Nuevas features
|
||
|
|
// - Versión patch (x.x.3): Correcciones
|
||
|
|
```
|
||
|
|
|
||
|
|
### B. Feature Flags
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface ContentFeatureFlag {
|
||
|
|
id: string;
|
||
|
|
enabled: boolean;
|
||
|
|
targetAudience?: string[]; // ['tes', 'formadores']
|
||
|
|
rolloutPercentage?: number; // 0-100
|
||
|
|
}
|
||
|
|
|
||
|
|
// Ejemplo:
|
||
|
|
// - Nuevo protocolo en beta: enabled=false para producción
|
||
|
|
// - Nuevo protocolo validado: enabled=true, rolloutPercentage=100
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
# PANEL DE ADMINISTRACIÓN
|
||
|
|
|
||
|
|
## 4.1 Funcionalidades Requeridas
|
||
|
|
|
||
|
|
### A. Gestión de Contenido
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// ============================================
|
||
|
|
// EDITOR DE PROTOCOLO
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
interface ProtocolEditor {
|
||
|
|
// Edición de texto
|
||
|
|
- Editar pasos (arrastrar para reordenar)
|
||
|
|
- Editar warnings, keyPoints
|
||
|
|
- Editar checklist integrado
|
||
|
|
|
||
|
|
// Gestión de recursos
|
||
|
|
- Asociar imágenes (drag-and-drop)
|
||
|
|
- Asociar vídeos (upload o URL)
|
||
|
|
- Previsualizar recursos
|
||
|
|
|
||
|
|
// Prioridades
|
||
|
|
- Cambiar prioridad (crítica/alta/media/baja)
|
||
|
|
- Marcar como crítico/opcional
|
||
|
|
|
||
|
|
// Versionado
|
||
|
|
- Ver historial de versiones
|
||
|
|
- Revertir a versión anterior
|
||
|
|
- Crear nueva versión
|
||
|
|
|
||
|
|
// Validación
|
||
|
|
- Solicitar validación
|
||
|
|
- Ver estado de validación
|
||
|
|
- Ver comentarios de validadores
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// GESTOR DE RECURSOS
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
interface ResourceManager {
|
||
|
|
// Subida
|
||
|
|
- Upload imágenes (drag-and-drop)
|
||
|
|
- Upload vídeos (con progreso)
|
||
|
|
- Subida múltiple
|
||
|
|
|
||
|
|
// Metadatos
|
||
|
|
- Editar alt text, caption
|
||
|
|
- Asignar tags, bloque, capítulo
|
||
|
|
- Cambiar prioridad
|
||
|
|
|
||
|
|
// Asociación
|
||
|
|
- Ver qué contenidos usan el recurso
|
||
|
|
- Asociar a contenido específico
|
||
|
|
- Desasociar recurso
|
||
|
|
|
||
|
|
// Organización
|
||
|
|
- Filtrar por tipo, bloque, prioridad
|
||
|
|
- Buscar recursos
|
||
|
|
- Ver recursos sin asociar
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// DASHBOARD DE ESTADO
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
interface ContentDashboard {
|
||
|
|
// Estado general
|
||
|
|
- Total de protocolos: X completos, Y incompletos
|
||
|
|
- Total de recursos: X imágenes, Y vídeos
|
||
|
|
- Protocolos sin recursos críticos (lista)
|
||
|
|
|
||
|
|
// Validación
|
||
|
|
- Contenido pendiente de validación
|
||
|
|
- Contenido validado recientemente
|
||
|
|
- Contenido obsoleto
|
||
|
|
|
||
|
|
// Actividad
|
||
|
|
- Últimos cambios
|
||
|
|
- Contenido más consultado (si hay analytics)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### B. Restricciones del Panel
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// ============================================
|
||
|
|
// LO QUE EL PANEL NO DEBE HACER
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
const PANEL_RESTRICTIONS = {
|
||
|
|
// ❌ NO modificar rutas de navegación
|
||
|
|
// ❌ NO cambiar estructura de componentes
|
||
|
|
// ❌ NO afectar Service Worker
|
||
|
|
// ❌ NO modificar código de la app
|
||
|
|
|
||
|
|
// ✅ SÍ puede:
|
||
|
|
// - Editar contenido (texto, pasos, etc.)
|
||
|
|
// - Subir recursos (imágenes, vídeos)
|
||
|
|
// - Asociar recursos a contenido
|
||
|
|
// - Cambiar prioridades
|
||
|
|
// - Activar/desactivar contenido
|
||
|
|
// - Versionar cambios
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4.2 Arquitectura del Panel
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────┐
|
||
|
|
│ ADMIN PANEL (React + Vite) │
|
||
|
|
├─────────────────────────────────────────┤
|
||
|
|
│ ┌───────────────────────────────────┐ │
|
||
|
|
│ │ Auth (JWT) │ │
|
||
|
|
│ └───────────────────────────────────┘ │
|
||
|
|
│ ┌───────────────────────────────────┐ │
|
||
|
|
│ │ Dashboard │ │
|
||
|
|
│ │ - Estado de contenido │ │
|
||
|
|
│ │ - Protocolos incompletos │ │
|
||
|
|
│ └───────────────────────────────────┘ │
|
||
|
|
│ ┌───────────────────────────────────┐ │
|
||
|
|
│ │ Editor de Protocolo │ │
|
||
|
|
│ │ - Tabs: Básico, Pasos, Checklist, │ │
|
||
|
|
│ │ Dosis, Fuentes, Recursos │ │
|
||
|
|
│ │ - Preview "Modo TES" │ │
|
||
|
|
│ └───────────────────────────────────┘ │
|
||
|
|
│ ┌───────────────────────────────────┐ │
|
||
|
|
│ │ Editor de Guía │ │
|
||
|
|
│ │ - 8 secciones Markdown │ │
|
||
|
|
│ │ - Preview con recursos │ │
|
||
|
|
│ └───────────────────────────────────┘ │
|
||
|
|
│ ┌───────────────────────────────────┐ │
|
||
|
|
│ │ Gestor de Recursos │ │
|
||
|
|
│ │ - Upload imágenes/vídeos │ │
|
||
|
|
│ │ - Asociar a contenido │ │
|
||
|
|
│ └───────────────────────────────────┘ │
|
||
|
|
│ ┌───────────────────────────────────┐ │
|
||
|
|
│ │ Biblioteca de Contenido │ │
|
||
|
|
│ │ - Lista con filtros │ │
|
||
|
|
│ │ - Búsqueda │ │
|
||
|
|
│ └───────────────────────────────────┘ │
|
||
|
|
│ ┌───────────────────────────────────┐ │
|
||
|
|
│ │ Auditoría │ │
|
||
|
|
│ │ - Historial de cambios │ │
|
||
|
|
│ │ - Comparar versiones │ │
|
||
|
|
│ └───────────────────────────────────┘ │
|
||
|
|
└─────────────────────────────────────────┘
|
||
|
|
│
|
||
|
|
│ (API REST)
|
||
|
|
▼
|
||
|
|
┌─────────────────────────────────────────┐
|
||
|
|
│ SUPABASE (Backend) │
|
||
|
|
│ - PostgreSQL Database │
|
||
|
|
│ - Storage (S3) │
|
||
|
|
│ - Auth │
|
||
|
|
└─────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
# ROADMAP DE IMPLEMENTACIÓN
|
||
|
|
|
||
|
|
## Fase 4.1: Modelo de Datos (Semana 1-2)
|
||
|
|
|
||
|
|
- [ ] Definir tipos TypeScript completos
|
||
|
|
- [ ] Crear esquema de base de datos (Supabase)
|
||
|
|
- [ ] Migrar contenido existente a formato canónico
|
||
|
|
- [ ] Crear scripts de migración
|
||
|
|
|
||
|
|
## Fase 4.2: Base de Datos (Semana 3-4)
|
||
|
|
|
||
|
|
- [ ] Configurar Supabase
|
||
|
|
- [ ] Crear tablas y relaciones
|
||
|
|
- [ ] Configurar Storage para recursos
|
||
|
|
- [ ] Configurar autenticación y RBAC
|
||
|
|
- [ ] Migrar contenido inicial
|
||
|
|
|
||
|
|
## Fase 2.1: Capa de Lectura (Semana 5-6)
|
||
|
|
|
||
|
|
- [ ] Implementar ContentAdapter interface
|
||
|
|
- [ ] Implementar LocalContentAdapter (fallback)
|
||
|
|
- [ ] Implementar PackContentAdapter
|
||
|
|
- [ ] Crear ContentAdapterFactory
|
||
|
|
- [ ] Integrar en hooks existentes (useProtocol, useDrug)
|
||
|
|
|
||
|
|
## Fase 2.2: Enlaces Bidireccionales (Semana 7)
|
||
|
|
|
||
|
|
- [ ] Extender protocol-guide-manual-mapping
|
||
|
|
- [ ] Crear componente ContentNavigationLinks
|
||
|
|
- [ ] Integrar en páginas de protocolos
|
||
|
|
- [ ] Integrar en guías formativas
|
||
|
|
- [ ] Integrar en manual
|
||
|
|
|
||
|
|
## Fase 3.1: Validación (Semana 8-9)
|
||
|
|
|
||
|
|
- [ ] Implementar sistema de validación
|
||
|
|
- [ ] Crear flujo de validación en panel
|
||
|
|
- [ ] Implementar feedback desde app
|
||
|
|
- [ ] Crear dashboard de validación
|
||
|
|
|
||
|
|
## Fase 3.2: Versionado (Semana 10)
|
||
|
|
|
||
|
|
- [ ] Implementar versionado semántico
|
||
|
|
- [ ] Crear sistema de versiones en BD
|
||
|
|
- [ ] Implementar rollback
|
||
|
|
- [ ] Crear historial de cambios
|
||
|
|
|
||
|
|
## Panel Admin: Fase 1 (Semana 11-12)
|
||
|
|
|
||
|
|
- [ ] Editor de protocolo (básico)
|
||
|
|
- [ ] Gestor de recursos (upload)
|
||
|
|
- [ ] Asociación recursos ⇄ contenido
|
||
|
|
- [ ] Dashboard de estado
|
||
|
|
|
||
|
|
## Panel Admin: Fase 2 (Semana 13-14)
|
||
|
|
|
||
|
|
- [ ] Editor de guías (Markdown)
|
||
|
|
- [ ] Editor de manual
|
||
|
|
- [ ] Biblioteca de contenido
|
||
|
|
- [ ] Auditoría y versiones
|
||
|
|
|
||
|
|
## Testing y Validación (Semana 15-16)
|
||
|
|
|
||
|
|
- [ ] Testing de adaptadores
|
||
|
|
- [ ] Testing de fallback
|
||
|
|
- [ ] Validación con TES reales
|
||
|
|
- [ ] Optimización de performance
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Métricas de Éxito
|
||
|
|
|
||
|
|
### Fase 4 (Base de Contenido):
|
||
|
|
- ✅ Modelo de datos completo y documentado
|
||
|
|
- ✅ Base de datos configurada y funcionando
|
||
|
|
- ✅ Contenido migrado sin pérdida de datos
|
||
|
|
|
||
|
|
### Fase 2 (Integración):
|
||
|
|
- ✅ App consume contenido externo sin romper
|
||
|
|
- ✅ Fallback a local funciona 100%
|
||
|
|
- ✅ Enlaces bidireccionales operativos
|
||
|
|
|
||
|
|
### Fase 3 (Validación):
|
||
|
|
- ✅ Sistema de validación funcional
|
||
|
|
- ✅ Feedback desde app operativo
|
||
|
|
- ✅ Versionado completo
|
||
|
|
|
||
|
|
### Panel Admin:
|
||
|
|
- ✅ Edición de contenido sin tocar código
|
||
|
|
- ✅ Subida de recursos funcional
|
||
|
|
- ✅ Asociación recursos ⇄ contenido operativa
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**Fin del Plan Técnico**
|
||
|
|
|