1265 lines
52 KiB
TypeScript
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>
|
||
|
|
);
|
||
|
|
}
|