Files
doclink_web/server.js
2026-04-13 15:34:20 +07:00

1958 lines
79 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 sampleLogin = {
username: "yogayogi",
doctorId: "31010002",
password: "123456",
};
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 emptyState(title, text, action = "") {
return `
<div class="empty-state">
<strong>${escapeHtml(title)}</strong>
<p>${escapeHtml(text)}</p>
${action}
</div>
`;
}
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 Pramita</strong>
<span>Workflow shell</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>DocLink Pramita</strong>
<p>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>
`;
}
async function dashboardPage(session) {
const [orders, results, fpp] = await Promise.all([
loadOrders(session, "", "All"),
loadResults(session, ""),
loadFpp(session, "All"),
]);
const stats = [
{ label: "Orders today", value: String(orders.length), trend: "Live", hint: "From API response" },
{ label: "Results pending", value: String(results.filter((item) => item.status !== "Released").length), trend: "Live", hint: "From API response" },
{ label: "FPP items", value: String(fpp.items.length), trend: "Live", hint: "From API response" },
{ label: "Special messages", value: String(orders.filter((item) => Boolean(item.message)).length), trend: "Live", hint: "From API response" },
];
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>
${orders.length
? orders
.slice(0, 5)
.map(
(order) => `
<tr>
<td><strong>${escapeHtml(order.patient || "Unknown 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("")
: `<tr><td colspan="4">${emptyState("No orders returned", "The order endpoint did not return any rows for this session.")}</td></tr>`}
</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] || null;
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>
${
orders.length
? `
<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 || "Unknown 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 || "Unknown 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>
`
: emptyState("No orders returned", "The order endpoint did not return any rows for this filter.")
}
</section>
<aside class="panel">
${panelHeader("Selected order", "Master-detail layout keeps context visible while reviewing the list.", selected ? `<a class="btn btn-secondary" href="/orders/${selected.id}">Open detail</a>` : "")}
${
selected
? `
<div class="stack">
<div class="card">
<strong>${escapeHtml(selected.patient || "Unknown patient")}</strong>
<p class="muted">${escapeHtml(selected.id || "-")} · ${escapeHtml(selected.updated || "-")}</p>
<div class="pill-row" style="margin-top:12px">
${statusBadge(selected.status)}
<span class="pill">Doctor ${escapeHtml(selected.doctor || "-")}</span>
<span class="pill">${escapeHtml(selected.orderDate || selected.updated || "-")}</span>
</div>
</div>
<div class="mini-list">
<div class="mini-item"><div><strong>NIK</strong><p>${escapeHtml(selected.orderNik || selected.diagnosis || "-")}</p></div></div>
<div class="mini-item"><div><strong>Phone</strong><p>${escapeHtml(selected.orderHp || "-")}</p></div></div>
<div class="mini-item"><div><strong>Address</strong><p>${escapeHtml(selected.orderAddress || selected.message || "-")}</p></div></div>
</div>
</div>
`
: emptyState("No selected order", "The API returned no rows for this search.")
}
</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.updated || "-")}</span></div>
<div class="card"><span class="muted">Doctor ID</span><strong style="display:block; margin-top:8px">${escapeHtml(order.doctor || "-")}</strong><span class="muted">Pramita Bandungraya</span></div>
</div>
</section>
<section class="detail-grid">
<div class="panel">
${panelHeader("Order details", "The source API returns identity fields, so the card copies stay aligned with the payload.")}
<div class="grid grid-2">
<div class="card"><strong>Order date</strong><p class="muted">${escapeHtml(order.orderDate || order.updated || "-")}</p></div>
<div class="card"><strong>NIK</strong><p class="muted">${escapeHtml(order.orderNik || "-")}</p></div>
<div class="card"><strong>Phone</strong><p class="muted">${escapeHtml(order.orderHp || "-")}</p></div>
<div class="card"><strong>Address</strong><p class="muted">${escapeHtml(order.orderAddress || "-")}</p></div>
</div>
</div>
<aside class="panel">
${panelHeader("Clinical note", "The payload doesn't expose test bundle details, so this view stays identity-focused.")}
<div class="note-box">${escapeHtml(order.orderAddress || "-")}</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>Order ID</strong><p>${escapeHtml(order.id || "-")}</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] || null;
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">${results.filter((item) => item.status === "Released").length} items</p></div>
<div class="card"><strong>Pending</strong><p class="muted">${results.filter((item) => item.status === "Pending").length} items</p></div>
<div class="card"><strong>Reviewed</strong><p class="muted">${results.filter((item) => item.status === "Review").length} items</p></div>
</div>
${
results.length
? `
<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 || "Unknown 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 || "Unknown 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>
`
: emptyState("No results returned", "The result endpoint did not return any rows for this search.")
}
</section>
<aside class="panel swap-zone" id="result-detail-fragment">
${panelHeader("Selected result", "Use the right pane for quick context without losing list position.", selected ? `<a class="btn btn-secondary" href="/results/${selected.id}">Open detail</a>` : "")}
${
selected
? `
<div class="stack">
<div class="card">
<strong>${escapeHtml(selected.patient || "Unknown 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>
`
: emptyState("No selected result", "The API returned no rows for this search.")
}
</aside>
</div>
`;
}
function renderResultDetail(result) {
if (!result) {
return `
<section class="panel swap-zone">
${panelHeader("Result detail", "The API returned no matching result.", '<a class="btn btn-secondary" href="/results">Back to results</a>')}
${emptyState("No result found", "No detail payload was returned for this result ID.")}
</section>
`;
}
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) {
const groupNames = ["All", ...Array.from(new Set(groups.items.map((item) => item.group)))];
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">
${groupNames
.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.length
? 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 class="panel">${emptyState("No FPP items", "The API returned no catalog rows for this filter.")}</section>`
}
</section>
</div>
`;
}
async function renderPatients(session) {
const orders = await loadOrders(session, "", "All");
const recentPatients = Array.from(
new Map(orders.filter((order) => order.patient).map((order) => [order.patient, order])).values(),
).slice(0, 4);
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", String(orders.length), "From API response"],
["Active visits", String(orders.filter((item) => item.status === "Processing").length), "From API response"],
["QR scans", String(orders.filter((item) => String(item.mode).toLowerCase().includes("qr")).length), "From API response"],
["Needs review", String(orders.filter((item) => item.status === "Needs review").length), "From API response"],
]
.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.")}
${
recentPatients.length
? `
<div class="mini-list">
${recentPatients
.map(
(person, index) => `
<div class="mini-item">
<div>
<strong>${escapeHtml(person.patient || "Unknown patient")}</strong>
<p>${escapeHtml(person.id || `Order ${index + 1}`)} · ${escapeHtml(person.gender || "-")}</p>
</div>
<span class="pill">${escapeHtml(person.updated || "Recent")}</span>
</div>
`,
)
.join("")}
</div>
`
: emptyState("No patients returned", "The order endpoint did not return any patient rows.")
}
</aside>
</section>
</div>
`;
}
function renderSettings(session) {
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(session?.username || "-")}</strong></div>
<div class="card"><span class="muted">Doctor ID</span><strong style="display:block; margin-top:8px">${escapeHtml(session?.doctorCode || session?.doctorId || "-")}</strong></div>
<div class="card"><span class="muted">Role</span><strong style="display:block; margin-top:8px">Doctor</strong></div>
<div class="card"><span class="muted">Internal ID</span><strong style="display:block; margin-top:8px">${escapeHtml(session?.doctorId || "-")}</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(session) {
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(session?.doctorCode || session?.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" value="${escapeHtml(sampleLogin.username)}" required /></label>
<label class="field"><span>Doctor ID</span><input name="doctor_id" value="${escapeHtml(sampleLogin.doctorId)}" required /></label>
<label class="field"><span>Password</span><input type="password" name="password" placeholder="${escapeHtml(sampleLogin.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 = [], selectedOrderId = "" } = {}) {
return layout("Orders", `<div id="orders-fragment">${renderOrdersTable(orders, selectedOrderId, query.status || "All")}</div>`, {
authenticated: true,
activePath: "/orders",
});
}
function resultsPage({ query = {}, results = [], selectedResultId = "" } = {}) {
return layout("Results", `<div id="results-fragment">${renderResultsTable(results, selectedResultId)}</div>`, {
authenticated: true,
activePath: "/results",
});
}
function fppPage({ group = "All", groups = [] } = {}) {
return layout("FPP", `<div id="fpp-fragment">${renderFpp({ items: groups, filter: group })}</div>`, {
authenticated: true,
activePath: "/fpp",
});
}
async function patientsPage(session) {
return layout("Patients", await renderPatients(session), { authenticated: true, activePath: "/patients" });
}
function settingsPage(session) {
return layout("Settings", renderSettings(session), { authenticated: true, activePath: "/settings" });
}
function changePasswordPage(session) {
return layout("Change Password", renderChangePassword(session), {
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 text = await response.text();
let body = text;
if (contentType.includes("application/json") || /^\s*[\[{]/.test(text)) {
try {
body = JSON.parse(text);
} catch {
body = 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 user = data?.user || data?.data || {};
const token = data?.token || data?.access_token || data?.accessToken || payload?.token || "";
const username = user?.M_UserUsername || data?.username || data?.M_UserUsername || data?.name || "";
const doctorId = user?.M_UserM_DoctorID || data?.doctor_id || data?.doctorId || "";
const doctorCode = user?.M_UserM_DoctorCode || data?.doctor_code || "";
const userId = user?.M_UserID || data?.M_UserID || data?.user_id || "";
return {
token,
username,
doctorId,
doctorCode,
userId,
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 status = raw?.status || raw?.order_status || raw?.state || "Processing";
return {
id: raw?.order_patient_id || raw?.order_id || raw?.id || raw?.OrderPatientID || raw?.OrderID || "",
patient: raw?.order_name || raw?.patient_name || raw?.name || raw?.patient || raw?.patient_fullname || "",
doctor: raw?.doctor_id || raw?.doctor_name || raw?.doctor || raw?.M_UserUsername || "",
updated: raw?.order_date || raw?.updated_at || raw?.updated || raw?.created_at || "",
status,
tone: statusClass(status),
mode: raw?.mode || raw?.visit_type || raw?.patient_type || "",
age: String(raw?.age || raw?.patient_age || ""),
gender: raw?.gender || raw?.patient_gender || "",
tests: [],
diagnosis: raw?.order_nik || raw?.patient_diagnosa || raw?.diagnosis || raw?.note || "",
message: raw?.order_address || raw?.message || raw?.patient_note || "",
orderDate: raw?.order_date || "",
orderNik: raw?.order_nik || "",
orderHp: raw?.order_hp || "",
orderAddress: raw?.order_address || "",
orderDob: raw?.order_dob || "",
};
}
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 || "",
patient: raw?.patient_name || raw?.name || raw?.patient || "",
test: raw?.test_name || raw?.item_name || raw?.test || "",
status,
tone: statusClass(status),
date: raw?.date || raw?.created_at || raw?.updated_at || "",
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 || sampleLogin.doctorId,
search: term,
},
session.token,
);
const rows = extractArray(payload) || [];
return rows.map((row, index) => normalizeOrder(row, index)).filter((item) => {
if (!item.id) return false;
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;
});
} catch {
return [];
}
return [];
}
async function loadResults(session, search = "") {
const term = String(search || "").trim();
try {
const orders = await loadOrders(session, "", "All");
const orderId = session.orderId || orders[0]?.id || "";
const payload = await apiPost(
"/order/hasil_belum_keluar_by_id",
{ token: session.token, order_id: orderId },
session.token,
);
const rows = extractArray(payload) || [];
return rows.map((row, index) => normalizeResult(row, index)).filter((item) => {
if (!item.id) return false;
if (!term) return true;
return [item.id, item.patient, item.test, item.status, item.summary].some((value) =>
String(value).toLowerCase().includes(term.toLowerCase()),
);
});
} catch {
return [];
}
return [];
}
async function loadResultDetail(session, resultId) {
try {
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 normalizeResult(payload, 0);
const related = await loadResults(session, resultId);
return related.find((item) => item.id === resultId) || null;
} catch {
return null;
}
}
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.",
}));
const filtered = group === "All" ? items : items.filter((item) => item.group === group);
return { items: filtered, filter: group };
} catch {
return { items: [], filter: group };
}
}
async function loadOrderDetail(session, orderId) {
const [orders, saran, hasil] = await Promise.all([
loadOrders(session, "", "All"),
apiPost(
"/order/get_order_saran_by_order_patient_id",
{ token: session.token, order_patient_id: orderId },
session.token,
).catch(() => null),
apiPost(
"/order/hasil_belum_keluar_by_id",
{ token: session.token, order_id: orderId },
session.token,
).catch(() => null),
]);
const order = orders.find((item) => item.id === orderId) || orders[0] || null;
if (!order) return null;
const detail = { ...order };
try {
detail.apiSaran = saran ? extractArray(saran) || saran?.message || saran?.note || saran?.result || "" : "";
detail.apiHasil = hasil ? extractArray(hasil) || hasil?.message || hasil?.note || hasil?.result || "" : "";
} catch {
detail.apiSaran = "";
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 fragmentOrdersTable(search = "", status = "All", ordersData = null) {
const orders = ordersData || [];
const selected = orders[0] || null;
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>
${
orders.length
? `
<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 || "Unknown 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>
`
: emptyState("No orders returned", "The order endpoint did not return any rows for this filter.")
}
</section>
<aside class="panel">
${panelHeader("Selected order", "Context stays visible while you review the table.", selected ? `<a class="btn btn-secondary" href="/orders/${selected.id}">Open detail</a>` : "")}
${
selected
? `
<div class="stack">
<div class="card">
<strong>${escapeHtml(selected.patient || "Unknown 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>
`
: emptyState("No selected order", "The API returned no rows for this search.")
}
</aside>
</div>
`;
}
function fragmentResultsTable(search = "", resultsData = null) {
const results = resultsData || [];
const selected = results[0] || null;
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">${results.filter((item) => item.status === "Released").length} items</p></div>
<div class="card"><strong>Pending</strong><p class="muted">${results.filter((item) => item.status === "Pending").length} items</p></div>
<div class="card"><strong>Reviewed</strong><p class="muted">${results.filter((item) => item.status === "Review").length} items</p></div>
</div>
${
results.length
? `
<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 || "Unknown 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>
`
: emptyState("No results returned", "The result endpoint did not return any rows for this search.")
}
</section>
<aside class="panel">
${panelHeader("Selected result", "Use the right pane for quick context without losing list position.", selected ? `<a class="btn btn-secondary" href="/results/${selected.id}">Open detail</a>` : "")}
${
selected
? `
<div class="stack">
<div class="card">
<strong>${escapeHtml(selected.patient || "Unknown 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>
`
: emptyState("No selected result", "The API returned no rows for this search.")
}
</aside>
</div>
`;
}
function fragmentFpp(group = "All", itemsData = null) {
const items = itemsData || [];
const groups = ["All", ...Array.from(new Set(items.map((item) => item.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">
${groups
.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.length
? 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 class="panel">${emptyState("No FPP items", "The API returned no catalog rows for this filter.")}</section>`
}
</section>
</div>
`;
}
function fragmentOrderStep(step) {
return renderOrderForm({}, step);
}
function fragmentPesanKhusus(orderId) {
const order = orderId
? { id: orderId, patient: "", status: "Processing", message: "", apiSaran: "" }
: { id: "", patient: "", status: "Processing", message: "", apiSaran: "" };
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 || "Order detail")}</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 || "",
doctorCode: normalized.doctorCode || body.doctor_id || "",
userId: normalized.userId || "",
raw: payload,
});
redirect(res, "/", {
"Set-Cookie": setCookie(sessionKey, sessionValue, { maxAge: 60 * 60 * 12 }),
});
} catch (error) {
html(res, 200, loginPage({ error: `Login failed: ${error.message}. 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.userId || "",
M_UserUsername: sessionData.username || sampleLogin.username,
},
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.userId || "",
username: sessionData.username || sampleLogin.username,
doctor_id: sessionData.doctorCode || sessionData.doctorId || sampleLogin.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 || "" }));
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 || "" }));
return;
}
if (path === "/results/historical" && isGet) {
if (!requireAuth(req, res)) return;
const results = await loadResults(session, "");
html(res, 200, layout("Historical results", `<section class="panel">${panelHeader("Historical results", "A compact historic view that still fits the responsive shell.")}${results.length ? `<div class="mini-list">${results.map((item) => `<a class="mini-item" href="/results/${item.id}"><div><strong>${escapeHtml(item.patient || "Unknown patient")}</strong><p>${escapeHtml(item.test || "-")} · ${escapeHtml(item.date || "-")}</p></div>${statusBadge(item.status)}</a>`).join("")}</div>` : emptyState("No results returned", "The API returned no historical result rows.")}</section>`, { authenticated: true, activePath: "/results" }));
return;
}
if (path === "/results/pending" && isGet) {
if (!requireAuth(req, res)) return;
const results = (await loadResults(session, "")).filter((item) => item.status !== "Released");
html(res, 200, layout("Pending results", `<section class="panel">${panelHeader("Pending results", "Items that need attention before release.")}${results.length ? `<div class="grid grid-2">${results.map((item) => `<article class="card"><div class="topbar-actions" style="justify-content:space-between"><strong>${escapeHtml(item.patient || "Unknown 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>` : emptyState("No pending results", "The API returned no unreleased rows.")}</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, await patientsPage(session));
return;
}
if ((path === "/settings" || path === "/settings/account") && isGet) {
if (!requireAuth(req, res)) return;
html(res, 200, settingsPage(session));
return;
}
if (path === "/settings/change-password" && isGet) {
if (!requireAuth(req, res)) return;
html(res, 200, changePasswordPage(session));
return;
}
if (path === "/" && isGet) {
if (!requireAuth(req, res)) return;
html(res, 200, layout("Dashboard", await dashboardPage(session), { 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);
if (!order) {
html(res, 200, layout("Pesan Khusus", `<section class="panel">${emptyState("No order found", "The API returned no matching order for this ID.")}</section>`, { authenticated: true, activePath: "/orders" }));
return;
}
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);
if (!order) {
html(res, 200, layout("Order Detail", `<section class="panel">${emptyState("No order found", "The API returned no matching order for this ID.")}</section>`, { authenticated: true, activePath: "/orders" }));
return;
}
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);
if (!result) {
html(res, 200, layout("Result Detail", `<section class="panel">${emptyState("No result found", "The API returned no matching result for this ID.")}</section>`, { authenticated: true, activePath: "/results" }));
return;
}
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 || ""));
return;
}
if (path.startsWith("/fragments/results/detail/") && isGet) {
if (!requireAuth(req, res)) return;
const resultId = path.split("/")[4] || "";
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 || sessionData.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}`);
});