// AppView — The 4 apartados of Cynamonurs. // Singular plan model: 1 dx, 1 obj, 1 int with strict selection limits. const { useState: uS, useMemo: uM, useEffect: uE } = React; // ─── CUENTA ────────────────────────────────────────────────── // La sección de Cuenta vive ahora en src/Cuenta.jsx (rediseño editorial). // Aquí solo la referenciamos desde window para no duplicar lógica. const CuentaSection = (props) => window.CuentaSection ? React.createElement(window.CuentaSection, props) : null; // ─── CASOS ────────────────────────────────────────────────── const CasesSection = () => { const [q, setQ] = uS(''); const [filter, setFilter] = uS('all'); const filtered = uM(() => { let list = CASOS; if (filter !== 'all') list = list.filter(c => c.status === filter); if (q.trim()) { const s = q.toLowerCase(); list = list.filter(c => c.paciente.toLowerCase().includes(s) || c.contexto.toLowerCase().includes(s) || c.rels.some(r => r.label.toLowerCase().includes(s) || r.code.toLowerCase().includes(s)) ); } return list; }, [q, filter]); const kindMeta = { dx: { dot: 'var(--wine-500)', code: 'Dx' }, ob: { dot: 'var(--sage-500)', code: 'Obj' }, iv: { dot: 'var(--blue-500)', code: 'Int' }, }; return (
setQ(e.target.value)}/>
{[['all', 'Todos'], ['active', 'Activos'], ['draft', 'Borradores'], ['archived', 'Archivados']].map(([k, l]) => ( ))}
{filtered.length === 0 ? (

Sin resultados

No encontramos casos con ese criterio. Prueba con otra palabra.

) : (
{filtered.map(c => (

{c.paciente}

{c.contexto}
{c.statusLabel}
{c.rels.map((r, i) => (
{kindMeta[r.kind].code} · {r.code} {r.label}
))}
Actualizado · {c.actualizado} Abrir →
))}
)}
); }; // ─── Smart search (priority + match context) ──────────────── function smartSearch(items, q, kind) { if (!q.trim()) return items.map(it => ({ item: it, where: null, score: 0 })); const tokens = q.toLowerCase().split(/\s+/).filter(t => t.length > 0); const fields = kind === 'dx' ? [['etiqueta', 1000, null], ['codigo', 900, null], ['definicion', 200, 'Definición'], ['caracteristicas', 400, 'Características'], ['factores', 400, 'Factores']] : kind === 'ob' ? [['titulo', 1000, null], ['codigo', 900, null], ['definicion', 200, 'Definición'], ['indicadores', 400, 'Indicadores']] : [['nombre', 1000, null], ['codigo', 900, null], ['definicion', 200, 'Definición'], ['actividades', 400, 'Actividades']]; const matches = []; for (const it of items) { let score = 0; let bestNonTitleField = null; let matchedSnippet = null; for (const [field, weight, label] of fields) { const val = it[field]; if (val == null) continue; if (Array.isArray(val)) { for (const v of val) { const txt = (typeof v === 'string' ? v : v.descripcion || '').toLowerCase(); if (tokens.every(t => txt.includes(t))) { score += weight; if (label && !bestNonTitleField) { bestNonTitleField = label; matchedSnippet = typeof v === 'string' ? v : v.descripcion; } break; } } } else { const txt = String(val).toLowerCase(); if (tokens.every(t => txt.includes(t))) { score += weight; if (label && !bestNonTitleField) { bestNonTitleField = label; matchedSnippet = val.length > 80 ? val.slice(0, 80) + '…' : val; } } } } if (score > 0) { matches.push({ item: it, where: bestNonTitleField, snippet: matchedSnippet, score }); } } matches.sort((a, b) => b.score - a.score); return matches; } // ─── DxSection ────────────────────────────────────────────── const DxSection = ({ onOpenFinal }) => { const [kind, setKind] = uS('dx'); const [q, setQ] = uS(''); const [domain, setDomain] = uS('all'); const [selected, setSelected] = uS(null); const [onlySuggested, setOnlySuggested] = uS(false); const [plan] = PlanStore.use(); const detailRef = React.useRef(null); // Scroll the detail into view whenever the user picks a new card. uE(() => { if (!selected) return; const t = setTimeout(() => { const el = detailRef.current; if (!el) return; const y = el.getBoundingClientRect().top + window.scrollY - 80; window.scrollTo({ top: y, behavior: 'instant' }); }, 80); return () => clearTimeout(t); }, [selected?.codigo]); const ds = window.DATA; if (!ds?.loaded) { return (

Preparando tu biblioteca…

Un momento, estamos terminando de indexar.

); } // Allow detail components to advance to the next stage. const goToKind = (next) => { setKind(next); setSelected(null); // Scroll to the top of the dx-section so the user lands on the new tab cleanly. const layoutEl = document.querySelector('.dx-segmented'); if (layoutEl) { const y = layoutEl.getBoundingClientRect().top + window.scrollY - 80; window.scrollTo({ top: y, behavior: 'instant' }); } }; const source = kind === 'dx' ? ds.dxs : kind === 'ob' ? ds.objs : ds.ints; // Suggested codes from the current diagnostic const suggestedSet = uM(() => { if (!plan.dx) return null; if (kind === 'ob') { const list = ds.relations?.objByDx?.[plan.dx.codigo] || []; return new Map(list.map(({ code, score }) => [code, score])); } if (kind === 'iv') { const list = ds.relations?.intByDx?.[plan.dx.codigo] || []; return new Map(list.map(({ code, score }) => [code, score])); } return null; }, [plan.dx, kind, ds.relations]); // Reset filters/selection when the user switches segment (Dx → Obj → Int). uE(() => { setSelected(null); setDomain('all'); setQ(''); }, [kind]); // Auto-enable "Sugeridos" when the current dx changes or kind changes. // IMPORTANTE: solo se activa si el diagnóstico TIENE relaciones guardadas. // Un diagnóstico nuevo (todavía sin entrada en relations.json) muestra la // lista completa en vez de una lista vacía. Así, al actualizar el libro de // diagnósticos en el futuro, los Dx nuevos siguen siendo usables sin romper nada. uE(() => { if ((kind === 'ob' || kind === 'iv') && plan.dx) { setOnlySuggested(!!(suggestedSet && suggestedSet.size > 0)); } else { setOnlySuggested(false); } }, [kind, plan.dx?.codigo, suggestedSet]); const filtered = uM(() => { let list = source.items; if (onlySuggested && suggestedSet) { list = list.filter(it => suggestedSet.has(it.codigo)); // Sort by suggestion score list = [...list].sort((a, b) => (suggestedSet.get(b.codigo) || 0) - (suggestedSet.get(a.codigo) || 0)); } if (domain !== 'all') list = list.filter(it => String(it.dominio_num) === String(domain)); // Objetivos/Intervenciones sin búsqueda: ordenar por dominio → clase → código if ((kind === 'ob' || kind === 'iv') && !q.trim() && !onlySuggested) { list = [...list].sort((a, b) => { const dn = (Number(a.dominio_num) || 0) - (Number(b.dominio_num) || 0); if (dn !== 0) return dn; const ca = String(a.clase_codigo || a.clase || ''); const cb = String(b.clase_codigo || b.clase || ''); const cc = ca.localeCompare(cb, 'es', { numeric: true }); if (cc !== 0) return cc; return String(a.codigo).localeCompare(String(b.codigo), 'es', { numeric: true }); }); return list.slice(0, 400).map(it => ({ item: it, where: null, score: 0 })); } const ranked = smartSearch(list, q, kind); // If suggested mode and no query, preserve suggestion order if (onlySuggested && !q.trim() && suggestedSet) { return list.slice(0, 200).map(it => ({ item: it, where: null, score: suggestedSet.get(it.codigo) || 0 })); } return ranked.slice(0, 200); }, [source, q, domain, kind, onlySuggested, suggestedSet]); const domainsList = [ { num: 'all', name: kind === 'dx' ? 'Todos los dominios' : kind === 'ob' ? 'Todos los dominios' : 'Todos los campos', count: source.items.length }, ...source.dominios.map(d => ({ num: String(d.num), name: d.name.replace(/^(Dominio|Campo)\s*\d+:\s*/, ''), count: source.items.filter(it => String(it.dominio_num) === String(d.num)).length })), ]; const cssKind = kind === 'dx' ? 'nanda' : kind === 'ob' ? 'noc' : 'nic'; const getLabel = (it) => it.etiqueta || it.titulo || it.nombre; const getMeta = (it) => kind === 'dx' ? it.dominio : (kind === 'ob' ? `${it.escala?.length || 5} niveles · ${it.indicadores?.length || 0} indicadores` : it.dominio); const canShowSuggestedToggle = (kind === 'ob' || kind === 'iv') && plan.dx; // ¿Este diagnóstico aún no tiene relaciones guardadas? (p. ej. un Dx nuevo // agregado al actualizar el libro, antes de regenerar relations.json). const noRels = canShowSuggestedToggle && !(suggestedSet && suggestedSet.size > 0); // Agrupar por dominio → clase al navegar Objetivos/Intervenciones (sin búsqueda ni modo sugeridos). const grouped = (kind === 'ob' || kind === 'iv') && !q.trim() && !onlySuggested; const renderCard = ({ item: it, where, snippet }) => ( ); const renderGroupedList = () => { const out = []; let curKey = null; filtered.forEach((entry) => { const it = entry.item; const claseTxt = (it.clase || '').replace(/^Clase\s*[\w\d]+:?\s*/i, ''); const key = (domain === 'all' ? (it.dominio_num + '|') : '') + (it.clase_codigo || it.clase || '—'); if (key !== curKey) { curKey = key; out.push(
{domain === 'all' && ( {kind === 'iv' ? 'Campo' : 'Dominio'} {it.dominio_num} · {(it.dominio || '').replace(/^(Campo|Dominio)\s*\d+:?\s*/i, '')} )} {it.clase_codigo ? 'Clase ' + it.clase_codigo + ' · ' : ''}{claseTxt || 'Sin clase'}
); } out.push(renderCard(entry)); }); return out; }; return (
{(() => { const ek = kind === 'dx' ? 'nanda' : kind === 'ob' ? 'noc' : 'nic'; const ed = window.EDITIONS?.[ek]; if (!ed) return null; return (
Edición soportada: {ed.edicion}
); })()} {canShowSuggestedToggle && (
{noRels ? 'Aún sin sugerencias para este diagnóstico' : 'Sugerencias para tu diagnóstico'}
{plan.dx.codigo} · {plan.dx.etiqueta} {noRels ? ` · te mostramos ${kind === 'ob' ? 'todos los objetivos' : 'todas las intervenciones'} para que elijas` : `${' · '}${(suggestedSet?.size || 0)} ${kind === 'ob' ? 'objetivos' : 'intervenciones'} sugerid${kind === 'ob' ? 'os' : 'as'} por contenido`}
{!noRels && ( )}
)}
{/* Domain / campo filter — horizontal chip row */}
{kind === 'iv' ? 'Campo:' : 'Dominio:'}
{domainsList.map(d => ( ))}
setQ(e.target.value)}/> {filtered.length} / {onlySuggested && suggestedSet ? suggestedSet.size : source.items.length}
{grouped ? renderGroupedList() : filtered.map((entry) => renderCard(entry))} {filtered.length === 0 && (

Sin resultados

{onlySuggested ? 'Pulsa “Ver todos” para mostrar la lista completa.' : 'Prueba con otra palabra o quita el filtro de dominio.'}

)}
{selected && (
{kind === 'dx' ? : kind === 'ob' ? : }
)}
); }; // ─── Detail: Diagnóstico ──────────────────────────────────── function DxDetail({ item, plan, goToKind }) { const type = detectDxType(item.etiqueta); const inPlan = plan.dx?.codigo === item.codigo; const planDx = inPlan ? plan.dx : null; const dxReady = inPlan && isDxReady(planDx); const isCharSelectable = type === 'real' || type === 'promocion'; const isFactSelectable = type === 'real' || type === 'riesgo' || type === 'sindrome'; const charLabel = type === 'promocion' ? 'Signos de deseo de mejora' : 'Características definitorias'; const factLabel = type === 'riesgo' ? 'Factores de riesgo' : 'Factores relacionados'; const isSelected = (group, v) => planDx?.[group]?.includes(v); return (
Diagnóstico · {item.codigo} {item.dominio && {item.dominio.replace(/^Dominio\s*\d+:\s*/, '')}} {item.clase && {item.clase.replace(/^Clase\s*\w+:\s*/, '')}} {dxTypeLabel(type)}

{item.etiqueta}

Definición

"{item.definicion}"

Estructura del diagnóstico {dxTypeStructure(type)}
{!inPlan && (
Selecciona este diagnóstico para tu plan
Después marca hasta {MAX_CARS} característica(s) y {MAX_FACTORS} factor.
)} {item.caracteristicas?.length > 0 && (
{charLabel} {isCharSelectable && inPlan && ( · {planDx.selectedDefinitorias.length}/{MAX_CARS} seleccionada(s) )}
isSelected('selectedDefinitorias', v)} onToggle={(v) => togglePlanDxItem('selectedDefinitorias', v)} atLimit={planDx && planDx.selectedDefinitorias.length >= MAX_CARS} />
)} {item.factores?.length > 0 && (
{factLabel} {isFactSelectable && inPlan && ( · {planDx.selectedFactores.length}/{MAX_FACTORS} seleccionado )}
isSelected('selectedFactores', v)} onToggle={(v) => togglePlanDxItem('selectedFactores', v)} atLimit={planDx && planDx.selectedFactores.length >= MAX_FACTORS} />
)} {item.poblacion_riesgo?.length > 0 && (
Población en riesgo · solo lectura
)} {item.condiciones_asociadas?.length > 0 && (
Condiciones asociadas · solo lectura
)} {inPlan && (
Manifestaciones · tu valoración (texto libre)

Describe cómo se manifiesta el diagnóstico en tu paciente. Se mostrará en el documento final como "manifestado por…".