// Plan de cuidados — single-of-each model with strict selection limits. // • 1 diagnóstico (≤4 características, ≤1 factor) // • 1 objetivo (N indicadores, Diana basal→meta usando la escala del propio objetivo) // • 1 intervención (≤10 actividades) // Relations between dx → obj/int are content-based (built offline in data/relations.json). const { useState: pUS, useEffect: pUE, useMemo: pUM, useRef: pUR } = React; // Limits const MAX_CARS = 4; const MAX_FACTORS = 1; const MAX_INDICATORS = 5; const MAX_ACTIVITIES = 10; // ─── Diagnostic type detection ────────────────────────────── function detectDxType(etiqueta = '') { const e = etiqueta.toLowerCase(); if (/^riesgo\s+/.test(e)) return 'riesgo'; if (/^disposici[oó]n\s+para\s+mejorar/.test(e) || /^disposici[oó]n\s+para/.test(e)) return 'promocion'; if (/^s[ií]ndrome\s+/.test(e)) return 'sindrome'; return 'real'; } function dxTypeLabel(t) { return { real: 'Diagnóstico real (enfocado al problema)', riesgo: 'Diagnóstico de riesgo', promocion: 'Diagnóstico de promoción de la salud', sindrome: 'Diagnóstico de síndrome', }[t]; } function dxTypeStructure(t) { return { real: 'PES — Etiqueta + relacionado con + factor + manifestado por + características', riesgo: 'PE — Etiqueta + relacionado con + factor de riesgo', promocion: 'Etiqueta + manifestado por + signos de deseo de mejora', sindrome: 'Etiqueta del síndrome + relacionado con + factor relacionado', }[t]; } function buildDxSentence(dx) { if (!dx) return ''; const etiqueta = dx.etiqueta || ''; const factores = (dx.selectedFactores || []).filter(Boolean); const defin = (dx.selectedDefinitorias || []).filter(Boolean); const type = dx.type || detectDxType(etiqueta); const joinList = (arr) => { if (!arr.length) return '___'; if (arr.length === 1) return arr[0].toLowerCase(); if (arr.length === 2) return arr.join(' y ').toLowerCase(); return arr.slice(0, -1).join(', ').toLowerCase() + ' y ' + arr.slice(-1)[0].toLowerCase(); }; if (type === 'real') { return `${etiqueta} relacionado con ${joinList(factores)}, manifestado por ${joinList(defin)}.`; } if (type === 'riesgo') { return `${etiqueta} relacionado con ${joinList(factores)}.`; } if (type === 'promocion') { return `${etiqueta} manifestado por ${joinList(defin)}.`; } return `${etiqueta} relacionado con ${joinList(factores)}.`; } // ─── Shared store ─────────────────────────────────────────── const PlanStore = { state: { dx: null, obj: null, int: null }, listeners: new Set(), set(updater) { this.state = typeof updater === 'function' ? updater(this.state) : { ...this.state, ...updater }; this.listeners.forEach(fn => fn(this.state)); }, use() { const [, force] = pUS(0); pUE(() => { const fn = () => force(t => t + 1); this.listeners.add(fn); return () => this.listeners.delete(fn); }, []); return [this.state, (u) => this.set(u)]; }, reset() { this.set({ dx: null, obj: null, int: null }); }, }; // ─── Mutators ────────────────────────────────────────────── function setPlanDx(dx) { const type = detectDxType(dx.etiqueta); PlanStore.set({ dx: { codigo: dx.codigo, etiqueta: dx.etiqueta, definicion: dx.definicion, dominio: dx.dominio, dominio_num: dx.dominio_num, clase: dx.clase, clase_num: dx.clase_num, type, selectedDefinitorias: [], selectedFactores: [], manifestaciones: '', } }); } // Texto libre de manifestaciones clínicas (valoración del profesional) function updatePlanDxManifestaciones(text) { PlanStore.set(s => s.dx ? ({ ...s, dx: { ...s.dx, manifestaciones: text } }) : s); } function togglePlanDxItem(group, value) { const limit = group === 'selectedFactores' ? MAX_FACTORS : MAX_CARS; PlanStore.set(s => { if (!s.dx) return s; const list = s.dx[group] || []; let next; if (list.includes(value)) { next = list.filter(v => v !== value); } else { if (list.length >= limit) { // Auto-evict oldest when at limit. next = [...list.slice(1), value]; } else { next = [...list, value]; } } return { ...s, dx: { ...s.dx, [group]: next } }; }); } function setPlanObjective(obj) { PlanStore.set({ obj: { codigo: obj.codigo, titulo: obj.titulo, definicion: obj.definicion, dominio: obj.dominio, dominio_num: obj.dominio_num, clase: obj.clase, clase_codigo: obj.clase_codigo, escala: obj.escala || [], indicadores: obj.indicadores || [], selectedIndicadores: [], indScores: {}, // { [codigo]: { basal, meta } } — escala individual por indicador basal: 1, meta: 5, } }); } function clearPlanObjective() { PlanStore.set({ obj: null }); } // Diana por defecto según número de niveles de la escala (basal bajo, meta alta) function defaultScores(escala) { const n = (escala && escala.length) || 5; return { basal: Math.min(3, Math.max(1, Math.round(n / 2))), meta: n }; } function togglePlanObjectiveIndicator(codigo) { PlanStore.set(s => { if (!s.obj) return s; const sel = s.obj.selectedIndicadores || []; const scores = { ...(s.obj.indScores || {}) }; if (sel.includes(codigo)) { delete scores[codigo]; return { ...s, obj: { ...s.obj, selectedIndicadores: sel.filter(v => v !== codigo), indScores: scores } }; } if (sel.length >= MAX_INDICATORS) return s; // hard cap scores[codigo] = defaultScores(s.obj.escala); return { ...s, obj: { ...s.obj, selectedIndicadores: [...sel, codigo], indScores: scores } }; }); } // Actualiza la escala basal/meta de UN indicador concreto function updatePlanObjectiveIndicator(codigo, patch) { PlanStore.set(s => { if (!s.obj) return s; const scores = { ...(s.obj.indScores || {}) }; scores[codigo] = { ...(scores[codigo] || defaultScores(s.obj.escala)), ...patch }; return { ...s, obj: { ...s.obj, indScores: scores } }; }); } function updatePlanObjective(patch) { PlanStore.set(s => s.obj ? ({ ...s, obj: { ...s.obj, ...patch } }) : s); } function setPlanIntervention(int) { PlanStore.set({ int: { codigo: int.codigo, nombre: int.nombre, dominio: int.dominio, dominio_num: int.dominio_num, clase: int.clase, clase_codigo: int.clase_codigo, definicion: int.definicion, actividades: int.actividades || [], selectedActividades: [], } }); } function clearPlanIntervention() { PlanStore.set({ int: null }); } function togglePlanInterventionActivity(value) { PlanStore.set(s => { if (!s.int) return s; const sel = s.int.selectedActividades || []; if (sel.includes(value)) { return { ...s, int: { ...s.int, selectedActividades: sel.filter(v => v !== value) } }; } if (sel.length >= MAX_ACTIVITIES) return s; // hard cap return { ...s, int: { ...s.int, selectedActividades: [...sel, value] } }; }); } // ─── Plan readiness flags ─────────────────────────────────── function isDxReady(dx) { if (!dx) return false; const t = dx.type; if (t === 'riesgo' || t === 'sindrome') return dx.selectedFactores?.length > 0; if (t === 'promocion') return dx.selectedDefinitorias?.length > 0; return dx.selectedDefinitorias?.length > 0 && dx.selectedFactores?.length > 0; } function isObjReady(obj) { return !!obj && obj.selectedIndicadores?.length > 0; } function isIntReady(intv) { return !!intv && intv.selectedActividades?.length > 0; } // ─── Inline Plan Summary (right rail) ─────────────────────── function PlanSummary({ onOpenFinal, onGoToKind, currentKind }) { const [plan] = PlanStore.use(); const dxReady = isDxReady(plan.dx); const objReady = isObjReady(plan.obj); const intReady = isIntReady(plan.int); const ready = dxReady && objReady && intReady; const completed = [dxReady, objReady, intReady].filter(Boolean).length; return ( ); } // ─── Floating Plan Widget (legacy — kept for fallback at narrow widths) ── function PlanWidget({ onOpenFinal }) { const [plan] = PlanStore.use(); const [expanded, setExpanded] = pUS(false); const dxReady = isDxReady(plan.dx); const objReady = isObjReady(plan.obj); const intReady = isIntReady(plan.int); const ready = dxReady && objReady && intReady; if (!plan.dx && !plan.obj && !plan.int) return null; return (
{expanded && (
Diagnóstico
{plan.dx ? (
{plan.dx.codigo}
{plan.dx.etiqueta}
{plan.dx.selectedDefinitorias.length}/{MAX_CARS} característica(s) · {plan.dx.selectedFactores.length}/{MAX_FACTORS} factor
) : (
Selecciona un diagnóstico para comenzar.
)}
Objetivo
{plan.obj ? (
{plan.obj.codigo}
{plan.obj.titulo}
{plan.obj.selectedIndicadores.length}/{MAX_INDICATORS} indicador(es) · escala por indicador
) : (
Aún no hay objetivo.
)}
Intervención
{plan.int ? (
{plan.int.codigo}
{plan.int.nombre}
{plan.int.selectedActividades.length}/{MAX_ACTIVITIES} actividad(es)
) : (
Aún no hay intervención.
)}
)}
); } // ─── Final Table (matches reference image) ────────────────── function FinalTable({ onClose }) { const [plan] = PlanStore.use(); const [showShare, setShowShare] = pUS(false); const docRef = pUR(null); const editKey = plan.dx && plan.obj && plan.int ? `cyna_final_edit_${plan.dx.codigo}_${plan.obj.codigo}_${plan.int.codigo}` : null; const [editMode, setEditMode] = pUS(false); const [editedHtml, setEditedHtml] = pUS(() => { try { return editKey ? (localStorage.getItem(editKey) || null) : null; } catch { return null; } }); const startEdit = () => { setEditMode(true); setTimeout(() => { if (docRef.current) { docRef.current.setAttribute('contenteditable', 'true'); docRef.current.focus(); } }, 30); }; const saveEdit = () => { if (docRef.current) { docRef.current.removeAttribute('contenteditable'); const html = docRef.current.innerHTML; setEditedHtml(html); try { if (editKey) localStorage.setItem(editKey, html); } catch {} } setEditMode(false); }; const restoreOriginal = () => { if (!confirm('¿Descartar tus ediciones y restaurar el documento original?')) return; setEditedHtml(null); setEditMode(false); try { if (editKey) localStorage.removeItem(editKey); } catch {} }; const { dx, obj, int: intv } = plan; if (!dx || !obj || !intv) return null; const sentence = buildDxSentence(dx); const dominioNum = dx.dominio_num ?? (dx.dominio || '').match(/\d+/)?.[0] ?? '—'; const dominioName = (dx.dominio || '').replace(/^Dominio\s*\d+:?\s*/i, ''); const claseNum = dx.clase_num ?? (dx.clase || '').match(/Clase\s*([\dA-Za-z]+)/i)?.[1] ?? '—'; const claseName = (dx.clase || '').replace(/^Clase\s*[\w\d]+:?\s*/i, ''); const objDominioNum = obj.dominio_num ?? '—'; const objClaseCode = obj.clase_codigo ?? (obj.clase || '').match(/Clase\s*([A-Za-z\d]+)/i)?.[1] ?? '—'; const intDominioNum = intv.dominio_num ?? '—'; const intClaseCode = intv.clase_codigo ?? (intv.clase || '').match(/Clase\s*([A-Za-z\d]+)/i)?.[1] ?? '—'; const intDominioName = (intv.dominio || '').replace(/^(Campo|Dominio)\s*\d+:?\s*/i, ''); const intClaseName = (intv.clase || '').replace(/^Clase\s*[\w\d]+:?\s*/i, ''); const selectedIndicators = obj.selectedIndicadores .map(code => obj.indicadores.find(i => i.codigo === code)) .filter(Boolean); const print = () => window.print(); const exportSummary = () => { const lines = []; lines.push(`DIAGNÓSTICO DE ENFERMERÍA: ${sentence}`); lines.push(`Dominio ${dominioNum}: ${dominioName} · Clase ${claseNum}: ${claseName}`); lines.push(''); lines.push(`OBJETIVO (RESULTADO): ${obj.titulo} (${obj.codigo})`); lines.push(`Dominio ${objDominioNum} · Clase ${objClaseCode}`); selectedIndicators.forEach(i => { const sc = (obj.indScores && obj.indScores[i.codigo]) || { basal: obj.basal, meta: obj.meta }; lines.push(` • ${i.codigo} — ${i.descripcion} [Diana: mantener ${sc.basal} → aumentar ${sc.meta}]`); }); lines.push('Escala: ' + obj.escala.map((e, i) => `${e} (${i+1})`).join(' · ')); lines.push(''); lines.push(`INTERVENCIÓN: ${intv.nombre} (${intv.codigo})`); lines.push(`Campo ${intDominioNum} · Clase ${intClaseCode}`); intv.selectedActividades.forEach(a => lines.push(` • ${a}`)); return lines.join('\n'); }; const shareText = exportSummary(); const shareNative = async () => { if (navigator.share) { try { await navigator.share({ title: 'Plan de cuidados', text: shareText }); } catch {} } else { setShowShare(true); } }; const copyClipboard = async () => { try { await navigator.clipboard.writeText(shareText); alert('Copiado al portapapeles'); } catch { alert('No se pudo copiar'); } }; const enc = encodeURIComponent(shareText); const subject = encodeURIComponent('Plan de cuidados de enfermería'); const shareLinks = [ { name: 'WhatsApp', icon: 'wa', url: `https://wa.me/?text=${enc}` }, { name: 'Gmail', icon: 'gm', url: `https://mail.google.com/mail/?view=cm&fs=1&su=${subject}&body=${enc}` }, { name: 'Outlook', icon: 'ol', url: `https://outlook.live.com/mail/0/deeplink/compose?subject=${subject}&body=${enc}` }, { name: 'Correo', icon: 'ml', url: `mailto:?subject=${subject}&body=${enc}` }, { name: 'Telegram', icon: 'tg', url: `https://t.me/share/url?url=cynamonurs.com&text=${enc}` }, { name: 'Facebook', icon: 'fb', url: `https://www.facebook.com/sharer/sharer.php?u=https://cynamonurs.com"e=${enc}` }, ]; // Build table rows: one row per selected indicator. First row spans others. const rowCount = Math.max(1, selectedIndicators.length); return (
e.stopPropagation()}>
Resultado
Plan de cuidados de enfermería
{!editMode ? ( ) : ( )} {editedHtml && !editMode && ( )}
{editMode && (
Modo edición: escribe directamente sobre el documento. Pulsa Guardar cambios al terminar.
)} {editedHtml != null ? (
) : (
{selectedIndicators.map((ind, i) => { const sc = (obj.indScores && obj.indScores[ind.codigo]) || { basal: obj.basal, meta: obj.meta }; return ( {i === 0 && ( )} {i === 0 && ( )} ); })}
DIAGNÓSTICO DE ENFERMERÍA:
{sentence}
{dx.manifestaciones && dx.manifestaciones.trim() && (
Manifestaciones: {dx.manifestaciones.trim()}
)}
Dominio: {dominioNum}{dominioName ? `, ${dominioName.toLowerCase()}` : ''}
Clase: {claseNum}{claseName ? `, ${claseName.toLowerCase()}` : ''}
RESULTADO(S) CÓDIGO(S) INDICADOR(ES) ESCALAS DE MEDICIÓN PUNTUACIÓN DIANA
Mantener    Aumentar
{obj.titulo}
Dominio: {objDominioNum}
Clase: {objClaseCode}
{ind.codigo} {ind.descripcion} {obj.escala.map((e, idx) => (
{e} ({idx + 1})
))}
{sc.basal} {sc.meta}
{/* Intervention block, matching the image */}
INTERVENCIÓN DE ENFERMERÍA: {intv.nombre}
Campo: {intDominioNum}{intDominioName ? `. ${intDominioName.toLowerCase()}` : ''}
Clase: {intClaseCode}{intClaseName ? `. ${intClaseName.toLowerCase()}` : ''}
{intv.nombre} ({intv.codigo})
    {intv.selectedActividades.map((a, idx) =>
  • {a}
  • )}
Cynamonurs · plan generado el {new Date().toLocaleDateString('es-MX', { day: '2-digit', month: 'long', year: 'numeric' })}
Enfermera responsable
)}
{showShare && (
setShowShare(false)}>
e.stopPropagation()}>

Compartir plan de cuidados

Elige una plataforma. Tu plan se envía como mensaje listo para enviar.

{shareLinks.map(s => (
{shareIcon(s.icon)}
{s.name}
))}
)}
); } function shareIcon(name) { const props = { width: 22, height: 22, viewBox: '0 0 24 24', fill: 'currentColor' }; switch (name) { case 'wa': return ; case 'gm': return ; case 'ol': return ; case 'ml': return ; case 'tg': return ; case 'fb': return ; default: return null; } } Object.assign(window, { PlanStore, MAX_CARS, MAX_FACTORS, MAX_INDICATORS, MAX_ACTIVITIES, detectDxType, dxTypeLabel, dxTypeStructure, buildDxSentence, setPlanDx, togglePlanDxItem, updatePlanDxManifestaciones, setPlanObjective, clearPlanObjective, togglePlanObjectiveIndicator, updatePlanObjective, updatePlanObjectiveIndicator, setPlanIntervention, clearPlanIntervention, togglePlanInterventionActivity, isDxReady, isObjReady, isIntReady, PlanWidget, PlanSummary, FinalTable });