pradsa / templates /dsa_analysis.html
plokmii's picture
Upload templates/dsa_analysis.html with huggingface_hub
15070ba verified
Raw
History Blame Contribute Delete
21.2 kB
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DSA 統計分析</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
<style>
.chart-container { position: relative; height: 540px; background: #fff; border: 1px solid #dee2e6; border-radius: 6px; padding: 1rem; margin-bottom: 1.5rem; }
.ab-table { font-size: .82rem; }
.strength-strong { --bs-table-bg: #f8d7da; background-color: #f8d7da !important; font-weight: bold; }
.strength-strong > td { background-color: #f8d7da !important; }
.strength-weak { --bs-table-bg: #fff9c4; background-color: #fff9c4 !important; }
.strength-weak > td { background-color: #fff9c4 !important; }
.spec-strong-bg { background-color: #f8d7da; padding: 3px 6px; border-radius: 3px; margin-bottom: 2px; display: block; }
.spec-weak-bg { background-color: #fff9c4; padding: 3px 6px; border-radius: 3px; display: block; }
</style>
</head>
<body>
<div class="container py-4" style="max-width: 1200px;">
<div class="d-flex justify-content-between align-items-center mb-3">
<div><h2 class="mb-0">DSA 統計分析</h2></div>
<div>
<a href="/" class="btn btn-dark btn-sm me-1">首頁</a>
<a href="/dsa/history" class="btn btn-outline-secondary btn-sm me-1">歷史紀錄</a>
{% if chart_no and patient %}
<a href="/dsa/history/{{ chart_no }}" class="btn btn-outline-info btn-sm me-1">此病患歷史</a>
{% endif %}
<a href="/dsa/new" class="btn btn-outline-primary btn-sm">新增資料</a>
</div>
</div>
<form class="row g-2 mb-4" method="get" action="/dsa/analysis">
<div class="col-sm-6">
<select class="form-select" id="patientSelect" onchange="selectPatient(this.value)">
<option value="">-- 選擇病人 --</option>
{% for p in patients %}
<option value="{{ p.chart_no }}" {{ 'selected' if p.chart_no == chart_no else '' }}>
{{ p.patient_name }} ({{ p.chart_no }})
</option>
{% endfor %}
</select>
<input type="hidden" name="chart_no" id="chartNoInput" value="{{ chart_no }}">
</div>
</form>
<script>
function selectPatient(val) {
if (!val) return;
document.getElementById('chartNoInput').value = val;
document.getElementById('chartNoInput').form.submit();
}
</script>
{% if patient %}
<div class="card mb-3">
<div class="card-header fw-bold">{{ patient.patient_name }} ({{ patient.chart_no }}) — Donor HLA</div>
<div class="card-body" style="font-size:.85rem;">
<style>.donor-hla { width: 80px !important; padding: 2px 4px !important; text-align: center; }</style>
<div class="row">
<div class="col-md-5">
<div class="fw-bold mb-1">Serology</div>
<table class="table table-sm table-bordered mb-1"><tbody>
<tr><td class="fw-bold">A</td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-A-1" oninput="highlightDSA()"></td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-A-2" oninput="highlightDSA()"></td>
<td class="fw-bold">DR</td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-DR-1" oninput="highlightDSA()"></td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-DR-2" oninput="highlightDSA()"></td></tr>
<tr><td class="fw-bold">B</td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-B-1" oninput="highlightDSA()"></td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-B-2" oninput="highlightDSA()"></td>
<td class="fw-bold">DQ</td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-DQ-1" oninput="highlightDSA()"></td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-DQ-2" oninput="highlightDSA()"></td></tr>
<tr><td class="fw-bold">Cw</td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-Cw-1" oninput="highlightDSA()"></td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-Cw-2" oninput="highlightDSA()"></td>
<td class="fw-bold">DP</td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-DP-1" oninput="highlightDSA()"></td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-DP-2" oninput="highlightDSA()"></td></tr>
</tbody></table>
</div>
<div class="col-md-7">
<div class="fw-bold mb-1">DNA Typing</div>
<table class="table table-sm table-bordered mb-1"><tbody>
<tr><td class="fw-bold">A*</td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-dna-A-1" oninput="highlightDSA()"></td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-dna-A-2" oninput="highlightDSA()"></td>
<td class="fw-bold">DRB1*</td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-dna-DRB1-1" oninput="highlightDSA()"></td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-dna-DRB1-2" oninput="highlightDSA()"></td></tr>
<tr><td class="fw-bold">B*</td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-dna-B-1" oninput="highlightDSA()"></td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-dna-B-2" oninput="highlightDSA()"></td>
<td class="fw-bold">DQB1*</td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-dna-DQB1-1" oninput="highlightDSA()"></td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-dna-DQB1-2" oninput="highlightDSA()"></td></tr>
<tr><td class="fw-bold">C*</td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-dna-C-1" oninput="highlightDSA()"></td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-dna-C-2" oninput="highlightDSA()"></td>
<td class="fw-bold">DQA1*</td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-dna-DQA1-1" oninput="highlightDSA()"></td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-dna-DQA1-2" oninput="highlightDSA()"></td></tr>
<tr><td></td><td></td><td></td>
<td class="fw-bold">DPB1*</td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-dna-DPB1-1" oninput="highlightDSA()"></td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-dna-DPB1-2" oninput="highlightDSA()"></td></tr>
<tr><td></td><td></td><td></td>
<td class="fw-bold">DPA1*</td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-dna-DPA1-1" oninput="highlightDSA()"></td>
<td><input type="text" class="form-control form-control-sm donor-hla" id="donor-dna-DPA1-2" oninput="highlightDSA()"></td></tr>
</tbody></table>
</div>
</div>
<div class="text-end">
<button class="btn btn-sm btn-outline-danger" onclick="saveDonorHLA(this)">存檔</button>
</div>
</div>
</div>
<script>
function getDonorSet() {
const s = new Set();
['A','B','Cw','DR','DQ','DP'].forEach(l => {
[1,2].forEach(n => {
const el = document.getElementById('donor-' + l + '-' + n);
if (el && el.value.trim()) s.add(l + el.value.trim());
});
});
['A','B','C','DRB1','DQB1','DQA1','DPB1','DPA1'].forEach(l => {
[1,2].forEach(n => {
const el = document.getElementById('donor-dna-' + l + '-' + n);
if (el && el.value.trim()) s.add(l + '*' + el.value.trim());
});
});
return s;
}
function highlightTokensInCell(cell, donorSet) {
if (!cell) return;
const text = cell.dataset.orig || cell.textContent;
cell.dataset.orig = text;
const tokens = text.split(/,\s*/);
cell.innerHTML = tokens.map(tok => {
const t = tok.trim();
if (!t) return tok;
for (const ag of donorSet) {
if (t.includes(ag)) return '<span style="color:#dc3545;font-weight:bold;">' + tok + '</span>';
}
return tok;
}).join(', ');
}
function highlightSpecText(specEl, donorSet) {
if (!specEl) return;
const orig = specEl.dataset.orig || specEl.textContent;
specEl.dataset.orig = orig;
const tokens = orig.match(/\S+?\([^)]*\)|\S+/g) || [];
specEl.innerHTML = tokens.map(seg => {
const m = seg.match(/^([^(]+)(\([^)]*\))?$/);
if (!m) return seg;
const sero = m[1];
const paren = m[2] || '';
if (!/^[A-Za-z]/.test(sero)) return seg;
let isDSA = false;
for (const ag of donorSet) {
if (!ag) continue;
if (sero === ag || (paren && paren.indexOf(ag) >= 0)) { isDSA = true; break; }
}
if (isDSA) return '<span style="color:#dc3545;font-weight:bold;">' + seg + '</span>';
const seroHtml = '<span style="color:#1E40AF;font-weight:bold;">' + sero + '</span>';
if (!paren) return seroHtml;
const inner = paren.slice(1, -1);
const alleleHtml = inner.split(/\s+/).map(a =>
a ? '<span style="color:#374151;">' + a + '</span>' : a
).join(' ');
return seroHtml + '(' + alleleHtml + ')';
}).join(' ');
}
function highlightDSA() {
const ds = getDonorSet();
document.querySelectorAll('.spec-cell').forEach(el => highlightSpecText(el, ds));
document.querySelectorAll('.ab-table tbody tr').forEach(tr => {
highlightTokensInCell(tr.cells[0], ds);
highlightTokensInCell(tr.cells[1], ds);
});
['chart1', 'chart2'].forEach(cid => {
const fn = window['rebuildDsaChart_' + cid];
if (typeof fn === 'function') fn();
});
}
function setDsaFilter(chartId, btn, val) {
btn.parentElement.querySelectorAll('.btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const fn = window['updateDsaFilter_' + chartId];
if (typeof fn === 'function') fn(val);
}
function saveDonorHLA(btn) {
const obj = {};
document.querySelectorAll('.donor-hla').forEach(el => {
if (el.value.trim()) obj[el.id] = el.value.trim();
});
btn.disabled = true;
fetch('/save_donor_hla', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ chart_no: '{{ patient.chart_no }}', donor_hla: JSON.stringify(obj) })
}).then(r => r.json()).then(d => {
btn.disabled = false;
if (d.ok) { btn.classList.remove('btn-outline-danger'); btn.classList.add('btn-danger'); btn.textContent = '已存檔'; }
});
highlightDSA();
}
window.addEventListener('DOMContentLoaded', function() {
const saved = {{ donor_hla | default('', true) | tojson }};
if (saved) {
try {
const obj = JSON.parse(saved);
Object.keys(obj).forEach(k => {
const el = document.getElementById(k);
if (el) el.value = obj[k];
});
} catch(e) {}
}
// After donor inputs are populated, re-apply default (may switch to DSA mode)
['chart1', 'chart2'].forEach(cid => {
const fn = window['applyDsaDefault_' + cid];
if (typeof fn === 'function') fn();
});
highlightDSA();
});
</script>
<h5>Reports</h5>
<table class="table table-bordered table-sm bg-white mb-4" style="font-size:.9rem;">
<thead class="table-light"><tr><th>Date</th><th>Class</th><th>%SA</th><th>Overall</th><th>Specificity</th></tr></thead>
<tbody>
{% for r in reports %}
<tr>
<td>{{ r.report_date }}</td>
<td>{{ r.dsa_class }}</td>
<td class="fw-bold">{{ r.pct_sa }}%</td>
<td><span class="badge {{ 'bg-danger' if r.overall == 'Positive' else 'bg-success' }}">{{ r.overall }}</span></td>
<td class="small" style="max-width:400px;">
{%- set spec_text = (r.specificity or '') | replace('&gt;', '>') -%}
{%- if 'Weak (MFI' in spec_text -%}
{%- set parts = spec_text.split('Weak (MFI', 1) -%}
<div class="spec-cell spec-strong-bg">{{ parts[0]|trim }}</div>
<div class="spec-cell spec-weak-bg">Weak (MFI{{ parts[1] }}</div>
{%- else -%}
<div class="spec-cell">{{ spec_text or '(-)' }}</div>
{%- endif -%}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% for comp_label, comp_id, comp_dates, comp_antigens in [
('DSA Class I', 'chart1', comparison_class1.dates if comparison_class1 else [], comparison_class1.antigens if comparison_class1 else []),
('DSA Class II', 'chart2', comparison_class2.dates if comparison_class2 else [], comparison_class2.antigens if comparison_class2 else [])
] %}
{% if comp_dates and comp_dates | length >= 2 %}
<h5>{{ comp_label }} &mdash; MFI Trend</h5>
<div class="mb-2">
<small class="text-muted me-2">點圖例可顯示/隱藏抗體</small>
<div class="btn-group btn-group-sm" id="filter-{{ comp_id }}">
<button type="button" class="btn btn-outline-secondary" onclick="setDsaFilter('{{ comp_id }}',this,'all')">全部</button>
<button type="button" class="btn btn-outline-danger" onclick="setDsaFilter('{{ comp_id }}',this,'dsa')">DSA</button>
<button type="button" class="btn btn-outline-secondary" onclick="setDsaFilter('{{ comp_id }}',this,'10')">Top 10</button>
<button type="button" class="btn btn-outline-secondary" onclick="setDsaFilter('{{ comp_id }}',this,'20')">Top 20</button>
</div>
</div>
<div class="chart-container"><canvas id="{{ comp_id }}"></canvas></div>
<script>
(function() {
const dates = {{ comp_dates | tojson }};
const allData = [
{% for ag in comp_antigens %}
{ antigen: '{{ ag.antigen }}', allele: '{{ ag.allele }}',
values: [{% for d in comp_dates %}{{ ag.mfi_by_date.get(d, {}).get('max_mfi', 'null') }}{% if not loop.last %},{% endif %}{% endfor %}] },
{% endfor %}
];
function getColor(ag, i) {
const a = ['#e53935','#d81b60','#8e24aa','#5e35b1','#3949ab','#1e88e5','#ef5350','#ec407a'];
const b = ['#00897b','#43a047','#7cb342','#c0ca33','#26a69a','#66bb6a','#9ccc65','#d4e157'];
const c = ['#ff8f00','#ff6f00','#f57f17','#ffb300','#ffa000'];
if (ag.startsWith('A') || ag.startsWith('DR')) return a[i % a.length];
if (ag.startsWith('B') || ag.startsWith('DQ')) return b[i % b.length];
return c[i % c.length];
}
allData.sort((x, y) => Math.max(...y.values.filter(v=>v!==null)) - Math.max(...x.values.filter(v=>v!==null)));
let chartInstance = null;
let currentFilter = '20';
function matchesDonor(ag, ds) {
if (ag.antigen && ds.has(ag.antigen)) return true;
if (ag.allele && ds.has(ag.allele)) return true;
return false;
}
function buildChart(limit) {
if (limit) currentFilter = limit;
let data;
if (currentFilter === 'dsa') {
const ds = (typeof getDonorSet === 'function') ? getDonorSet() : new Set();
data = allData.filter(ag => matchesDonor(ag, ds));
} else if (currentFilter === 'all') {
data = allData;
} else {
data = allData.slice(0, parseInt(currentFilter));
}
const datasets = data.map((ag, i) => ({
label: ag.antigen + (ag.allele && ag.allele !== ag.antigen ? ' (' + ag.allele + ')' : ''),
data: ag.values, borderColor: getColor(ag.antigen, i),
backgroundColor: getColor(ag.antigen, i) + '33',
borderWidth: 2, pointRadius: 4, pointHoverRadius: 6, tension: 0.3, spanGaps: true,
}));
if (chartInstance) chartInstance.destroy();
chartInstance = new Chart(document.getElementById('{{ comp_id }}'), {
type: 'line', data: { labels: dates, datasets: datasets },
options: { responsive: true, maintainAspectRatio: false,
interaction: { mode: 'nearest', intersect: false },
plugins: { legend: { position: 'right', labels: { font: { size: 11 }, boxWidth: 12, padding: 6 } } },
scales: { y: { title: { display: true, text: 'Max MFI' }, beginAtZero: true },
x: { title: { display: true, text: 'Report Date' } } } }
});
}
window['rebuildDsaChart_{{ comp_id }}'] = function() { buildChart(); };
window['updateDsaFilter_{{ comp_id }}'] = function(val) { buildChart(val); };
function applyDefault() {
let defaultFilter = '20';
try {
const ds = (typeof getDonorSet === 'function') ? getDonorSet() : new Set();
if (ds.size > 0 && allData.some(ag => matchesDonor(ag, ds))) defaultFilter = 'dsa';
} catch (e) {}
buildChart(defaultFilter);
document.querySelectorAll('#filter-{{ comp_id }} .btn').forEach(b => {
b.classList.remove('active');
if ((defaultFilter === 'dsa' && b.textContent === 'DSA') ||
(defaultFilter === '20' && b.textContent === 'Top 20'))
b.classList.add('active');
});
}
window['applyDsaDefault_{{ comp_id }}'] = applyDefault;
applyDefault();
})();
</script>
{% endif %}
{% endfor %}
<h5>Antibody Strength</h5>
{% for r in reports %}
{% if r.antibodies %}
<div class="card mb-3">
<div class="card-header"><strong>{{ r.dsa_class }}</strong> &mdash; {{ r.report_date }}
<span class="fw-bold ms-1">%SA {{ r.pct_sa }}%</span></div>
<div class="card-body p-2">
<table class="table table-sm table-bordered ab-table mb-0">
<thead class="table-light"><tr>
<th>Antigen</th><th>Allele</th><th>Strength</th><th class="text-end">Max MFI</th>
<th class="text-end">Mean MFI</th><th class="text-center">No of Beads</th><th>Bead IDs</th>
</tr></thead>
<tbody>
{% for a in r.antibodies %}
<tr class="{% if a.strength == 'Strong' %}strength-strong{% elif a.strength == 'Weak' %}strength-weak{% endif %}">
<td class="fw-bold">{{ a.antigen }}</td><td class="small">{{ a.allele }}</td>
<td>{{ a.strength or '' }}</td>
<td class="text-end">{{ a.max_mfi|int }}</td>
<td class="text-end">{{ a.mean_mfi|int }}</td>
<td class="text-center">{{ a.no_of_beads }}</td>
<td class="small text-muted">{{ a.bead_ids }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{% endfor %}
{% else %}
<div class="alert alert-info">請先選擇病人。</div>
{% endif %}
</div>
</body>
</html>