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 (
{dynamicCategories.map((cat) => ( ))} {rows.map((sg) => ( {dynamicCategories.map((cat) => ( ))} ))}
Subgroup ID Name Line of therapy{labelize(cat)}
{sg.subgroup_id} {sg.name || ''} {sg.line_of_therapy || ''}
    {getCatRules(sg, cat).map((t, i) => (
  • {t}
  • ))}
); }; return (

Care Options

API: {apiBase || window.location.origin}
{loading ? (
Loading…
) : error ? (
{String(error)}
) : ( {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) ) ) ) ) ) ); })}
ID Type Title Sponsor Phase Domain
)}
); } const root = ReactDOM.createRoot(document.getElementById('root')); root.render();