Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions backend/src/main/java/com/workwell/measure/MeasureService.java
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,36 @@ private UUID latestMeasureVersionId(UUID measureId) {
}
}

public List<ValueSetRef> 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<ValueSetRef> listAttachedValueSets(UUID measureId) {
UUID measureVersionId = latestMeasureVersionId(measureId);
String sql = """
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ public List<MeasureService.ValueSetRef> listValueSets() {
return measureService.listValueSets();
}

@GetMapping("/api/measures/versions/{measureVersionId}/value-sets")
public List<MeasureService.ValueSetRef> listValueSetsByVersion(@PathVariable UUID measureVersionId) {
return measureService.listValueSetsByVersionId(measureVersionId);
}

@GetMapping("/api/osha-references")
public List<MeasureService.OshaReference> listOshaReferences() {
return measureService.listOshaReferences();
Expand Down
93 changes: 80 additions & 13 deletions frontend/app/(dashboard)/cases/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -128,6 +138,7 @@ export default function CaseDetailPage() {
const [appointmentNotes, setAppointmentNotes] = useState("");
const [scheduling, setScheduling] = useState(false);
const [evidence, setEvidence] = useState<EvidenceAttachment[]>([]);
const [linkedValueSets, setLinkedValueSets] = useState<LinkedValueSet[]>([]);
const [evidenceFile, setEvidenceFile] = useState<File | null>(null);
const [evidenceDescription, setEvidenceDescription] = useState("");
const [uploadingEvidence, setUploadingEvidence] = useState(false);
Expand All @@ -139,6 +150,12 @@ export default function CaseDetailPage() {
const data = await api.get<CaseDetail>(`/api/cases/${caseId}`);
setCaseDetail(data);
setAssigneeInput(data.assignee ?? "");
try {
const vsets = await api.get<LinkedValueSet[]>(`/api/measures/versions/${data.measureVersionId}/value-sets`);
setLinkedValueSets(vsets);
} catch {
setLinkedValueSets([]);
}
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
} finally {
Expand Down Expand Up @@ -761,22 +778,72 @@ export default function CaseDetailPage() {

<div className="rounded-3xl border border-slate-200 bg-white p-6 shadow-sm">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-slate-500">Why Flagged</p>
<h4 className="mt-2 text-xl font-semibold">Structured evidence trail</h4>
<div className="mt-4 space-y-3">
{(caseDetail.evidenceJson.expressionResults ?? []).map((row, index) => (
<div
key={`${String(row.define ?? index)}-${index}`}
className="flex items-center justify-between gap-4 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3"
>
<div>
<p className="text-sm font-medium text-slate-900">{String(row.define ?? "define")}</p>
<p className="text-xs text-slate-500">Evidence item {index + 1}</p>
<h4 className="mt-2 text-xl font-semibold">Code evidence explorer</h4>
<div className="mt-4 space-y-2">
{(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 (
<div
key={`${defineStr}-${index}`}
className="flex items-center justify-between gap-4 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3"
>
<p className="text-sm font-medium text-slate-900">{defineStr}</p>
<span className={`rounded-full px-3 py-1 text-xs ${chipClass}`}>{chipLabel}</span>
</div>
<p className="text-sm font-semibold text-slate-900">{String(row.result)}</p>
</div>
))}
);
})}
</div>

{linkedValueSets.length > 0 ? (
<div className="mt-6 rounded-2xl border border-indigo-100 bg-indigo-50 p-4">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-indigo-700">Declared value sets</p>
<p className="mt-1 text-xs text-indigo-600">These are the code sets the CQL was evaluating against for this measure version.</p>
<div className="mt-3 space-y-2">
{linkedValueSets.map((vs) => (
<div key={vs.id} className="flex items-center justify-between rounded-xl border border-indigo-200 bg-white px-3 py-2">
<div>
<p className="text-xs font-semibold text-slate-800">{vs.name}</p>
<p className="font-mono text-[10px] text-slate-500">{vs.oid}</p>
</div>
<div className="flex items-center gap-2 text-right">
<span className="text-[10px] text-slate-500">{vs.codeCount} code{vs.codeCount !== 1 ? "s" : ""}</span>
<span className={`rounded-full px-2 py-0.5 text-[10px] font-semibold ${vs.resolvabilityStatus === "RESOLVED" ? "bg-emerald-100 text-emerald-800" : "bg-amber-100 text-amber-800"}`}>
{vs.resolvabilityLabel}
</span>
</div>
</div>
))}
</div>
</div>
) : null}

<div className="mt-6 rounded-2xl border border-slate-200 bg-slate-50 p-4">
<p className="text-sm font-semibold text-slate-900">why_flagged</p>
{caseDetail.evidenceJson.why_flagged ? (
Expand Down
Loading