Patient Match

This commit is contained in:
2026-02-24 00:26:55 -05:00
parent c25b6e0c80
commit 51115c9a64
9 changed files with 884 additions and 0 deletions

View File

@@ -63,6 +63,10 @@
Refresh Chart
</button>
</form>
<button class="btn btn-outline" type="button" onclick="findInRimidi('{{.Patient.FHIRID}}')" title="Search for this patient in the Rimidi system">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
Find in Rimidi
</button>
<span class="text-xs text-muted">
Last Synced: {{if .LatestSync}}{{formatDateTime .LatestSync.SyncedAt}}{{else}}Never{{end}}
</span>
@@ -469,5 +473,137 @@ function onSyncClick() {
// Helper for splitting scopes string
function split(s, sep) { return s.split(sep); }
/* ── "Find in Rimidi" match modal ────────────────────────────────── */
function findInRimidi(patientFHIRID) {
var overlay = document.getElementById('rimidiMatchOverlay');
var body = document.getElementById('rimidiMatchBody');
overlay.style.display = 'flex';
body.innerHTML =
'<div style="text-align:center;padding:40px;">' +
'<div class="animate-spin" style="width:24px;height:24px;border:3px solid #ddd;border-top-color:#0d6efd;border-radius:50%;display:inline-block;"></div>' +
'<p style="margin-top:12px;color:var(--color-text-muted);">Searching Rimidi…</p></div>';
var url = '/api/patient-match-proxy';
if (patientFHIRID) {
url += '?patient_id=' + encodeURIComponent(patientFHIRID);
}
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})
.then(function(resp) {
if (!resp.ok) {
return resp.json().then(function(d) { throw new Error(d.error || 'HTTP ' + resp.status); });
}
return resp.json();
})
.then(function(data) {
renderRimidiMatchResults(body, data);
})
.catch(function(err) {
body.innerHTML =
'<div style="padding:20px;color:var(--color-danger);background:var(--color-danger-bg);border-radius:8px;margin:20px;">' +
'<strong>Error:</strong> ' + escapeHtml(err.message) + '</div>';
});
}
function closeRimidiMatch() {
document.getElementById('rimidiMatchOverlay').style.display = 'none';
}
function escapeHtml(s) {
var d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
function renderRimidiMatchResults(container, data) {
var matches = data.matches || [];
var local = data.local_patient || {};
if (matches.length === 0) {
container.innerHTML =
'<div style="padding:20px;text-align:center;color:var(--color-text-muted);">' +
'No matching patients found in the Rimidi system.</div>';
return;
}
var html = '<div style="padding:16px;">';
html += '<p style="margin-bottom:16px;color:var(--color-text-muted);">' +
'Found <strong>' + matches.length + '</strong> potential match' +
(matches.length > 1 ? 'es' : '') + ' in Rimidi.</p>';
var fieldLabels = {
first_name: 'First Name', last_name: 'Last Name',
email: 'Email', dob: 'Date of Birth', sex: 'Sex'
};
var fieldOrder = ['first_name', 'last_name', 'email', 'dob', 'sex'];
for (var i = 0; i < matches.length; i++) {
var m = matches[i];
var f = m.fields || {};
var borderColor = m.score >= 4 ? 'var(--color-success)' : (m.score >= 3 ? 'var(--color-warning, #f0ad4e)' : 'var(--color-brand)');
var badgeClass = m.score >= 4 ? 'badge-success' : (m.score >= 3 ? 'badge-warning' : 'badge-blue');
html += '<div class="panel" style="margin-bottom:16px;border-left:4px solid ' + borderColor + ';padding:0;">';
html += '<div style="display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid var(--color-border-soft);">';
html += '<strong>Match #' + (i + 1) + '</strong>';
html += '<span class="badge ' + badgeClass + '">' + m.score + '/5 fields match</span>';
html += '</div>';
html += '<table style="width:100%;border-collapse:collapse;">';
html += '<thead><tr style="background:var(--color-bg-panel);">';
html += '<th style="padding:8px 12px;text-align:left;width:25%;font-size:12px;text-transform:uppercase;color:var(--color-text-muted);">Field</th>';
html += '<th style="padding:8px 12px;text-align:left;width:30%;font-size:12px;text-transform:uppercase;color:var(--color-text-muted);">HRS (Local)</th>';
html += '<th style="padding:8px 12px;text-align:left;width:30%;font-size:12px;text-transform:uppercase;color:var(--color-text-muted);">Rimidi (Remote)</th>';
html += '<th style="padding:8px 12px;text-align:center;width:15%;font-size:12px;text-transform:uppercase;color:var(--color-text-muted);">Match</th>';
html += '</tr></thead><tbody>';
for (var j = 0; j < fieldOrder.length; j++) {
var key = fieldOrder[j];
var fld = f[key] || {};
var localVal = local[key] || '—';
var remoteVal = fld.value || '—';
var isMatch = fld.match === true;
var icon = isMatch
? '<span style="color:var(--color-success);font-size:16px;">✓</span>'
: '<span style="color:var(--color-danger);font-size:16px;">✗</span>';
var rowBg = isMatch ? '' : 'background:rgba(240,173,78,0.1);';
html += '<tr style="border-top:1px solid var(--color-border-soft);' + rowBg + '">';
html += '<td style="padding:8px 12px;font-weight:600;">' + fieldLabels[key] + '</td>';
html += '<td style="padding:8px 12px;">' + escapeHtml(localVal) + '</td>';
html += '<td style="padding:8px 12px;">' + escapeHtml(remoteVal) + '</td>';
html += '<td style="padding:8px 12px;text-align:center;">' + icon + '</td>';
html += '</tr>';
}
html += '</tbody></table></div>';
}
html += '</div>';
container.innerHTML = html;
}
</script>
<!-- Rimidi Patient Match Modal Overlay -->
<div id="rimidiMatchOverlay" style="display:none;position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.5);align-items:center;justify-content:center;">
<div style="background:var(--color-bg-main);border-radius:12px;width:90%;max-width:700px;max-height:80vh;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,0.3);">
<div style="display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid var(--color-border-soft);">
<h3 style="margin:0;font-size:18px;">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:text-bottom;margin-right:6px;"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
Find Patient in Rimidi
</h3>
<button onclick="closeRimidiMatch()" style="background:none;border:none;font-size:24px;cursor:pointer;color:var(--color-text-muted);padding:0 4px;">&times;</button>
</div>
<div id="rimidiMatchBody" style="overflow-y:auto;flex:1;">
</div>
<div style="padding:12px 20px;border-top:1px solid var(--color-border-soft);text-align:right;">
<button class="btn btn-secondary" onclick="closeRimidiMatch()">Close</button>
</div>
</div>
</div>
{{end}}