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

648 lines
24 KiB
TypeScript

/**
* Editor de Fármaco (Vademécum TES)
*
* Editor completo para crear/editar fármacos
* Basado en: docs/VADEMECUM_COMPLETO_TES.md
*/
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Save, X, Send, CheckCircle, Plus, Trash2 } from 'lucide-react';
import { useAuth } from '../contexts/AuthContext';
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000';
interface DrugFormData {
generic_name: string;
trade_name?: string;
category: string;
line: 'first' | 'second';
frequency: 'high' | 'medium' | 'low';
presentation: string;
adult_dose: string;
pediatric_dose?: string;
routes: string[];
dilution?: string;
indications: string[];
contraindications: string[];
side_effects?: string;
antidote?: string;
notes: string[];
critical_points: string[];
source?: string;
status: 'draft' | 'in_review' | 'approved' | 'published' | 'archived';
}
const ROUTES_OPTIONS = ['IV', 'IO', 'IM', 'Subcutánea', 'Oral', 'Rectal', 'Intranasal', 'Nebulización', 'MDI'];
const CATEGORIES = [
'cardiovascular',
'respiratorio',
'neurologico',
'analgesico',
'fluidos',
'antidoto',
'hemostatico',
'diuretico',
'corticosteroide',
'antiepileptico',
'anestesico',
'metabolico',
'antiagregante',
];
export default function DrugEditorPage() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { hasPermission } = useAuth();
const isNew = id === 'new';
const [isLoading, setIsLoading] = useState(!isNew);
const [isSaving, setIsSaving] = useState(false);
const [formData, setFormData] = useState<DrugFormData>({
generic_name: '',
trade_name: '',
category: 'cardiovascular',
line: 'first',
frequency: 'high',
presentation: '',
adult_dose: '',
pediatric_dose: '',
routes: [],
dilution: '',
indications: [],
contraindications: [],
side_effects: '',
antidote: '',
notes: [],
critical_points: [],
source: '',
status: 'draft',
});
// Cargar fármaco si es edición
useEffect(() => {
if (!isNew && id) {
loadDrug(id);
}
}, [id, isNew]);
const loadDrug = async (drugId: string) => {
setIsLoading(true);
try {
const token = localStorage.getItem('admin_token');
const response = await fetch(`${API_URL}/api/drugs/${drugId}`, {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (response.ok) {
const drug = await response.json();
setFormData({
generic_name: drug.generic_name || '',
trade_name: drug.trade_name || '',
category: drug.category || 'cardiovascular',
line: drug.line || 'first',
frequency: drug.frequency || 'high',
presentation: drug.presentation || '',
adult_dose: drug.adult_dose || '',
pediatric_dose: drug.pediatric_dose || '',
routes: drug.routes || [],
dilution: drug.dilution || '',
indications: drug.indications || [],
contraindications: drug.contraindications || [],
side_effects: drug.side_effects || '',
antidote: drug.antidote || '',
notes: drug.notes || [],
critical_points: drug.critical_points || [],
source: drug.source || '',
status: drug.status || 'draft',
});
} else {
alert('Error cargando fármaco');
navigate('/drugs');
}
} catch (error) {
console.error('Error cargando fármaco:', error);
alert('Error cargando fármaco');
} finally {
setIsLoading(false);
}
};
const handleSave = async () => {
setIsSaving(true);
try {
const token = localStorage.getItem('admin_token');
const url = isNew ? `${API_URL}/api/drugs` : `${API_URL}/api/drugs/${id}`;
const method = isNew ? 'POST' : 'PUT';
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify(formData),
});
if (response.ok) {
const data = await response.json();
if (isNew) {
navigate(`/drugs/${data.drug.id}/edit`);
} else {
alert('Fármaco guardado correctamente');
}
} else {
const error = await response.json();
alert(`Error: ${error.error || 'Error al guardar'}\n${error.details?.join('\n') || ''}`);
}
} catch (error) {
console.error('Error guardando fármaco:', error);
alert('Error al guardar fármaco');
} finally {
setIsSaving(false);
}
};
const handleSubmit = async () => {
if (!confirm('¿Enviar este fármaco a revisión?')) return;
try {
const token = localStorage.getItem('admin_token');
const response = await fetch(`${API_URL}/api/drugs/${id}/submit`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
});
if (response.ok) {
alert('Fármaco enviado a revisión');
navigate('/drugs');
} else {
const error = await response.json();
alert(`Error: ${error.error || 'Error al enviar a revisión'}`);
}
} catch (error) {
console.error('Error enviando a revisión:', error);
alert('Error al enviar a revisión');
}
};
const addArrayItem = (field: 'routes' | 'indications' | 'contraindications' | 'notes' | 'critical_points') => {
setFormData(prev => ({
...prev,
[field]: [...prev[field], ''],
}));
};
const updateArrayItem = (
field: 'routes' | 'indications' | 'contraindications' | 'notes' | 'critical_points',
index: number,
value: string
) => {
setFormData(prev => ({
...prev,
[field]: prev[field].map((item, i) => i === index ? value : item),
}));
};
const removeArrayItem = (
field: 'routes' | 'indications' | 'contraindications' | 'notes' | 'critical_points',
index: number
) => {
setFormData(prev => ({
...prev,
[field]: prev[field].filter((_, i) => i !== index),
}));
};
if (isLoading) {
return <div className="p-6">Cargando fármaco...</div>;
}
return (
<div className="p-6 space-y-6 max-w-5xl mx-auto">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-foreground">
{isNew ? 'Nuevo Fármaco' : `Editar: ${formData.generic_name}`}
</h1>
<p className="text-muted-foreground mt-1">
{isNew ? 'Crear nuevo fármaco en el vademécum' : 'Editar información del fármaco'}
</p>
</div>
<div className="flex gap-2">
<button
onClick={() => navigate('/drugs')}
className="px-4 py-2 bg-background border border-border rounded-lg hover:bg-muted transition-colors flex items-center gap-2"
>
<X className="w-4 h-4" />
Cancelar
</button>
{!isNew && hasPermission('content:submit') && formData.status === 'draft' && (
<button
onClick={handleSubmit}
className="px-4 py-2 bg-orange-500 text-white rounded-lg hover:bg-orange-600 transition-colors flex items-center gap-2"
>
<Send className="w-4 h-4" />
Enviar a Revisión
</button>
)}
{!isNew && hasPermission('validation:approve') && formData.status === 'in_review' && (
<button
onClick={async () => {
if (!confirm('¿Aprobar este fármaco?')) return;
try {
const token = localStorage.getItem('admin_token');
const response = await fetch(`${API_URL}/api/drugs/${id}/approve`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ notes: '' }),
});
if (response.ok) {
alert('Fármaco aprobado');
navigate('/drugs');
} else {
const error = await response.json();
alert(`Error: ${error.error || 'Error al aprobar'}`);
}
} catch (error) {
console.error('Error aprobando fármaco:', error);
alert('Error al aprobar fármaco');
}
}}
className="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors flex items-center gap-2"
>
<CheckCircle className="w-4 h-4" />
Aprobar
</button>
)}
{!isNew && hasPermission('content:publish') && formData.status === 'approved' && (
<button
onClick={async () => {
if (!confirm('¿Publicar este fármaco? (Requiere pediatric_dose)')) return;
try {
const token = localStorage.getItem('admin_token');
const response = await fetch(`${API_URL}/api/drugs/${id}/publish`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
});
if (response.ok) {
alert('Fármaco publicado');
navigate('/drugs');
} else {
const error = await response.json();
alert(`Error: ${error.error || 'Error al publicar'}`);
}
} catch (error) {
console.error('Error publicando fármaco:', error);
alert('Error al publicar fármaco');
}
}}
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors flex items-center gap-2"
>
<CheckCircle className="w-4 h-4" />
Publicar
</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>
{/* Información Básica */}
<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 className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-foreground mb-1">
Nombre Genérico <span className="text-red-500">*</span>
</label>
<input
type="text"
value={formData.generic_name}
onChange={(e) => setFormData(prev => ({ ...prev, generic_name: 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"
placeholder="Ej: Adrenalina"
/>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-1">Nombre Comercial</label>
<input
type="text"
value={formData.trade_name}
onChange={(e) => setFormData(prev => ({ ...prev, trade_name: 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"
placeholder="Ej: Adrenalina 1mg/1ml"
/>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-1">
Categoría <span className="text-red-500">*</span>
</label>
<select
value={formData.category}
onChange={(e) => setFormData(prev => ({ ...prev, category: 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"
>
{CATEGORIES.map(cat => (
<option key={cat} value={cat}>{cat}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-1">
Línea <span className="text-red-500">*</span>
</label>
<select
value={formData.line}
onChange={(e) => setFormData(prev => ({ ...prev, line: e.target.value as 'first' | 'second' }))}
className="w-full px-3 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
>
<option value="first">Primera línea</option>
<option value="second">Segunda línea</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-1">
Frecuencia <span className="text-red-500">*</span>
</label>
<select
value={formData.frequency}
onChange={(e) => setFormData(prev => ({ ...prev, frequency: e.target.value as 'high' | 'medium' | 'low' }))}
className="w-full px-3 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
>
<option value="high">Alta</option>
<option value="medium">Media</option>
<option value="low">Baja</option>
</select>
</div>
</div>
</div>
{/* Presentación y Dosificación */}
<div className="bg-card border border-border rounded-xl p-6 space-y-4">
<h2 className="text-xl font-semibold text-foreground">Presentación y Dosificación</h2>
<div>
<label className="block text-sm font-medium text-foreground mb-1">
Presentación <span className="text-red-500">*</span>
</label>
<input
type="text"
value={formData.presentation}
onChange={(e) => setFormData(prev => ({ ...prev, presentation: 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"
placeholder="Ej: 1mg/1ml ampolla"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-foreground mb-1">
Dosis Adulto <span className="text-red-500">*</span>
</label>
<textarea
value={formData.adult_dose}
onChange={(e) => setFormData(prev => ({ ...prev, adult_dose: 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"
rows={2}
placeholder="Ej: 1mg IV/IO cada 3-5 min"
/>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-1">
Dosis Pediátrica {formData.status === 'published' && <span className="text-red-500">*</span>}
</label>
<textarea
value={formData.pediatric_dose}
onChange={(e) => setFormData(prev => ({ ...prev, pediatric_dose: 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"
rows={2}
placeholder="Ej: 0.01mg/kg IV/IO"
/>
{formData.status === 'published' && !formData.pediatric_dose && (
<p className="text-xs text-red-500 mt-1">Obligatorio para publicar</p>
)}
</div>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-1">Vías de Administración</label>
<div className="flex flex-wrap gap-2">
{ROUTES_OPTIONS.map(route => (
<label key={route} className="flex items-center gap-2 px-3 py-2 bg-background border border-border rounded-lg cursor-pointer hover:bg-muted">
<input
type="checkbox"
checked={formData.routes.includes(route)}
onChange={(e) => {
if (e.target.checked) {
setFormData(prev => ({ ...prev, routes: [...prev.routes, route] }));
} else {
setFormData(prev => ({ ...prev, routes: prev.routes.filter(r => r !== route) }));
}
}}
className="rounded"
/>
<span className="text-sm">{route}</span>
</label>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-1">Dilución</label>
<input
type="text"
value={formData.dilution}
onChange={(e) => setFormData(prev => ({ ...prev, dilution: 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"
placeholder="Ej: Diluir en 20ml SF 0.9%"
/>
</div>
</div>
{/* Indicaciones y Contraindicaciones */}
<div className="bg-card border border-border rounded-xl p-6 space-y-4">
<h2 className="text-xl font-semibold text-foreground">Indicaciones y Contraindicaciones</h2>
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-medium text-foreground">Indicaciones</label>
<button
onClick={() => addArrayItem('indications')}
className="p-1 text-primary hover:bg-primary/10 rounded"
>
<Plus className="w-4 h-4" />
</button>
</div>
{formData.indications.map((indication, index) => (
<div key={index} className="flex gap-2 mb-2">
<input
type="text"
value={indication}
onChange={(e) => updateArrayItem('indications', index, e.target.value)}
className="flex-1 px-3 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="Ej: Parada cardiorrespiratoria (RCP)"
/>
<button
onClick={() => removeArrayItem('indications', index)}
className="p-2 text-red-500 hover:bg-red-500/10 rounded"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
</div>
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-medium text-foreground">Contraindicaciones</label>
<button
onClick={() => addArrayItem('contraindications')}
className="p-1 text-primary hover:bg-primary/10 rounded"
>
<Plus className="w-4 h-4" />
</button>
</div>
{formData.contraindications.map((contraindication, index) => (
<div key={index} className="flex gap-2 mb-2">
<input
type="text"
value={contraindication}
onChange={(e) => updateArrayItem('contraindications', index, e.target.value)}
className="flex-1 px-3 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="Ej: Hipertensión arterial severa"
/>
<button
onClick={() => removeArrayItem('contraindications', index)}
className="p-2 text-red-500 hover:bg-red-500/10 rounded"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-1">Efectos Adversos</label>
<textarea
value={formData.side_effects}
onChange={(e) => setFormData(prev => ({ ...prev, side_effects: 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"
rows={3}
placeholder="Ej: Taquicardia, hipertensión, arritmias..."
/>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-1">Antídoto</label>
<input
type="text"
value={formData.antidote}
onChange={(e) => setFormData(prev => ({ ...prev, antidote: 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"
placeholder="Ej: Naloxona (para opioides)"
/>
</div>
</div>
{/* Información Específica TES */}
<div className="bg-card border border-border rounded-xl p-6 space-y-4">
<h2 className="text-xl font-semibold text-foreground">Información Específica TES</h2>
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-medium text-foreground">Notas</label>
<button
onClick={() => addArrayItem('notes')}
className="p-1 text-primary hover:bg-primary/10 rounded"
>
<Plus className="w-4 h-4" />
</button>
</div>
{formData.notes.map((note, index) => (
<div key={index} className="flex gap-2 mb-2">
<textarea
value={note}
onChange={(e) => updateArrayItem('notes', index, e.target.value)}
className="flex-1 px-3 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
rows={2}
placeholder="Ej: En RCP, administrar cada 3-5 minutos"
/>
<button
onClick={() => removeArrayItem('notes', index)}
className="p-2 text-red-500 hover:bg-red-500/10 rounded"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
</div>
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-medium text-foreground">Puntos Críticos TES</label>
<button
onClick={() => addArrayItem('critical_points')}
className="p-1 text-primary hover:bg-primary/10 rounded"
>
<Plus className="w-4 h-4" />
</button>
</div>
{formData.critical_points.map((point, index) => (
<div key={index} className="flex gap-2 mb-2">
<textarea
value={point}
onChange={(e) => updateArrayItem('critical_points', index, e.target.value)}
className="flex-1 px-3 py-2 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
rows={2}
placeholder="Ej: Verificar dosis según peso en pediatría"
/>
<button
onClick={() => removeArrayItem('critical_points', index)}
className="p-2 text-red-500 hover:bg-red-500/10 rounded"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-1">Fuente</label>
<input
type="text"
value={formData.source}
onChange={(e) => setFormData(prev => ({ ...prev, source: 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"
placeholder="Ej: Manual TES Digital, ERC 2021"
/>
</div>
</div>
</div>
);
}