const { useEffect, useMemo, useState, useRef } = React; const { DragDropContext, Droppable, Draggable } = window.ReactBeautifulDnd || {}; 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 buildPivotTree(rows, hierarchyKeys, metricAggs) { const makePathKey = (parentPath, value) => (parentPath ? `${parentPath}||${String(value)}` : String(value)); const root = { level: -1, value: null, pathKey: '', children: new Map(), _distinctSets: Object.create(null), }; for (const def of metricAggs) { if (def.type === 'sum') root[def.key] = 0; else if (def.type === 'countDistinct') root._distinctSets[def.key] = new Set(); } function createNode(level, value, parent) { const node = { level, value, pathKey: makePathKey(parent.pathKey, value), children: new Map(), _distinctSets: Object.create(null), }; for (const def of metricAggs) { if (def.type === 'sum') node[def.key] = 0; else if (def.type === 'countDistinct') node._distinctSets[def.key] = new Set(); } return node; } function updateNodeMetrics(node, row) { for (const def of metricAggs) { if (def.type === 'sum') { const v = Number(row[def.source] || 0); node[def.key] += Number.isFinite(v) ? v : 0; } else if (def.type === 'countDistinct') { const dv = row[def.source]; if (dv != null && dv !== '') node._distinctSets[def.key].add(dv); } } } for (const row of rows) { let node = root; updateNodeMetrics(node, row); for (let level = 0; level < hierarchyKeys.length; level += 1) { const key = hierarchyKeys[level]; const value = row[key] != null && row[key] !== '' ? row[key] : '—'; if (!node.children.has(value)) { node.children.set(value, createNode(level, value, node)); } node = node.children.get(value); updateNodeMetrics(node, row); } } function finalize(node) { for (const def of metricAggs) { if (def.type === 'countDistinct') { node[def.key] = node._distinctSets[def.key]?.size || 0; } } delete node._distinctSets; for (const child of node.children.values()) finalize(child); } finalize(root); return root; } function Toggle({ expanded, onClick }) { return ( ); } function App() { const apiBase = useApiBase(); const [rows, setRows] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const columnCatalog = useMemo(() => ([ { key: 'sponsor', label: 'Sponsor', width: '18%' }, { key: 'care_option_id', label: 'Care Option', width: '22%', minWidth: '115px' }, { key: 'category', label: 'Category', width: '14%' }, { key: 'concept_name', label: 'Concept', width: '22%' }, { key: 'rule_display_name', label: 'Rule', width: '30%' }, ]), []); const columnMap = useMemo(() => Object.fromEntries(columnCatalog.map((c) => [c.key, c])), [columnCatalog]); const [allColumnsOrder, setAllColumnsOrder] = useState(() => columnCatalog.map((c) => c.key)); const [selectedKeys, setSelectedKeys] = useState(() => [ 'category', 'concept_name', 'rule_display_name', 'sponsor', 'care_option_id', ]); const availableKeys = useMemo(() => allColumnsOrder.filter((k) => !selectedKeys.includes(k)), [allColumnsOrder, selectedKeys]); const hierarchy = useMemo(() => selectedKeys.map((k) => columnMap[k]).filter(Boolean), [selectedKeys, columnMap]); const metricOptionsCatalog = useMemo(() => ([ { key: 'careOptions', label: 'Care Options', width: '115px', aggType: 'countDistinct', source: 'care_option_id' }, { key: 'category', label: 'Category', width: '90px', aggType: 'countDistinct', source: 'category' }, { key: 'rule_display_name', label: 'Rules', width: '60px', aggType: 'countDistinct', source: 'rule_display_name' }, { key: 'subgroup_id', label: 'Subgroups', width: '100px', aggType: 'countDistinct', source: 'subgroup_id' }, { key: 'concept_name', label: 'Concept', width: '100px', aggType: 'countDistinct', source: 'concept_name' }, { key: 'sponsor', label: 'Sponsor', width: '100px', aggType: 'countDistinct', source: 'sponsor' }, ]), []); const metricOptionsMap = useMemo(() => Object.fromEntries(metricOptionsCatalog.map((m) => [m.key, m])), [metricOptionsCatalog]); const [selectedMetricKeys, setSelectedMetricKeys] = useState(() => ['careOptions', 'category', 'rule_display_name', 'subgroup_id']); const metrics = useMemo(() => selectedMetricKeys.map((k) => metricOptionsMap[k]).filter(Boolean), [selectedMetricKeys, metricOptionsMap]); const metricsSelectRef = useRef(null); useEffect(() => { const el = metricsSelectRef.current; const ChoicesLib = window.Choices; if (!el || !ChoicesLib) return; const choices = new ChoicesLib(el, { removeItemButton: true, searchEnabled: true, shouldSort: false, placeholder: true, placeholderValue: 'Select metrics…', position: 'bottom', }); const onChange = () => { const values = Array.from(el.selectedOptions).map((o) => o.value); setSelectedMetricKeys(values); }; el.addEventListener('change', onChange); return () => { el.removeEventListener('change', onChange); choices.destroy(); }; }, []); const [openNodes, setOpenNodes] = useState(() => new Set()); useEffect(() => { setLoading(true); setError(null); fetchJson(`${apiBase}/care-option-subgroups/pivot`) .then((data) => { setRows(Array.isArray(data) ? data : []); }) .catch((e) => setError(e.message || String(e))) .finally(() => setLoading(false)); }, [apiBase]); useEffect(() => { setOpenNodes(new Set()); }, [selectedKeys]); const metricAggs = useMemo(() => metrics.map((m) => ({ key: m.key, type: m.aggType, source: m.source })), [metrics]); const pivotRoot = useMemo(() => buildPivotTree(rows, hierarchy.map((h) => h.key), metricAggs), [rows, hierarchy, metricAggs]); const toggleNode = (key) => setOpenNodes((prev) => { const next = new Set(prev); if (next.has(key)) next.delete(key); else next.add(key); return next; }); function renderRows(node) { const rowsOut = []; const children = Array.from(node.children.values()).sort((a, b) => String(a.value).localeCompare(String(b.value))); for (const child of children) { const hasChildren = child.children && child.children.size > 0; const isOpen = openNodes.has(child.pathKey); rowsOut.push( {hierarchy.map((h, idx) => ( idx === child.level ? (
{hasChildren ? ( toggleNode(child.pathKey)} /> ) : ( )} {String(child.value)}
) : ( ) ))} {metrics.map((m) => ( {child[m.key] ?? 0} ))} ); if (hasChildren && isOpen) { rowsOut.push(...renderRows(child)); } } return rowsOut; } function reorder(list, startIndex, endIndex) { const result = Array.from(list); const [removed] = result.splice(startIndex, 1); result.splice(endIndex, 0, removed); return result; } function onDragEnd(result) { if (!result || !result.destination) return; const { source, destination } = result; if (source.droppableId === destination.droppableId && source.index === destination.index) return; if (source.droppableId === 'hierarchy' && destination.droppableId === 'hierarchy') { setSelectedKeys((prev) => reorder(prev, source.index, destination.index)); return; } if (source.droppableId === 'available' && destination.droppableId === 'available') { setAllColumnsOrder((prevAll) => { const avail = prevAll.filter((k) => !selectedKeys.includes(k)); const newAvail = reorder(avail, source.index, destination.index); const selectedSet = new Set(selectedKeys); const rebuilt = []; let a = 0; for (const k of prevAll) { if (selectedSet.has(k)) rebuilt.push(k); else { rebuilt.push(newAvail[a]); a += 1; } } return rebuilt; }); return; } if (source.droppableId === 'available' && destination.droppableId === 'hierarchy') { const fromKey = availableKeys[source.index]; setSelectedKeys((prev) => { const next = Array.from(prev); next.splice(destination.index, 0, fromKey); return next; }); return; } if (source.droppableId === 'hierarchy' && destination.droppableId === 'available') { setSelectedKeys((prev) => { const next = Array.from(prev); next.splice(source.index, 1); return next; }); } } return (

Pivot Viewer

API: {apiBase || window.location.origin}
{DragDropContext && (
{(provided) => (
{availableKeys.map((key, index) => ( {(dragProvided) => (
{columnMap[key]?.label || key}
)}
))} {provided.placeholder}
)}
{(provided) => (
{hierarchy.map((col, index) => ( {(dragProvided) => (
{col.label}
)}
))} {provided.placeholder}
)}
)}
{loading ? (
Loading…
) : error ? (
{String(error)}
) : ( {hierarchy.map((h) => ( ))} {metrics.map((m) => ( ))} {hierarchy.length === 0 ? ( {metrics.map((m) => ( ))} ) : ( renderRows(pivotRoot) )}
{h.label} {m.label}
{pivotRoot[m.key] ?? 0}
)}
); } const root = ReactDOM.createRoot(document.getElementById('root')); root.render();