Polish results, patients, and nav state

This commit is contained in:
sas.fajri
2026-04-13 15:10:36 +07:00
parent a6f9b60362
commit bc6d4fcdad
2 changed files with 118 additions and 43 deletions

112
server.js
View File

@@ -200,6 +200,14 @@ function layout(title, body, { authenticated = false, activePath = "/", subtitle
var root = document.getElementById('modal-root');
if (root) root.scrollTop = 0;
}
if (event.detail && event.detail.target && event.detail.target.classList) {
event.detail.target.classList.remove('swap-in');
void event.detail.target.offsetWidth;
event.detail.target.classList.add('swap-in');
window.setTimeout(function () {
event.detail.target.classList.remove('swap-in');
}, 240);
}
});
})();
</script>
@@ -719,7 +727,7 @@ function renderResultsTable(results, selectedResultId) {
.join("")}
</div>
</section>
<aside class="panel" id="result-detail-fragment">
<aside class="panel swap-zone" id="result-detail-fragment">
${panelHeader("Selected result", "Use the right pane for quick context without losing list position.", `<a class="btn btn-secondary" href="/results/${selected.id}">Open detail</a>`)}
<div class="stack">
<div class="card">
@@ -737,7 +745,7 @@ function renderResultsTable(results, selectedResultId) {
function renderResultDetail(result) {
return `
<section class="panel">
<section class="panel swap-zone">
${panelHeader(`${result.patient} · ${result.id}`, "Result detail with summary, status, and interpretation fields.", '<a class="btn btn-secondary" href="/results">Back to results</a>')}
<div class="grid grid-3">
<div class="card"><span class="muted">Status</span><div style="margin-top:8px">${statusBadge(result.status)}</div></div>
@@ -763,7 +771,7 @@ function renderResultDetail(result) {
function renderFpp(groups) {
return `
<div class="stack">
<div class="stack swap-zone">
<section class="panel">
${panelHeader("FPP catalog", "Filter blocks and list cards on mobile, richer panel on desktop.")}
<div class="pill-row">
@@ -822,50 +830,72 @@ function renderPatients() {
return `
<div class="stack">
<section class="panel">
${panelHeader("Patient registration", "Landing page for registration, lookup, and entry flow.")}
<div class="grid grid-3">
<div class="card">
<strong>New patient</strong>
<p class="muted">Open the registration stepper and start a fresh case.</p>
<a class="btn btn-primary" href="/orders/new/demografi">Start registration</a>
</div>
<div class="card">
<strong>Lookup</strong>
<p class="muted">Search an existing patient and reuse their visit data.</p>
<button class="btn btn-secondary" type="button">Search patient</button>
</div>
<div class="card">
<strong>QR entry</strong>
<p class="muted">Fast path for scan-based intake.</p>
<a class="btn btn-secondary" href="/orders/new/qrcode">Open QR flow</a>
</div>
</div>
</section>
<section class="panel">
${panelHeader("Recent patients", "A compact list that becomes cards on mobile.")}
<div class="table-wrap">
<table>
<thead>
<tr><th>Name</th><th>MRN</th><th>Gender</th><th>Last visit</th><th>Note</th></tr>
</thead>
<tbody>
${mockPatients
${panelHeader("Patient registration", "A landing zone for registration, lookup, and intake shortcuts.", '<a class="btn btn-primary" href="/orders/new/demografi">Start registration</a>')}
<div class="grid grid-4">
${[
["Today's intake", "11", "New or updated patients"],
["Active visits", "7", "Cases linked to lab orders"],
["QR scans", "4", "Fast entry from QR code"],
["Needs review", "2", "Patients waiting verification"],
]
.map(
(person) => `
<tr>
<td><strong>${escapeHtml(person.name)}</strong></td>
<td>${escapeHtml(person.mrn)}</td>
<td>${escapeHtml(person.gender)}</td>
<td>${escapeHtml(person.lastVisit)}</td>
<td>${escapeHtml(person.note)}</td>
</tr>
([label, value, hint]) => `
<article class="card metric">
<div class="kicker">
<span>${escapeHtml(label)}</span>
<span class="trend">Live</span>
</div>
<strong>${escapeHtml(value)}</strong>
<span class="muted">${escapeHtml(hint)}</span>
</article>
`,
)
.join("")}
</tbody>
</table>
</div>
</section>
<section class="detail-grid">
<div class="panel">
${panelHeader("Quick intake", "Route into the proper entry flow without forcing the user through extra screens.")}
<div class="grid grid-2">
<div class="card">
<strong>Lookup patient</strong>
<p class="muted">Search existing records and attach them to the next order.</p>
<div class="field" style="margin-top:12px">
<label><span class="muted">MRN / name</span></label>
<input placeholder="Siti Amelia" />
</div>
<button class="btn btn-secondary" type="button" style="margin-top:12px">Search</button>
</div>
<div class="card">
<strong>New patient</strong>
<p class="muted">Jump straight into demographic capture.</p>
<div class="pill-row" style="margin-top:12px">
<a class="pill" href="/orders/new/demografi">Demografi</a>
<a class="pill" href="/orders/new/qrcode">QR entry</a>
</div>
<div class="note-box" style="margin-top:14px">Use the registration stepper when the patient is not yet in the system.</div>
</div>
</div>
</div>
<aside class="panel">
${panelHeader("Recent patients", "A compact list that stays readable on mobile and still works as a table on desktop.")}
<div class="mini-list">
${mockPatients
.map(
(person) => `
<div class="mini-item">
<div>
<strong>${escapeHtml(person.name)}</strong>
<p>${escapeHtml(person.mrn)} · ${escapeHtml(person.gender)}</p>
</div>
<span class="pill">${escapeHtml(person.lastVisit)}</span>
</div>
`,
)
.join("")}
</div>
</aside>
</section>
</div>
`;
}

View File

@@ -205,6 +205,19 @@ button {
color: white;
background: linear-gradient(135deg, var(--brand), #0f9488);
box-shadow: 0 12px 24px rgba(15, 118, 110, 0.24);
transform: translateX(2px);
position: relative;
}
.nav-link.active::before {
content: "";
position: absolute;
left: 8px;
top: 10px;
bottom: 10px;
width: 3px;
border-radius: 99px;
background: rgba(255, 255, 255, 0.78);
}
.nav-link small {
@@ -832,6 +845,15 @@ tr:last-child td {
text-align: center;
}
.mobile-nav .nav-link.active {
transform: translateY(-2px);
box-shadow: 0 14px 22px rgba(15, 118, 110, 0.18);
}
.mobile-nav .nav-link.active::before {
display: none;
}
.mobile-nav .nav-link small {
display: none;
}
@@ -873,6 +895,25 @@ tr:last-child td {
margin-bottom: 18px;
}
.swap-zone {
animation: swap-in 220ms ease;
}
.swap-in {
animation: swap-in 220ms ease;
}
@keyframes swap-in {
from {
opacity: 0;
transform: translateY(6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.desktop-only {
display: block;
}
@@ -962,6 +1003,10 @@ tr:last-child td {
.auth-mini-grid {
grid-template-columns: 1fr;
}
.mobile-nav .nav-link.active {
transform: translateY(-1px);
}
}
@media (max-width: 620px) {