Módulo 1 of 0
En Progreso

HERRAMIENTAS

Protectómetro DIMS–SIMS
// NOTE: This is a JavaScript (no TypeScript types) version of your component const { useEffect, useMemo, useRef, useState } = React; // --- Helpers --- const uid = () => Math.random().toString(36).slice(2, 10); const clamp = (v, min = 0, max = 10) => Math.max(min, Math.min(max, v)); const defaultPatient = () => ({ id: uid(), name: "Paciente demo", createdAt: new Date().toISOString(), baseThreat: 5, items: [ { id: uid(), type: "SIM", label: "Dormir bien", weight: 1, category: "Biológico" }, { id: uid(), type: "DIM", label: "Miedo a romperme", weight: 1.5, category: "Psicológico" }, { id: uid(), type: "SIM", label: "Apoyo de amigos", weight: 0.75, category: "Social" }, ], }); const STORAGE_KEY = "protectometro_mvp_v1"; function App() { const [patient, setPatient] = useState(() => { try { const raw = localStorage.getItem(STORAGE_KEY); if (raw) return JSON.parse(raw); } catch {} return defaultPatient(); }); useEffect(() => { localStorage.setItem(STORAGE_KEY, JSON.stringify(patient)); }, [patient]); const totals = useMemo(() => { const sim = patient.items.filter(i => i.type === "SIM").reduce((a, b) => a + b.weight, 0); const dim = patient.items.filter(i => i.type === "DIM").reduce((a, b) => a + b.weight, 0); const threat = clamp(Number((patient.baseThreat + dim - sim).toFixed(2))); return { sim, dim, net: dim - sim, threat }; }, [patient]); const addItem = it => setPatient(p => ({ ...p, items: [{ id: uid(), ...it }, ...p.items] })); const removeItem = id => setPatient(p => ({ ...p, items: p.items.filter(i => i.id !== id) })); const clearAll = () => setPatient(p => ({ ...p, items: [] })); const importRef = useRef(null); const exportJSON = () => { const blob = new Blob([JSON.stringify(patient, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `protectometro_${patient.name.replace(/\s+/g, "_")}.json`; a.click(); URL.revokeObjectURL(url); }; const exportCSV = () => { const header = ["type", "label", "weight", "category", "notes"].join(","); const rows = patient.items.map(i => [ i.type, `"${String(i.label).replace(/"/g, '""')}"`, i.weight, i.category, `"${String(i.notes||"").replace(/"/g, '""')}"` ].join(",")); const csv = [header, ...rows].join("\n"); const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `items_${patient.name.replace(/\s+/g, "_")}.csv`; a.click(); URL.revokeObjectURL(url); }; const importJSON = (file) => { const reader = new FileReader(); reader.onload = () => { try { const data = JSON.parse(String(reader.result)); if (!data || !("items" in data)) throw new Error("Archivo inválido"); setPatient(data); } catch (e) { alert("No se pudo importar el archivo"); } }; reader.readAsText(file); }; return (

Protectómetro DIMS–SIMS

{const f=e.target.files?.[0]; if(f) importJSON(f);}} />
{/* Left: Controls & Gauge */}

Paciente

setPatient(p => ({ ...p, name: e.target.value }))} />
Amenaza basal {patient.baseThreat.toFixed(1)}/10
setPatient(p => ({ ...p, baseThreat: Number(e.target.value) }))} className="w-full" />

Estima clínica del punto de partida (contexto, irritabilidad, historia...).

Protectómetro

−{totals.sim.toFixed(2)}
SIMS
+{totals.dim.toFixed(2)}
DIMS
{totals.net.toFixed(2)}
Neto
{/* Middle: Add item */}

Añadir elemento

SIM DIM • Peso recomendado: 0.25–2 (ajústalo según impacto/creencia/contexto).

Lista de elementos

{patient.items.length === 0 ? (

Sin elementos todavía. Añade SIMS/DIMS arriba.

) : (
{patient.items.map(i => ( ))}
Tipo Descripción Peso Categoría Efecto
{i.type} {i.label} {i.weight} {i.category} {i.type === "SIM" ? `−${i.weight}` : `+${i.weight}`}
)}
© {new Date().getFullYear()} Protectómetro (MVP didáctico). No sustituye juicio clínico. Modelo de cálculo: amenaza = basal + ΣDIMS − ΣSIMS (limitado 0–10). Ajusta pesos según impacto.
); } // --- Subcomponents --- function Card({ children, className = "" }) { return (
{children}
); } function Tag({ children, color = "gray" }) { const map = { emerald: "bg-emerald-50 text-emerald-700 border-emerald-200", rose: "bg-rose-50 text-rose-700 border-rose-200", gray: "bg-gray-50 text-gray-700 border-gray-200", }; return {children}; } function Gauge({ value }) { const pct = (value / 10) * 100; const gradient = `linear-gradient(90deg, #10b981 0%, #f59e0b 50%, #ef4444 100%)`; return (
{value.toFixed(1)}
Amenaza (0–10)
0246810
); } function AddItemForm({ onAdd }) { const [type, setType] = useState("SIM"); const [label, setLabel] = useState(""); const [weight, setWeight] = useState(1); const [category, setCategory] = useState("Psicológico"); const [notes, setNotes] = useState(""); const submit = (e) => { e.preventDefault(); if (!label.trim()) return; onAdd({ type, label: label.trim(), weight: Number(weight), category, notes }); setLabel(""); setNotes(""); }; return (
setLabel(e.target.value)} />
setWeight(e.target.value)}> {[0.25,0.5,0.75,1,1.25,1.5,1.75,2].map(v => {v})}
setCategory(e.target.value)}> {["Biológico","Psicológico","Social","Otro"].map(v => {v})}
setNotes(e.target.value)} />
); } const root = ReactDOM.createRoot(document.getElementById('root')); root.render();

Responses