/* Eno.Health Console — Reference data: Terminologies (LOINC/UCUM) + Laboratories & test types */ const R = window.ENO; const TAPI = window.EnoAdminAPI; /* small helpers shared in this file */ function dbStatusPill(status) { if (status === "custom") return h(Pill, { tone: "purple", soft: true }, "custom"); if (status === "deprecated") return h(Pill, { tone: "neutral", soft: true }, "deprecated"); return h(Pill, { tone: "ok", soft: true }, "active"); } function fmtRef(a) { if (a.refLow != null && a.refHigh != null) return a.refLow + "–" + a.refHigh; if (a.refHigh != null) return "< " + a.refHigh; if (a.refLow != null) return "> " + a.refLow; return "—"; } function fallbackRanges(ranges) { const blank = { low: null, high: null }; return { male: { ...blank, ...(ranges && ranges.male ? ranges.male : {}) }, female: { ...blank, ...(ranges && ranges.female ? ranges.female : {}) }, femaleMenopausal: { ...blank, ...(ranges && ranges.femaleMenopausal ? ranges.femaleMenopausal : {}) }, femalePregnant: { ...blank, ...(ranges && ranges.femalePregnant ? ranges.femalePregnant : {}) }, }; } function bmDisplayName(biomarkers, id) { return (biomarkers.find((b) => b.id === id) || {}).name || R.bmName(id); } function liveAgo(date) { const seconds = Math.max(0, Math.round((Date.now() - date.getTime()) / 1000)); if (seconds < 60) return seconds + "s ago"; if (seconds < 3600) return Math.round(seconds / 60) + "m ago"; if (seconds < 86400) return Math.round(seconds / 3600) + "h ago"; return Math.round(seconds / 86400) + "d ago"; } function syncDisplay(db) { const importedAt = db.importedAt ? new Date(db.importedAt) : null; if (importedAt && !Number.isNaN(importedAt.getTime())) { return { relative: liveAgo(importedAt), exact: fmtDate(importedAt) + " · " + fmtTime(importedAt), }; } return { relative: db.lastSyncMin ? fmtAgo(R.ago(db.lastSyncMin * R.MIN)) : "not synced", exact: "last sync", }; } /* ======================================================================== */ /* TERMINOLOGIES — LOINC · Biomarkers · UCUM */ /* ======================================================================== */ function TerminologyView() { const [tab, setTab] = useState("loinc"); // Biomarker ↔ LOINC links are editable from both terminology views, so the // live dictionary stays here and is shared down to both panels. const [biomarkers, setBiomarkers] = useState(() => R.BIOMARKERS.map((b) => ({ ...b, loinc: [...b.loinc], phenotypes: [...b.phenotypes], ranges: fallbackRanges(b.ranges) }))); const [loincCodes, setLoincCodes] = useState(() => R.LOINC_CODES.map((c) => ({ ...c }))); const [loincPage, setLoincPage] = useState({ total: R.LOINC_CODES.length, loadedAll: true }); const [loincGroups, setLoincGroups] = useState([]); const [loincFacets, setLoincFacets] = useState({ units: [], classes: [], systems: [], statuses: [], biomarkers: [] }); const [ucumUnits, setUcumUnits] = useState(() => R.UCUM_UNITS.map((u) => ({ ...u }))); const [loincDB, setLoincDB] = useState(R.LOINC_DB); const [ucumDB, setUcumDB] = useState(R.UCUM_DB); const [reviews, setReviews] = useState({ items: [], total: 0, pending: 0, loading: false }); const [live, setLive] = useState({ loading: true, error: "", status: "mock" }); const toast = useToast(); const loadSummary = async () => { const summary = await TAPI.terminologySummary(); setLoincDB(summary.loincDB); setUcumDB(summary.ucumDB); }; const loadLoinc = async (params = {}) => { const hasSearch = Boolean(params.query && String(params.query).trim()); if (!hasSearch) { const browseParams = { sort: "code", direction: "asc", limit: 50, ...params }; const page = await TAPI.loincTerms(browseParams); setLoincCodes(page.items.length ? page.items : []); setLoincPage({ total: page.total || page.items.length || 0, loadedAll: false }); const hasFacetFilters = ["units", "classes", "systems", "statuses", "biomarkers"].some((key) => Array.isArray(params[key]) && params[key].length); if (!hasFacetFilters && (!params.class || params.class === "all") && (!params.group_id || params.group_id === "all") && (!params.status || params.status === "all")) { const mappedPage = await TAPI.loincTerms({ limit: 1, mapped_only: true }); setLoincDB((db) => ({ ...db, mappedInUse: mappedPage.total || 0 })); } return page; } const limit = 200; let offset = 0; let total = 0; const items = []; do { const page = await TAPI.loincTerms({ ...params, limit, offset }); items.push(...(page.items || [])); total = page.total || items.length; offset += limit; } while (items.length < total); const page = { items, total, limit: items.length, offset: 0, status: "ok" }; setLoincCodes(items); setLoincPage({ total: items.length, loadedAll: true }); return page; }; const loadLoincGroups = async (params = {}) => { const page = await TAPI.loincGroups({ limit: 500, ...params }); setLoincGroups(page.items || []); return page; }; const loadLoincFacets = async (params = {}) => { const facets = await TAPI.loincFacets(params); setLoincFacets(facets); return facets; }; const loadUCUM = async (params = {}) => { const page = await TAPI.ucumUnits({ limit: 100, ...params }); setUcumUnits(page.items.length ? page.items : []); return page; }; const loadBiomarkers = async (params = {}) => { const page = await TAPI.biomarkers({ limit: 200, ...params }); setBiomarkers((page.items || []).map((b) => ({ ...b, ranges: fallbackRanges(b.ranges), loinc: [...b.loinc], phenotypes: [...b.phenotypes] }))); return page; }; const loadReviews = async (params = {}) => { const page = await TAPI.biomarkerMappingReviews({ limit: 100, status: "pending", ...params }); setReviews({ items: page.items || [], total: page.total || 0, pending: page.total || 0, loading: false }); return page; }; useEffect(() => { let cancelled = false; async function run() { setLive({ loading: true, error: "", status: "loading" }); try { await Promise.all([loadSummary(), loadLoinc(), loadLoincGroups(), loadLoincFacets(), loadUCUM(), loadBiomarkers(), loadReviews()]); if (!cancelled) setLive({ loading: false, error: "", status: "live" }); } catch (err) { if (!cancelled) setLive({ loading: false, error: err.message || "Unable to load terminology data", status: "mock" }); } } run(); return () => { cancelled = true; }; }, []); useEffect(() => { if (live.status !== "live") return; const tick = () => { if (document.hidden) return; loadReviews().catch((err) => setReviews((cur) => ({ ...cur, loading: false, error: err.message }))); }; const timer = setInterval(tick, 15000); return () => clearInterval(timer); }, [live.status]); const replaceBiomarker = (bm) => setBiomarkers((xs) => xs.map((b) => b.id === bm.id ? { ...bm, ranges: fallbackRanges(bm.ranges), loinc: [...bm.loinc], phenotypes: [...bm.phenotypes] } : b)); const bmApi = { linkLoinc: async (bmId, code) => { const bm = biomarkers.find((b) => b.id === bmId); try { replaceBiomarker(await TAPI.linkLoinc(bmId, code, bm && bm.specimen)); toast("Linked " + code + " → " + bmDisplayName(biomarkers, bmId), "ok"); } catch (err) { toast(err.message, "bad"); } }, unlinkLoinc: async (bmId, code) => { try { replaceBiomarker(await TAPI.unlinkLoinc(bmId, code)); toast("Unlinked " + code + " from " + bmDisplayName(biomarkers, bmId)); } catch (err) { toast(err.message, "bad"); } }, patch: async (bmId, patch) => { setBiomarkers((xs) => xs.map((b) => b.id === bmId ? { ...b, ...patch } : b)); try { replaceBiomarker(await TAPI.patchBiomarker(bmId, patch)); } catch (err) { toast(err.message, "bad"); } }, togglePhenotype: async (bmId, ph) => { const bm = biomarkers.find((b) => b.id === bmId); if (!bm) return; const phenotypes = bm.phenotypes.includes(ph) ? bm.phenotypes.filter((p) => p !== ph) : [...bm.phenotypes, ph]; await bmApi.patch(bmId, { phenotypes }); }, setRange: async (bmId, demo, bound, val) => { const bm = biomarkers.find((b) => b.id === bmId); if (!bm) return; const ranges = { ...fallbackRanges(bm.ranges), [demo]: { ...fallbackRanges(bm.ranges)[demo], [bound]: val === "" || val == null ? null : Number(val) } }; await bmApi.patch(bmId, { ranges }); }, addBiomarker: async (bm) => { try { const created = await TAPI.createBiomarker({ ...bm, ranges: fallbackRanges(bm.ranges) }); setBiomarkers((xs) => [{ ...created, ranges: fallbackRanges(created.ranges), loinc: [...created.loinc], phenotypes: [...created.phenotypes] }, ...xs]); toast("Biomarker “" + created.name + "” created", "ok"); return created; } catch (err) { toast(err.message, "bad"); return null; } }, markReviewed: async (bmId) => { try { replaceBiomarker(await TAPI.patchBiomarker(bmId, { confidence: 100 })); toast("“" + bmDisplayName(biomarkers, bmId) + "” marked reviewed", "ok"); } catch (err) { toast(err.message, "bad"); } }, addLoinc: (code) => { setLoincCodes((xs) => [code, ...xs]); toast("Custom code " + code.code + " added", "ok"); }, resolveReview: async (review, biomarkerId) => { try { await TAPI.resolveBiomarkerMappingReview(review.id, { biomarker_id: biomarkerId, loinc_code: review.candidate_loinc_code, specimen: review.specimen, curator_notes: "Approved from biomarker mapping review queue", reviewed_by: "admin-console", }); toast("Linked " + review.candidate_loinc_code + " from review queue", "ok"); await Promise.all([loadBiomarkers(), loadLoinc(), loadReviews()]); } catch (err) { toast(err.message, "bad"); } }, rejectReview: async (review) => { try { await TAPI.rejectBiomarkerMappingReview(review.id, { curator_notes: "Rejected from admin console", reviewed_by: "admin-console" }); toast("Review item rejected"); await loadReviews(); } catch (err) { toast(err.message, "bad"); } }, }; const tabs = [ { key: "review", label: "Review", count: reviews.pending }, { key: "loinc", label: "LOINC", count: loincDB.mappedInUse }, { key: "biomarker", label: "Biomarkers", count: biomarkers.length }, { key: "ucum", label: "UCUM", count: ucumDB.mappedInUse }, ]; return h("div", { className: "view" }, h(PageHead, { title: "Terminologies", sub: "Versioned vocabularies and Eno's biomarker dictionary — what normalizes an extracted value into a result a practitioner can read" }), h("div", { className: "ref-banner" }, h(Icon, { name: "link", size: 16, className: "dim" }), h("div", null, "Extraction maps each observation to a ", h("strong", null, "LOINC"), " code; a ", h("strong", null, "Biomarker"), " groups the LOINC codes that mean the same thing and attaches the ", h("strong", null, "UCUM"), " unit, specimen and reference ranges used to interpret it.")), h(Tabs, { tabs, active: tab, onChange: setTab }), h("div", { className: "tabbody" }, tab === "review" ? h(BiomarkerReviewPanel, { reviews, biomarkers, api: bmApi, onRefresh: loadReviews }) : tab === "loinc" ? h(LoincPanel, { biomarkers, api: bmApi, loincCodes, loincPage, loincGroups, loincFacets, db: loincDB, live, onSearch: loadLoinc }) : tab === "biomarker" ? h(BiomarkerPanel, { biomarkers, api: bmApi, loincCodes }) : h(UcumPanel, { db: ucumDB, units: ucumUnits, live, onSearch: loadUCUM }))); } function BiomarkerReviewPanel({ reviews, biomarkers, api, onRefresh }) { const [q, setQ] = useState(""); const filtered = (reviews.items || []).filter((item) => !q || (item.observed_name + " " + item.observed_unit + " " + item.candidate_loinc_code).toLowerCase().includes(q.toLowerCase())); const head = ["Observation", "Candidate", "Reason", "Seen", "Action"]; return h("div", { className: "view" }, h(Card, { pad: false, title: "Biomarker review queue", subtitle: "Document-derived observations that need administrator curation before future ingestion can auto-normalize them.", actions: h(Button, { variant: "ghost", size: "sm", icon: "refresh", onClick: () => onRefresh() }, "Refresh") }, h("div", { className: "toolbar" }, h("div", { className: "search" }, h(Icon, { name: "search", size: 16 }), h("input", { className: "search-input", placeholder: "Search observation, unit or LOINC…", value: q, onChange: (e) => setQ(e.target.value) })), h(Pill, { tone: reviews.pending ? "warn" : "ok", soft: true }, reviews.pending + " pending"), h("span", { className: "toolbar-count" }, "Auto-refreshes every 15s")), h("div", { className: "table-wrap flush" }, h("table", { className: "table" }, h("thead", null, h("tr", null, head.map((c) => h("th", { key: c }, c)))), h("tbody", null, filtered.map((item) => h(BiomarkerReviewRow, { key: item.id, item, biomarkers, api })), !filtered.length && h("tr", null, h("td", { colSpan: 5 }, h(Empty, { icon: "check", title: "No pending biomarker reviews", sub: "New document observations will appear here automatically." })))))))); } function BiomarkerReviewRow({ item, biomarkers, api }) { const [pick, setPick] = useState(item.candidate_biomarker_id || ""); const options = [{ value: "", label: "Select biomarker" }, ...biomarkers.map((b) => ({ value: b.id, label: b.name }))]; const reasons = item.review_reasons || []; return h("tr", null, h("td", null, h("div", { className: "strong" }, item.observed_name), h("div", { className: "dim" }, [item.observed_value, item.observed_unit, item.specimen].filter(Boolean).join(" · ") || "No value context")), h("td", null, item.candidate_loinc_code ? h(Mono, { copy: true, className: "code-strong" }, item.candidate_loinc_code) : h("span", { className: "dim" }, "No candidate"), h("div", { className: "dim" }, Math.round((item.candidate_score || 0) * 100), "% confidence")), h("td", null, h("span", { className: "tags" }, reasons.slice(0, 2).map((reason) => h("span", { className: "tag", key: reason }, reason))), reasons.length > 2 && h("div", { className: "dim" }, "+", reasons.length - 2, " more")), h("td", null, h("div", null, fmtNum(item.occurrence_count || 1), " occurrence", (item.occurrence_count || 1) === 1 ? "" : "s"), h("div", { className: "dim" }, item.last_seen_at ? fmtAgo(new Date(item.last_seen_at)) : "recent")), h("td", null, h("div", { className: "toolbar", style: { padding: 0, gap: 8 } }, h(Select, { value: pick, onChange: setPick, options }), h(Button, { variant: "primary", size: "sm", icon: "link", disabled: !pick || !item.candidate_loinc_code, onClick: () => api.resolveReview(item, pick) }, "Link"), h(Button, { variant: "ghost", size: "sm", icon: "x", onClick: () => api.rejectReview(item) }, "Reject")))); } function DbInfoCard({ db, kind }) { const toast = useToast(); const sync = syncDisplay(db); return h(Card, { pad: false, title: db.name + " release " + db.version, subtitle: db.source + " · " + db.license, actions: h(React.Fragment, null, h(Button, { variant: "ghost", size: "sm", icon: "refresh", onClick: () => toast("Checked for updates · " + db.next, "ok") }, "Check for updates"), h(Button, { variant: "default", size: "sm", icon: "upload", disabled: true, title: "Imports run through terminology-importer for now" }, "Import release")) }, h("div", { className: "td-stats" }, h("div", { className: "td-stat" }, h("span", { className: "td-num mono" }, db.version), h("span", { className: "dim" }, "loaded version")), h("div", { className: "td-stat" }, h("span", { className: "td-num" }, fmtNum(kind === "loinc" ? db.totalCodes : db.totalUnits)), h("span", { className: "dim" }, kind === "loinc" ? "total codes" : "total units")), h("div", { className: "td-stat" }, h("span", { className: "td-num" }, fmtNum(db.mappedInUse)), h("span", { className: "dim" }, "mapped in use")), h("div", { className: "td-stat" }, h("span", { className: "td-num" }, db.custom), h("span", { className: "dim" }, "custom entries")), h("div", { className: "td-stat" }, h("span", { className: "td-num" }, sync.relative), h("span", { className: "dim" }, sync.exact)))); } function TableHeader({ field, label, sort, onSort, facet, selected, activeFacet, setActiveFacet, onToggleFacet, onClearFacet }) { const active = sort.field === field; const filterOpen = activeFacet === field; const selectedSet = new Set(selected || []); return h("div", { className: "th-controls" }, h("span", null, label), h("button", { type: "button", className: "th-icon" + (active ? " th-icon-on" : ""), onClick: () => onSort(field, active && sort.direction === "asc" ? "desc" : "asc"), title: "Sort " + label, }, h(Icon, { name: "sort", size: 14 })), facet && h("button", { type: "button", className: "th-icon" + (selectedSet.size ? " th-icon-on" : ""), onClick: () => setActiveFacet(filterOpen ? "" : field), title: "Group by " + label, }, h(Icon, { name: "filter", size: 13 })), facet && filterOpen && h("div", { className: "facet-menu", onClick: (e) => e.stopPropagation() }, h("div", { className: "facet-menu-head" }, h("span", null, label), h("button", { type: "button", onClick: () => onClearFacet(field) }, "Clear")), h("div", { className: "facet-list" }, (facet.items || []).map((item) => h("label", { className: "facet-option", key: item.value }, h("input", { type: "checkbox", checked: selectedSet.has(item.value), onChange: () => onToggleFacet(field, item.value), }), h("span", { className: "facet-value" }, item.value || "—"), h("span", { className: "facet-count" }, fmtNum(item.count || 0)))), !(facet.items || []).length && h("div", { className: "facet-empty" }, "No values")))); } function compareLoinc(a, b, sort, linkedTo) { const linkedCount = (term) => (linkedTo(term.code).length || term.linkedBiomarkers?.length || term.used || 0); const value = (term) => { if (sort.field === "code") return term.code; if (sort.field === "name") return term.name; if (sort.field === "system") return term.system; if (sort.field === "unit") return term.unit; if (sort.field === "class") return term.cls; if (sort.field === "status") return term.status; return linkedCount(term); }; const av = value(a); const bv = value(b); const result = typeof av === "number" || typeof bv === "number" ? Number(av || 0) - Number(bv || 0) : String(av || "").localeCompare(String(bv || ""), undefined, { numeric: true, sensitivity: "base" }); if (result !== 0) return sort.direction === "desc" ? -result : result; return String(a.code).localeCompare(String(b.code), undefined, { numeric: true }); } function LoincPanel({ biomarkers, api, loincCodes, loincPage, loincGroups, loincFacets, db, live, onSearch }) { const toast = useToast(); const [q, setQ] = useState(""); const [group, setGroup] = useState("all"); const [groupText, setGroupText] = useState("Any LOINC group"); const [sort, setSort] = useState({ field: "code", direction: "asc" }); const [facetFilters, setFacetFilters] = useState({ units: [], classes: [], systems: [], statuses: [], biomarkers: [] }); const [activeFacet, setActiveFacet] = useState(""); const [sel, setSel] = useState(null); const [adding, setAdding] = useState(false); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(50); const groupOptions = [{ value: "all", label: "Any LOINC group" }, ...(loincGroups || []).map((g) => ({ value: g.id, label: (g.category && g.category !== "Other" ? g.category + " · " : "") + g.name + (g.memberCount ? " (" + fmtNum(g.memberCount) + ")" : ""), }))]; const facetByField = { unit: { key: "units", items: loincFacets.units || [] }, class: { key: "classes", items: loincFacets.classes || [] }, biomarkers: { key: "biomarkers", items: loincFacets.biomarkers || [] }, system: { key: "systems", items: loincFacets.systems || [] }, status: { key: "statuses", items: loincFacets.statuses || [] }, }; const toggleFacet = (field, value) => { const key = facetByField[field].key; setFacetFilters((cur) => { const current = cur[key] || []; return { ...cur, [key]: current.includes(value) ? current.filter((item) => item !== value) : [...current, value] }; }); }; const clearFacet = (field) => { const key = facetByField[field].key; setFacetFilters((cur) => ({ ...cur, [key]: [] })); }; const applyGroupText = (text) => { setGroupText(text); const normalized = String(text || "").trim().toLowerCase(); const match = groupOptions.find((g) => g.label.toLowerCase() === normalized || g.value.toLowerCase() === normalized); setGroup(match ? match.value : "all"); }; const commitGroupText = (text) => { const normalized = String(text || "").trim().toLowerCase(); const match = groupOptions.find((g) => g.label.toLowerCase() === normalized || g.value.toLowerCase() === normalized); setGroup(match ? match.value : "all"); setGroupText(match ? match.label : "Any LOINC group"); }; const linkedTo = (code) => biomarkers.filter((b) => b.loinc.includes(code)); const clientFiltered = live.status === "live" ? loincCodes : loincCodes.filter((c) => (!facetFilters.classes.length || facetFilters.classes.includes(c.cls)) && (!facetFilters.systems.length || facetFilters.systems.includes(c.system)) && (!facetFilters.units.length || facetFilters.units.includes(c.unit)) && (!facetFilters.statuses.length || facetFilters.statuses.includes(c.status)) && (!facetFilters.biomarkers.length || linkedTo(c.code).some((b) => facetFilters.biomarkers.includes(b.name)) || (c.linkedBiomarkers || []).some((b) => facetFilters.biomarkers.includes(b))) && (!q || (c.code + " " + c.name + " " + c.component).toLowerCase().includes(q.toLowerCase()))); const rows = live.status === "live" ? clientFiltered : [...clientFiltered].sort((a, b) => compareLoinc(a, b, sort, linkedTo)); const usesServerPaging = live.status === "live" && !loincPage.loadedAll && !q; const totalRows = usesServerPaging ? loincPage.total : rows.length; const pageCount = Math.max(1, Math.ceil(totalRows / pageSize)); const safePage = Math.min(page, pageCount); const pageStart = totalRows ? (safePage - 1) * pageSize : 0; const pageRows = usesServerPaging ? rows : rows.slice(pageStart, pageStart + pageSize); const pageSizeOptions = [25, 50, 100, 250, 500].map((n) => ({ value: String(n), label: String(n) })); const setSortField = (field, direction) => setSort({ field, direction }); const queryFilters = { units: facetFilters.units, classes: facetFilters.classes, systems: facetFilters.systems, statuses: facetFilters.statuses, biomarkers: facetFilters.biomarkers }; useEffect(() => { setPage(1); }, [q, group, pageSize, sort.field, sort.direction, facetFilters.units, facetFilters.classes, facetFilters.systems, facetFilters.statuses, facetFilters.biomarkers]); useEffect(() => { if (page > pageCount) setPage(pageCount); }, [page, pageCount]); useEffect(() => { if (live.status !== "live" || !q) return; const timer = setTimeout(() => onSearch({ query: q, group_id: group, sort: sort.field, direction: sort.direction, ...queryFilters }), 250); return () => clearTimeout(timer); }, [q, group, sort.field, sort.direction, live.status, facetFilters.units, facetFilters.classes, facetFilters.systems, facetFilters.statuses, facetFilters.biomarkers]); useEffect(() => { if (live.status !== "live" || q) return; onSearch({ group_id: group, sort: sort.field, direction: sort.direction, limit: pageSize, offset: (safePage - 1) * pageSize, ...queryFilters }); }, [pageSize, safePage, q, group, sort.field, sort.direction, live.status, facetFilters.units, facetFilters.classes, facetFilters.systems, facetFilters.statuses, facetFilters.biomarkers]); const head = [ ["code", "Code"], ["name", "Long common name"], ["system", "System"], ["unit", "Units"], ["class", "Class"], ["biomarkers", "Biomarkers"], ["status", "Status"], ]; return h("div", { className: "view" }, h(DbInfoCard, { db, kind: "loinc" }), h(Card, { pad: false }, h("div", { className: "toolbar" }, h("div", { className: "search" }, h(Icon, { name: "search", size: 16 }), h("input", { className: "search-input", placeholder: "Search code, name or component…", value: q, onChange: (e) => setQ(e.target.value) })), h("div", { className: "autocomplete-filter" }, h("input", { className: "input", list: "loinc-group-options", value: groupText, placeholder: "Any LOINC group", onFocus: (e) => e.target.select(), onChange: (e) => applyGroupText(e.target.value), onBlur: (e) => commitGroupText(e.target.value), }), h("datalist", { id: "loinc-group-options" }, groupOptions.map((g) => h("option", { key: g.value, value: g.label })))), h(Select, { value: String(pageSize), onChange: (value) => setPageSize(Number(value)), options: pageSizeOptions }), h("span", { className: "toolbar-count" }, totalRows ? pageStart + 1 : 0, "–", Math.min(pageStart + pageSize, totalRows), " of ", totalRows, usesServerPaging ? " codes" : " matches"), h(Button, { variant: "default", size: "sm", icon: "plus", disabled: true, title: "Custom LOINC persistence is not enabled yet" }, "Custom code")), h("div", { className: "table-wrap flush" }, h("table", { className: "table" }, h("thead", null, h("tr", null, head.map(([field, label]) => h("th", { key: field }, h(TableHeader, { field, label, sort, onSort: setSortField, facet: facetByField[field], selected: facetByField[field] ? facetFilters[facetByField[field].key] : [], activeFacet, setActiveFacet, onToggleFacet: toggleFacet, onClearFacet: clearFacet, }))))), h("tbody", null, pageRows.map((c) => { const linked = linkedTo(c.code); return h("tr", { key: c.code, className: "row-click", onClick: () => setSel(c.code) }, h("td", null, h(Mono, { copy: true, className: "code-strong" }, c.code)), h("td", null, c.name), h("td", null, h(Mono, { className: "dim" }, c.system)), h("td", null, c.unit === "—" ? h("span", { className: "dim" }, "—") : h("span", { className: "tag" }, c.unit)), h("td", null, h("span", { className: "tag" + (c.cls === "CUSTOM" ? " tag-accent" : "") }, c.cls)), h("td", null, linked.length || c.linkedBiomarkers?.length ? h("span", { className: "tags" }, linked.length ? linked.map((b) => h("span", { className: "tag tag-accent", key: b.id }, b.name)) : c.linkedBiomarkers.map((name) => h("span", { className: "tag tag-accent", key: name }, name))) : h("span", { className: "dim" }, "— unlinked —")), h("td", null, dbStatusPill(c.status))); }), !rows.length && h("tr", null, h("td", { colSpan: 7 }, h(Empty, { title: "No matching codes", sub: "Adjust your search or class filter." })))))), h("div", { className: "pager" }, h(Button, { variant: "ghost", size: "sm", icon: "chevronL", disabled: safePage <= 1, onClick: () => setPage((p) => Math.max(1, p - 1)) }, "Previous"), h("span", { className: "pager-status" }, "Page ", safePage, " of ", pageCount), h(Button, { variant: "ghost", size: "sm", iconRight: "chevronR", disabled: safePage >= pageCount, onClick: () => setPage((p) => Math.min(pageCount, p + 1)) }, "Next"))), h(LoincDrawer, { code: sel, biomarkers, api, loincCodes, onClose: () => setSel(null) }), h(CustomLoincModal, { open: adding, existing: loincCodes, onClose: () => setAdding(false), onCreate: (c) => { api.addLoinc(c); setAdding(false); } })); } function LoincDrawer({ code, biomarkers, api, loincCodes, onClose }) { const [pick, setPick] = useState(""); const [detail, setDetail] = useState(null); useEffect(() => { let cancelled = false; if (!code) { setDetail(null); return; } TAPI.localizedLoinc(code, "nl-BE").then((data) => { if (!cancelled) setDetail(data); }).catch(() => { if (!cancelled) setDetail(null); }); return () => { cancelled = true; }; }, [code]); if (!code) return h(Drawer, { open: false, onClose }); const c = (loincCodes || []).find((x) => x.code === code) || R.loincByCode(code) || {}; const linked = biomarkers.filter((b) => b.loinc.includes(code)); const available = biomarkers.filter((b) => !b.loinc.includes(code)); const groups = detail ? (detail.groups || []).map((g) => ({ code: g.group_id, name: g.name || g.description || g.group_id, members: [] })) : R.loincGroupsFor(code); const intl = detail ? (detail.linguistic_variants || []).map((v) => ({ locale: v.locale, lang: v.language || v.locale, name: v.display_name || v.long_common_name || v.short_name || v.component })) : R.loincIntl(code); return h(Drawer, { open: true, onClose, width: 600, title: c.name || code, sub: h(React.Fragment, null, h(Mono, { copy: true, className: "code-strong" }, code), h("span", { className: "dim" }, " · ", c.cls, " · ", c.system)) }, h("div", { className: "refbox" }, h("div", null, h("p", { className: "refsec-title" }, "LOINC attributes"), h("div", { className: "meta-grid" }, h("div", { className: "meta-row" }, h("span", { className: "meta-k" }, "Component"), h("span", { className: "meta-v" }, c.component)), h("div", { className: "meta-row" }, h("span", { className: "meta-k" }, "Property"), h("span", { className: "meta-v mono" }, c.property)), h("div", { className: "meta-row" }, h("span", { className: "meta-k" }, "System"), h("span", { className: "meta-v mono" }, c.system)), h("div", { className: "meta-row" }, h("span", { className: "meta-k" }, "Example units"), h("span", { className: "meta-v" }, c.unit)))), h("div", { className: "refsec" }, h("div", { className: "refsec-head" }, h("p", { className: "refsec-title" }, "LOINC groups"), h("span", { className: "toolbar-count" }, groups.length, " group", groups.length === 1 ? "" : "s")), groups.length ? h("div", { className: "link-list" }, groups.map((g) => h("div", { className: "link-item", key: g.code }, h("div", { className: "link-item-main" }, h("span", { className: "link-t" }, g.name), h("span", { className: "link-s" }, h(Mono, { copy: true }, g.code), g.members.length ? " · " + g.members.length + " members" : "")), h("button", { className: "link-rm", title: "Group member browsing is not enabled yet", onClick: () => {} }, h(Icon, { name: "external", size: 14 }))))) : h("p", { className: "dim", style: { fontSize: "12.5px", margin: 0 } }, "This term is not a member of any parent group.")), h("div", { className: "refsec" }, h("div", { className: "refsec-head" }, h("p", { className: "refsec-title" }, "Linguistic variants"), h("span", { className: "toolbar-count" }, intl.length, " language", intl.length === 1 ? "" : "s")), intl.length ? h("div", { className: "intl-list" }, h("div", { className: "intl-item" }, h("span", { className: "intl-flag" }, "🇬🇧"), h("div", { className: "intl-main" }, h("span", { className: "intl-name" }, c.name), h("span", { className: "intl-lang" }, "English", h(Mono, { className: "dim" }, "en-US"), h("span", { className: "tag tag-accent" }, "primary")))), intl.map((t) => h("div", { className: "intl-item", key: t.locale }, h("span", { className: "intl-flag" }, LOCALE_FLAG[t.locale] || "🌐"), h("div", { className: "intl-main" }, h("span", { className: "intl-name" }, t.name), h("span", { className: "intl-lang" }, t.lang, h(Mono, { className: "dim" }, t.locale)))))) : h("p", { className: "dim", style: { fontSize: "12.5px", margin: 0 } }, "No linguistic variants loaded for this code in the current release.")), h("div", { className: "refsec" }, h("div", { className: "refsec-head" }, h("p", { className: "refsec-title" }, "Linked biomarkers"), h("span", { className: "toolbar-count" }, linked.length, " linked")), linked.length ? h("div", { className: "link-list" }, linked.map((b) => h("div", { className: "link-item", key: b.id }, h("div", { className: "link-item-main" }, h("span", { className: "link-t" }, b.name, h("span", { className: "tag" }, b.unit)), h("span", { className: "link-s" }, b.specimen, " · ", b.loinc.length, " LOINC code", b.loinc.length === 1 ? "" : "s")), h("button", { className: "link-rm", title: "Unlink biomarker", onClick: () => api.unlinkLoinc(b.id, code) }, h(Icon, { name: "x", size: 15 }))))) : h(Empty, { icon: "link", title: "No biomarker linked", sub: "Values for this code stay un-interpreted until linked to a biomarker." }), h("div", { className: "link-add" }, h(Select, { value: pick, onChange: setPick, options: [{ value: "", label: available.length ? "Link a biomarker…" : "All biomarkers linked" }, ...available.map((b) => ({ value: b.id, label: b.name }))] }), h(Button, { variant: "primary", icon: "link", disabled: !pick, onClick: () => { if (pick) { api.linkLoinc(pick, code); setPick(""); } } }, "Link"))))); } const LOCALE_FLAG = { "nl-NL": "🇳🇱", "fr-FR": "🇫🇷", "de-DE": "🇩🇪", "es-ES": "🇪🇸", "nl-BE": "🇧🇪", "fr-BE": "🇧🇪", "it-IT": "🇮🇹" }; /* ---- Biomarker dictionary ------------------------------------------------ */ function BiomarkerPanel({ biomarkers, api, loincCodes }) { const toast = useToast(); const [q, setQ] = useState(""); const [ph, setPh] = useState("all"); const [sel, setSel] = useState(null); const [creating, setCreating] = useState(false); const [exporting, setExporting] = useState(false); const [reviewing, setReviewing] = useState(false); const rows = biomarkers.filter((b) => (ph === "all" || b.phenotypes.includes(ph)) && (!q || (b.name + " " + b.specimen + " " + b.unit).toLowerCase().includes(q.toLowerCase()))); const selBm = biomarkers.find((b) => b.id === sel); const review = biomarkers.filter((b) => b.confidence < R.BM_REVIEW_THRESHOLD); const head = ["Biomarker", "Specimen", "Phenotypes", "Unit", "LOINC", "Status"]; return h("div", { className: "view" }, h("div", { className: "stat-grid stat-grid-4" }, h(Card, { pad: false }, h(Stat, { label: "Biomarkers", value: biomarkers.length, icon: "database" })), h(Card, { pad: false }, h(Stat, { label: "Active", value: biomarkers.filter((b) => b.status === "active").length, tone: "ok", icon: "check" })), h(Card, { pad: false }, h(Stat, { label: "Draft", value: biomarkers.filter((b) => b.status === "draft").length, icon: "clock" })), h("button", { type: "button", className: "stat-clickable" + (review.length ? " stat-flag" : ""), onClick: () => setReviewing(true), title: "Review low-confidence mappings" }, h(Card, { pad: false }, h(Stat, { label: "Requires review", value: review.length, tone: review.length ? "warn" : "ok", icon: review.length ? "alert" : "check" })))), h(Card, { pad: false, title: R.BIOMARKER_DB.name, subtitle: "Curated by " + R.BIOMARKER_DB.curatedBy + " · v" + R.BIOMARKER_DB.version, actions: h(React.Fragment, null, h(Select, { value: ph, onChange: setPh, options: [{ value: "all", label: "All phenotypes" }, ...R.FM_PHENOTYPES.map((p) => ({ value: p.id, label: p.label }))] }), h(Button, { variant: "default", size: "sm", icon: "download", onClick: () => setExporting(true) }, "Export"), h(Button, { variant: "primary", size: "sm", icon: "plus", onClick: () => setCreating(true) }, "New biomarker")) }, h("div", { className: "toolbar" }, h("div", { className: "search" }, h(Icon, { name: "search", size: 16 }), h("input", { className: "search-input", placeholder: "Search biomarker, specimen or unit…", value: q, onChange: (e) => setQ(e.target.value) })), h("span", { className: "toolbar-count" }, rows.length, " of ", biomarkers.length)), h("div", { className: "table-wrap flush" }, h("table", { className: "table" }, h("thead", null, h("tr", null, head.map((c) => h("th", { key: c }, c)))), h("tbody", null, rows.map((b) => h("tr", { key: b.id, className: "row-click", onClick: () => setSel(b.id) }, h("td", null, h("span", { style: { fontWeight: 600 } }, b.name)), h("td", { className: "dim" }, b.specimen), h("td", null, h("span", { className: "tags" }, b.phenotypes.slice(0, 2).map((p) => h("span", { className: "tag", key: p }, R.phenoLabel(p))), b.phenotypes.length > 2 ? h("span", { className: "tag" }, "+" + (b.phenotypes.length - 2)) : null, !b.phenotypes.length ? h("span", { className: "dim" }, "—") : null)), h("td", null, h("span", { className: "tag" }, b.unit)), h("td", null, b.loinc.length ? h("span", { className: "mono" }, b.loinc.length) : h("span", { className: "warn-ink", style: { fontWeight: 600 } }, "0")), h("td", null, b.status === "active" ? h(Pill, { tone: "ok", soft: true }, "active") : h(Pill, { tone: "neutral", soft: true }, "draft")))), !rows.length && h("tr", null, h("td", { colSpan: 6 }, h(Empty, { icon: "database", title: "No biomarkers", sub: "Adjust search or phenotype filter." }))))))), h(BiomarkerDrawer, { bm: selBm, biomarkers, api, loincCodes, onClose: () => setSel(null) }), h(NewBiomarkerModal, { open: creating, existing: biomarkers, onClose: () => setCreating(false), onCreate: async (bm) => { const created = await api.addBiomarker(bm); if (created) { setCreating(false); setSel(created.id); } } }), h(ExportBiomarkersModal, { open: exporting, biomarkers, onClose: () => setExporting(false) }), h(ReviewQueueModal, { open: reviewing, biomarkers, loincCodes, api, onClose: () => setReviewing(false), onOpenBm: (id) => { setReviewing(false); setSel(id); } })); } const RANGE_DEMOS = [ { key: "male", label: "Male", color: "var(--info)", group: 0 }, { key: "female", label: "Female", color: "#d6589a", group: 0 }, { key: "femaleMenopausal", label: "Menopausal", color: "var(--purple)", group: 1 }, { key: "femalePregnant", label: "Pregnant", color: "var(--accent)", group: 1 }, ]; function BiomarkerDrawer({ bm, biomarkers, api, loincCodes, onClose }) { const toast = useToast(); const [pick, setPick] = useState(""); if (!bm) return h(Drawer, { open: false, onClose }); const codeList = loincCodes || R.LOINC_CODES; const linkedCodes = bm.loinc.map((code) => codeList.find((c) => c.code === code) || { code, name: "(not in loaded release)", system: "—" }); const availCodes = codeList.filter((c) => !bm.loinc.includes(c.code) && c.status !== "deprecated"); const rangeRow = (d) => { const r = bm.ranges[d.key] || { low: null, high: null }; const numCell = (bound) => h("td", { className: "rng-num" }, h("span", { className: "rng-cell" }, h("input", { type: "number", step: "any", className: "input rng-input", value: r[bound] == null ? "" : r[bound], placeholder: "—", onChange: (e) => api.setRange(bm.id, d.key, bound, e.target.value) }))); return h("tr", { key: d.key }, h("td", null, h("span", { className: "rng-demo" }, h("span", { className: "dot-sex", style: { background: d.color } }), d.label)), numCell("low"), numCell("high"), h("td", { className: "rng-unit" }, bm.unit)); }; return h(Drawer, { open: true, onClose, width: 720, title: bm.name, sub: h(React.Fragment, null, h(Pill, { tone: bm.status === "active" ? "ok" : "neutral", soft: true }, bm.status), h("span", { className: "dim" }, bm.specimen, " · ", bm.loinc.length, " LOINC · ", bm.phenotypes.length, " phenotype", bm.phenotypes.length === 1 ? "" : "s")), footer: h("div", { className: "drawer-actions" }, h(Button, { variant: "primary", icon: "check", onClick: () => { toast("Biomarker saved", "ok"); onClose(); } }, "Save changes"), h(Button, { variant: "default", icon: "external", disabled: true, title: "Result drill-down will use the document/result search API once available" }, "View results")) }, h("div", { className: "refbox" }, h("div", { className: "td-stats", style: { border: "1px solid var(--line)", borderRadius: "var(--r)", overflow: "hidden" } }, h("div", { className: "td-stat" }, h("span", { className: "td-num " + (bm.loinc.length ? "" : "warn-ink") }, bm.loinc.length), h("span", { className: "dim" }, "LOINC codes")), h("div", { className: "td-stat" }, h("span", { className: "td-num" }, bm.phenotypes.length), h("span", { className: "dim" }, "phenotypes")), h("div", { className: "td-stat" }, h("span", { className: "td-num mono" }, bm.unit), h("span", { className: "dim" }, "unit (UCUM)")), h("div", { className: "td-stat" }, h("span", { className: "td-num" }, fmtNum(bm.used)), h("span", { className: "dim" }, "results"))), !bm.loinc.length && h("div", { className: "ref-banner tone-warn" }, h(Icon, { name: "alert", size: 16 }), h("div", null, h("strong", null, "Not linked to any LOINC code."), " Extracted values can't be routed to this biomarker until at least one LOINC code is linked below.")), // specimen + unit h("div", { className: "form-grid", style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "14px" } }, h(Field, { label: "Specimen type" }, h(Select, { value: bm.specimen, onChange: (v) => api.patch(bm.id, { specimen: v }), options: R.SPECIMENS })), h(Field, { label: "Unit (UCUM)", hint: "Canonical unit results are normalized to" }, h(Select, { value: bm.unit, onChange: (v) => api.patch(bm.id, { unit: v }), options: R.UCUM_UNITS.map((u) => ({ value: u.code, label: u.code + " — " + u.name })) }))), // phenotypes h("div", { className: "refsec" }, h("p", { className: "refsec-title" }, "Functional Medicine phenotypes"), h("div", { className: "chip-set" }, R.FM_PHENOTYPES.map((p) => { const on = bm.phenotypes.includes(p.id); return h("button", { key: p.id, type: "button", className: "chip-tog" + (on ? " on" : ""), onClick: () => api.togglePhenotype(bm.id, p.id) }, h(Icon, { name: on ? "check" : "plus", size: 13 }), p.label); }))), // linked loinc h("div", { className: "refsec" }, h("div", { className: "refsec-head" }, h("p", { className: "refsec-title" }, "Linked LOINC codes"), h("span", { className: "toolbar-count" }, bm.loinc.length, " linked")), bm.loinc.length ? h("div", { className: "link-list" }, linkedCodes.map((c) => h("div", { className: "link-item", key: c.code }, h("div", { className: "link-item-main" }, h("span", { className: "link-t" }, h(Mono, { copy: true, className: "code-strong" }, c.code)), h("span", { className: "link-s" }, c.name)), h("button", { className: "link-rm", title: "Unlink LOINC code", onClick: () => api.unlinkLoinc(bm.id, c.code) }, h(Icon, { name: "x", size: 15 }))))) : h(Empty, { icon: "link", title: "No LOINC codes linked" }), h("div", { className: "link-add" }, h(Select, { value: pick, onChange: setPick, options: [{ value: "", label: "Link a LOINC code…" }, ...availCodes.map((c) => ({ value: c.code, label: c.code + " — " + c.name }))] }), h(Button, { variant: "primary", icon: "link", disabled: !pick, onClick: () => { if (pick) { api.linkLoinc(bm.id, pick); setPick(""); } } }, "Link"))), // reference ranges h("div", { className: "refsec" }, h("p", { className: "refsec-title" }, "Reference ranges"), h("table", { className: "rng-table" }, h("thead", null, h("tr", null, h("th", null, "Population"), h("th", { className: "rng-num" }, "Min"), h("th", { className: "rng-num" }, "Max"), h("th", null, "Unit"))), h("tbody", null, RANGE_DEMOS.filter((d) => d.group === 0).map(rangeRow), h("tr", { className: "rng-sub" }, h("td", { colSpan: 4, className: "rng-grouplbl" }, "Female-specific")), RANGE_DEMOS.filter((d) => d.group === 1).map(rangeRow)))))); } function UcumPanel({ db, units, live, onSearch }) { const toast = useToast(); const [q, setQ] = useState(""); const rows = units.filter((u) => !q || (u.code + " " + u.name + " " + u.dim).toLowerCase().includes(q.toLowerCase())); useEffect(() => { if (live.status !== "live") return; const timer = setTimeout(() => onSearch({ query: q }), 250); return () => clearTimeout(timer); }, [q, live.status]); const head = ["Code", "Unit name", "Dimension", "Canonical", "Conversion factor", "In use", "Status"]; return h("div", { className: "view" }, h(DbInfoCard, { db, kind: "ucum" }), h(Card, { pad: false }, h("div", { className: "toolbar" }, h("div", { className: "search" }, h(Icon, { name: "search", size: 16 }), h("input", { className: "search-input", placeholder: "Search unit code, name or dimension…", value: q, onChange: (e) => setQ(e.target.value) })), h("span", { className: "toolbar-count" }, rows.length, " of ", units.length, " loaded"), h(Button, { variant: "default", size: "sm", icon: "plus", disabled: true, title: "Custom UCUM persistence is not enabled yet" }, "Custom unit")), h("div", { className: "table-wrap flush" }, h("table", { className: "table" }, h("thead", null, h("tr", null, head.map((c) => h("th", { key: c }, c)))), h("tbody", null, rows.map((u) => h("tr", { key: u.code }, h("td", null, h(Mono, { copy: true, className: "code-strong" }, u.code)), h("td", null, u.name), h("td", { className: "dim" }, u.dim), h("td", null, h("span", { className: "tag" }, u.canonical)), h("td", { className: "mono dim" }, u.factor), h("td", { className: "mono" }, u.used ? fmtNum(u.used) : h("span", { className: "dim" }, "0")), h("td", null, dbStatusPill(u.status)))), !rows.length && h("tr", null, h("td", { colSpan: 7 }, h(Empty, { title: "No matching units", sub: "Adjust your search." })))))))); } /* ======================================================================== */ /* LABORATORIES & TEST TYPES */ /* ======================================================================== */ function LaboratoriesView() { const [tab, setTab] = useState("labs"); const tabs = [ { key: "labs", label: "Laboratories", count: R.LABS.length }, { key: "panels", label: "Test types", count: R.TEST_PANELS.length }, ]; return h("div", { className: "view" }, h(PageHead, { title: "Laboratories & test types", sub: "Document authors the pipeline recognizes, and the panels their results map to — the basis of extraction confidence" }), h(Tabs, { tabs, active: tab, onChange: setTab }), h("div", { className: "tabbody" }, tab === "labs" ? h(LabsPanel, null) : h(PanelsPanel, null))); } function confTone(status) { return R.LAB_STATUS[status].tone; } function LabsPanel() { const [labs, setLabs] = useState(R.LABS); const [sel, setSel] = useState(null); const [filter, setFilter] = useState("all"); const toast = useToast(); const counts = { supported: labs.filter((l) => l.status === "supported").length, partial: labs.filter((l) => l.status === "partial").length, unsupported: labs.filter((l) => l.status === "unsupported").length, }; const meanConf = (labs.reduce((a, l) => a + l.confidence, 0) / labs.length).toFixed(1); const rows = labs.filter((l) => filter === "all" || l.status === filter); const selLab = labs.find((l) => l.id === sel); const promote = (id) => { setLabs((x) => x.map((l) => l.id === id ? { ...l, status: "supported", confidence: Math.max(l.confidence, 95.0), profile: l.profile || (l.id.replace("lab_", "") + "-v1"), templates: Math.max(1, l.templates), unrecognized: 0 } : l)); toast("Extraction profile created · author promoted to supported", "ok"); }; const head = ["Laboratory", "Jurisdiction", "Formats", "Profile", "Templates", "Extraction confidence", "Docs", "Status"]; return h("div", { className: "view" }, h("div", { className: "ref-banner" }, h(Icon, { name: "beaker", size: 16, className: "dim" }), h("div", null, "When a document's author has a tuned profile and a recognized template, observations are extracted ", h("strong", null, "structurally"), " with high confidence. ", "Unrecognized authors fall back to generic LLM extraction — lower confidence and a higher share of documents routed to manual review.")), h("div", { className: "stat-grid stat-grid-4" }, h(Card, { pad: false }, h(Stat, { label: "Supported", value: counts.supported, tone: "ok", icon: "check" })), h(Card, { pad: false }, h(Stat, { label: "Partial", value: counts.partial, tone: "warn", icon: "alert" })), h(Card, { pad: false }, h(Stat, { label: "Unsupported", value: counts.unsupported, tone: "bad", icon: "x" })), h(Card, { pad: false }, h(Stat, { label: "Mean confidence", value: meanConf, unit: "%", icon: "activity" }))), h(Card, { pad: false, title: "Recognized laboratories", subtitle: "Document authors and their extraction profiles", actions: h(React.Fragment, null, h(Select, { value: filter, onChange: setFilter, options: [{ value: "all", label: "All statuses" }, { value: "supported", label: "Supported" }, { value: "partial", label: "Partial" }, { value: "unsupported", label: "Unsupported" }] }), h(Button, { variant: "primary", size: "sm", icon: "plus", onClick: () => toast("Add laboratory (mock)") }, "Add laboratory")) }, h("div", { className: "table-wrap flush" }, h("table", { className: "table" }, h("thead", null, h("tr", null, head.map((c) => h("th", { key: c }, c)))), h("tbody", null, rows.map((l) => { const m = R.LAB_STATUS[l.status]; return h("tr", { key: l.id, className: "row-click", onClick: () => setSel(l.id) }, h("td", null, h("span", { className: "ref-cell" }, h("span", { className: "ref-flag" }, l.flag), h("span", null, l.name, h("div", { className: "ref-sub" }, l.country)))), h("td", null, h(Mono, { className: "dim" }, l.jurisdiction)), h("td", null, h("span", { className: "tags" }, l.formats.map((f) => h("span", { className: "tag", key: f }, f)))), h("td", null, l.profile ? h(Mono, null, l.profile) : h("span", { className: "dim" }, "— none —")), h("td", { className: "mono" }, l.templates, l.unrecognized ? h("span", { className: "warn-ink" }, " +" + l.unrecognized + " new") : null), h("td", null, h("div", { className: "conf-cell" }, h(Bar, { value: l.confidence, max: 100, tone: confTone(l.status), height: 6 }), h("span", { className: "conf-pct" }, l.confidence.toFixed(1) + "%"))), h("td", { className: "mono" }, fmtNum(l.docs)), h("td", null, h(Pill, { tone: m.tone, soft: l.status !== "supported" }, m.label))); }), !rows.length && h("tr", null, h("td", { colSpan: 8 }, h(Empty, { icon: "beaker", title: "No laboratories", sub: "No authors match this filter." }))))))), h(LabDrawer, { lab: selLab, onClose: () => setSel(null), onPromote: promote })); } function LabDrawer({ lab, onClose, onPromote }) { const toast = useToast(); if (!lab) return h(Drawer, { open: false, onClose }); const m = R.LAB_STATUS[lab.status]; return h(Drawer, { open: true, onClose, width: 640, title: lab.name, sub: h(React.Fragment, null, h(Pill, { tone: m.tone, soft: lab.status !== "supported" }, m.label), h("span", { className: "dim" }, lab.flag, " ", lab.country, " · ", h(Mono, null, lab.jurisdiction))), footer: h("div", { className: "drawer-actions" }, lab.status === "unsupported" && h(Button, { variant: "primary", icon: "sparkles", onClick: () => { onPromote(lab.id); onClose(); } }, "Create extraction profile"), lab.status === "partial" && h(Button, { variant: "primary", icon: "upload", onClick: () => toast("Upload sample to extend profile (mock)") }, "Extend profile"), h(Button, { variant: "default", icon: "documents", onClick: () => toast("Open author documents (mock)") }, "View documents")) }, h("div", { className: "refbox" }, h("div", { className: "td-stats", style: { border: "1px solid var(--line)", borderRadius: "var(--r)", overflow: "hidden" } }, h("div", { className: "td-stat" }, h("span", { className: "td-num " + (m.tone === "ok" ? "ok-ink" : m.tone === "bad" ? "bad-ink" : "") }, lab.confidence.toFixed(1) + "%"), h("span", { className: "dim" }, "confidence")), h("div", { className: "td-stat" }, h("span", { className: "td-num" }, fmtNum(lab.docs)), h("span", { className: "dim" }, "documents")), h("div", { className: "td-stat" }, h("span", { className: "td-num" }, lab.templates), h("span", { className: "dim" }, "templates")), h("div", { className: "td-stat" }, h("span", { className: "td-num " + (lab.reviewRate > 10 ? "bad-ink" : "") }, lab.reviewRate + "%"), h("span", { className: "dim" }, "manual review"))), lab.status === "unsupported" && h("div", { className: "ref-banner tone-bad" }, h(Icon, { name: "alert", size: 16 }), h("div", null, h("strong", null, "No extraction profile."), " Documents from this author use generic LLM extraction (~", lab.confidence.toFixed(0), "% confidence). Entity extraction may be incomplete, so ~", lab.reviewRate, "% of these documents are routed to manual review. Create a profile from sample documents to recognize their layout and lift confidence.")), lab.status === "partial" && h("div", { className: "ref-banner tone-warn" }, h(Icon, { name: "alert", size: 16 }), h("div", null, h("strong", null, lab.unrecognized + " new layout" + (lab.unrecognized === 1 ? "" : "s") + " unrecognized."), " Values from these templates fall back to generic extraction and are flagged for review. Add a sample to extend ", h(Mono, null, lab.profile), ".")), lab.status === "supported" && h("div", { className: "ref-banner tone-ok" }, h(Icon, { name: "check", size: 16 }), h("div", null, h("strong", null, "Structured extraction active."), " All known templates recognized — observations map directly to LOINC/UCUM.")), h("div", null, h("p", { className: "refsec-title" }, "Extraction profile"), h("div", { className: "meta-grid" }, h("div", { className: "meta-row" }, h("span", { className: "meta-k" }, "Profile"), h("span", { className: "meta-v mono" }, lab.profile || "none")), h("div", { className: "meta-row" }, h("span", { className: "meta-k" }, "Formats"), h("span", { className: "meta-v" }, lab.formats.join(", "))), h("div", { className: "meta-row" }, h("span", { className: "meta-k" }, "Recognized templates"), h("span", { className: "meta-v mono" }, lab.templates)), h("div", { className: "meta-row" }, h("span", { className: "meta-k" }, "Last document"), h("span", { className: "meta-v" }, fmtAgo(R.ago(lab.lastMin * R.MIN)))))), h("div", null, h("p", { className: "refsec-title" }, "Test panels covered"), lab.panels.length ? h("span", { className: "tags" }, lab.panels.map((k) => h("span", { className: "tag tag-accent", key: k }, R.panelName(k)))) : h("span", { className: "dim" }, "No panels mapped — extraction relies on generic field detection.")))); } function PanelsPanel() { const [sel, setSel] = useState(null); const toast = useToast(); const selPanel = R.TEST_PANELS.find((p) => p.key === sel); const head = ["Panel", "Category", "LOINC codes", "Key analytes", "Supported labs", "Status"]; return h("div", { className: "view" }, h("div", { className: "ref-banner" }, h(Icon, { name: "hash", size: 16, className: "dim" }), h("div", null, "Each panel pins its analytes to LOINC codes and UCUM units, with reference ranges. Extraction uses these maps to recognize and normalize results regardless of how a lab labels them.")), h(Card, { pad: false, title: "Supported test types", subtitle: R.TEST_PANELS.length + " panels mapped to LOINC / UCUM", actions: h(Button, { variant: "primary", size: "sm", icon: "plus", onClick: () => toast("New test panel (mock)") }, "New panel") }, h("div", { className: "table-wrap flush" }, h("table", { className: "table" }, h("thead", null, h("tr", null, head.map((c) => h("th", { key: c }, c)))), h("tbody", null, R.TEST_PANELS.map((p) => h("tr", { key: p.key, className: "row-click", onClick: () => setSel(p.key) }, h("td", null, h("span", { style: { fontWeight: 600 } }, p.name)), h("td", { className: "dim" }, p.category), h("td", { className: "mono" }, p.analytes.length), h("td", null, h("span", { className: "tags" }, p.analytes.slice(0, 3).map((a) => h("span", { className: "tag", key: a.loinc }, a.analyte)), p.analytes.length > 3 ? h("span", { className: "tag" }, "+" + (p.analytes.length - 3)) : null)), h("td", { className: "mono" }, p.labs), h("td", null, p.status === "active" ? h(Pill, { tone: "ok", soft: true }, "active") : h(Pill, { tone: "neutral", soft: true }, "draft")))))))), h(PanelDrawer, { panel: selPanel, onClose: () => setSel(null) })); } function PanelDrawer({ panel, onClose }) { const toast = useToast(); if (!panel) return h(Drawer, { open: false, onClose }); return h(Drawer, { open: true, onClose, width: 640, title: panel.name, sub: h(React.Fragment, null, h(Pill, { tone: panel.status === "active" ? "ok" : "neutral", soft: true }, panel.status), h("span", { className: "dim" }, panel.category, " · ", panel.analytes.length, " analytes · ", panel.labs, " labs")), footer: h("div", { className: "drawer-actions" }, h(Button, { variant: "primary", icon: "plus", onClick: () => toast("Add analyte (mock)") }, "Add analyte"), h(Button, { variant: "default", icon: "external", onClick: () => toast("Edit panel (mock)") }, "Edit panel")) }, h("div", { className: "refbox" }, h("p", { className: "refsec-title" }, "Analyte → LOINC → UCUM mapping"), h("div", { className: "table-wrap" }, h("table", { className: "table table-tight" }, h("thead", null, h("tr", null, ["Analyte", "LOINC", "Unit (UCUM)", "Reference range"].map((c) => h("th", { key: c }, c)))), h("tbody", null, panel.analytes.map((a) => h("tr", { key: a.loinc }, h("td", null, a.analyte), h("td", null, h(Mono, { copy: true, className: "code-strong" }, a.loinc)), h("td", null, h("span", { className: "tag" }, a.unit)), h("td", { className: "mono dim" }, fmtRef(a))))))))); } /* ======================================================================== */ /* MODALS — New biomarker · Custom LOINC code · Export */ /* ======================================================================== */ const LOINC_PROPERTIES = ["MCnc", "SCnc", "NCnc", "ACnc", "CCnc", "MFr", "VFr", "Type", "RelTime", "Nom"]; const LOINC_SYSTEMS = ["Ser/Plas", "Bld", "Plas", "Urine", "Saliva", "Stool", "CSF", "PPP"]; function NewBiomarkerModal({ open, existing, onClose, onCreate }) { const [name, setName] = useState(""); const [specimen, setSpecimen] = useState(R.SPECIMENS[0]); const [unit, setUnit] = useState("mg/dL"); const [status, setStatus] = useState("draft"); const [phenotypes, setPhenotypes] = useState([]); useEffect(() => { if (open) { setName(""); setSpecimen(R.SPECIMENS[0]); setUnit("mg/dL"); setStatus("draft"); setPhenotypes([]); } }, [open]); const dup = existing.some((b) => b.name.trim().toLowerCase() === name.trim().toLowerCase()); const valid = name.trim() && !dup; const submit = () => { if (!valid) return; const id = "bm_" + name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").slice(0, 24) + "_" + Math.random().toString(36).slice(2, 5); const blank = { low: null, high: null }; onCreate({ id, name: name.trim(), specimen, unit, status, phenotypes: [...phenotypes], loinc: [], used: 0, ranges: { male: { ...blank }, female: { ...blank }, femaleMenopausal: { ...blank }, femalePregnant: { ...blank } } }); }; const toggle = (p) => setPhenotypes((xs) => xs.includes(p) ? xs.filter((x) => x !== p) : [...xs, p]); return h(Modal, { open, onClose, title: "New biomarker", width: 560, footer: h(React.Fragment, null, h(Button, { variant: "ghost", onClick: onClose }, "Cancel"), h(Button, { variant: "primary", icon: "plus", disabled: !valid, onClick: submit }, "Create biomarker")) }, h("div", { className: "mform" }, h(Field, { label: "Biomarker name", hint: dup ? "A biomarker with this name already exists." : "Clinical concept, e.g. “Vitamin B12, serum”" }, h("input", { className: "input", autoFocus: true, value: name, placeholder: "e.g. Ferritin", onChange: (e) => setName(e.target.value) })), h("div", { className: "mform-2" }, h(Field, { label: "Specimen type" }, h(Select, { value: specimen, onChange: setSpecimen, options: R.SPECIMENS })), h(Field, { label: "Unit (UCUM)" }, h(Select, { value: unit, onChange: setUnit, options: R.UCUM_UNITS.map((u) => ({ value: u.code, label: u.code + " — " + u.name })) }))), h(Field, { label: "Status" }, h(Select, { value: status, onChange: setStatus, options: [{ value: "draft", label: "Draft — not yet used for interpretation" }, { value: "active", label: "Active — used in results" }] })), h(Field, { label: "Functional Medicine phenotypes" }, h("div", { className: "chip-set" }, R.FM_PHENOTYPES.map((p) => { const on = phenotypes.includes(p.id); return h("button", { key: p.id, type: "button", className: "chip-tog" + (on ? " on" : ""), onClick: () => toggle(p.id) }, h(Icon, { name: on ? "check" : "plus", size: 13 }), p.label); }))), h("p", { className: "mform-note" }, h(Icon, { name: "sparkles", size: 14 }), "Link LOINC codes and set reference ranges in the next step — the biomarker opens for editing right after you create it."))); } function CustomLoincModal({ open, existing, onClose, onCreate }) { const [code, setCode] = useState("ENO-"); const [name, setName] = useState(""); const [component, setComponent] = useState(""); const [property, setProperty] = useState("MCnc"); const [system, setSystem] = useState("Ser/Plas"); const [unit, setUnit] = useState("mg/dL"); useEffect(() => { if (open) { setCode("ENO-"); setName(""); setComponent(""); setProperty("MCnc"); setSystem("Ser/Plas"); setUnit("mg/dL"); } }, [open]); const dup = existing.some((c) => c.code.trim().toLowerCase() === code.trim().toLowerCase()); const valid = code.trim() && code.trim() !== "ENO-" && name.trim() && !dup; const submit = () => { if (!valid) return; onCreate({ code: code.trim(), name: name.trim(), component: component.trim() || name.trim(), property, system, scale: "Qn", unit, cls: "CUSTOM", used: 0, status: "custom" }); }; return h(Modal, { open, onClose, title: "Add custom code", width: 560, footer: h(React.Fragment, null, h(Button, { variant: "ghost", onClick: onClose }, "Cancel"), h(Button, { variant: "primary", icon: "plus", disabled: !valid, onClick: submit }, "Add code")) }, h("div", { className: "mform" }, h("p", { className: "mform-note" }, h(Icon, { name: "alert", size: 14 }), "Custom codes are local to this tenant and won't sync with the Regenstrief release. Use an ", h("strong", null, "ENO-"), " prefix so they're easy to spot."), h("div", { className: "mform-2" }, h(Field, { label: "Code", hint: dup ? "This code already exists." : "Local identifier" }, h("input", { className: "input mono", value: code, onChange: (e) => setCode(e.target.value) })), h(Field, { label: "Class" }, h("input", { className: "input", value: "CUSTOM", disabled: true }))), h(Field, { label: "Long common name" }, h("input", { className: "input", value: name, placeholder: "e.g. Omega-3 index [Mass fraction] in Blood", onChange: (e) => setName(e.target.value) })), h(Field, { label: "Component", hint: "What is measured (defaults to the name)" }, h("input", { className: "input", value: component, placeholder: "e.g. Omega-3 index", onChange: (e) => setComponent(e.target.value) })), h("div", { className: "mform-2" }, h(Field, { label: "Property" }, h(Select, { value: property, onChange: setProperty, options: LOINC_PROPERTIES })), h(Field, { label: "System (specimen)" }, h(Select, { value: system, onChange: setSystem, options: LOINC_SYSTEMS }))), h(Field, { label: "Example units (UCUM)" }, h(Select, { value: unit, onChange: setUnit, options: R.UCUM_UNITS.map((u) => ({ value: u.code, label: u.code + " — " + u.name })) })))); } function bmRangeStr(r) { if (r.low != null && r.high != null) return r.low + "–" + r.high; if (r.high != null) return "<" + r.high; if (r.low != null) return ">" + r.low; return ""; } const EXPORT_COLS = ["Biomarker", "Specimen", "Unit (UCUM)", "Phenotypes", "LOINC codes", "Status", "Male min", "Male max", "Female min", "Female max", "Menopausal min", "Menopausal max", "Pregnant min", "Pregnant max"]; function bmRow(b) { const r = b.ranges, n = (v) => (v == null ? "" : v); return [b.name, b.specimen, b.unit, b.phenotypes.map(R.phenoLabel).join("; "), b.loinc.join("; "), b.status, n(r.male.low), n(r.male.high), n(r.female.low), n(r.female.high), n(r.femaleMenopausal.low), n(r.femaleMenopausal.high), n(r.femalePregnant.low), n(r.femalePregnant.high)]; } function downloadBlob(filename, mime, content) { const blob = new Blob([content], { type: mime }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 1500); } function exportCSV(biomarkers) { const esc = (v) => { const s = String(v); return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; }; const lines = [EXPORT_COLS.map(esc).join(","), ...biomarkers.map((b) => bmRow(b).map(esc).join(","))]; downloadBlob("eno-biomarkers.csv", "text/csv;charset=utf-8", "\uFEFF" + lines.join("\r\n")); } function exportXLS(biomarkers) { const esc = (v) => String(v).replace(/&/g, "&").replace(//g, ">"); const th = EXPORT_COLS.map((c) => "