diff --git a/backend/src/main/java/com/workwell/measure/MeasureService.java b/backend/src/main/java/com/workwell/measure/MeasureService.java index 23a2a8b..4992d8d 100644 --- a/backend/src/main/java/com/workwell/measure/MeasureService.java +++ b/backend/src/main/java/com/workwell/measure/MeasureService.java @@ -793,6 +793,36 @@ private UUID latestMeasureVersionId(UUID measureId) { } } + public List listValueSetsByVersionId(UUID measureVersionId) { + String sql = """ + SELECT vs.id, vs.oid, vs.name, vs.version, vs.last_resolved_at, + COALESCE(jsonb_array_length(vs.codes_json), 0) AS code_count + FROM measure_value_set_links l + JOIN value_sets vs ON vs.id = l.value_set_id + WHERE l.measure_version_id = ? + ORDER BY vs.name ASC, vs.oid ASC + """; + return jdbcTemplate.query(sql, (rs, rowNum) -> { + String oid = rs.getString("oid"); + int codeCount = rs.getInt("code_count"); + boolean demoResolved = oid != null && oid.startsWith("urn:workwell:vs:"); + String resolvabilityStatus = demoResolved || codeCount > 0 ? "RESOLVED" : "UNRESOLVED"; + String resolvabilityLabel = demoResolved ? "Resolved (demo)" : (codeCount > 0 ? "Resolved" : "Unresolved"); + String resolvabilityNote = demoResolved || codeCount > 0 ? "" : "Codes not yet loaded."; + return new ValueSetRef( + (UUID) rs.getObject("id"), + oid, + rs.getString("name"), + rs.getString("version"), + toInstant(rs.getObject("last_resolved_at")), + resolvabilityStatus, + resolvabilityLabel, + resolvabilityNote, + codeCount + ); + }, measureVersionId); + } + private List listAttachedValueSets(UUID measureId) { UUID measureVersionId = latestMeasureVersionId(measureId); String sql = """ diff --git a/backend/src/main/java/com/workwell/web/MeasureController.java b/backend/src/main/java/com/workwell/web/MeasureController.java index 6e62d35..3659fef 100644 --- a/backend/src/main/java/com/workwell/web/MeasureController.java +++ b/backend/src/main/java/com/workwell/web/MeasureController.java @@ -101,6 +101,11 @@ public List listValueSets() { return measureService.listValueSets(); } + @GetMapping("/api/measures/versions/{measureVersionId}/value-sets") + public List listValueSetsByVersion(@PathVariable UUID measureVersionId) { + return measureService.listValueSetsByVersionId(measureVersionId); + } + @GetMapping("/api/osha-references") public List listOshaReferences() { return measureService.listOshaReferences(); diff --git a/frontend/app/(dashboard)/cases/[id]/page.tsx b/frontend/app/(dashboard)/cases/[id]/page.tsx index aeb98f8..f1008c8 100644 --- a/frontend/app/(dashboard)/cases/[id]/page.tsx +++ b/frontend/app/(dashboard)/cases/[id]/page.tsx @@ -99,6 +99,16 @@ type EvidenceAttachment = { uploadedAt: string; }; +type LinkedValueSet = { + id: string; + oid: string; + name: string; + version: string | null; + resolvabilityStatus: string; + resolvabilityLabel: string; + codeCount: number; +}; + export default function CaseDetailPage() { const params = useParams<{ id: string }>(); const caseId = params.id; @@ -128,6 +138,7 @@ export default function CaseDetailPage() { const [appointmentNotes, setAppointmentNotes] = useState(""); const [scheduling, setScheduling] = useState(false); const [evidence, setEvidence] = useState([]); + const [linkedValueSets, setLinkedValueSets] = useState([]); const [evidenceFile, setEvidenceFile] = useState(null); const [evidenceDescription, setEvidenceDescription] = useState(""); const [uploadingEvidence, setUploadingEvidence] = useState(false); @@ -139,6 +150,12 @@ export default function CaseDetailPage() { const data = await api.get(`/api/cases/${caseId}`); setCaseDetail(data); setAssigneeInput(data.assignee ?? ""); + try { + const vsets = await api.get(`/api/measures/versions/${data.measureVersionId}/value-sets`); + setLinkedValueSets(vsets); + } catch { + setLinkedValueSets([]); + } } catch (err) { setError(err instanceof Error ? err.message : "Unknown error"); } finally { @@ -761,22 +778,72 @@ export default function CaseDetailPage() {

Why Flagged

-

Structured evidence trail

-
- {(caseDetail.evidenceJson.expressionResults ?? []).map((row, index) => ( -
-
-

{String(row.define ?? "define")}

-

Evidence item {index + 1}

+

Code evidence explorer

+
+ {(caseDetail.evidenceJson.expressionResults ?? []).map((row, index) => { + const defineStr = String(row.define ?? "define"); + const resultStr = String(row.result ?? ""); + const isOutcomeStatus = defineStr === "Outcome Status"; + const isTrue = resultStr.toLowerCase() === "true"; + const isFalse = resultStr.toLowerCase() === "false"; + const isNull = resultStr === "null" || resultStr === ""; + const isDate = /^\d{4}-\d{2}-\d{2}/.test(resultStr); + const isNumber = !isNaN(Number(resultStr)) && resultStr !== "" && !isDate; + let chipClass = "bg-slate-100 text-slate-700"; + let chipLabel = resultStr || "—"; + if (isOutcomeStatus) { + chipClass = "bg-amber-100 text-amber-900 font-semibold"; + } else if (isTrue) { + chipClass = "bg-emerald-100 text-emerald-800"; + chipLabel = "✓ true"; + } else if (isFalse) { + chipClass = "bg-red-100 text-red-800"; + chipLabel = "✗ false"; + } else if (isNull) { + chipClass = "bg-slate-100 text-slate-500 italic"; + chipLabel = "not found"; + } else if (isDate) { + chipClass = "bg-blue-100 text-blue-800"; + chipLabel = `📅 ${resultStr.slice(0, 10)}`; + } else if (isNumber) { + const n = Number(resultStr); + chipClass = n > 0 ? "bg-orange-100 text-orange-800" : "bg-slate-100 text-slate-700"; + } + return ( +
+

{defineStr}

+ {chipLabel}
-

{String(row.result)}

-
- ))} + ); + })}
+ {linkedValueSets.length > 0 ? ( +
+

Declared value sets

+

These are the code sets the CQL was evaluating against for this measure version.

+
+ {linkedValueSets.map((vs) => ( +
+
+

{vs.name}

+

{vs.oid}

+
+
+ {vs.codeCount} code{vs.codeCount !== 1 ? "s" : ""} + + {vs.resolvabilityLabel} + +
+
+ ))} +
+
+ ) : null} +

why_flagged

{caseDetail.evidenceJson.why_flagged ? (