// 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 (
{[['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 => (
))}
{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.poblacion_riesgo.map((c, i) => - {c}
)}
)}
{item.condiciones_asociadas?.length > 0 && (
Condiciones asociadas · solo lectura
{item.condiciones_asociadas.map((c, i) => - {c}
)}
)}
{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…".
)}
{inPlan && (
{dxReady ? '✓ Diagnóstico listo' : '○ Faltan selecciones'}
Vista previa: {buildDxSentence(planDx)}
{dxReady && (
)}
)}
);
}
// ─── Detail: Objetivo ───────────────────────────────────────
function ObjDetail({ item, plan, goToKind }) {
const inPlan = plan.obj?.codigo === item.codigo;
const planObj = inPlan ? plan.obj : null;
const atIndLimit = planObj && planObj.selectedIndicadores.length >= MAX_INDICATORS;
const objReady = inPlan && isObjReady(planObj);
return (
Objetivo · {item.codigo}
{item.dominio && {item.dominio.replace(/^Dominio\s*\d+:\s*/, '')}}
{item.clase && {item.clase.replace(/^Clase\s*\w+:\s*/, '')}}
{item.titulo}
Definición
"{item.definicion}"
{item.escala?.length > 0 && (
Escala de medición · específica de este objetivo
{item.escala.map((n, i) => (
{i + 1}
{n}
))}
)}
{!inPlan ? (
Seleccionar este objetivo {plan.obj && · reemplazará el actual ({plan.obj.codigo})}
Después marca los indicadores y define la puntuación Diana usando esta escala.
) : (
<>
Indicadores · {planObj.selectedIndicadores.length}/{MAX_INDICATORS} seleccionado(s)
Marca un indicador y debajo define su escala de medición individual (puntuación basal → meta).
{item.indicadores.map((ind) => {
const on = planObj.selectedIndicadores.includes(ind.codigo);
const blocked = !on && atIndLimit;
const sc = (planObj.indScores && planObj.indScores[ind.codigo]) || { basal: 1, meta: item.escala?.length || 5 };
return (
-
{on && (
{ind.codigo} · Escala de medición de este indicador
updatePlanObjectiveIndicator(ind.codigo, { basal: v })}
/>
updatePlanObjectiveIndicator(ind.codigo, { meta: v })}
/>
Meta:
{item.escala?.[sc.basal - 1] || sc.basal}
{item.escala?.[sc.meta - 1] || sc.meta}
)}
);
})}
{objReady ? '✓ Objetivo listo' : '○ Marca al menos un indicador'}
{objReady && (
)}
>
)}
);
}
// ─── Detail: Intervención ───────────────────────────────────
function IntDetail({ item, plan }) {
const inPlan = plan.int?.codigo === item.codigo;
const planInt = inPlan ? plan.int : null;
const atLimit = planInt && planInt.selectedActividades.length >= MAX_ACTIVITIES;
const intReady = inPlan && isIntReady(planInt);
const dxReady = isDxReady(plan.dx);
const objReady = isObjReady(plan.obj);
const canOpenFinal = intReady && dxReady && objReady;
return (
Intervención · {item.codigo}
{item.dominio && {item.dominio.replace(/^(Campo|Dominio)\s*\d+:\s*/, '')}}
{item.clase && {item.clase.replace(/^Clase\s*\w+:\s*/, '')}}
{item.nombre}
Definición
"{item.definicion}"
{!inPlan ? (
Seleccionar esta intervención {plan.int && · reemplazará la actual ({plan.int.codigo})}
Después marca hasta {MAX_ACTIVITIES} actividad(es) a aplicar durante el cuidado.
) : (
<>
Actividades
· {planInt.selectedActividades.length}/{MAX_ACTIVITIES} seleccionada(s)
planInt.selectedActividades.includes(v)}
onToggle={(v) => togglePlanInterventionActivity(v)}
atLimit={atLimit}
/>
{intReady ? (canOpenFinal ? '✓ Intervención lista · plan completo' : '✓ Intervención lista · faltan otros pasos') : '○ Marca al menos una actividad'}
{intReady && (
)}
>
)}
);
}
// ─── Selectable list (checkbox rows) ────────────────────────
function SelectableList({ items, selectable, isOn, onToggle, atLimit }) {
return (
{items.map((c, i) => {
const value = typeof c === 'string' ? c : (c.descripcion || c);
const on = selectable ? isOn(value) : false;
const blocked = selectable && !on && atLimit;
return (
-
{selectable ? (
) : (
{value}
)}
);
})}
);
}
// ─── Scale selector (uses objective's own scale) ────────────
function ScaleSelector({ label, escala, value, onChange }) {
return (
{(escala || []).map((txt, i) => (
))}
);
}
// ─── APP ROOT ────────────────────────────────────────────────
const AppView = ({ initialTab = 'dx', onOpenFinal }) => {
const [tab, setTab] = uS(initialTab);
uE(() => { setTab(initialTab); }, [initialTab]);
uE(() => {
window.__cynaGoTab = (t) => setTab(t);
return () => { delete window.__cynaGoTab; };
}, []);
const counts = window.DATA?.loaded ? {
dxs: window.DATA.dxs.items.length,
objs: window.DATA.objs.items.length,
ints: window.DATA.ints.items.length,
} : { dxs: 0, objs: 0, ints: 0 };
const titles = {
cuenta: { eyebrow: '01 · Cuenta', title: 'Tu cuenta', sub: 'Perfil, suscripción, preferencias y sincronización.' },
tools: { eyebrow: '02 · Herramientas', title: 'Herramientas clínicas', sub: 'Calculadoras y escalas listas para tu turno.' },
casos: { eyebrow: '03 · Casos', title: 'Tus casos clínicos', sub: 'Pacientes, planes de cuidado y plantillas.' },
dx: { eyebrow: '04 · Diagnósticos · Objetivos · Intervenciones', title: 'Explorador clínico', sub: 'Selecciona un diagnóstico, luego la app sugerirá objetivos e intervenciones relacionados.' },
};
const t = titles[tab];
return (
{t.eyebrow}
{t.title}
{t.sub}
{tab === 'cuenta' && }
{tab === 'tools' && }
{tab === 'casos' && }
{tab === 'dx' && }
);
};
Object.assign(window, { AppView, CasesSection, DxSection });