# 🏗️ 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; getGuide(id: string): Promise; getManualChapter(id: string): Promise; getDrug(id: string): Promise; // Lectura de recursos getResource(id: string): Promise; getResourcesForContent(contentId: string, type?: 'image' | 'video'): Promise; // Búsqueda searchContent(query: string, filters?: ContentFilters): Promise; // Content Pack getLatestPack(): Promise; getPackVersion(version: string): Promise; } // ============================================ // 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 { // 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 { // 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( type: ContentType, id: string, localGetter: () => T | null ): Promise { 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 (
{navigation.protocolToGuide && ( 📚 Ver Guía de Refuerzo )} {navigation.guideToProtocol && ( 🔵 Ver Protocolo Operativo )} {navigation.manualToProtocols && navigation.manualToProtocols.map(protocolId => ( 🔵 Ver Protocolo {protocolId} ))}
); } ``` ### 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 ( <> {showFeedback && ( 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 ( {config.label} {validatedBy && ` por ${validatedBy}`} ); } ``` --- ## 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**