const { useEffect, useMemo, useState, useRef } = React;
function useApiBase() {
const params = new URLSearchParams(window.location.search);
const override = params.get('api');
if (override) return override.replace(/\/$/, '');
return '';
}
function fetchJson(url) {
return fetch(url).then((r) => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
});
}
function labelizeCategory(s) {
if (!s) return 'Other';
return s.split('_').map((p) => (p ? (p[0].toUpperCase() + p.slice(1)) : p)).join(' ');
}
function App() {
const apiBase = useApiBase();
const [rules, setRules] = useState([]);
const [concepts, setConcepts] = useState([]);
const [selectedRuleIds, setSelectedRuleIds] = useState([]);
const [selectedConceptCodes, setSelectedConceptCodes] = useState([]);
const [careOptions, setCareOptions] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [expandedIds, setExpandedIds] = useState(() => new Set());
const [subgroupsState, setSubgroupsState] = useState({});
const rulesSelectRef = useRef(null);
const conceptsSelectRef = useRef(null);
const rulesChoicesRef = useRef(null);
const conceptsChoicesRef = useRef(null);
const categoryOrder = [
'diagnosis',
'histology',
'biomarkers',
'interventions',
'stage',
'disease_descriptors',
'screening_items',
'demographics',
'comorbidity',
'clinical_score',
'lab_values',
];
const rulesByCategory = useMemo(() => {
const grouped = {};
for (const rule of rules) {
const raw = (rule && typeof rule.category === 'string') ? rule.category.trim() : '';
const cat = raw || 'other';
if (!grouped[cat]) grouped[cat] = [];
grouped[cat].push(rule);
}
return grouped;
}, [rules]);
const conceptsByCategory = useMemo(() => {
const grouped = {};
for (const concept of concepts) {
const raw = (concept && typeof concept.category === 'string') ? concept.category.trim() : '';
const cat = raw || 'other';
if (!grouped[cat]) grouped[cat] = [];
grouped[cat].push(concept);
}
return grouped;
}, [concepts]);
const orderedRuleCategories = useMemo(() => {
const present = Object.keys(rulesByCategory);
const preferred = categoryOrder.filter((c) => present.includes(c));
const extras = present.filter((c) => !categoryOrder.includes(c)).sort();
return [...preferred, ...extras];
}, [rulesByCategory]);
const orderedConceptCategories = useMemo(() => {
const present = Object.keys(conceptsByCategory);
const preferred = categoryOrder.filter((c) => present.includes(c));
const extras = present.filter((c) => !categoryOrder.includes(c)).sort();
return [...preferred, ...extras];
}, [conceptsByCategory]);
useEffect(() => {
Promise.all([
fetchJson(`${apiBase}/rules/?limit=500`),
fetchJson(`${apiBase}/concepts/?limit=500`),
])
.then(([rulesData, conceptsData]) => {
setRules(rulesData);
setConcepts(conceptsData);
})
.catch((e) => setError(e.message));
}, [apiBase]);
const queryString = useMemo(() => {
const params = new URLSearchParams();
if (selectedRuleIds.length) params.set('rule_ids', selectedRuleIds.join(','));
if (selectedConceptCodes.length) params.set('concept_codes', selectedConceptCodes.join(','));
params.set('limit', '50');
return params.toString();
}, [selectedRuleIds, selectedConceptCodes]);
const loadCareOptions = () => {
setLoading(true);
setError(null);
fetchJson(`${apiBase}/care-options/?${queryString}`)
.then(setCareOptions)
.catch((e) => setError(e.message))
.finally(() => setLoading(false));
};
useEffect(() => {
loadCareOptions();
}, [queryString, apiBase]);
useEffect(() => {
if (!window.Choices) return;
const el = rulesSelectRef.current;
if (!el) return;
if (rulesChoicesRef.current) {
rulesChoicesRef.current.destroy();
rulesChoicesRef.current = null;
}
const instance = new Choices(el, {
removeItemButton: true,
shouldSort: false,
searchEnabled: true,
placeholder: true,
placeholderValue: 'Select rules',
});
rulesChoicesRef.current = instance;
const onChange = (e) => {
const values = Array.from(e.target.selectedOptions).map((o) => o.value);
setSelectedRuleIds(values);
};
el.addEventListener('change', onChange);
return () => {
el.removeEventListener('change', onChange);
if (rulesChoicesRef.current) {
rulesChoicesRef.current.destroy();
rulesChoicesRef.current = null;
}
};
}, [rules]);
useEffect(() => {
if (!window.Choices) return;
const el = conceptsSelectRef.current;
if (!el) return;
if (conceptsChoicesRef.current) {
conceptsChoicesRef.current.destroy();
conceptsChoicesRef.current = null;
}
const instance = new Choices(el, {
removeItemButton: true,
shouldSort: false,
searchEnabled: true,
placeholder: true,
placeholderValue: 'Select concepts',
});
conceptsChoicesRef.current = instance;
const onChange = (e) => {
const values = Array.from(e.target.selectedOptions).map((o) => o.value);
setSelectedConceptCodes(values);
};
el.addEventListener('change', onChange);
return () => {
el.removeEventListener('change', onChange);
if (conceptsChoicesRef.current) {
conceptsChoicesRef.current.destroy();
conceptsChoicesRef.current = null;
}
};
}, [concepts]);
useEffect(() => {
const inst = rulesChoicesRef.current;
if (!inst) return;
inst.removeActiveItems();
if (selectedRuleIds.length) inst.setChoiceByValue(selectedRuleIds);
}, [selectedRuleIds]);
useEffect(() => {
const inst = conceptsChoicesRef.current;
if (!inst) return;
inst.removeActiveItems();
if (selectedConceptCodes.length) inst.setChoiceByValue(selectedConceptCodes);
}, [selectedConceptCodes]);
const resetFilters = () => {
setSelectedRuleIds([]);
setSelectedConceptCodes([]);
setExpandedIds(new Set());
setSubgroupsState({});
};
const toggleRow = (careOptionId) => {
setExpandedIds((prev) => {
const next = new Set(prev);
if (next.has(careOptionId)) {
next.delete(careOptionId);
} else {
next.add(careOptionId);
if (!subgroupsState[careOptionId]) {
setSubgroupsState((p) => ({ ...p, [careOptionId]: { loading: true, error: null, data: null } }));
fetchJson(`${apiBase}/care-options/${encodeURIComponent(careOptionId)}/subgroups`)
.then((rows) => {
setSubgroupsState((p) => ({ ...p, [careOptionId]: { loading: false, error: null, data: rows || [] } }));
})
.catch((e) => {
setSubgroupsState((p) => ({ ...p, [careOptionId]: { loading: false, error: String(e.message || e), data: [] } }));
});
}
}
return next;
});
};
const renderSubgroupTable = (careOptionId) => {
const st = subgroupsState[careOptionId];
if (!st || st.loading) {
return (
Loading subgroups…
);
}
if (st.error) {
return (
{st.error}
);
}
const rows = st.data || [];
if (!rows.length) {
return (
No subgroups found
);
}
const getCatRules = (sg, cat) => {
const rulesByCat = (sg.resolved_rules_by_category || {});
return rulesByCat[cat] || [];
};
const labelize = (s) => s.split('_').map((p) => (p ? (p[0].toUpperCase() + p.slice(1)) : p)).join(' ');
const categoryOrder = [
'diagnosis',
'histology',
'biomarkers',
'interventions',
'stage',
'disease_descriptors',
'screening_items',
'demographics',
'comorbidity',
'clinical_score',
'lab_values',
];
const present = new Set();
rows.forEach((sg) => {
const byCat = sg.resolved_rules_by_category || {};
Object.keys(byCat).forEach((k) => {
if (Array.isArray(byCat[k]) && byCat[k].length) present.add(k);
});
});
const dynamicCategories = categoryOrder.filter((c) => present.has(c))
.concat(Array.from(present).filter((c) => !categoryOrder.includes(c)));
return (
| Subgroup ID |
Name |
Line of therapy |
{dynamicCategories.map((cat) => (
{labelize(cat)} |
))}
{rows.map((sg) => (
| {sg.subgroup_id} |
{sg.name || ''} |
{sg.line_of_therapy || ''} |
{dynamicCategories.map((cat) => (
{getCatRules(sg, cat).map((t, i) => (- {t}
))}
|
))}
))}
);
};
return (
{loading ? (
Loading…
) : error ? (
{String(error)}
) : (
| ID |
Type |
Title |
Sponsor |
Phase |
Domain |
{careOptions.map((co) => {
const isExpanded = expandedIds.has(co.care_option_id);
return (
React.createElement(React.Fragment, { key: co.care_option_id },
React.createElement('tr', { onClick: () => toggleRow(co.care_option_id), style: { cursor: 'pointer' } },
React.createElement('td', null, (isExpanded ? '▼ ' : '▶ '), co.care_option_id),
React.createElement('td', null, co.care_option_type || ''),
React.createElement('td', null, co.title || ''),
React.createElement('td', null, co.sponsor || ''),
React.createElement('td', null, co.phase || ''),
React.createElement('td', null, co.domain || '')
),
isExpanded && (
React.createElement('tr', null,
React.createElement('td', { colSpan: 6 },
React.createElement('div', { style: { padding: '10px 0', maxWidth: '1120px' } },
renderSubgroupTable(co.care_option_id)
)
)
)
)
)
);
})}
)}
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render();