feat: multi-point patient verification (MRN, Name, DOB) via URL parameters
This commit is contained in:
@@ -1425,13 +1425,14 @@
|
||||
|
||||
<!-- Single Machine Detailed View Wrapper -->
|
||||
<div id="singleViewWrapper" style="display: none; flex-direction: column; flex: 1;">
|
||||
<!-- Targeted Patient MRN Waiting State (shown when filtered patient hasn't connected yet) -->
|
||||
<div id="singleTargetWaitingState" style="display: none; flex-direction: column; align-items: center; justify-content: center; padding: 60px 20px; text-align: center; background: var(--bg-card); border: 1px dashed rgba(6, 182, 212, 0.4); border-radius: 16px; margin: 20px 0;">
|
||||
<!-- Targeted Patient Demographics Waiting State (shown when filtered patient hasn't connected yet) -->
|
||||
<div id="singleTargetWaitingState" style="display: none; flex-direction: column; align-items: center; justify-content: center; padding: 50px 20px; text-align: center; background: var(--bg-card); border: 1px dashed rgba(6, 182, 212, 0.4); border-radius: 16px; margin: 20px 0;">
|
||||
<div style="font-size: 40px; margin-bottom: 12px;">⏳</div>
|
||||
<div style="font-size: 19px; font-weight: 700; color: var(--text-primary);">Waiting for Contec Monitor Data</div>
|
||||
<div style="font-size: 15px; color: var(--spo2-color); font-weight: 600; margin-top: 6px;" id="targetWaitingMrnBadge">Patient MRN: ---</div>
|
||||
<div style="font-size: 13px; color: var(--text-secondary); max-width: 520px; margin-top: 10px; line-height: 1.5;">
|
||||
Filtered to show exclusively this patient's teleconsult telemetry. Once the Contec CMS7000PLUS monitor connects or transmits data, live waveforms and vitals will stream automatically.
|
||||
<div style="font-size: 13px; color: var(--text-secondary); margin-top: 6px;">Verifying incoming monitor telemetry against required teleconsult patient:</div>
|
||||
<div style="margin-top: 12px; display: flex; gap: 8px; justify-content: center; flex-wrap: wrap;" id="targetWaitingMrnBadge">Patient Demographics: ---</div>
|
||||
<div style="font-size: 13px; color: var(--text-secondary); max-width: 540px; margin-top: 14px; line-height: 1.5;">
|
||||
Live waveforms and telemetry stream automatically once the Contec CMS7000PLUS monitor connects and transmits telemetry matching all requested demographics (MRN, Name, DOB).
|
||||
</div>
|
||||
<div style="margin-top: 18px; display: inline-flex; align-items: center; gap: 8px; padding: 6px 14px; background: rgba(6, 182, 212, 0.1); border: 1px solid rgba(6, 182, 212, 0.25); border-radius: 20px; font-size: 12px; color: var(--spo2-color);">
|
||||
<span style="width: 8px; height: 8px; border-radius: 50%; background: var(--spo2-color); display: inline-block;"></span> Listening for live connection...
|
||||
@@ -1774,65 +1775,110 @@
|
||||
|
||||
function getTargetPatientBadge() {
|
||||
const items = [];
|
||||
if (targetMrn) items.push(`MRN: <strong>${targetMrn}</strong>`);
|
||||
if (targetName) items.push(`Name: <strong>${targetName}</strong>`);
|
||||
if (targetDob) items.push(`DOB: <strong>${targetDob}</strong>`);
|
||||
if (targetGender) items.push(`Gender: <strong>${targetGender}</strong>`);
|
||||
return items.length > 0 ? items.join(' • ') : 'Target Patient';
|
||||
if (targetMrn) items.push(`<span class="patient-mrn-badge" style="padding: 4px 10px; font-size: 12px;">📋 MRN: <strong>${targetMrn}</strong></span>`);
|
||||
if (targetName) items.push(`<span class="patient-name-badge" style="padding: 4px 10px; background: rgba(14, 165, 233, 0.15); border: 1px solid rgba(14, 165, 233, 0.3); border-radius: 6px; font-size: 12px; color: #38bdf8;">👤 Name: <strong>${targetName}</strong></span>`);
|
||||
if (targetDob) items.push(`<span class="patient-dob-badge" style="padding: 4px 10px; font-size: 12px;">🎂 DOB: <strong>${targetDob}</strong></span>`);
|
||||
if (targetGender) items.push(`<span class="patient-gender-badge" style="padding: 4px 10px; font-size: 12px;">⚧ Gender: <strong>${targetGender}</strong></span>`);
|
||||
return items.length > 0 ? items.join('') : '<span>Target Patient</span>';
|
||||
}
|
||||
|
||||
// Helper: Extract 4-digit year and digits for robust DOB matching
|
||||
function normalizeDob(str) {
|
||||
if (!str) return { raw: '', digits: '', year: null };
|
||||
const clean = String(str).trim();
|
||||
const yearMatch = clean.match(/\b(19\d\d|20[0-2]\d)\b/);
|
||||
const digits = clean.replace(/\D/g, '');
|
||||
return {
|
||||
raw: clean.toLowerCase(),
|
||||
digits: digits,
|
||||
year: yearMatch ? yearMatch[1] : null
|
||||
};
|
||||
}
|
||||
|
||||
// Helper: Extract individual words for flexible name matching
|
||||
function normalizeNameTokens(str) {
|
||||
if (!str) return [];
|
||||
return String(str)
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s]/g, '')
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(w => w.length >= 2);
|
||||
}
|
||||
|
||||
// Check verification for each requested parameter (MRN, Name, DOB, Gender)
|
||||
function checkPatientVerification(dev) {
|
||||
const res = {
|
||||
mrnMatched: !targetMrn,
|
||||
nameMatched: !targetName,
|
||||
dobMatched: !targetDob,
|
||||
genderMatched: !targetGender,
|
||||
allMatched: false
|
||||
};
|
||||
if (!dev) return res;
|
||||
|
||||
const pMrn = String(dev?.mrn || dev?.patient_id || '').toLowerCase().trim();
|
||||
const pName = String(dev?.patient_name || dev?.name || '').toLowerCase().trim();
|
||||
const pDob = String(dev?.patient_dob || dev?.dob || '').toLowerCase().trim();
|
||||
const pGender = String(dev?.patient_gender || dev?.gender || '').toLowerCase().trim();
|
||||
|
||||
// 1. MRN verification
|
||||
if (targetMrn) {
|
||||
const tm = targetMrn.toLowerCase().trim();
|
||||
if (pMrn && (pMrn === tm || pMrn.includes(tm) || tm.includes(pMrn))) {
|
||||
res.mrnMatched = true;
|
||||
} else {
|
||||
const numT = parseInt(tm, 10);
|
||||
const numMrn = parseInt(pMrn, 10);
|
||||
if (!isNaN(numT) && !isNaN(numMrn) && numT === numMrn && tm.length >= 3 && pMrn.length >= 3) {
|
||||
res.mrnMatched = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Name verification (supports "Shahil", "Shahil Kumar", "Kumar Shahil")
|
||||
if (targetName) {
|
||||
const tn = targetName.toLowerCase().trim();
|
||||
const targetTokens = normalizeNameTokens(targetName);
|
||||
const patientTokens = normalizeNameTokens(pName);
|
||||
if (pName && (pName.includes(tn) || tn.includes(pName))) {
|
||||
res.nameMatched = true;
|
||||
} else if (targetTokens.length > 0 && targetTokens.some(t => patientTokens.includes(t))) {
|
||||
res.nameMatched = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. DOB verification (supports 1960, 1960-06-22, 22/06/1960, 19600622)
|
||||
if (targetDob) {
|
||||
const td = targetDob.toLowerCase().trim();
|
||||
const tNorm = normalizeDob(targetDob);
|
||||
const pNorm = normalizeDob(pDob);
|
||||
if (pDob && (pDob.includes(td) || td.includes(pDob))) {
|
||||
res.dobMatched = true;
|
||||
} else if (tNorm.year && pNorm.year && tNorm.year === pNorm.year) {
|
||||
res.dobMatched = true;
|
||||
} else if (tNorm.digits && pNorm.digits && (tNorm.digits.includes(pNorm.digits) || pNorm.digits.includes(tNorm.digits))) {
|
||||
res.dobMatched = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Gender verification
|
||||
if (targetGender) {
|
||||
const tg = targetGender.toLowerCase().trim();
|
||||
if (pGender && (pGender.startsWith(tg[0]) || pGender.includes(tg))) {
|
||||
res.genderMatched = true;
|
||||
}
|
||||
}
|
||||
|
||||
res.allMatched = res.mrnMatched && res.nameMatched && res.dobMatched && res.genderMatched;
|
||||
return res;
|
||||
}
|
||||
|
||||
// Helper function: Check if a device or patient matches the target demographics (MRN, Name, DOB, Gender)
|
||||
function matchesTargetPatient(dev, devId) {
|
||||
if (!hasTargetFilter) return true;
|
||||
if (!dev && !devId) return false;
|
||||
|
||||
const pMrn = String(dev?.mrn || dev?.patient_id || '').toLowerCase().trim();
|
||||
const pName = String(dev?.patient_name || dev?.name || '').toLowerCase().trim();
|
||||
const pDob = String(dev?.patient_dob || dev?.dob || '').toLowerCase().trim();
|
||||
const pGender = String(dev?.patient_gender || dev?.gender || '').toLowerCase().trim();
|
||||
const pBed = String(dev?.bed_id || dev?.bedLabel || '').toLowerCase().trim();
|
||||
const dId = String(devId || dev?.device_id || '').toLowerCase().trim();
|
||||
|
||||
// 1. If targetName is explicitly specified: must match patient name
|
||||
if (targetName) {
|
||||
const tn = targetName.toLowerCase().trim();
|
||||
const nameMatch = pName && (pName.includes(tn) || tn.includes(pName));
|
||||
if (!nameMatch) return false;
|
||||
}
|
||||
|
||||
// 2. If targetDob is explicitly specified: must match patient DOB / birth year
|
||||
if (targetDob) {
|
||||
const td = targetDob.toLowerCase().trim();
|
||||
const dobMatch = pDob && (pDob.includes(td) || td.includes(pDob));
|
||||
if (!dobMatch) return false;
|
||||
}
|
||||
|
||||
// 3. If targetGender is explicitly specified: must match gender
|
||||
if (targetGender) {
|
||||
const tg = targetGender.toLowerCase().trim();
|
||||
const genderMatch = pGender && (pGender.startsWith(tg[0]) || pGender.includes(tg));
|
||||
if (!genderMatch) return false;
|
||||
}
|
||||
|
||||
// 4. If targetMrn is specified: strictly match patient MRN only
|
||||
if (targetMrn) {
|
||||
const tm = targetMrn.toLowerCase().trim();
|
||||
let mrnMatch = false;
|
||||
|
||||
if (pMrn) {
|
||||
if (pMrn === tm) mrnMatch = true;
|
||||
// Also support numeric equivalence (e.g. "09091102" vs "9091102") with minimum length 3
|
||||
const numT = parseInt(tm, 10);
|
||||
const numMrn = parseInt(pMrn, 10);
|
||||
if (!isNaN(numT) && !isNaN(numMrn) && numT === numMrn && tm.length >= 3 && pMrn.length >= 3) {
|
||||
mrnMatch = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mrnMatch) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return checkPatientVerification(dev).allMatched;
|
||||
}
|
||||
|
||||
// Backward-compatible alias
|
||||
@@ -2121,8 +2167,13 @@
|
||||
}
|
||||
|
||||
// 2. Load latest readings to backfill vitals for each device
|
||||
const backfillUrl = hasTargetFilter && targetMrn
|
||||
? `${API_BASE}/latest-readings?limit=50&mrn=${encodeURIComponent(targetMrn)}`
|
||||
const backfillParams = new URLSearchParams();
|
||||
backfillParams.set('limit', '50');
|
||||
if (targetMrn) backfillParams.set('mrn', targetMrn);
|
||||
if (targetName) backfillParams.set('name', targetName);
|
||||
if (targetDob) backfillParams.set('dob', targetDob);
|
||||
const backfillUrl = hasTargetFilter
|
||||
? `${API_BASE}/latest-readings?${backfillParams.toString()}`
|
||||
: `${API_BASE}/latest-readings?limit=100`;
|
||||
const readResp = await fetch(backfillUrl);
|
||||
if (readResp.ok) {
|
||||
@@ -2560,7 +2611,7 @@
|
||||
if (waitingState) {
|
||||
waitingState.style.display = 'flex';
|
||||
const badge = document.getElementById('targetWaitingMrnBadge');
|
||||
if (badge) badge.innerHTML = `Target Patient: ${getTargetPatientBadge()}`;
|
||||
if (badge) badge.innerHTML = getTargetPatientBadge();
|
||||
}
|
||||
if (patientBanner) patientBanner.style.display = 'none';
|
||||
if (vitalsGrid) vitalsGrid.style.display = 'none';
|
||||
@@ -2584,10 +2635,11 @@
|
||||
|
||||
const bannerEl = document.getElementById('singlePatientBanner');
|
||||
if (bannerEl) {
|
||||
document.getElementById('bannerPatientName').textContent = pName;
|
||||
document.getElementById('bannerMrn').textContent = `MRN: ${pMrn}`;
|
||||
document.getElementById('bannerGender').textContent = `Gender: ${pGender}`;
|
||||
document.getElementById('bannerDob').textContent = `DOB: ${pDob}`;
|
||||
const verifiedTag = '<span style="display:inline-flex;align-items:center;gap:3px;padding:2px 6px;background:rgba(16,185,129,0.15);border:1px solid rgba(16,185,129,0.35);border-radius:4px;color:#10b981;font-size:10px;font-weight:700;margin-left:6px;">✓ Verified</span>';
|
||||
document.getElementById('bannerPatientName').innerHTML = `${pName} ${targetName ? verifiedTag : ''}`;
|
||||
document.getElementById('bannerMrn').innerHTML = `MRN: ${pMrn} ${targetMrn ? verifiedTag : ''}`;
|
||||
document.getElementById('bannerGender').innerHTML = `Gender: ${pGender} ${targetGender ? verifiedTag : ''}`;
|
||||
document.getElementById('bannerDob').innerHTML = `DOB: ${pDob} ${targetDob ? verifiedTag : ''}`;
|
||||
document.getElementById('bannerBed').textContent = `Location: ${pBed}`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user