codigo0/admin-panel/src/pages/ProtocolEditorPage.tsx

1265 lines
52 KiB
TypeScript

/**
* Editor de Protocolo Completo
*
* Permite crear y editar protocolos operativos con:
* - Pasos rápidos estructurados
* - Checklist integrado
* - Dosis inline
* - Herramientas de contexto
* - Fuentes clínicas
* - Vista previa "modo TES"
*/
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Save,
Plus,
Trash2,
GripVertical,
AlertCircle,
Eye,
ArrowLeft,
Pill,
BookOpen,
Calculator,
CheckSquare,
} from 'lucide-react';
import { contentService } from '../services/content';
import { useAuth } from '../contexts/AuthContext';
import ResourcesManager from '../components/content/ResourcesManager';
import ValidationHistory from '../components/content/ValidationHistory';
import type {
Protocol,
ProtocolStep,
ProtocolChecklistItem,
InlineDose,
ProtocolContextTool,
ClinicalSource,
} from '../../shared/types/content';
export default function ProtocolEditorPage() {
const { id } = useParams<{ id?: string }>();
const navigate = useNavigate();
const { hasPermission } = useAuth();
const isEdit = !!id;
const [isLoading, setIsLoading] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [showPreview, setShowPreview] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const [activeTab, setActiveTab] = useState<'basic' | 'steps' | 'checklist' | 'doses' | 'tools' | 'sources' | 'resources'>('basic');
const [associatedResources, setAssociatedResources] = useState<any[]>([]);
const [showResourceSelector, setShowResourceSelector] = useState(false);
// Estado del protocolo
const [protocol, setProtocol] = useState<Partial<Protocol>>({
id: id || '',
type: 'protocol',
level: 'operativo',
title: '',
shortTitle: '',
description: '',
category: 'soporte_vital',
subcategory: '',
priority: 'critico',
ageGroup: 'adulto',
status: 'draft',
content: {
pasosRapidos: [],
warnings: [],
keyPoints: [],
equipment: [],
drugs: [],
version: 1,
lastUpdated: new Date().toISOString(),
},
});
// Cargar protocolo existente
useEffect(() => {
if (isEdit && id) {
setIsLoading(true);
contentService
.getById(id)
.then((data: any) => {
setProtocol({
...data,
content: data.content || {
pasosRapidos: [],
warnings: [],
keyPoints: [],
equipment: [],
drugs: [],
version: 1,
lastUpdated: new Date().toISOString(),
},
});
})
.catch((error) => {
console.error('Error cargando protocolo:', error);
setErrors({ general: 'Error al cargar el protocolo' });
})
.finally(() => setIsLoading(false));
}
}, [id, isEdit]);
// Validación
const validate = (): boolean => {
const newErrors: Record<string, string> = {};
if (!protocol.id?.trim()) {
newErrors.id = 'ID es requerido';
}
if (!protocol.title?.trim()) {
newErrors.title = 'Título es requerido';
}
if (!protocol.content?.pasosRapidos || protocol.content.pasosRapidos.length === 0) {
newErrors.pasosRapidos = 'Debe tener al menos un paso';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// Guardar
const handleSave = async () => {
if (!validate()) {
return;
}
if (!hasPermission('content:write:protocol')) {
setErrors({ general: 'No tienes permisos para editar protocolos' });
return;
}
setIsSaving(true);
try {
const payload = {
id: protocol.id,
type: 'protocol',
level: 'operativo',
title: protocol.title,
shortTitle: protocol.shortTitle,
description: protocol.description,
category: protocol.category,
subcategory: protocol.subcategory,
priority: protocol.priority,
ageGroup: protocol.ageGroup,
content: protocol.content,
status: protocol.status || 'draft',
};
if (isEdit) {
await contentService.update(id!, payload);
} else {
await contentService.create(payload);
}
navigate('/content');
} catch (error: any) {
console.error('Error guardando protocolo:', error);
setErrors({ general: error.response?.data?.error || 'Error al guardar' });
} finally {
setIsSaving(false);
}
};
// Gestión de pasos rápidos
const addStep = () => {
const newStep: ProtocolStep = {
order: (protocol.content?.pasosRapidos?.length || 0) + 1,
text: '',
critical: false,
};
setProtocol({
...protocol,
content: {
...protocol.content!,
pasosRapidos: [...(protocol.content?.pasosRapidos || []), newStep],
},
});
};
const removeStep = (order: number) => {
const steps = protocol.content?.pasosRapidos?.filter((s) => s.order !== order) || [];
steps.forEach((step, index) => {
step.order = index + 1;
});
setProtocol({
...protocol,
content: {
...protocol.content!,
pasosRapidos: steps,
},
});
};
const updateStep = (order: number, updates: Partial<ProtocolStep>) => {
const steps = protocol.content?.pasosRapidos?.map((step) =>
step.order === order ? { ...step, ...updates } : step
) || [];
setProtocol({
...protocol,
content: {
...protocol.content!,
pasosRapidos: steps,
},
});
};
// Gestión de warnings
const addWarning = () => {
const warnings = [...(protocol.content?.warnings || []), ''];
setProtocol({
...protocol,
content: {
...protocol.content!,
warnings,
},
});
};
const updateWarning = (index: number, value: string) => {
const warnings = [...(protocol.content?.warnings || [])];
warnings[index] = value;
setProtocol({
...protocol,
content: {
...protocol.content!,
warnings: warnings.filter((w) => w.trim()),
},
});
};
// Gestión de dosis inline
const addDose = () => {
const newDose: InlineDose = {
drugId: '',
drugName: '',
adultDose: '',
route: 'IV',
};
setProtocol({
...protocol,
content: {
...protocol.content!,
dosisInline: [...(protocol.content?.dosisInline || []), newDose],
},
});
};
const removeDose = (index: number) => {
const doses = protocol.content?.dosisInline?.filter((_, i) => i !== index) || [];
setProtocol({
...protocol,
content: {
...protocol.content!,
dosisInline: doses,
},
});
};
const updateDose = (index: number, updates: Partial<InlineDose>) => {
const doses = [...(protocol.content?.dosisInline || [])];
doses[index] = { ...doses[index], ...updates };
setProtocol({
...protocol,
content: {
...protocol.content!,
dosisInline: doses,
},
});
};
// Gestión de fuentes clínicas
const addSource = () => {
const newSource: ClinicalSource = {
organization: '',
guideline: '',
year: new Date().getFullYear(),
};
setProtocol({
...protocol,
content: {
...protocol.content!,
fuentes: [...(protocol.content?.fuentes || []), newSource],
},
});
};
const removeSource = (index: number) => {
const sources = protocol.content?.fuentes?.filter((_, i) => i !== index) || [];
setProtocol({
...protocol,
content: {
...protocol.content!,
fuentes: sources,
},
});
};
const updateSource = (index: number, updates: Partial<ClinicalSource>) => {
const sources = [...(protocol.content?.fuentes || [])];
sources[index] = { ...sources[index], ...updates };
setProtocol({
...protocol,
content: {
...protocol.content!,
fuentes: sources,
},
});
};
if (isLoading) {
return (
<div className="p-6">
<div className="text-muted-foreground">Cargando protocolo...</div>
</div>
);
}
return (
<div className="p-6 space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<button
onClick={() => navigate('/content')}
className="flex items-center gap-2 text-muted-foreground hover:text-foreground mb-2"
>
<ArrowLeft className="w-4 h-4" />
<span>Volver</span>
</button>
<h1 className="text-3xl font-bold text-foreground">
{isEdit ? 'Editar Protocolo' : 'Nuevo Protocolo'}
</h1>
<p className="text-muted-foreground mt-1">
Crea y edita protocolos operativos con checklist, dosis y fuentes
</p>
</div>
<div className="flex gap-2">
<button
onClick={() => setShowPreview(!showPreview)}
className="px-4 py-2 border border-border rounded-lg hover:bg-muted transition-colors flex items-center gap-2"
>
<Eye className="w-4 h-4" />
{showPreview ? 'Ocultar' : 'Vista'} Previa
</button>
<button
onClick={handleSave}
disabled={isSaving}
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors flex items-center gap-2 disabled:opacity-50"
>
<Save className="w-4 h-4" />
{isSaving ? 'Guardando...' : 'Guardar'}
</button>
</div>
</div>
{errors.general && (
<div className="bg-destructive/10 border border-destructive/30 rounded-lg p-4 flex items-center gap-2 text-destructive">
<AlertCircle className="w-5 h-5" />
<span>{errors.general}</span>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Formulario */}
<div className="space-y-6">
{/* Tabs */}
<div className="bg-card border border-border rounded-xl p-2">
<div className="flex gap-2 overflow-x-auto">
{[
{ id: 'basic', label: 'Básico' },
{ id: 'steps', label: 'Pasos' },
{ id: 'checklist', label: 'Checklist' },
{ id: 'doses', label: 'Dosis' },
{ id: 'tools', label: 'Herramientas' },
{ id: 'resources', label: 'Recursos' },
{ id: 'sources', label: 'Fuentes' },
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === tab.id
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:bg-muted'
}`}
>
{tab.label}
</button>
))}
</div>
</div>
{/* Tab: Básico */}
{activeTab === 'basic' && (
<div className="bg-card border border-border rounded-xl p-6 space-y-4">
<h2 className="text-xl font-semibold text-foreground">Información Básica</h2>
<div>
<label className="block text-sm font-medium mb-1">
ID <span className="text-destructive">*</span>
</label>
<input
type="text"
value={protocol.id || ''}
onChange={(e) => setProtocol({ ...protocol, id: e.target.value })}
disabled={isEdit}
className="w-full px-4 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="rcp-adulto-svb"
/>
{errors.id && <p className="text-sm text-destructive mt-1">{errors.id}</p>}
</div>
<div>
<label className="block text-sm font-medium mb-1">
Título <span className="text-destructive">*</span>
</label>
<input
type="text"
value={protocol.title || ''}
onChange={(e) => setProtocol({ ...protocol, title: e.target.value })}
className="w-full px-4 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="RCP Adulto - Soporte Vital Básico"
/>
{errors.title && <p className="text-sm text-destructive mt-1">{errors.title}</p>}
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Categoría</label>
<select
value={protocol.category || 'soporte_vital'}
onChange={(e) => setProtocol({ ...protocol, category: e.target.value as any })}
className="w-full px-4 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
>
<option value="soporte_vital">Soporte Vital</option>
<option value="patologias">Patologías</option>
<option value="escena">Escena</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">Prioridad</label>
<select
value={protocol.priority || 'critico'}
onChange={(e) => setProtocol({ ...protocol, priority: e.target.value as any })}
className="w-full px-4 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
>
<option value="critico">Crítico</option>
<option value="alto">Alto</option>
<option value="medio">Medio</option>
<option value="bajo">Bajo</option>
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-1">Grupo de Edad</label>
<select
value={protocol.ageGroup || 'adulto'}
onChange={(e) => setProtocol({ ...protocol, ageGroup: e.target.value as any })}
className="w-full px-4 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
>
<option value="adulto">Adulto</option>
<option value="pediatrico">Pediátrico</option>
<option value="neonatal">Neonatal</option>
<option value="todos">Todos</option>
</select>
</div>
</div>
)}
{/* Tab: Pasos */}
{activeTab === 'steps' && (
<div className="bg-card border border-border rounded-xl p-6 space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-foreground">Pasos Rápidos</h2>
<button
onClick={addStep}
className="px-3 py-1.5 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors flex items-center gap-2 text-sm"
>
<Plus className="w-4 h-4" />
Añadir Paso
</button>
</div>
{errors.pasosRapidos && (
<p className="text-sm text-destructive">{errors.pasosRapidos}</p>
)}
<div className="space-y-3">
{protocol.content?.pasosRapidos?.map((step) => (
<div
key={step.order}
className="bg-muted/50 border border-border rounded-lg p-4 space-y-3"
>
<div className="flex items-start gap-3">
<div className="pt-2">
<span className="text-sm text-muted-foreground">#{step.order}</span>
</div>
<div className="flex-1 space-y-2">
<textarea
value={step.text}
onChange={(e) => updateStep(step.order, { text: e.target.value })}
rows={2}
className="w-full px-3 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary text-sm"
placeholder="Texto del paso..."
/>
<div className="flex items-center gap-4">
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={step.critical || false}
onChange={(e) => updateStep(step.order, { critical: e.target.checked })}
className="rounded"
/>
<span className="text-muted-foreground">Crítico</span>
</label>
<input
type="text"
value={step.timeEstimate || ''}
onChange={(e) => updateStep(step.order, { timeEstimate: e.target.value })}
className="px-3 py-1.5 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary text-sm"
placeholder="Tiempo estimado (ej: 30-60s)"
/>
</div>
</div>
<button
onClick={() => removeStep(step.order)}
className="p-2 text-destructive hover:bg-destructive/10 rounded-lg transition-colors"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
))}
{(!protocol.content?.pasosRapidos || protocol.content.pasosRapidos.length === 0) && (
<div className="text-center py-8 text-muted-foreground">
<p>No hay pasos. Haz clic en "Añadir Paso" para comenzar.</p>
</div>
)}
</div>
{/* Warnings */}
<div className="mt-6 pt-6 border-t border-border">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-foreground">Advertencias</h3>
<button
onClick={addWarning}
className="px-3 py-1.5 text-sm border border-border rounded-lg hover:bg-muted transition-colors"
>
<Plus className="w-4 h-4 inline mr-1" />
Añadir
</button>
</div>
<div className="space-y-2">
{protocol.content?.warnings?.map((warning, index) => (
<input
key={index}
type="text"
value={warning}
onChange={(e) => updateWarning(index, e.target.value)}
className="w-full px-3 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary text-sm"
placeholder="Advertencia importante..."
/>
))}
</div>
</div>
{/* Key Points */}
<div className="mt-6 pt-6 border-t border-border">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-foreground">Puntos Clave</h3>
<button
onClick={() => {
const keyPoints = [...(protocol.content?.keyPoints || []), ''];
setProtocol({
...protocol,
content: {
...protocol.content!,
keyPoints,
},
});
}}
className="px-3 py-1.5 text-sm border border-border rounded-lg hover:bg-muted transition-colors"
>
<Plus className="w-4 h-4 inline mr-1" />
Añadir
</button>
</div>
<div className="space-y-2">
{protocol.content?.keyPoints?.map((point, index) => (
<input
key={index}
type="text"
value={point}
onChange={(e) => {
const keyPoints = [...(protocol.content?.keyPoints || [])];
keyPoints[index] = e.target.value;
setProtocol({
...protocol,
content: {
...protocol.content!,
keyPoints: keyPoints.filter((p) => p.trim()),
},
});
}}
className="w-full px-3 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary text-sm"
placeholder="Punto clave importante..."
/>
))}
</div>
</div>
{/* Equipment */}
<div className="mt-6 pt-6 border-t border-border">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-foreground">Equipamiento</h3>
<button
onClick={() => {
const equipment = [...(protocol.content?.equipment || []), ''];
setProtocol({
...protocol,
content: {
...protocol.content!,
equipment,
},
});
}}
className="px-3 py-1.5 text-sm border border-border rounded-lg hover:bg-muted transition-colors"
>
<Plus className="w-4 h-4 inline mr-1" />
Añadir
</button>
</div>
<div className="space-y-2">
{protocol.content?.equipment?.map((item, index) => (
<input
key={index}
type="text"
value={item}
onChange={(e) => {
const equipment = [...(protocol.content?.equipment || [])];
equipment[index] = e.target.value;
setProtocol({
...protocol,
content: {
...protocol.content!,
equipment: equipment.filter((e) => e.trim()),
},
});
}}
className="w-full px-3 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary text-sm"
placeholder="DEA, Bolsa-mascarilla..."
/>
))}
</div>
</div>
{/* Drugs (Referencias) */}
<div className="mt-6 pt-6 border-t border-border">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-foreground">Fármacos (Referencias)</h3>
<button
onClick={() => {
const drugs = [...(protocol.content?.drugs || []), ''];
setProtocol({
...protocol,
content: {
...protocol.content!,
drugs,
},
});
}}
className="px-3 py-1.5 text-sm border border-border rounded-lg hover:bg-muted transition-colors"
>
<Plus className="w-4 h-4 inline mr-1" />
Añadir
</button>
</div>
<div className="space-y-2">
{protocol.content?.drugs?.map((drug, index) => (
<input
key={index}
type="text"
value={drug}
onChange={(e) => {
const drugs = [...(protocol.content?.drugs || [])];
drugs[index] = e.target.value;
setProtocol({
...protocol,
content: {
...protocol.content!,
drugs: drugs.filter((d) => d.trim()),
},
});
}}
className="w-full px-3 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary text-sm"
placeholder="Adrenalina, Amiodarona..."
/>
))}
</div>
<p className="text-xs text-muted-foreground mt-2">
Referencias a fármacos. Las dosis detalladas se añaden en el tab "Dosis".
</p>
</div>
</div>
)}
{/* Tab: Dosis */}
{activeTab === 'doses' && (
<div className="bg-card border border-border rounded-xl p-6 space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-foreground">Dosis Inline</h2>
<button
onClick={addDose}
className="px-3 py-1.5 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors flex items-center gap-2 text-sm"
>
<Plus className="w-4 h-4" />
Añadir Dosis
</button>
</div>
<div className="space-y-4">
{protocol.content?.dosisInline?.map((dose, index) => (
<div
key={index}
className="bg-muted/50 border border-border rounded-lg p-4 space-y-3"
>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs text-muted-foreground mb-1">Fármaco</label>
<input
type="text"
value={dose.drugName}
onChange={(e) => updateDose(index, { drugName: e.target.value })}
className="w-full px-3 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary text-sm"
placeholder="Adrenalina"
/>
</div>
<div>
<label className="block text-xs text-muted-foreground mb-1">Vía</label>
<select
value={dose.route}
onChange={(e) => updateDose(index, { route: e.target.value as any })}
className="w-full px-3 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary text-sm"
>
<option value="IV">IV</option>
<option value="IM">IM</option>
<option value="IO">IO</option>
<option value="SC">SC</option>
<option value="Nebulizado">Nebulizado</option>
</select>
</div>
</div>
<div>
<label className="block text-xs text-muted-foreground mb-1">Dosis Adulto</label>
<input
type="text"
value={dose.adultDose}
onChange={(e) => updateDose(index, { adultDose: e.target.value })}
className="w-full px-3 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary text-sm"
placeholder="1 mg IV cada 3-5 min"
/>
</div>
<div>
<label className="block text-xs text-muted-foreground mb-1">Contexto</label>
<input
type="text"
value={dose.context || ''}
onChange={(e) => updateDose(index, { context: e.target.value })}
className="w-full px-3 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary text-sm"
placeholder="En PCR, una vez establecida vía IV/IO"
/>
</div>
<button
onClick={() => removeDose(index)}
className="w-full px-3 py-2 text-destructive hover:bg-destructive/10 rounded-lg transition-colors text-sm"
>
<Trash2 className="w-4 h-4 inline mr-1" />
Eliminar
</button>
</div>
))}
{(!protocol.content?.dosisInline || protocol.content.dosisInline.length === 0) && (
<div className="text-center py-8 text-muted-foreground">
<p>No hay dosis. Haz clic en "Añadir Dosis" para comenzar.</p>
</div>
)}
</div>
</div>
)}
{/* Tab: Checklist */}
{activeTab === 'checklist' && (
<div className="bg-card border border-border rounded-xl p-6 space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-foreground">Checklist Integrado</h2>
</div>
<div className="space-y-4">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={protocol.content?.checklist?.enabled || false}
onChange={(e) =>
setProtocol({
...protocol,
content: {
...protocol.content!,
checklist: {
...protocol.content?.checklist,
enabled: e.target.checked,
items: protocol.content?.checklist?.items || [],
title: protocol.content?.checklist?.title || 'Checklist',
} as any,
},
})
}
className="rounded"
/>
<span className="text-foreground font-medium">Habilitar checklist integrado</span>
</label>
{protocol.content?.checklist?.enabled && (
<div className="space-y-4 pl-6 border-l-2 border-primary">
<div>
<label className="block text-sm font-medium mb-1">Título del Checklist</label>
<input
type="text"
value={protocol.content.checklist.title || ''}
onChange={(e) =>
setProtocol({
...protocol,
content: {
...protocol.content!,
checklist: {
...protocol.content?.checklist,
title: e.target.value,
items: protocol.content?.checklist?.items || [],
} as any,
},
})
}
className="w-full px-4 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="Checklist RCP SVB"
/>
</div>
<div>
<p className="text-sm text-muted-foreground mb-4">
Los items del checklist se pueden crear desde el editor de checklists reutilizables
y luego referenciarlos aquí, o crear items específicos para este protocolo.
</p>
<p className="text-xs text-muted-foreground">
Funcionalidad completa de gestión de items pendiente de implementación.
</p>
</div>
</div>
)}
</div>
</div>
)}
{/* Tab: Herramientas */}
{activeTab === 'tools' && (
<div className="bg-card border border-border rounded-xl p-6 space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-foreground">Herramientas de Contexto</h2>
<button
onClick={() => {
const newTool: ProtocolContextTool = {
id: `tool-${Date.now()}`,
name: '',
type: 'calculator',
};
setProtocol({
...protocol,
content: {
...protocol.content!,
herramientasContexto: [
...(protocol.content?.herramientasContexto || []),
newTool,
],
},
});
}}
className="px-3 py-1.5 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors flex items-center gap-2 text-sm"
>
<Plus className="w-4 h-4" />
Añadir Herramienta
</button>
</div>
<div className="space-y-4">
{protocol.content?.herramientasContexto?.map((tool, index) => (
<div
key={tool.id}
className="bg-muted/50 border border-border rounded-lg p-4 space-y-3"
>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs text-muted-foreground mb-1">Nombre</label>
<input
type="text"
value={tool.name}
onChange={(e) => {
const tools = [...(protocol.content?.herramientasContexto || [])];
tools[index] = { ...tools[index], name: e.target.value };
setProtocol({
...protocol,
content: {
...protocol.content!,
herramientasContexto: tools,
},
});
}}
className="w-full px-3 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary text-sm"
placeholder="Calculadora de Dosis Pediátrica"
/>
</div>
<div>
<label className="block text-xs text-muted-foreground mb-1">Tipo</label>
<select
value={tool.type}
onChange={(e) => {
const tools = [...(protocol.content?.herramientasContexto || [])];
tools[index] = { ...tools[index], type: e.target.value as any };
setProtocol({
...protocol,
content: {
...protocol.content!,
herramientasContexto: tools,
},
});
}}
className="w-full px-3 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary text-sm"
>
<option value="calculator">Calculadora</option>
<option value="algorithm">Algoritmo</option>
<option value="reference">Referencia</option>
<option value="checklist">Checklist</option>
</select>
</div>
</div>
<div>
<label className="block text-xs text-muted-foreground mb-1">Descripción</label>
<input
type="text"
value={tool.description || ''}
onChange={(e) => {
const tools = [...(protocol.content?.herramientasContexto || [])];
tools[index] = { ...tools[index], description: e.target.value };
setProtocol({
...protocol,
content: {
...protocol.content!,
herramientasContexto: tools,
},
});
}}
className="w-full px-3 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary text-sm"
placeholder="Calcula dosis por peso para pacientes pediátricos"
/>
</div>
<button
onClick={() => {
const tools = protocol.content?.herramientasContexto?.filter(
(_, i) => i !== index
) || [];
setProtocol({
...protocol,
content: {
...protocol.content!,
herramientasContexto: tools,
},
});
}}
className="w-full px-3 py-2 text-destructive hover:bg-destructive/10 rounded-lg transition-colors text-sm"
>
<Trash2 className="w-4 h-4 inline mr-1" />
Eliminar
</button>
</div>
))}
{(!protocol.content?.herramientasContexto ||
protocol.content.herramientasContexto.length === 0) && (
<div className="text-center py-8 text-muted-foreground">
<p>No hay herramientas. Haz clic en "Añadir Herramienta" para comenzar.</p>
</div>
)}
</div>
</div>
)}
{/* Tab: Recursos */}
{activeTab === 'resources' && (
<div className="bg-card border border-border rounded-xl p-6 space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-foreground">Recursos Multimedia</h2>
<button
onClick={() => setShowResourceSelector(true)}
className="px-3 py-1.5 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors flex items-center gap-2 text-sm"
>
<Plus className="w-4 h-4" />
Asociar Recurso
</button>
</div>
<ResourcesManager
contentId={id || ''}
resources={associatedResources}
onResourcesChange={setAssociatedResources}
showSelector={showResourceSelector}
onCloseSelector={() => setShowResourceSelector(false)}
/>
</div>
)}
{/* Tab: Fuentes */}
{activeTab === 'sources' && (
<div className="bg-card border border-border rounded-xl p-6 space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-foreground">Fuentes Clínicas</h2>
<button
onClick={addSource}
className="px-3 py-1.5 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors flex items-center gap-2 text-sm"
>
<Plus className="w-4 h-4" />
Añadir Fuente
</button>
</div>
<div className="space-y-4">
{protocol.content?.fuentes?.map((source, index) => (
<div
key={index}
className="bg-muted/50 border border-border rounded-lg p-4 space-y-3"
>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs text-muted-foreground mb-1">Organización</label>
<input
type="text"
value={source.organization}
onChange={(e) => updateSource(index, { organization: e.target.value })}
className="w-full px-3 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary text-sm"
placeholder="ERC"
/>
</div>
<div>
<label className="block text-xs text-muted-foreground mb-1">Año</label>
<input
type="number"
value={source.year}
onChange={(e) => updateSource(index, { year: parseInt(e.target.value) || new Date().getFullYear() })}
className="w-full px-3 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary text-sm"
/>
</div>
</div>
<div>
<label className="block text-xs text-muted-foreground mb-1">Guía</label>
<input
type="text"
value={source.guideline}
onChange={(e) => updateSource(index, { guideline: e.target.value })}
className="w-full px-3 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary text-sm"
placeholder="European Resuscitation Council Guidelines 2021"
/>
</div>
<button
onClick={() => removeSource(index)}
className="w-full px-3 py-2 text-destructive hover:bg-destructive/10 rounded-lg transition-colors text-sm"
>
<Trash2 className="w-4 h-4 inline mr-1" />
Eliminar
</button>
</div>
))}
{(!protocol.content?.fuentes || protocol.content.fuentes.length === 0) && (
<div className="text-center py-8 text-muted-foreground">
<p>No hay fuentes. Haz clic en "Añadir Fuente" para comenzar.</p>
</div>
)}
</div>
</div>
)}
</div>
{/* Vista Previa "Modo TES" */}
{showPreview && (
<div className="bg-card border border-border rounded-xl p-6 space-y-4">
<h2 className="text-xl font-semibold text-foreground">Vista Previa - Modo TES</h2>
<div className="space-y-4">
{/* Header */}
<div>
<h3 className="text-2xl font-bold text-foreground mb-2">
{protocol.title || 'Título del Protocolo'}
</h3>
{protocol.shortTitle && (
<p className="text-muted-foreground">{protocol.shortTitle}</p>
)}
<div className="flex gap-2 mt-2">
<span className="px-2 py-1 bg-red-500/20 text-red-600 dark:text-red-400 rounded text-xs font-medium">
{protocol.priority || 'Crítico'}
</span>
<span className="px-2 py-1 bg-blue-500/20 text-blue-600 dark:text-blue-400 rounded text-xs font-medium">
{protocol.ageGroup || 'Adulto'}
</span>
</div>
</div>
{/* Pasos */}
{protocol.content?.pasosRapidos && protocol.content.pasosRapidos.length > 0 && (
<div>
<h4 className="font-semibold text-foreground mb-3 flex items-center gap-2">
<AlertCircle className="w-4 h-4 text-yellow-500" />
Pasos del Protocolo
</h4>
<ol className="space-y-2 list-decimal list-inside">
{protocol.content.pasosRapidos.map((step) => (
<li
key={step.order}
className={`text-foreground pl-2 ${
step.critical ? 'font-semibold text-red-600 dark:text-red-400' : ''
}`}
>
{step.text || 'Paso sin texto'}
{step.timeEstimate && (
<span className="text-xs text-muted-foreground ml-2">
({step.timeEstimate})
</span>
)}
</li>
))}
</ol>
</div>
)}
{/* Dosis Inline */}
{protocol.content?.dosisInline && protocol.content.dosisInline.length > 0 && (
<div className="pt-4 border-t border-border">
<h4 className="font-semibold text-foreground mb-3 flex items-center gap-2">
<Pill className="w-4 h-4 text-primary" />
Dosis de Fármacos
</h4>
<div className="space-y-2">
{protocol.content.dosisInline.map((dose, index) => (
<div key={index} className="bg-muted/50 rounded-lg p-3">
<div className="font-medium text-foreground">{dose.drugName || 'Fármaco'}</div>
<div className="text-sm text-muted-foreground">
{dose.adultDose} ({dose.route})
</div>
{dose.context && (
<div className="text-xs text-muted-foreground mt-1">{dose.context}</div>
)}
</div>
))}
</div>
</div>
)}
{/* Fuentes */}
{protocol.content?.fuentes && protocol.content.fuentes.length > 0 && (
<div className="pt-4 border-t border-border">
<h4 className="font-semibold text-foreground mb-3 flex items-center gap-2">
<BookOpen className="w-4 h-4" />
Fuentes Clínicas
</h4>
<div className="space-y-1 text-sm text-muted-foreground">
{protocol.content.fuentes.map((source, index) => (
<div key={index}>
{source.organization} - {source.guideline} ({source.year})
</div>
))}
</div>
</div>
)}
{/* Warnings */}
{protocol.content?.warnings && protocol.content.warnings.length > 0 && (
<div className="pt-4 border-t border-border">
<h4 className="font-semibold text-foreground mb-3">Advertencias</h4>
<ul className="space-y-1">
{protocol.content.warnings.map((warning, index) => (
<li key={index} className="text-sm text-muted-foreground flex items-start gap-2">
<span className="text-orange-500 mt-1"></span>
<span>{warning}</span>
</li>
))}
</ul>
</div>
)}
{/* Key Points */}
{protocol.content?.keyPoints && protocol.content.keyPoints.length > 0 && (
<div className="pt-4 border-t border-border">
<h4 className="font-semibold text-foreground mb-3">Puntos Clave</h4>
<ul className="space-y-1">
{protocol.content.keyPoints.map((point, index) => (
<li key={index} className="text-sm text-muted-foreground flex items-start gap-2">
<span className="text-primary mt-1"></span>
<span>{point}</span>
</li>
))}
</ul>
</div>
)}
{/* Equipment */}
{protocol.content?.equipment && protocol.content.equipment.length > 0 && (
<div className="pt-4 border-t border-border">
<h4 className="font-semibold text-foreground mb-3">Equipamiento</h4>
<div className="flex flex-wrap gap-2">
{protocol.content.equipment.map((item, index) => (
<span
key={index}
className="px-3 py-1 bg-muted rounded-full text-sm text-foreground"
>
{item}
</span>
))}
</div>
</div>
)}
{/* Herramientas de Contexto */}
{protocol.content?.herramientasContexto &&
protocol.content.herramientasContexto.length > 0 && (
<div className="pt-4 border-t border-border">
<h4 className="font-semibold text-foreground mb-3 flex items-center gap-2">
<Calculator className="w-4 h-4" />
Herramientas de Contexto
</h4>
<div className="space-y-2">
{protocol.content.herramientasContexto.map((tool, index) => (
<div key={tool.id} className="bg-muted/50 rounded-lg p-3">
<div className="font-medium text-foreground">{tool.name || 'Herramienta'}</div>
{tool.description && (
<div className="text-sm text-muted-foreground mt-1">{tool.description}</div>
)}
<span className="text-xs text-muted-foreground mt-1 inline-block">
Tipo: {tool.type}
</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
)}
</div>
</div>
);
}