Files
doclink_web/server.js
2026-04-13 15:17:24 +07:00

2003 lines
75 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import http from "node:http";
import { readFile } from "node:fs/promises";
import { extname } from "node:path";
const PORT = Number(process.env.PORT || 5173);
const API_BASE =
process.env.DOCLINK_API_BASE ||
"https://devbandungraya.aplikasi.web.id/one-api-doctor/doctor_mitra";
const sessionKey = "doclink_session";
const mockUser = {
name: "dr. Fajri",
role: "Dokter Mitra",
hospital: "Pramita Bandungraya",
doctorId: "DR-10024",
};
const mockOrders = [
{
id: "ORD-24001",
patient: "Siti Amelia",
doctor: "dr. Fajri",
updated: "2m ago",
status: "Processing",
tone: "warning",
mode: "Inpatient",
age: "34",
gender: "F",
tests: ["Hematology", "Glucose", "Urine"],
diagnosis: "Check-up rutin",
message: "Prioritize fasting sample and note dizziness symptom.",
},
{
id: "ORD-24002",
patient: "Budi Santoso",
doctor: "dr. Fajri",
updated: "14m ago",
status: "Ready",
tone: "success",
mode: "Outpatient",
age: "52",
gender: "M",
tests: ["Lipid Profile", "Liver Function"],
diagnosis: "Kontrol hipertensi",
message: "Call if LDL exceeds threshold.",
},
{
id: "ORD-24003",
patient: "Nia Putri",
doctor: "dr. Fajri",
updated: "40m ago",
status: "Needs review",
tone: "danger",
mode: "Emergency",
age: "27",
gender: "F",
tests: ["Electrolytes", "CBC", "CRP"],
diagnosis: "Dehydration",
message: "Urgent release once CRP is complete.",
},
];
const mockResults = [
{
id: "RES-8821",
patient: "Siti Amelia",
test: "CBC",
status: "Released",
tone: "success",
date: "13 Apr 2026",
summary: "Hemoglobin slightly below baseline.",
value: "11.2 g/dL",
},
{
id: "RES-8822",
patient: "Budi Santoso",
test: "Lipid Profile",
status: "Pending",
tone: "warning",
date: "13 Apr 2026",
summary: "Awaiting final approval.",
value: "LDL 145 mg/dL",
},
{
id: "RES-8823",
patient: "Nia Putri",
test: "CRP",
status: "Review",
tone: "danger",
date: "12 Apr 2026",
summary: "Result flagged for clinical review.",
value: "18.4 mg/L",
},
];
const mockFppGroups = [
{ group: "HEMATOLOGI", count: 6, desc: "Complete blood count and related panels." },
{ group: "KLINIK RUTIN", count: 5, desc: "Daily screening and basic chemistry." },
{ group: "IMUNOLOGI", count: 3, desc: "Inflammation and antibody markers." },
{ group: "URINALISA", count: 4, desc: "Urine screening and microscopic checks." },
];
const mockPatients = [
{ name: "Siti Amelia", mrn: "MRN-10011", gender: "Female", lastVisit: "Today", note: "Active order" },
{ name: "Budi Santoso", mrn: "MRN-10012", gender: "Male", lastVisit: "Yesterday", note: "Repeat visit" },
{ name: "Nia Putri", mrn: "MRN-10013", gender: "Female", lastVisit: "12 Apr", note: "New registration" },
];
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
function statusClass(status) {
const mapping = {
Processing: "warning",
Ready: "success",
"Needs review": "danger",
Released: "success",
Pending: "warning",
Review: "danger",
};
return mapping[status] || "neutral";
}
function statusBadge(text) {
return `<span class="status ${statusClass(text)}">${escapeHtml(text)}</span>`;
}
function icon(name) {
const map = {
search:
'<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"></circle><path d="M20 20l-3.5-3.5"></path></svg>',
plus:
'<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"></path></svg>',
bell:
'<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 17H9m8-4V9a5 5 0 10-10 0v4l-2 3h14l-2-3Z"></path></svg>',
arrow:
'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14m-6-6 6 6-6 6"></path></svg>',
login:
'<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 17l5-5-5-5"></path><path d="M15 12H3"></path><path d="M21 3v18"></path></svg>',
};
return map[name] || "";
}
function layout(title, body, { authenticated = false, activePath = "/", subtitle = "", shell = true } = {}) {
const nav = authenticated ? desktopNav(activePath) : "";
const mobile = authenticated ? mobileNav(activePath) : "";
const header = authenticated ? topbar(activePath, subtitle) : "";
return `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${escapeHtml(title)}</title>
<meta name="description" content="DocLink web rebuild" />
<link rel="stylesheet" href="/styles.css" />
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
</head>
<body>
<div class="bg-orb orb-one"></div>
<div class="bg-orb orb-two"></div>
<div class="bg-grid"></div>
<div id="app">
${shell && authenticated ? `
<div class="page-shell">
${nav}
<main class="content">
<div class="content-inner">
${header}
${body}
</div>
</main>
${mobile}
</div>
` : body}
</div>
<div id="modal-root"></div>
<script>
(function () {
function closeModal() {
var modalRoot = document.getElementById('modal-root');
if (modalRoot) modalRoot.innerHTML = '';
}
document.addEventListener('click', function (event) {
var close = event.target.closest && event.target.closest('[data-modal-close]');
if (!close) return;
event.preventDefault();
closeModal();
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') closeModal();
});
document.addEventListener('htmx:afterSwap', function (event) {
if (event.detail && event.detail.target && event.detail.target.id === 'modal-root') {
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>
</body>
</html>`;
}
function topbar(activePath, subtitle) {
const titleMap = {
"/": ["Dashboard", "Overview of orders, results, and work queues."],
"/orders": ["Orders", "Search, review, and create patient orders."],
"/results": ["Results", "Monitor released and pending laboratory results."],
"/fpp": ["FPP", "Browse grouped reference packages and filters."],
"/patients": ["Patients", "Landing for patient registration and lookup."],
"/settings": ["Settings", "Account profile and password management."],
};
const [title, fallbackSubtitle] = titleMap[activePath] || ["DocLink Web", "Responsive clinical workflow shell."];
const searchForm = activePath === "/orders"
? `
<form class="search" hx-get="/fragments/orders/table" hx-target="#orders-fragment" hx-push-url="true">
${icon("search")}
<input name="search" type="search" placeholder="Search patients, orders, results" />
</form>
`
: activePath === "/results"
? `
<form class="search" hx-get="/fragments/results/table" hx-target="#results-fragment" hx-push-url="true">
${icon("search")}
<input name="search" type="search" placeholder="Search results" />
</form>
`
: `
<a class="btn btn-secondary" href="/orders">Search orders</a>
`;
return `
<header class="topbar">
<div class="topbar-main">
<div class="eyebrow">${escapeHtml(activePath === "/" ? "Clinical dashboard" : "Workflow view")}</div>
<h1 class="page-title">${escapeHtml(title)}</h1>
<p class="page-subtitle">${escapeHtml(subtitle || fallbackSubtitle)}</p>
</div>
<div class="topbar-actions">
${searchForm}
<button class="icon-btn" type="button" data-action="alert" title="Notifications">${icon("bell")}</button>
<a class="btn btn-primary" href="/orders/new">${icon("plus")} New order</a>
</div>
</header>
`;
}
function desktopNav(activePath) {
const items = [
["/", "Home", "Dashboard"],
["/orders", "Order", "Create & list"],
["/results", "Result", "History"],
["/fpp", "FPP", "Catalog"],
["/settings", "Akun", "Profile"],
];
const active = (href) => activePath === href || activePath.startsWith(`${href}/`);
return `
<aside class="sidebar">
<div class="sidebar-card">
<div class="brand">
<div class="brand-mark">DL</div>
<div class="brand-text">
<strong>DocLink Web</strong>
<span>${escapeHtml(mockUser.hospital)}</span>
</div>
</div>
<div class="nav-group">
<div class="nav-label">Workspace</div>
${items
.map(
([href, label, hint]) => `
<a class="nav-link ${active(href) ? "active" : ""}" href="${href}">
<span>${escapeHtml(label)}</span>
<small>${escapeHtml(hint)}</small>
</a>
`,
)
.join("")}
</div>
<div class="nav-group">
<div class="nav-label">Quick access</div>
<a class="nav-link ${activePath === "/settings/change-password" ? "active" : ""}" href="/settings/change-password">
<span>Change password</span>
<small>Security</small>
</a>
<a class="nav-link ${activePath === "/problem-login" ? "active" : ""}" href="/problem-login">
<span>Problem login</span>
<small>Fallback state</small>
</a>
</div>
<div class="sidebar-footer">
<strong>${escapeHtml(mockUser.name)}</strong>
<p>${escapeHtml(mockUser.role)} with access to orders, results, and FPP workflows.</p>
</div>
</div>
</aside>
`;
}
function mobileNav(activePath) {
const items = [
["/", "Home", "Dashboard"],
["/orders", "Order", "Create"],
["/results", "Result", "History"],
["/fpp", "FPP", "Catalog"],
["/settings", "Akun", "Profile"],
];
const active = (href) => activePath === href || activePath.startsWith(`${href}/`);
return `
<nav class="mobile-nav" aria-label="Primary">
<div class="mobile-nav-inner">
${items
.map(
([href, label, hint]) => `
<a class="nav-link ${active(href) ? "active" : ""}" href="${href}">
<span>${escapeHtml(label)}</span>
<small>${escapeHtml(hint)}</small>
</a>
`,
)
.join("")}
</div>
</nav>
`;
}
function panelHeader(title, text, action = "") {
return `
<div class="panel-header">
<div>
<h2>${escapeHtml(title)}</h2>
<p>${escapeHtml(text)}</p>
</div>
<div>${action}</div>
</div>
`;
}
function dashboardPage() {
const stats = [
{ label: "Orders today", value: "28", trend: "+12%", hint: "Vs yesterday" },
{ label: "Results pending", value: "7", trend: "-3%", hint: "Still waiting release" },
{ label: "FPP items", value: "14", trend: "+2", hint: "Active reference set" },
{ label: "Special messages", value: "5", trend: "+1", hint: "Need follow-up" },
];
const shortcuts = [
{ title: "New order", desc: "Start registration flow", href: "/orders/new" },
{ title: "Pending results", desc: "Review unreleased items", href: "/results/pending" },
{ title: "FPP catalog", desc: "Inspect lab groups", href: "/fpp" },
{ title: "Change password", desc: "Update account security", href: "/settings/change-password" },
];
return `
<div class="stack">
<section class="grid grid-4">
${stats
.map(
(item) => `
<article class="card metric">
<div class="kicker">
<span>${escapeHtml(item.label)}</span>
<span class="trend">${escapeHtml(item.trend)}</span>
</div>
<strong>${escapeHtml(item.value)}</strong>
<span class="muted">${escapeHtml(item.hint)}</span>
</article>
`,
)
.join("")}
</section>
<section class="panel">
${panelHeader("Quick actions", "Jump into the common flows without digging through nested screens.")}
<div class="grid grid-4">
${shortcuts
.map(
(item) => `
<a class="card" href="${item.href}">
<strong>${escapeHtml(item.title)}</strong>
<p class="muted">${escapeHtml(item.desc)}</p>
<span class="pill">${icon("arrow")} Open</span>
</a>
`,
)
.join("")}
</div>
</section>
<section class="detail-grid">
<div class="panel">
${panelHeader("Recent orders", "A compact snapshot of the latest patient orders and their state.", '<a class="btn btn-secondary" href="/orders">View all</a>')}
<div class="table-wrap">
<table>
<thead>
<tr><th>Patient</th><th>Order</th><th>Status</th><th>Updated</th></tr>
</thead>
<tbody>
${mockOrders
.map(
(order) => `
<tr>
<td><strong>${escapeHtml(order.patient)}</strong><br /><span class="muted">${escapeHtml(order.mode)}</span></td>
<td><a href="/orders/${order.id}">${escapeHtml(order.id)}</a><br /><span class="muted">${escapeHtml(order.diagnosis)}</span></td>
<td>${statusBadge(order.status)}</td>
<td>${escapeHtml(order.updated)}</td>
</tr>
`,
)
.join("")}
</tbody>
</table>
</div>
</div>
<div class="panel">
${panelHeader("Todays notes", "Items that need attention now, not later.")}
<div class="mini-list">
${[
["FPP ready for release", "Review Hematology group before rounding."],
["Pending sample", "One fasting sample still waiting in the queue."],
["Password rotation", "Prompt user to refresh credentials after 90 days."],
]
.map(
([title, text]) => `
<div class="mini-item">
<div>
<strong>${escapeHtml(title)}</strong>
<p>${escapeHtml(text)}</p>
</div>
<span class="pill">${icon("arrow")}</span>
</div>
`,
)
.join("")}
</div>
</div>
</section>
</div>
`;
}
function renderOrdersTable(orders, selectedOrderId, filter = "All") {
const selected = orders.find((item) => item.id === selectedOrderId) || orders[0] || mockOrders[0];
return `
<div class="detail-grid">
<section class="panel">
${panelHeader("Search orders", "Use the filter to match the old app flow without giving up desktop readability.", '<a class="btn btn-primary" href="/orders/new">Create order</a>')}
<form class="pill-row" hx-get="/fragments/orders/table" hx-target="#orders-fragment" hx-push-url="true">
<input type="hidden" name="status" value="${escapeHtml(filter)}" />
${["All", "Processing", "Ready", "Needs review"]
.map(
(item) => `
<button class="pill ${filter === item ? "active" : ""}" name="status" value="${escapeHtml(item)}" type="submit">${escapeHtml(item)}</button>
`,
)
.join("")}
</form>
<div id="orders-table" class="desktop-only table-wrap" style="margin-top:16px">
<table>
<thead>
<tr><th>Patient</th><th>Order ID</th><th>Doctor</th><th>Status</th><th>Updated</th></tr>
</thead>
<tbody>
${orders
.map(
(order) => `
<tr>
<td><strong>${escapeHtml(order.patient)}</strong><br /><span class="muted">${escapeHtml(order.mode)}</span></td>
<td><a href="/orders/${order.id}">${escapeHtml(order.id)}</a></td>
<td>${escapeHtml(order.doctor)}</td>
<td>${statusBadge(order.status)}</td>
<td>${escapeHtml(order.updated)}</td>
</tr>
`,
)
.join("")}
</tbody>
</table>
</div>
<div class="mobile-only list" style="margin-top:16px">
${orders
.map(
(order) => `
<article class="card">
<div class="topbar-actions" style="justify-content:space-between">
<div>
<strong>${escapeHtml(order.patient)}</strong>
<p class="muted">${escapeHtml(order.id)} · ${escapeHtml(order.mode)}</p>
</div>
${statusBadge(order.status)}
</div>
<div class="pill-row" style="margin-top:12px">
<span class="pill">${escapeHtml(order.doctor)}</span>
<span class="pill">${escapeHtml(order.updated)}</span>
</div>
</article>
`,
)
.join("")}
</div>
</section>
<aside class="panel">
${panelHeader("Selected order", "Master-detail layout keeps context visible while reviewing the list.", `<a class="btn btn-secondary" href="/orders/${selected.id}">Open detail</a>`)}
<div class="stack">
<div class="card">
<strong>${escapeHtml(selected.patient)}</strong>
<p class="muted">${escapeHtml(selected.id)} · ${escapeHtml(selected.mode)}</p>
<div class="pill-row" style="margin-top:12px">
${statusBadge(selected.status)}
<span class="pill">${escapeHtml(selected.age)} years</span>
<span class="pill">${escapeHtml(selected.gender)}</span>
</div>
</div>
<div class="mini-list">
<div class="mini-item"><div><strong>Diagnosis</strong><p>${escapeHtml(selected.diagnosis)}</p></div></div>
<div class="mini-item"><div><strong>Requested tests</strong><p>${escapeHtml(selected.tests.join(", "))}</p></div></div>
<div class="mini-item"><div><strong>Special note</strong><p>${escapeHtml(selected.message)}</p></div></div>
</div>
</div>
</aside>
</div>
`;
}
function renderOrderDetail(order) {
return `
<div class="stack">
<section class="panel">
${panelHeader(
`${order.patient} · ${order.id}`,
"Order detail with the same structure as the proposed master-detail workflow.",
`<button class="btn btn-secondary" type="button" hx-get="/fragments/modals/pesan-khusus?order_id=${escapeHtml(order.id)}" hx-target="#modal-root" hx-swap="innerHTML">Pesan khusus</button>`,
)}
<div class="grid grid-3">
<div class="card"><span class="muted">Status</span><div style="margin-top:8px">${statusBadge(order.status)}</div></div>
<div class="card"><span class="muted">Patient</span><strong style="display:block; margin-top:8px">${escapeHtml(order.patient)}</strong><span class="muted">${escapeHtml(order.mode)} · ${escapeHtml(order.age)} years</span></div>
<div class="card"><span class="muted">Doctor</span><strong style="display:block; margin-top:8px">${escapeHtml(order.doctor)}</strong><span class="muted">${escapeHtml(mockUser.hospital)}</span></div>
</div>
</section>
<section class="detail-grid">
<div class="panel">
${panelHeader("Requested tests", "List/table treatment on desktop, cards on smaller screens.")}
<div class="grid grid-2">
${order.tests
.map(
(test) => `
<div class="card"><strong>${escapeHtml(test)}</strong><p class="muted">Included in the current order bundle.</p></div>
`,
)
.join("")}
</div>
</div>
<aside class="panel">
${panelHeader("Clinical note", "A compact summary for quick review.")}
<div class="note-box">${escapeHtml(order.message)}</div>
${order.apiSaran ? `<div style="height:14px"></div><div class="note-box">${escapeHtml(typeof order.apiSaran === "string" ? order.apiSaran : JSON.stringify(order.apiSaran))}</div>` : ""}
<div style="height:14px"></div>
<div class="mini-list">
<div class="mini-item"><div><strong>Diagnosis</strong><p>${escapeHtml(order.diagnosis)}</p></div></div>
<div class="mini-item"><div><strong>Updated</strong><p>${escapeHtml(order.updated)}</p></div></div>
</div>
</aside>
</section>
</div>
`;
}
function renderOrderForm(step, stepKey = "demografi") {
const steps = [
["demografi", "Demografi", "Patient identity and contact details."],
["diagnosa", "Diagnosa", "Clinical indication and diagnosis."],
["pemeriksaan", "Pemeriksaan", "Choose examination groups."],
["qrcode", "QR Code", "Fast entry for existing records."],
["review", "Review", "Check before submit."],
];
const activeIndex = Math.max(0, steps.findIndex((item) => item[0] === stepKey));
const stepBody = {
demografi: `
<div class="grid grid-2">
<label class="field"><span>Patient name</span><input placeholder="Siti Amelia" /></label>
<label class="field"><span>Medical record no.</span><input placeholder="MRN-10011" /></label>
<label class="field"><span>Gender</span><select><option>Female</option><option>Male</option></select></label>
<label class="field"><span>Date of birth</span><input type="date" /></label>
<label class="field" style="grid-column:1/-1"><span>Address</span><input placeholder="Bandung" /></label>
</div>
`,
diagnosa: `
<div class="grid grid-2">
<label class="field"><span>Diagnosis</span><input placeholder="Kontrol hipertensi" /></label>
<label class="field"><span>Patient note</span><input placeholder="Fasting sample required" /></label>
<label class="field" style="grid-column:1/-1"><span>Clinical complaint</span><textarea placeholder="Add symptoms and context"></textarea></label>
</div>
`,
pemeriksaan: `
<div class="pill-row">
${["Hematology", "Clinical Chemistry", "Urinalysis", "Immunology", "Microbiology"].map((item) => `<span class="pill">${escapeHtml(item)}</span>`).join("")}
</div>
<div style="height:14px"></div>
<div class="grid grid-2">
${["CBC", "Glucose", "Lipid Profile", "CRP"].map((item) => `
<label class="card">
<input type="checkbox" /> ${escapeHtml(item)}
<p class="muted">Selectable test item</p>
</label>
`).join("")}
</div>
`,
qrcode: `
<div class="grid grid-2">
<div class="card">
<strong>Scan existing patient QR</strong>
<p class="muted">Use a QR code to pull patient and visit context instantly.</p>
<div class="note-box">QR preview area</div>
</div>
<div class="card">
<strong>Manual fallback</strong>
<p class="muted">Still allow manual entry if the scan is unavailable.</p>
<label class="field"><span>QR token</span><input placeholder="DOCLINK-QR-0001" /></label>
</div>
</div>
`,
review: `
<div class="stack">
<div class="card"><strong>Summary</strong><p class="muted">All steps are merged into a final review before submit.</p></div>
<div class="grid grid-2">
<div class="mini-item"><div><strong>Patient</strong><p>Siti Amelia</p></div></div>
<div class="mini-item"><div><strong>Diagnosis</strong><p>Check-up rutin</p></div></div>
<div class="mini-item"><div><strong>Tests</strong><p>CBC, Glucose, Urine</p></div></div>
<div class="mini-item"><div><strong>Message</strong><p>Prioritize fasting sample</p></div></div>
</div>
</div>
`,
}[stepKey];
const prev = steps[activeIndex - 1];
const next = steps[activeIndex + 1];
return `
<section class="panel">
${panelHeader("Create new order", "The new project collapses the old step-heavy flow into a cleaner shell while keeping the same workflow.", '<a class="btn btn-secondary" href="/orders">Cancel</a>')}
<div class="stepper">
${steps
.map(
(item, index) => `
<a class="step ${index === activeIndex ? "active" : ""}" href="/orders/new/${item[0]}" hx-get="/fragments/forms/order-step/${item[0]}" hx-target="#order-step-fragment" hx-push-url="true">
<strong>${escapeHtml(item[1])}</strong>
<span>${escapeHtml(item[2])}</span>
</a>
`,
)
.join("")}
</div>
</section>
<section class="panel">
<div class="stack">
${stepBody}
<div class="topbar-actions" style="justify-content: space-between; margin-top: 10px;">
<a class="btn btn-secondary" href="/orders/new/${prev ? prev[0] : "demografi"}">Back</a>
<div class="pill-row">
<a class="btn btn-ghost" href="/orders">Save draft</a>
<a class="btn btn-primary" href="/orders/new/${next ? next[0] : "review"}">${next ? "Continue" : "Submit order"}</a>
</div>
</div>
</div>
</section>
`;
}
function renderResultsTable(results, selectedResultId) {
const selected = results.find((item) => item.id === selectedResultId) || results[0] || mockResults[0];
return `
<div class="detail-grid">
<section class="panel">
${panelHeader("Result history", "Desktop shows table detail. Mobile collapses into stacked cards.")}
<div class="grid grid-3" style="margin-bottom:16px">
<div class="card"><strong>Released</strong><p class="muted">24 today</p></div>
<div class="card"><strong>Pending</strong><p class="muted">7 need approval</p></div>
<div class="card"><strong>Reviewed</strong><p class="muted">12 flagged</p></div>
</div>
<div id="results-table" class="desktop-only table-wrap">
<table>
<thead>
<tr><th>Patient</th><th>Result ID</th><th>Test</th><th>Status</th><th>Date</th></tr>
</thead>
<tbody>
${results
.map(
(result) => `
<tr>
<td><strong>${escapeHtml(result.patient)}</strong><br /><span class="muted">${escapeHtml(result.summary)}</span></td>
<td><a href="/results/${result.id}" hx-get="/fragments/results/detail/${result.id}" hx-target="#result-detail-fragment" hx-swap="innerHTML">${escapeHtml(result.id)}</a></td>
<td>${escapeHtml(result.test)}</td>
<td>${statusBadge(result.status)}</td>
<td>${escapeHtml(result.date)}</td>
</tr>
`,
)
.join("")}
</tbody>
</table>
</div>
<div class="mobile-only list" style="margin-top:16px">
${results
.map(
(result) => `
<article class="card">
<div class="topbar-actions" style="justify-content:space-between">
<div>
<strong>${escapeHtml(result.patient)}</strong>
<p class="muted">${escapeHtml(result.test)} · ${escapeHtml(result.date)}</p>
</div>
${statusBadge(result.status)}
</div>
<div style="height:12px"></div>
<a class="btn btn-secondary" href="/results/${result.id}" hx-get="/fragments/results/detail/${result.id}" hx-target="#result-detail-fragment" hx-swap="innerHTML">Open detail</a>
<p class="muted" style="margin-top:10px">${escapeHtml(result.summary)}</p>
</article>
`,
)
.join("")}
</div>
</section>
<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">
<strong>${escapeHtml(selected.patient)}</strong>
<p class="muted">${escapeHtml(selected.test)} · ${escapeHtml(selected.id)}</p>
<div style="margin-top:12px">${statusBadge(selected.status)}</div>
</div>
<div class="note-box">${escapeHtml(selected.summary)}</div>
<div class="mini-item"><div><strong>Value</strong><p>${escapeHtml(selected.value)}</p></div></div>
</div>
</aside>
</div>
`;
}
function renderResultDetail(result) {
return `
<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>
<div class="card"><span class="muted">Test</span><strong style="display:block; margin-top:8px">${escapeHtml(result.test)}</strong></div>
<div class="card"><span class="muted">Value</span><strong style="display:block; margin-top:8px">${escapeHtml(result.value)}</strong></div>
</div>
<div style="height:18px"></div>
<div class="detail-grid">
<div class="panel" style="padding:0; box-shadow:none; background:transparent; border:0">
<div class="note-box">${escapeHtml(result.summary)}</div>
</div>
<aside class="panel">
<div class="mini-list">
<div class="mini-item"><div><strong>Date</strong><p>${escapeHtml(result.date)}</p></div></div>
<div class="mini-item"><div><strong>Interpretation</strong><p>Borderline value requires doctor review.</p></div></div>
<div class="mini-item"><div><strong>Action</strong><p>Keep in pending state until confirmed.</p></div></div>
</div>
</aside>
</div>
</section>
`;
}
function renderFpp(groups) {
return `
<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">
${["All", ...mockFppGroups.map((item) => item.group)]
.map(
(item) => `
<button
class="pill ${groups.filter === item ? "active" : ""}"
type="button"
hx-get="/fragments/fpp/list?group=${encodeURIComponent(item)}"
hx-target="#fpp-fragment"
hx-swap="innerHTML"
hx-push-url="true"
>
${escapeHtml(item)}
</button>
`,
)
.join("")}
</div>
</section>
<section class="grid grid-2">
${groups.items
.map(
(item) => `
<article class="panel">
<div class="panel-header">
<div>
<h3>${escapeHtml(item.group)}</h3>
<p>${escapeHtml(item.desc)}</p>
</div>
<span class="pill">${escapeHtml(item.count)} items</span>
</div>
<div class="grid grid-2">
${["Filter", "Inspect", "Select", "Export"]
.map(
(action) => `
<div class="card">
<strong>${escapeHtml(action)}</strong>
<p class="muted">Workflow action for ${escapeHtml(item.group)}.</p>
</div>
`,
)
.join("")}
</div>
</article>
`,
)
.join("")}
</section>
</div>
`;
}
function renderPatients() {
return `
<div class="stack">
<section class="panel">
${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(
([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("")}
</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>
`;
}
function renderSettings() {
return `
<div class="detail-grid">
<section class="panel">
${panelHeader("Account", "Profile data and security entry points.")}
<div class="grid grid-2">
<div class="card"><span class="muted">Name</span><strong style="display:block; margin-top:8px">${escapeHtml(mockUser.name)}</strong></div>
<div class="card"><span class="muted">Doctor ID</span><strong style="display:block; margin-top:8px">${escapeHtml(mockUser.doctorId)}</strong></div>
<div class="card"><span class="muted">Role</span><strong style="display:block; margin-top:8px">${escapeHtml(mockUser.role)}</strong></div>
<div class="card"><span class="muted">Hospital</span><strong style="display:block; margin-top:8px">${escapeHtml(mockUser.hospital)}</strong></div>
</div>
</section>
<aside class="panel">
${panelHeader("Settings actions", "Keep the common account actions obvious.")}
<div class="stack">
<a class="card" href="/settings/change-password">
<strong>Change password</strong>
<p class="muted">Simple form in both mobile and desktop layouts.</p>
</a>
<form action="/logout" method="post">
<button class="card" type="submit" style="text-align:left; width:100%">
<strong>Logout</strong>
<p class="muted">Clear the local session and return to login.</p>
</button>
</form>
</div>
</aside>
</div>
`;
}
function renderChangePassword() {
return `
<section class="panel">
${panelHeader("Change password", "Inline validation and a straightforward submit path.")}
<form class="form" action="/settings/change-password" method="post">
<div class="field-inline">
<label class="field"><span>Current password</span><input type="password" name="current_password" placeholder="••••••••" required /></label>
<label class="field"><span>New password</span><input type="password" name="new_password" placeholder="At least 8 characters" required /></label>
</div>
<div class="field-inline">
<label class="field"><span>Confirm password</span><input type="password" name="confirm_password" placeholder="Repeat new password" required /></label>
<label class="field"><span>Doctor ID</span><input name="doctor_id" value="${escapeHtml(mockUser.doctorId)}" readonly /></label>
</div>
<div class="topbar-actions" style="justify-content:flex-start">
<button class="btn btn-primary" type="submit">Save password</button>
<a class="btn btn-secondary" href="/settings">Cancel</a>
</div>
</form>
</section>
`;
}
function loginPage({ error = "" } = {}) {
return layout(
"DocLink Login",
`
<main class="auth-screen">
<section class="auth-card">
<div class="auth-visual">
<h1>DocLink Pramita</h1>
</div>
<div class="auth-form">
${error ? `<div class="note-box">${escapeHtml(error)}</div>` : ""}
<form class="form" action="/login" method="post">
<label class="field"><span>Username</span><input name="username" placeholder="dr.fajri" required /></label>
<label class="field"><span>Doctor ID</span><input name="doctor_id" placeholder="DR-10024" required /></label>
<label class="field"><span>Password</span><input type="password" name="password" placeholder="Enter password" required /></label>
<button class="btn btn-primary" type="submit">${icon("login")} Login</button>
</form>
</div>
</section>
</main>
`,
{ authenticated: false, shell: false, activePath: "/login" },
);
}
function splashPage() {
return layout(
"DocLink Splash",
`
<main class="auth-screen">
<section class="auth-card">
<div class="auth-visual">
<h1>DocLink Pramita</h1>
</div>
<div class="auth-form">
<div class="empty-state">
<strong>Redirecting</strong>
<p>We will move you to the correct route in a moment.</p>
</div>
</div>
</section>
</main>
`,
{ authenticated: false, shell: false, activePath: "/splash" },
);
}
function problemLoginPage() {
return layout(
"Problem Login",
`
<main class="auth-screen">
<section class="auth-card">
<div class="auth-visual">
<h1>DocLink Pramita</h1>
</div>
<div class="auth-form">
<div class="empty-state">
<strong>Session expired</strong>
<p>The session was cleared. Go back to login and sign in again.</p>
<div style="height:16px"></div>
<a class="btn btn-primary" href="/login">Back to login</a>
</div>
</div>
</section>
</main>
`,
{ authenticated: false, shell: false, activePath: "/problem-login" },
);
}
function orderNewPage(path) {
const step = path.split("/").filter(Boolean)[2] || "demografi";
return layout("Create Order", `<div id="order-step-fragment">${renderOrderForm({}, step)}</div>`, {
authenticated: true,
activePath: "/orders",
subtitle: "The new project collapses the old step-heavy flow into a cleaner shell while keeping the same workflow.",
});
}
function ordersPage({ query = {}, orders = mockOrders, selectedOrderId = mockOrders[0].id } = {}) {
return layout("Orders", `<div id="orders-fragment">${renderOrdersTable(orders, selectedOrderId, query.status || "All")}</div>`, {
authenticated: true,
activePath: "/orders",
});
}
function resultsPage({ query = {}, results = mockResults, selectedResultId = mockResults[0].id } = {}) {
return layout("Results", `<div id="results-fragment">${renderResultsTable(results, selectedResultId)}</div>`, {
authenticated: true,
activePath: "/results",
});
}
function fppPage({ group = "All", groups = mockFppGroups } = {}) {
return layout("FPP", `<div id="fpp-fragment">${renderFpp({ items: groups, filter: group })}</div>`, {
authenticated: true,
activePath: "/fpp",
});
}
function patientsPage() {
return layout("Patients", renderPatients(), { authenticated: true, activePath: "/patients" });
}
function settingsPage() {
return layout("Settings", renderSettings(), { authenticated: true, activePath: "/settings" });
}
function changePasswordPage() {
return layout("Change Password", renderChangePassword(), {
authenticated: true,
activePath: "/settings/change-password",
});
}
function orderDetailPage(order) {
return layout("Order Detail", renderOrderDetail(order), {
authenticated: true,
activePath: "/orders",
});
}
function resultDetailPage(result) {
return layout("Result Detail", renderResultDetail(result), {
authenticated: true,
activePath: "/results",
});
}
function specialMessagePage(order) {
return layout(
"Pesan Khusus",
`
<section class="panel">
${panelHeader("Pesan khusus", "Desktop can treat this as a modal-style panel; mobile reads it as a dedicated page.", `<a class="btn btn-secondary" href="/orders/${order.id}">Back</a>`)}
<div class="detail-grid">
<div class="card">
<strong>${escapeHtml(order.patient)}</strong>
<p class="muted">${escapeHtml(order.id)}</p>
<div style="margin-top:12px">${statusBadge(order.status)}</div>
<p style="margin-top:14px" class="muted">${escapeHtml(order.message)}</p>
${order.apiSaran ? `<div class="note-box" style="margin-top:14px">${escapeHtml(typeof order.apiSaran === "string" ? order.apiSaran : JSON.stringify(order.apiSaran))}</div>` : ""}
</div>
<form class="form card" action="/orders/${order.id}/pesan-khusus" method="post">
<label class="field">
<span>Special message</span>
<textarea name="pesan_khusus" placeholder="Type a special note for this order..." required>${escapeHtml(order.message)}</textarea>
</label>
<div class="topbar-actions" style="justify-content:flex-start">
<button class="btn btn-primary" type="submit">Save message</button>
<a class="btn btn-secondary" href="/orders/${order.id}">Cancel</a>
</div>
</form>
</div>
</section>
`,
{ authenticated: true, activePath: "/orders" },
);
}
function emptyRoute(path) {
return layout("Not Found", `<section class="panel"><div class="empty-state"><strong>Route not found</strong><p>${escapeHtml(path)} is not part of the current rebuild scope.</p><div style="height:14px"></div><a class="btn btn-primary" href="/">Go to dashboard</a></div></section>`, {
authenticated: true,
activePath: "/",
});
}
function cookieHeader(req) {
return req.headers.cookie || "";
}
function getCookie(req, name) {
const cookies = cookieHeader(req)
.split(";")
.map((part) => part.trim())
.filter(Boolean);
for (const item of cookies) {
const [key, ...rest] = item.split("=");
if (key === name) return decodeURIComponent(rest.join("="));
}
return "";
}
function setCookie(name, value, opts = {}) {
const parts = [`${name}=${encodeURIComponent(value)}`, "Path=/", "HttpOnly", "SameSite=Lax"];
if (opts.maxAge != null) parts.push(`Max-Age=${opts.maxAge}`);
if (opts.secure) parts.push("Secure");
return parts.join("; ");
}
function deleteCookie(name) {
return `${name}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
}
function readSession(req) {
const raw = getCookie(req, sessionKey);
if (!raw) return null;
try {
return JSON.parse(raw);
} catch {
return null;
}
}
function requireAuth(req, res) {
const session = readSession(req);
if (session) return session;
redirect(res, "/login");
return null;
}
async function fetchJson(url, options = {}) {
const response = await fetch(url, options);
const contentType = response.headers.get("content-type") || "";
const body = contentType.includes("application/json") ? await response.json() : await response.text();
if (!response.ok) {
const error = new Error(`Upstream error ${response.status}`);
error.status = response.status;
error.body = body;
throw error;
}
return body;
}
async function apiPost(path, payload, token = "") {
const headers = { "Content-Type": "application/json" };
if (token) {
headers.Authorization = `Bearer ${token}`;
payload.token = payload.token || token;
}
const url = `${API_BASE}${path}`;
return fetchJson(url, {
method: "POST",
headers,
body: JSON.stringify(payload),
});
}
function normalizeSession(payload) {
const data = payload?.data || payload?.result || payload;
const token = data?.token || data?.access_token || data?.accessToken || payload?.token || "";
const username = data?.username || data?.M_UserUsername || data?.name || "";
const doctorId = data?.doctor_id || data?.M_UserID || data?.doctorId || "";
return {
token,
username,
doctorId,
raw: payload,
};
}
function extractArray(payload) {
if (Array.isArray(payload)) return payload;
if (!payload || typeof payload !== "object") return null;
for (const key of ["data", "result", "results", "items", "list"]) {
const value = payload[key];
const found = extractArray(value);
if (found) return found;
}
return null;
}
function normalizeOrder(raw, index = 0) {
const testsSource = raw?.details || raw?.tests || raw?.items || raw?.order_details || [];
const tests = Array.isArray(testsSource)
? testsSource
.map((item) => {
if (typeof item === "string") return item;
return item?.name || item?.test_name || item?.detail_name || item?.exam || item?.label || "";
})
.filter(Boolean)
: [];
const status = raw?.status || raw?.order_status || raw?.state || "Processing";
return {
id:
raw?.order_patient_id ||
raw?.order_id ||
raw?.id ||
raw?.OrderPatientID ||
raw?.OrderID ||
`ORD-${String(index + 1).padStart(5, "0")}`,
patient: raw?.patient_name || raw?.name || raw?.patient || raw?.patient_fullname || "Patient",
doctor: raw?.doctor_name || raw?.doctor || raw?.M_UserUsername || mockUser.name,
updated: raw?.updated_at || raw?.updated || raw?.created_at || "Today",
status,
tone: statusClass(status),
mode: raw?.mode || raw?.visit_type || raw?.patient_type || "Outpatient",
age: String(raw?.age || raw?.patient_age || ""),
gender: raw?.gender || raw?.patient_gender || "",
tests,
diagnosis: raw?.patient_diagnosa || raw?.diagnosis || raw?.note || "",
message: raw?.message || raw?.patient_note || "",
};
}
function normalizeResult(raw, index = 0) {
const status = raw?.status || raw?.result_status || "Released";
return {
id:
raw?.result_id ||
raw?.order_id ||
raw?.id ||
raw?.hasil_id ||
`RES-${String(index + 1).padStart(4, "0")}`,
patient: raw?.patient_name || raw?.name || raw?.patient || "Patient",
test: raw?.test_name || raw?.item_name || raw?.test || "Result",
status,
tone: statusClass(status),
date: raw?.date || raw?.created_at || raw?.updated_at || "Today",
summary: raw?.summary || raw?.note || raw?.result_summary || "",
value: raw?.value || raw?.result_value || raw?.result || "",
};
}
async function loadOrders(session, search = "", status = "All") {
const term = String(search || "").trim();
try {
const payload = await apiPost(
"/order/search_order_pasien_by_doktorid",
{
token: session.token,
OrderPatientM_DoctorID: session.doctorId || mockUser.doctorId,
search: term,
},
session.token,
);
const rows = extractArray(payload) || [];
const orders = rows.map((row, index) => normalizeOrder(row, index)).filter((item) => {
const matchesSearch =
!term ||
[item.id, item.patient, item.status, item.diagnosis, item.message].some((value) =>
String(value).toLowerCase().includes(term.toLowerCase()),
);
const matchesStatus = !status || status === "All" || item.status === status;
return matchesSearch && matchesStatus;
});
if (orders.length) return orders;
} catch {
// Fall through to mock data.
}
return filterOrders(term, status);
}
async function loadResults(session, search = "") {
const term = String(search || "").trim();
try {
const payload = await apiPost(
"/order/hasil_belum_keluar_by_id",
{ token: session.token, order_id: session.orderId || "" },
session.token,
);
const rows = extractArray(payload) || [];
const results = rows.map((row, index) => normalizeResult(row, index)).filter((item) => {
if (!term) return true;
return [item.id, item.patient, item.test, item.status, item.summary].some((value) =>
String(value).toLowerCase().includes(term.toLowerCase()),
);
});
if (results.length) return results;
} catch {
// Fall through to mock data.
}
return filterResults(term);
}
async function loadResultDetail(session, resultId) {
const fallback = mockResults.find((item) => item.id === resultId) || mockResults[0];
try {
// Inferred from project-specs note about an additional result base path.
const payload = await apiPost(
"/result/getResult",
{
token: session.token,
result_id: resultId,
order_id: resultId,
},
session.token,
);
const rows = extractArray(payload);
if (rows?.length) return normalizeResult(rows[0], 0);
if (payload && typeof payload === "object") {
return {
...fallback,
...normalizeResult(payload, 0),
id: resultId || fallback.id,
};
}
} catch {
// Fall through to local seed data when the upstream result API is unavailable.
}
return fallback;
}
async function loadFpp(session, group = "All") {
try {
const payload = await apiPost("/Fpp/load/1/1", { token: session.token }, session.token);
const rows = extractArray(payload) || [];
const items = rows.map((row, index) => ({
group: row?.group || row?.kategori || row?.name || `GROUP-${index + 1}`,
count: Number(row?.count || row?.total || row?.qty || 1),
desc: row?.description || row?.desc || "Laboratory reference item.",
}));
if (items.length) {
const filtered = group === "All" ? items : items.filter((item) => item.group === group);
return { items: filtered, filter: group };
}
} catch {
// Fall through to mock data.
}
const items = group === "All" ? mockFppGroups : mockFppGroups.filter((item) => item.group === group);
return { items, filter: group };
}
async function loadOrderDetail(session, orderId) {
const order = mockOrders.find((item) => item.id === orderId) || mockOrders[0];
const detail = { ...order };
try {
const saran = await apiPost(
"/order/get_order_saran_by_order_patient_id",
{ token: session.token, order_patient_id: orderId },
session.token,
);
detail.apiSaran = extractArray(saran) || saran?.message || saran?.note || saran?.result || "";
} catch {
detail.apiSaran = "";
}
try {
const hasil = await apiPost(
"/order/hasil_belum_keluar_by_id",
{ token: session.token, order_id: orderId },
session.token,
);
detail.apiHasil = extractArray(hasil) || hasil?.message || hasil?.note || hasil?.result || "";
} catch {
detail.apiHasil = "";
}
return detail;
}
function json(res, status, payload) {
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify(payload));
}
function redirect(res, location, headers = {}) {
res.writeHead(302, { Location: location, ...headers });
res.end();
}
function html(res, status, body, headers = {}) {
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", ...headers });
res.end(body);
}
function isHtmx(req) {
return req.headers["hx-request"] === "true";
}
async function readBody(req) {
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const raw = Buffer.concat(chunks).toString("utf8");
const contentType = req.headers["content-type"] || "";
if (contentType.includes("application/json")) return raw ? JSON.parse(raw) : {};
return Object.fromEntries(new URLSearchParams(raw));
}
function filterOrders(search, status) {
const term = String(search || "").trim().toLowerCase();
return mockOrders.filter((order) => {
const matchesSearch =
!term ||
[order.id, order.patient, order.status, order.diagnosis, order.message].some((value) =>
String(value).toLowerCase().includes(term),
);
const matchesStatus = !status || status === "All" || order.status === status;
return matchesSearch && matchesStatus;
});
}
function filterResults(search) {
const term = String(search || "").trim().toLowerCase();
return mockResults.filter((result) => {
if (!term) return true;
return [result.id, result.patient, result.test, result.status, result.summary].some((value) =>
String(value).toLowerCase().includes(term),
);
});
}
function fragmentOrdersTable(search = "", status = "All", ordersData = null) {
const orders = ordersData || filterOrders(search, status);
const selected = orders[0] || mockOrders[0];
return `
<div class="detail-grid">
<section class="panel">
${panelHeader("Search orders", "Filter the list without full page refresh.")}
<form class="pill-row" hx-get="/fragments/orders/table" hx-target="#orders-fragment" hx-push-url="true">
<input type="hidden" name="search" value="${escapeHtml(search)}" />
${["All", "Processing", "Ready", "Needs review"]
.map(
(item) => `
<button class="pill ${status === item ? "active" : ""}" name="status" value="${escapeHtml(item)}" type="submit">${escapeHtml(item)}</button>
`,
)
.join("")}
</form>
<div id="orders-table" class="table-wrap" style="margin-top:16px">
<table>
<thead>
<tr><th>Patient</th><th>Order ID</th><th>Doctor</th><th>Status</th><th>Updated</th></tr>
</thead>
<tbody>
${orders
.map(
(order) => `
<tr>
<td><strong>${escapeHtml(order.patient)}</strong><br /><span class="muted">${escapeHtml(order.mode)}</span></td>
<td><a href="/orders/${order.id}">${escapeHtml(order.id)}</a></td>
<td>${escapeHtml(order.doctor)}</td>
<td>${statusBadge(order.status)}</td>
<td>${escapeHtml(order.updated)}</td>
</tr>
`,
)
.join("")}
</tbody>
</table>
</div>
</section>
<aside class="panel">
${panelHeader("Selected order", "Context stays visible while you review the table.", `<a class="btn btn-secondary" href="/orders/${selected.id}">Open detail</a>`)}
<div class="stack">
<div class="card">
<strong>${escapeHtml(selected.patient)}</strong>
<p class="muted">${escapeHtml(selected.id)} · ${escapeHtml(selected.mode)}</p>
<div class="pill-row" style="margin-top:12px">${statusBadge(selected.status)}<span class="pill">${escapeHtml(selected.age)} years</span><span class="pill">${escapeHtml(selected.gender)}</span></div>
</div>
<div class="mini-list">
<div class="mini-item"><div><strong>Diagnosis</strong><p>${escapeHtml(selected.diagnosis)}</p></div></div>
<div class="mini-item"><div><strong>Requested tests</strong><p>${escapeHtml(selected.tests.join(", "))}</p></div></div>
<div class="mini-item"><div><strong>Special note</strong><p>${escapeHtml(selected.message)}</p></div></div>
</div>
</div>
</aside>
</div>
`;
}
function fragmentResultsTable(search = "", resultsData = null) {
const results = resultsData || filterResults(search);
const selected = results[0] || mockResults[0];
return `
<div class="detail-grid">
<section class="panel">
${panelHeader("Result history", "Desktop shows table detail. Mobile collapses into stacked cards.")}
<div class="grid grid-3" style="margin-bottom:16px">
<div class="card"><strong>Released</strong><p class="muted">24 today</p></div>
<div class="card"><strong>Pending</strong><p class="muted">7 need approval</p></div>
<div class="card"><strong>Reviewed</strong><p class="muted">12 flagged</p></div>
</div>
<div id="results-table" class="table-wrap">
<table>
<thead>
<tr><th>Patient</th><th>Result ID</th><th>Test</th><th>Status</th><th>Date</th></tr>
</thead>
<tbody>
${results
.map(
(result) => `
<tr>
<td><strong>${escapeHtml(result.patient)}</strong><br /><span class="muted">${escapeHtml(result.summary)}</span></td>
<td><a href="/results/${result.id}">${escapeHtml(result.id)}</a></td>
<td>${escapeHtml(result.test)}</td>
<td>${statusBadge(result.status)}</td>
<td>${escapeHtml(result.date)}</td>
</tr>
`,
)
.join("")}
</tbody>
</table>
</div>
</section>
<aside class="panel">
${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">
<strong>${escapeHtml(selected.patient)}</strong>
<p class="muted">${escapeHtml(selected.test)} · ${escapeHtml(selected.id)}</p>
<div style="margin-top:12px">${statusBadge(selected.status)}</div>
</div>
<div class="note-box">${escapeHtml(selected.summary)}</div>
<div class="mini-item"><div><strong>Value</strong><p>${escapeHtml(selected.value)}</p></div></div>
</div>
</aside>
</div>
`;
}
function fragmentFpp(group = "All", itemsData = null) {
const items = itemsData || (group === "All" ? mockFppGroups : mockFppGroups.filter((item) => item.group === group));
return `
<div class="stack">
<section class="panel">
${panelHeader("FPP catalog", "Filter blocks and list cards on mobile, richer panel on desktop.")}
<div class="pill-row">
${["All", ...mockFppGroups.map((item) => item.group)]
.map(
(item) => `
<a class="pill ${group === item ? "active" : ""}" href="/fpp?group=${encodeURIComponent(item)}">${escapeHtml(item)}</a>
`,
)
.join("")}
</div>
</section>
<section class="grid grid-2">
${items
.map(
(item) => `
<article class="panel">
<div class="panel-header">
<div>
<h3>${escapeHtml(item.group)}</h3>
<p>${escapeHtml(item.desc)}</p>
</div>
<span class="pill">${escapeHtml(item.count)} items</span>
</div>
<div class="grid grid-2">
${["Filter", "Inspect", "Select", "Export"]
.map(
(action) => `
<div class="card">
<strong>${escapeHtml(action)}</strong>
<p class="muted">Workflow action for ${escapeHtml(item.group)}.</p>
</div>
`,
)
.join("")}
</div>
</article>
`,
)
.join("")}
</section>
</div>
`;
}
function fragmentOrderStep(step) {
return renderOrderForm({}, step);
}
function fragmentPesanKhusus(orderId) {
const order = mockOrders.find((item) => item.id === orderId) || mockOrders[0];
return `
<div class="modal-shell" role="dialog" aria-modal="true" aria-labelledby="pesan-khusus-title">
<div class="modal-backdrop" data-modal-close></div>
<section class="modal-card panel">
${panelHeader("Pesan khusus", "Desktop opens this as a modal, mobile can still use it as a full sheet.", `<button class="btn btn-secondary" type="button" data-modal-close>Close</button>`)}
<div class="detail-grid">
<div class="card">
<strong>${escapeHtml(order.patient)}</strong>
<p class="muted">${escapeHtml(order.id)}</p>
<div style="margin-top:12px">${statusBadge(order.status)}</div>
<p style="margin-top:14px" class="muted">${escapeHtml(order.message)}</p>
</div>
<form class="form card" action="/orders/${order.id}/pesan-khusus" method="post">
<label class="field">
<span>Special message</span>
<textarea name="pesan_khusus" placeholder="Type a special note for this order..." required>${escapeHtml(order.message)}</textarea>
</label>
<div class="topbar-actions" style="justify-content:flex-start">
<button class="btn btn-primary" type="submit">Save message</button>
<button class="btn btn-secondary" type="button" data-modal-close>Cancel</button>
</div>
</form>
</div>
</section>
</div>
`;
}
async function fragmentResultDetail(session, resultId) {
const result = await loadResultDetail(session, resultId);
return `
<div id="result-detail-fragment">
${renderResultDetail(result)}
</div>
`;
}
async function renderRoute(req, res, url) {
const path = url.pathname;
const query = Object.fromEntries(url.searchParams.entries());
const session = readSession(req);
const authed = Boolean(session);
const isGet = req.method === "GET" || req.method === "HEAD";
if (path === "/styles.css") {
const css = await readFile(new URL("./styles.css", import.meta.url), "utf8");
res.writeHead(200, { "Content-Type": "text/css; charset=utf-8" });
res.end(css);
return;
}
if (path === "/login" && isGet) {
html(res, 200, loginPage(), { "Cache-Control": "no-store" });
return;
}
if (path === "/splash" && isGet) {
html(res, 200, splashPage(), { "Cache-Control": "no-store" });
return;
}
if (path === "/problem-login" && isGet) {
html(res, 200, problemLoginPage(), { "Cache-Control": "no-store" });
return;
}
if (path === "/login" && req.method === "POST") {
const body = await readBody(req);
try {
const payload = await apiPost("/auth/login", body);
const normalized = normalizeSession(payload);
const upstreamFailed = String(payload?.status || payload?.result_status || "").toUpperCase() === "ERR" || !normalized.token;
if (upstreamFailed) {
throw new Error(payload?.message || "Invalid login response");
}
const sessionValue = JSON.stringify({
token: normalized.token,
username: normalized.username || body.username || "",
doctorId: normalized.doctorId || body.doctor_id || "",
raw: payload,
});
redirect(res, "/", {
"Set-Cookie": setCookie(sessionKey, sessionValue, { maxAge: 60 * 60 * 12 }),
});
} catch (error) {
if (process.env.DOCLINK_DEMO_MODE !== "0") {
const sessionValue = JSON.stringify({
token: "demo-token",
username: body.username || mockUser.name,
doctorId: body.doctor_id || mockUser.doctorId,
raw: { demo: true },
});
redirect(res, "/", {
"Set-Cookie": setCookie(sessionKey, sessionValue, { maxAge: 60 * 60 * 12 }),
});
} else {
html(res, 200, loginPage({ error: "Login failed or upstream API unavailable. Check credentials and network." }));
}
}
return;
}
if (path === "/logout" && req.method === "POST") {
const sessionData = readSession(req);
if (sessionData?.token) {
try {
await apiPost(
"/auth/logout",
{
M_UserID: sessionData.doctorId || mockUser.doctorId,
M_UserUsername: sessionData.username || mockUser.name,
},
sessionData.token,
);
} catch {
// Ignore upstream logout failures and still clear local session.
}
}
redirect(res, "/login", { "Set-Cookie": deleteCookie(sessionKey) });
return;
}
if (path === "/settings/change-password" && req.method === "POST") {
const sessionData = requireAuth(req, res);
if (!sessionData) return;
const body = await readBody(req);
try {
await apiPost(
"/auth/change_password",
{
token: sessionData.token,
M_UserID: sessionData.doctorId || mockUser.doctorId,
username: sessionData.username || mockUser.name,
doctor_id: sessionData.doctorId || mockUser.doctorId,
new_password: body.new_password,
confirm_password: body.confirm_password,
},
sessionData.token,
);
redirect(res, "/settings");
} catch (error) {
html(
res,
200,
layout(
"Change Password",
`<section class="panel">${panelHeader("Change password", "Inline validation and a straightforward submit path.")}<div class="note-box">Upstream change password call failed. Check API availability.</div></section>`,
{ authenticated: true, activePath: "/settings/change-password" },
),
);
}
return;
}
if (path === "/orders/new" && isGet) {
if (!requireAuth(req, res)) return;
html(res, 200, orderNewPage(path));
return;
}
if (path.startsWith("/orders/new/") && isGet) {
if (!requireAuth(req, res)) return;
html(res, 200, orderNewPage(path));
return;
}
if (path === "/orders" && isGet) {
if (!requireAuth(req, res)) return;
const orders = await loadOrders(session, query.search || "", query.status || "All");
html(res, 200, ordersPage({ query, orders, selectedOrderId: orders[0]?.id || mockOrders[0].id }));
return;
}
if (path === "/results" && isGet) {
if (!requireAuth(req, res)) return;
const results = await loadResults(session, query.search || "");
html(res, 200, resultsPage({ query, results, selectedResultId: results[0]?.id || mockResults[0].id }));
return;
}
if (path === "/results/historical" && isGet) {
if (!requireAuth(req, res)) return;
html(
res,
200,
layout(
"Historical results",
`<section class="panel">${panelHeader("Historical results", "A compact historic view that still fits the responsive shell.")}<div class="mini-list">${mockResults.map((item) => `<a class="mini-item" href="/results/${item.id}"><div><strong>${escapeHtml(item.patient)}</strong><p>${escapeHtml(item.test)} · ${escapeHtml(item.date)}</p></div>${statusBadge(item.status)}</a>`).join("")}</div></section>`,
{ authenticated: true, activePath: "/results" },
),
);
return;
}
if (path === "/results/pending" && isGet) {
if (!requireAuth(req, res)) return;
html(
res,
200,
layout(
"Pending results",
`<section class="panel">${panelHeader("Pending results", "Items that need attention before release.")}<div class="grid grid-2">${mockResults.filter((item) => item.status !== "Released").map((item) => `<article class="card"><div class="topbar-actions" style="justify-content:space-between"><strong>${escapeHtml(item.patient)}</strong>${statusBadge(item.status)}</div><p class="muted">${escapeHtml(item.test)} · ${escapeHtml(item.summary)}</p><a class="btn btn-secondary" href="/results/${item.id}">Open detail</a></article>`).join("")}</div></section>`,
{ authenticated: true, activePath: "/results" },
),
);
return;
}
if (path === "/fpp" && isGet) {
if (!requireAuth(req, res)) return;
const groups = await loadFpp(session, query.group || "All");
html(res, 200, fppPage({ group: groups.filter, groups: groups.items }));
return;
}
if (path === "/patients" && isGet) {
if (!requireAuth(req, res)) return;
html(res, 200, patientsPage());
return;
}
if ((path === "/settings" || path === "/settings/account") && isGet) {
if (!requireAuth(req, res)) return;
html(res, 200, settingsPage());
return;
}
if (path === "/settings/change-password" && isGet) {
if (!requireAuth(req, res)) return;
html(res, 200, changePasswordPage());
return;
}
if (path === "/" && isGet) {
if (!requireAuth(req, res)) return;
html(res, 200, layout("Dashboard", dashboardPage(), { authenticated: true, activePath: "/" }));
return;
}
if (path.startsWith("/orders/") && path.endsWith("/pesan-khusus") && isGet) {
if (!requireAuth(req, res)) return;
const orderId = path.split("/")[2];
const order = await loadOrderDetail(session, orderId);
html(res, 200, specialMessagePage(order));
return;
}
if (path.startsWith("/orders/") && isGet) {
if (!requireAuth(req, res)) return;
const orderId = path.split("/")[2];
const order = await loadOrderDetail(session, orderId);
html(res, 200, orderDetailPage(order));
return;
}
if (path.startsWith("/results/") && isGet) {
if (!requireAuth(req, res)) return;
const resultId = path.split("/")[2];
const result = await loadResultDetail(session, resultId);
html(res, 200, resultDetailPage(result));
return;
}
if (path === "/fragments/orders/table" && isGet) {
if (!requireAuth(req, res)) return;
const orders = await loadOrders(session, query.search || "", query.status || "All");
html(res, 200, fragmentOrdersTable(query.search || "", query.status || "All", orders), { "HX-Trigger": JSON.stringify({ "doclink:orders-updated": true }) });
return;
}
if (path === "/fragments/results/table" && isGet) {
if (!requireAuth(req, res)) return;
const results = await loadResults(session, query.search || "");
html(res, 200, fragmentResultsTable(query.search || "", results));
return;
}
if (path === "/fragments/fpp/list" && isGet) {
if (!requireAuth(req, res)) return;
const groups = await loadFpp(session, query.group || "All");
html(res, 200, fragmentFpp(groups.filter || "All", groups.items));
return;
}
if (path.startsWith("/fragments/forms/order-step/") && isGet) {
if (!requireAuth(req, res)) return;
const step = path.split("/")[4] || "demografi";
html(res, 200, fragmentOrderStep(step));
return;
}
if (path === "/fragments/modals/pesan-khusus" && isGet) {
if (!requireAuth(req, res)) return;
html(res, 200, fragmentPesanKhusus(query.order_id || mockOrders[0].id));
return;
}
if (path.startsWith("/fragments/results/detail/") && isGet) {
if (!requireAuth(req, res)) return;
const resultId = path.split("/")[4] || mockResults[0].id;
html(res, 200, await fragmentResultDetail(session, resultId));
return;
}
if (path.startsWith("/api/") && isGet) {
json(res, 200, { ok: true });
return;
}
if (path === "/orders" && req.method === "POST") {
const sessionData = requireAuth(req, res);
if (!sessionData) return;
const body = await readBody(req);
try {
await apiPost(
"/order/order_patient",
{
token: sessionData.token,
M_MouID: body.M_MouID || mockUser.doctorId,
patient_name: body.patient_name || "",
patient_diagnosa: body.patient_diagnosa || "",
patient_address: body.patient_address || "",
patient_nik: body.patient_nik || "",
patient_hp: body.patient_hp || "",
patient_dob: body.patient_dob || "",
patient_note: body.patient_note || "",
details: body.details || [],
},
sessionData.token,
);
redirect(res, "/orders");
} catch {
html(
res,
200,
layout(
"Create Order",
`<section class="panel">${panelHeader("Create new order", "The new project collapses the old step-heavy flow into a cleaner shell while keeping the same workflow.")}<div class="note-box">Upstream order create call failed. Check API availability.</div></section>`,
{ authenticated: true, activePath: "/orders" },
),
);
}
return;
}
if (path.startsWith("/orders/") && path.endsWith("/pesan-khusus") && req.method === "POST") {
const sessionData = requireAuth(req, res);
if (!sessionData) return;
const orderId = path.split("/")[2];
const body = await readBody(req);
try {
await apiPost(
"/Pesankhusus/add_pesan_khusus",
{
token: sessionData.token,
order_id: orderId,
pesan_khusus: body.pesan_khusus,
},
sessionData.token,
);
redirect(res, `/orders/${orderId}`);
} catch {
html(
res,
200,
layout(
"Pesan Khusus",
`<section class="panel">${panelHeader("Pesan khusus", "Desktop can treat this as a modal-style panel; mobile reads it as a dedicated page.")}<div class="note-box">Upstream save failed. Check API availability.</div></section>`,
{ authenticated: true, activePath: "/orders" },
),
);
}
return;
}
html(res, 404, emptyRoute(path));
}
const server = http.createServer(async (req, res) => {
try {
const url = new URL(req.url, `http://${req.headers.host}`);
await renderRoute(req, res, url);
} catch (error) {
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
res.end(`Internal error: ${error.message}`);
}
});
server.listen(PORT, () => {
console.log(`DocLink Web running on http://localhost:${PORT}`);
});