Files
doclink_web/server.js
2026-04-14 06:47:40 +07:00

2624 lines
110 KiB
JavaScript

import http from "node:http";
import { readFile } from "node:fs/promises";
import { extname } from "node:path";
const PORT = Number(process.env.PORT || 5173);
const BASE_PATH = normalizeBasePath(process.env.DOCLINK_BASE_PATH || "");
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",
mouId: "2773",
password: "123456",
};
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
function normalizeBasePath(value) {
const raw = String(value || "").trim();
if (!raw || raw === "/") return "";
const withLeadingSlash = raw.startsWith("/") ? raw : `/${raw}`;
return withLeadingSlash.replace(/\/+$/, "");
}
function appPath(pathname = "/") {
const text = String(pathname || "/");
if (/^https?:\/\//i.test(text) || text.startsWith("//")) return text;
if (!BASE_PATH) return text.startsWith("/") ? text : `/${text}`;
if (text === BASE_PATH || text.startsWith(`${BASE_PATH}/`)) return text;
if (text === "/" || text === "") return BASE_PATH;
if (text.startsWith("/")) return `${BASE_PATH}${text}`;
return `${BASE_PATH}/${text}`;
}
function stripBasePath(pathname) {
const path = String(pathname || "/");
if (!BASE_PATH) return path || "/";
if (path === BASE_PATH) return "/";
if (path.startsWith(`${BASE_PATH}/`)) return path.slice(BASE_PATH.length) || "/";
return path;
}
function rewriteHtmlPaths(html) {
if (!BASE_PATH || typeof html !== "string") return html;
return html.replace(/\b(href|src|action|hx-get|hx-post)="\/(?!\/)/g, `$1="${BASE_PATH}/`);
}
function statusClass(status) {
const mapping = {
Processing: "warning",
Ready: "success",
"Needs review": "danger",
Released: "success",
Pending: "warning",
Review: "danger",
Confirmed: "success",
Unconfirmed: "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, accountName = "", accountMeta = "" } = {}) {
const nav = authenticated ? desktopNav(activePath) : "";
const mobile = authenticated ? mobileNav(activePath) : "";
const header = authenticated ? topbar(activePath, subtitle, accountName, accountMeta) : "";
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 () {
var ORDER_DRAFT_KEY = 'doclink.orderDraft';
function readOrderDraft() {
try {
var raw = sessionStorage.getItem(ORDER_DRAFT_KEY);
var draft = raw ? JSON.parse(raw) : {};
if (!draft || typeof draft !== 'object') return { fields: {}, details: [] };
if (!draft.fields || typeof draft.fields !== 'object') draft.fields = {};
if (!Array.isArray(draft.details)) draft.details = [];
return draft;
} catch (error) {
return { fields: {}, details: [] };
}
}
function writeOrderDraft(draft) {
sessionStorage.setItem(ORDER_DRAFT_KEY, JSON.stringify(draft));
}
function isOrderField(element) {
return element && element.closest && element.closest('[data-order-form]');
}
function updateSelectedCount(form) {
if (!form) return;
var target = form.querySelector('#order-selected-count');
if (!target) return;
var draft = readOrderDraft();
target.textContent = (draft.details || []).length + ' selected';
}
function syncOrderDraft(form) {
if (!form) return;
var draft = readOrderDraft();
form.querySelectorAll('input[name], textarea[name], select[name]').forEach(function (field) {
if (!field.name) return;
if (field.classList && field.classList.contains('fpp-select-toggle')) return;
if (field.type === 'button' || field.type === 'submit' || field.type === 'reset') return;
if (field.type === 'radio' && !field.checked) return;
if (field.type === 'checkbox') {
draft.fields[field.name] = field.checked ? '1' : '0';
return;
}
draft.fields[field.name] = field.value;
});
var details = [];
form.querySelectorAll('.fpp-select-toggle:checked').forEach(function (checkbox) {
details.push({
testId: checkbox.getAttribute('data-test-id') || '',
testName: checkbox.getAttribute('data-test-name') || '',
price: checkbox.getAttribute('data-test-price') || '0',
heading: checkbox.getAttribute('data-test-heading') || '',
subcategory: checkbox.getAttribute('data-test-subcategory') || '',
});
});
draft.details = details;
writeOrderDraft(draft);
updateSelectedCount(form);
}
function restoreOrderForm(form) {
if (!form) return;
var draft = readOrderDraft();
Object.keys(draft.fields || {}).forEach(function (name) {
var fields = form.querySelectorAll('[name="' + name.replace(/"/g, '\\"') + '"]');
if (!fields.length) return;
fields.forEach(function (field) {
if (field.classList && field.classList.contains('fpp-select-toggle')) return;
if (field.type === 'checkbox') {
field.checked = draft.fields[name] === '1' || draft.fields[name] === 'true';
return;
}
if (field.type === 'radio') {
field.checked = field.value === draft.fields[name];
return;
}
field.value = draft.fields[name];
});
});
var selectedIds = new Set((draft.details || []).map(function (item) { return item.testId; }));
form.querySelectorAll('.fpp-select-toggle').forEach(function (checkbox) {
checkbox.checked = selectedIds.has(checkbox.getAttribute('data-test-id') || '');
});
updateSelectedCount(form);
}
function injectOrderDraftPayload(form) {
var draft = readOrderDraft();
var payload = form.querySelector('[data-order-draft-payload]');
if (payload) payload.remove();
payload = document.createElement('div');
payload.hidden = true;
payload.setAttribute('data-order-draft-payload', 'true');
var currentNames = new Set();
form.querySelectorAll('input[name], textarea[name], select[name]').forEach(function (field) {
if (field.name) currentNames.add(field.name);
});
var mouId = form.getAttribute('data-mou-id') || '';
if (mouId && !currentNames.has('M_MouID')) {
var mouInput = document.createElement('input');
mouInput.type = 'hidden';
mouInput.name = 'M_MouID';
mouInput.value = mouId;
payload.appendChild(mouInput);
}
Object.keys(draft.fields || {}).forEach(function (name) {
if (currentNames.has(name)) return;
var value = draft.fields[name];
var input = document.createElement('input');
input.type = 'hidden';
input.name = name;
input.value = value == null ? '' : String(value);
payload.appendChild(input);
});
(draft.details || []).forEach(function (detail, index) {
var fields = {
test_id: detail.testId || '',
test_name: detail.testName || '',
price: detail.price || '0',
};
Object.keys(fields).forEach(function (key) {
var input = document.createElement('input');
input.type = 'hidden';
input.name = 'details[' + index + '][' + key + ']';
input.value = String(fields[key] || '');
payload.appendChild(input);
});
});
form.appendChild(payload);
}
function closeModal() {
var modalRoot = document.getElementById('modal-root');
if (modalRoot) modalRoot.innerHTML = '';
}
function normalizeDateForInput(value) {
var text = String(value || '').trim();
if (!text || text === 'null' || text === 'undefined') return '';
var iso = text.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);
if (iso) return iso[0];
var slash = text.match(/^(\\d{2})\\/(\\d{2})\\/(\\d{4})$/);
if (slash) return slash[3] + '-' + slash[2] + '-' + slash[1];
var dash = text.match(/^(\\d{2})-(\\d{2})-(\\d{4})$/);
if (dash) return dash[3] + '-' + dash[2] + '-' + dash[1];
return '';
}
function fillPatientFromPick(button) {
var form = document.querySelector('[data-order-form]');
if (!form || !button) return;
var fields = {
patient_name: button.getAttribute('data-patient-name') || '',
patient_dob: normalizeDateForInput(button.getAttribute('data-patient-dob') || ''),
patient_nik: button.getAttribute('data-patient-nik') || '',
patient_hp: button.getAttribute('data-patient-hp') || '',
patient_address: button.getAttribute('data-patient-address') || '',
};
Object.keys(fields).forEach(function (name) {
var field = form.querySelector('[name="' + name.replace(/"/g, '\\"') + '"]');
if (!field) return;
field.value = fields[name];
field.dispatchEvent(new Event('input', { bubbles: true }));
field.dispatchEvent(new Event('change', { bubbles: true }));
});
syncOrderDraft(form);
closeModal();
}
document.addEventListener('input', function (event) {
var form = isOrderField(event.target);
if (!form) return;
syncOrderDraft(form);
});
document.addEventListener('change', function (event) {
var form = isOrderField(event.target);
if (!form) return;
syncOrderDraft(form);
});
document.addEventListener('submit', function (event) {
var form = event.target;
if (!(form instanceof HTMLFormElement) || !form.matches('[data-order-form]')) return;
var step = form.getAttribute('data-order-step') || '';
if (step !== 'pemeriksaan') {
event.preventDefault();
return;
}
var draft = readOrderDraft();
if (!draft.details || !draft.details.length) {
event.preventDefault();
return;
}
injectOrderDraftPayload(form);
});
document.addEventListener('click', function (event) {
var patientPick = event.target.closest && event.target.closest('[data-patient-pick]');
if (patientPick) {
event.preventDefault();
fillPatientFromPick(patientPick);
return;
}
var printTrigger = event.target.closest && event.target.closest('[data-action="print-order"]');
if (printTrigger) {
event.preventDefault();
document.body.classList.add('print-order');
window.setTimeout(function () {
window.print();
}, 50);
return;
}
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();
});
window.addEventListener('afterprint', function () {
document.body.classList.remove('print-order');
});
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.id === 'order-step-fragment') {
var form = document.querySelector('[data-order-form]');
if (form) restoreOrderForm(form);
}
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);
}
});
var orderForm = document.querySelector('[data-order-form]');
if (orderForm) restoreOrderForm(orderForm);
if (document.querySelector('[data-order-saved="true"]')) {
sessionStorage.removeItem(ORDER_DRAFT_KEY);
}
})();
</script>
</body>
</html>`;
}
function topbar(activePath, subtitle, accountName = "") {
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 displayName = accountName || sampleLogin.username;
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">
<a class="btn btn-primary" href="/orders/new">${icon("plus")} New order</a>
<a class="btn btn-secondary account-chip" href="/settings">
<span class="account-avatar" aria-hidden="true">DL</span>
<span class="account-copy">
<strong>${escapeHtml(displayName)}</strong>
</span>
</a>
</div>
</header>
`;
}
function desktopNav(activePath) {
const items = [
["/", "Home", "Dashboard"],
["/orders", "Order", "Create & list"],
["/results", "Result", "History"],
];
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>
</div>
<div class="sidebar-footer sidebar-footer-logo">
<img src="/logo.png" alt="Pramita Lab" class="sidebar-logo-image" />
</div>
</div>
</aside>
`;
}
function mobileNav(activePath) {
const items = [
["/", "Home", "Dashboard"],
["/orders", "Order", "Create"],
["/results", "Result", "History"],
];
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 resolveFppRouteIds(session) {
return {
doctorId: String(session?.doctorId || sampleLogin.doctorId || "1"),
mouId: String(session?.mouId || sampleLogin.mouId || "1"),
};
}
function accountLayoutOptions(session = {}) {
return {
accountName: session?.displayName || session?.username || sampleLogin.username,
};
}
async function dashboardPage(session) {
const orders = await loadDesktopOrders(session, { currentPage: 1, month: new Date().getMonth() + 1, year: new Date().getFullYear() });
const stats = [
{ label: "Orders this month", value: String(orders.items.length), trend: `${orders.month}/${orders.year}`, hint: "" },
{ label: "Confirmed", value: String(orders.items.filter((item) => item.status === "Confirmed").length), trend: "Home", hint: "" },
{ label: "Unconfirmed", value: String(orders.items.filter((item) => item.status === "Unconfirmed").length), trend: "Home", hint: "" },
];
return `
<div class="stack">
<section class="grid grid-3">
${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>
${item.hint ? `<span class="muted">${escapeHtml(item.hint)}</span>` : ""}
</article>
`,
)
.join("")}
</section>
<section class="panel">
${panelHeader("Recent orders", "", '<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.items || []).length
? orders.items
.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 || "Unconfirmed")}</td>
<td>${escapeHtml(order.updated || "-")}</td>
</tr>
`,
)
.join("")
: `<tr><td colspan="4">${emptyState("No orders returned", "The desktop order endpoint did not return any rows for this page.")}</td></tr>`}
</tbody>
</table>
</div>
</section>
</div>
`;
}
function renderOrdersTable({
items = [],
search = "",
month = "",
year = "",
currentPage = 1,
hasNext = false,
hasPrev = false,
selectedOrderId = "",
} = {}) {
const now = new Date();
const resolvedMonth = String(month || now.getMonth() + 1).padStart(2, "0");
const resolvedYear = String(year || now.getFullYear());
const monthOptions = Array.from({ length: 12 }, (_, index) => {
const value = String(index + 1).padStart(2, "0");
return `<option value="${value}" ${value === resolvedMonth ? "selected" : ""}>${value}</option>`;
}).join("");
const yearStart = Number(resolvedYear) - 1;
const yearOptions = Array.from({ length: 3 }, (_, index) => {
const value = String(yearStart + index);
return `<option value="${value}" ${value === resolvedYear ? "selected" : ""}>${value}</option>`;
}).join("");
const prevPage = Math.max(1, Number(currentPage) - 1);
const nextPage = Math.max(1, Number(currentPage) + 1);
const selected = items.find((item) => item.id === selectedOrderId) || items[0] || null;
const selectedId = selected?.id || "";
return `
<div class="detail-grid">
<section class="panel">
${panelHeader("Orders", "Monthly order list from the desktop endpoint with name, month, and year filters.", '<a class="btn btn-primary" href="/orders/new">Create order</a>')}
<form class="card" hx-get="/fragments/orders/table" hx-target="#orders-fragment" hx-push-url="true">
<div class="grid grid-3">
<label class="field"><span>Search patient</span><input name="search" type="search" value="${escapeHtml(search)}" placeholder="ANDI SETADI" /></label>
<label class="field"><span>Month</span><select name="month">${monthOptions}</select></label>
<label class="field"><span>Year</span><select name="year">${yearOptions}</select></label>
</div>
<input type="hidden" name="current_page" value="1" />
<input type="hidden" name="selected" value="${escapeHtml(selectedId)}" />
<div class="topbar-actions" style="justify-content:flex-end; margin-top:14px">
<button class="btn btn-primary" type="submit">Apply filter</button>
</div>
</form>
${
items.length
? `
<div class="table-wrap" style="margin-top:16px">
<table>
<thead>
<tr><th>Patient</th><th>Order ID</th><th>Order QR</th><th>Date</th></tr>
</thead>
<tbody>
${items
.map(
(order) => `
<tr
class="order-row ${selectedId === order.id ? "active" : ""}"
hx-get="/fragments/orders/table?search=${encodeURIComponent(search)}&month=${encodeURIComponent(resolvedMonth)}&year=${encodeURIComponent(resolvedYear)}&current_page=${encodeURIComponent(String(currentPage || 1))}&selected=${encodeURIComponent(order.id)}"
hx-target="#orders-fragment"
hx-push-url="true"
hx-trigger="click"
>
<td><strong>${escapeHtml(order.patient || "Unknown patient")}</strong><br /><span class="muted">${escapeHtml(order.diagnosis || "-")}</span></td>
<td>${escapeHtml(order.id || "-")}</td>
<td>${escapeHtml(order.qrcode || "-")}</td>
<td>${escapeHtml(order.updated || "-")}</td>
</tr>
`,
)
.join("")}
</tbody>
</table>
</div>
`
: emptyState("No orders returned", "The desktop order endpoint did not return any rows for this filter.")
}
<div class="topbar-actions" style="justify-content:space-between; margin-top:16px">
<button
class="btn btn-secondary"
type="button"
hx-get="/fragments/orders/table?search=${encodeURIComponent(search)}&month=${encodeURIComponent(resolvedMonth)}&year=${encodeURIComponent(resolvedYear)}&current_page=${encodeURIComponent(String(prevPage))}&selected=${encodeURIComponent(selectedId)}"
hx-target="#orders-fragment"
hx-push-url="true"
${hasPrev ? "" : "disabled"}
>Previous</button>
<span class="pill">Page ${escapeHtml(String(currentPage || 1))}</span>
<button
class="btn btn-secondary"
type="button"
hx-get="/fragments/orders/table?search=${encodeURIComponent(search)}&month=${encodeURIComponent(resolvedMonth)}&year=${encodeURIComponent(resolvedYear)}&current_page=${encodeURIComponent(String(nextPage))}&selected=${encodeURIComponent(selectedId)}"
hx-target="#orders-fragment"
hx-push-url="true"
${hasNext ? "" : "disabled"}
>Next</button>
</div>
</section>
<aside class="panel">
${panelHeader("Selected order", "Click a row to update the detail pane. The row you pick is highlighted.", selected ? `<span class="pill">Selected</span>` : "")}
${
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">
${selected.status ? statusBadge(selected.status) : ""}
<span class="pill">${escapeHtml(selected.qrcode || "-")}</span>
</div>
</div>
<div class="mini-list">
<div class="mini-item"><div><strong>NIK</strong><p>${escapeHtml(selected.orderNik || "-")}</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 || "-")}</p></div></div>
<div class="mini-item"><div><strong>Diagnosis</strong><p>${escapeHtml(selected.diagnosis || "-")}</p></div></div>
<div class="mini-item"><div><strong>Note</strong><p>${escapeHtml(selected.message || "-")}</p></div></div>
</div>
</div>
`
: emptyState("No selected order", "Click one row on the left to see its detail here.")
}
</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 qrImageUrl(value, size = 240) {
const data = encodeURIComponent(String(value || ""));
return `https://api.qrserver.com/v1/create-qr-code/?size=${size}x${size}&data=${data}`;
}
function formatOrderDisplayDate(value) {
const text = String(value || "").trim();
const match = text.match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}:\d{2}:\d{2}))?$/);
if (!match) return text;
return `${match[3]}-${match[2]}-${match[1]}${match[4] ? ` ${match[4]}` : ""}`;
}
function renderOrderSaved(order) {
const details = Array.isArray(order.details) ? order.details : [];
const qrText = order.orderCode || order.id || "";
const printableDate = formatOrderDisplayDate(order.orderDate || "");
const printableDoctor = order.doctorName || order.doctor || sampleLogin.username;
return `
<div class="stack order-created-shell" data-order-saved="true">
<section class="panel">
${panelHeader(
"QR Step",
"Order is saved. Share it to WhatsApp or print the slip below.",
`<div class="pill-row">
<a class="btn btn-secondary" href="/orders/${escapeHtml(order.id || "")}">Open detail</a>
<a class="btn btn-primary" href="/orders/new">Create new order</a>
</div>`,
)}
<div class="detail-grid">
<div class="card order-created-qr">
<div class="qr-slip">
<div class="qr-slip-header">
<div class="qr-slip-label">Tanggal Order</div>
<div class="qr-slip-value">${escapeHtml(printableDate || "-")}</div>
<div class="qr-slip-label">Nama</div>
<div class="qr-slip-value">${escapeHtml(order.patient || "-")}</div>
<div class="qr-slip-label">Dokter</div>
<div class="qr-slip-value">${escapeHtml(printableDoctor)}</div>
</div>
<div class="qr-slip-section-title">Pemeriksaan</div>
<div class="qr-slip-tests">
${
details.length
? details
.map(
(item) => `<div class="qr-slip-test">- ${escapeHtml(item.test_name || item.testName || "-")}</div>`,
)
.join("")
: `<div class="qr-slip-test muted">-</div>`
}
</div>
<div class="qr-preview">
${
qrText
? `<img class="image_qrcode" src="${qrImageUrl(qrText, 320)}" alt="QR code for order ${escapeHtml(qrText)}" loading="lazy" />`
: `<div class="note-box">QR code not available.</div>`
}
</div>
<div class="qr-code-text">${escapeHtml(qrText || "-")}</div>
<div class="pill-row qr-actions">
${
qrText
? `<a class="btn btn-secondary" href="${escapeHtml(
`https://wa.me/?text=${encodeURIComponent(
`Order ${order.id || ""} | ${order.patient || ""} | QR ${qrText}`,
)}`,
)}" target="_blank" rel="noreferrer">Share WhatsApp</a>`
: ""
}
<button class="btn btn-primary" type="button" data-action="print-order">Print</button>
</div>
</div>
</div>
<div class="card">
<strong>Review</strong>
<p class="muted">Saved payload summary from the backend response.</p>
<div class="mini-list" style="margin-top:12px">
<div class="mini-item"><div><strong>Patient</strong><p>${escapeHtml(order.patient || "-")}</p></div></div>
<div class="mini-item"><div><strong>Diagnosis</strong><p>${escapeHtml(order.diagnosis || "-")}</p></div></div>
<div class="mini-item"><div><strong>Notes</strong><p>${escapeHtml(order.message || "-")}</p></div></div>
<div class="mini-item"><div><strong>Saved at</strong><p>${escapeHtml(order.orderDate || "-")}</p></div></div>
</div>
</div>
</div>
</section>
<section class="panel">
${panelHeader("Selected tests", "These items are the `details[]` sent to the backend on submit.")}
${
details.length
? `<div class="mini-list">${details
.map(
(item) => `
<div class="mini-item">
<div>
<strong>${escapeHtml(item.test_name || item.testName || "-")}</strong>
<p>Test ID ${escapeHtml(item.test_id || item.testId || "-")} · Rp ${escapeHtml(item.price || "0")}</p>
</div>
</div>
`,
)
.join("")}</div>`
: emptyState("No tests saved", "The saved order did not return any detail rows.")
}
</section>
</div>
`;
}
function renderOrderForm(step, stepKey = "demografi", fppTests = [], mouId = "") {
const fppGroups = groupFppTestsForSelection(fppTests);
const packedGroups = packFppGroupsIntoColumns(fppGroups, 5);
const steps = [
["demografi", "Demografi", "Patient identity and contact details."],
["diagnosa", "Diagnosis", "Clinical indication and diagnosis."],
["pemeriksaan", "Test", "Choose examination groups."],
];
const normalizedStep = steps.some((item) => item[0] === stepKey) ? stepKey : "demografi";
const activeIndex = Math.max(0, steps.findIndex((item) => item[0] === normalizedStep));
const stepBody = {
demografi: `
<div class="stack">
<div class="panel" style="padding:0; box-shadow:none; background:transparent; border:0">
${panelHeader("Mandatory", "These fields are required before save.", '<button class="btn btn-primary" type="button" hx-get="/fragments/modals/patient-search" hx-target="#modal-root" hx-swap="innerHTML">Search patient</button>')}
<div class="grid grid-2">
<label class="field"><span>Patient name</span><input name="patient_name" required /></label>
</div>
</div>
<div class="panel" style="padding:0; box-shadow:none; background:transparent; border:0">
${panelHeader("Optional", "These fields can be filled when available.")}
<div class="grid grid-2">
<label class="field"><span>Date of birth</span><input name="patient_dob" type="date" /></label>
<label class="field"><span>NIK</span><input name="patient_nik" placeholder="16 digits if available" /></label>
<label class="field"><span>Phone</span><input name="patient_hp" placeholder="08xxxxxxxxxx" /></label>
<label class="field"><span>Address</span><input name="patient_address" /></label>
</div>
</div>
</div>
`,
diagnosa: `
<div class="stack">
<div class="panel" style="padding:0; box-shadow:none; background:transparent; border:0">
${panelHeader("Diagnosis", "")}
<div class="grid grid-2">
<label class="field" style="grid-column:1/-1"><span>Diagnosis</span><input name="patient_diagnosa" required /></label>
</div>
</div>
<div class="panel" style="padding:0; box-shadow:none; background:transparent; border:0">
${panelHeader("Optional", "Notes are useful for the lab or front office.")}
<div class="grid grid-2">
<label class="field" style="grid-column:1/-1"><span>Patient note</span><textarea name="patient_note"></textarea></label>
</div>
</div>
</div>
`,
pemeriksaan: `
<div class="stack">
<div class="panel" style="padding:0; box-shadow:none; background:transparent; border:0">
${panelHeader("Tests", "", '<button class="btn btn-primary" type="submit">Submit order</button>')}
<div class="note-box fpp-selection-summary">
<strong>Selected tests</strong>
<p id="order-selected-count">0 selected</p>
</div>
${
packedGroups.length
? `<div class="fpp-select-board">${packedGroups
.map(
(column) => `
<div class="fpp-select-stack">
${column
.map(
(group) => `
<article class="fpp-select-column">
<div class="fpp-select-head">
<div class="fpp-select-title">${escapeHtml(group.heading)}</div>
<div class="fpp-select-count">${group.sections.reduce((sum, section) => sum + section.tests.length, 0)} tests</div>
</div>
<div class="fpp-select-body">
${group.sections
.map(
(section) => `
<section class="fpp-select-section">
${section.title ? `<div class="fpp-select-section-head">${escapeHtml(section.title)}</div>` : ""}
<ul class="fpp-select-list">
${section.tests
.map(
(test) => `
<li class="fpp-select-row ${test.doctortest ? "" : "is-disabled"}">
<label class="fpp-select-label">
<input
class="fpp-select-toggle"
type="checkbox"
${test.doctortest ? "" : "disabled"}
data-test-id="${escapeHtml(test.testId)}"
data-test-name="${escapeHtml(test.name)}"
data-test-price="${escapeHtml(test.price)}"
data-test-heading="${escapeHtml(group.heading)}"
data-test-subcategory="${escapeHtml(section.title || "")}"
/>
<span class="fpp-select-name">${escapeHtml(test.name)}</span>
</label>
</li>
`,
)
.join("")}
</ul>
</section>
`,
)
.join("")}
</div>
</article>
`,
)
.join("")}
</div>
`,
)
.join("")}</div>`
: `<div class="note-box">FPP catalog not available yet.</div>`
}
<div class="topbar-actions" style="justify-content:flex-start; margin-top:14px">
<button class="btn btn-primary" type="submit">Submit order</button>
</div>
</div>
</div>
`,
}[normalizedStep];
const prev = steps[activeIndex - 1];
const next = steps[activeIndex + 1];
return `
<div class="stack order-create-stack">
<section class="panel order-create-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>
<form class="stack" action="/orders" method="post" data-order-form data-order-step="${escapeHtml(normalizedStep)}" data-mou-id="${escapeHtml(mouId || sampleLogin.mouId)}">
<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>
${next ? `<a class="btn btn-primary" href="/orders/new/${next[0]}" hx-get="/fragments/forms/order-step/${next[0]}" hx-target="#order-step-fragment" hx-push-url="true">Continue</a>` : ""}
</div>
</div>
</section>
</form>
</div>
`;
}
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>Diagnosis</strong><p>${escapeHtml(result.summary || "-")}</p></div></div>
<div class="mini-item"><div><strong>Note</strong><p>${escapeHtml(result.value || "-")}</p></div></div>
</div>
${result.details?.length ? `<div style="height:14px"></div><div class="mini-list">${result.details.map((item) => `<div class="mini-item"><div><strong>Detail</strong><p>${escapeHtml(item)}</p></div></div>`).join("")}</div>` : ""}
</aside>
</div>
</section>
`;
}
function groupFppTestsForSelection(tests = []) {
const groups = new Map();
for (const test of tests) {
const heading = test.heading || "FPP";
const sectionTitle = test.subcategory || "";
if (!groups.has(heading)) {
groups.set(heading, { heading, sections: new Map() });
}
const group = groups.get(heading);
const sectionKey = sectionTitle || "__ungrouped__";
if (!group.sections.has(sectionKey)) {
group.sections.set(sectionKey, { title: sectionTitle, tests: [] });
}
group.sections.get(sectionKey).tests.push(test);
}
return Array.from(groups.values()).map((group) => ({
heading: group.heading,
sections: Array.from(group.sections.values()),
}));
}
function packFppGroupsIntoColumns(groups = [], maxColumns = 5) {
const columnCount = Math.min(Math.max(groups.length, 1), maxColumns);
const columns = Array.from({ length: columnCount }, () => ({ items: [], score: 0 }));
const estimateScore = (group) =>
1 +
group.sections.length +
group.sections.reduce((sum, section) => sum + section.tests.length * 0.12, 0);
for (const group of groups) {
let target = columns[0];
for (const column of columns) {
if (column.score < target.score) target = column;
}
target.items.push(group);
target.score += estimateScore(group);
}
return columns.map((column) => column.items);
}
function renderFpp(groups, activeGroup = "All") {
const availableGroups = groups.map((item) => item.heading);
const visibleGroups = activeGroup === "All" ? groups : groups.filter((item) => item.heading === activeGroup);
const packedGroups = packFppGroupsIntoColumns(visibleGroups, 5);
return `
<div class="stack swap-zone fpp-shell">
<section class="panel fpp-toolbar">
${panelHeader("FPP", `Paper-style catalog with ${availableGroups.length} grouped panels, aligned to the manual reference sheet.`)}
</section>
${
visibleGroups.length
? `
<section class="fpp-sheet">
<div class="fpp-sheet-head">
<div class="fpp-sheet-title">Pemeriksaan</div>
<div class="fpp-sheet-meta">Hitam putih, padat, dan mengikuti pembagian grup seperti form manual.</div>
</div>
<div class="fpp-board">
${packedGroups
.map(
(column) => `
<div class="fpp-select-stack">
${column
.map(
(group) => `
<article class="fpp-column">
<div class="fpp-column-head">
<div class="fpp-column-title">${escapeHtml(group.heading)}</div>
<div class="fpp-column-count">${group.sections.reduce((sum, section) => sum + section.tests.length, 0)} tests</div>
</div>
<div class="fpp-column-body">
${group.sections
.map(
(section) => `
<section class="fpp-section">
${section.title ? `<div class="fpp-section-head">${escapeHtml(section.title)}</div>` : ""}
<ul class="fpp-test-list">
${section.tests
.map(
(test) => `
<li class="fpp-test ${test.doctortest ? "" : "is-disabled"}" title="${test.doctortest ? "" : "Tidak dapat dicentang"}">
<span class="fpp-dot" aria-hidden="true"></span>
<span class="fpp-test-name">${escapeHtml(test.name)}</span>
</li>
`,
)
.join("")}
</ul>
</section>
`,
)
.join("")}
</div>
</article>
`,
)
.join("")}
</div>
`,
)
.join("")}
</div>
</section>
`
: `<section class="panel">${emptyState("No FPP items", "The API returned no catalog rows for this filter.")}</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>
</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?.displayName || session?.username || "-")}</strong></div>
<div class="card"><span class="muted">Username</span><strong style="display:block; margin-top:8px">${escapeHtml(session?.loginUsername || "-")}</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>
</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, error = "") {
return `
<section class="panel">
${panelHeader("Change password", "Inline validation and a straightforward submit path.")}
${error ? `<div class="note-box" style="margin-bottom:16px">${escapeHtml(error)}</div>` : ""}
<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>
<input type="hidden" name="login_username" value="${escapeHtml(session?.loginUsername || "")}" />
<label class="field"><span>Username</span><input value="${escapeHtml(session?.loginUsername || "-")}" readonly /></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">
<div class="note-box" style="margin-right:auto">Password minimal 8 digit, 1 huruf kecil, 1 huruf besar, dan 1 angka.</div>
<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" 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" },
);
}
async function orderNewPage(session, path) {
const allowedSteps = new Set(["demografi", "diagnosa", "pemeriksaan"]);
const step = path.split("/").filter(Boolean)[2] || "demografi";
const normalizedStep = allowedSteps.has(step) ? step : "demografi";
const fppTests = await loadFppCatalog(session);
return layout("Create Order", `<div id="order-step-fragment">${renderOrderForm({}, normalizedStep, fppTests, session?.mouId || "")}</div>`, {
authenticated: true,
activePath: "/orders",
subtitle: "The new project collapses the old step-heavy flow into a cleaner shell while keeping the same workflow.",
...accountLayoutOptions(session),
});
}
function ordersPage(data = {}, session = {}) {
return layout("Orders", `<div id="orders-fragment">${renderOrdersTable(data)}</div>`, {
authenticated: true,
activePath: "/orders",
...accountLayoutOptions(session),
});
}
function resultsPage({ query = {}, results = [], selectedResultId = "" } = {}, session = {}) {
return layout("Results", `<div id="results-fragment">${renderResultsTable(results, selectedResultId)}</div>`, {
authenticated: true,
activePath: "/results",
...accountLayoutOptions(session),
});
}
function fppPage({ group = "All", groups = [] } = {}, session = {}) {
return layout("FPP", `<div id="fpp-fragment">${renderFpp(groups, group)}</div>`, {
authenticated: true,
activePath: "/fpp",
...accountLayoutOptions(session),
});
}
async function patientsPage(session) {
return layout("Patients", await renderPatients(session), { authenticated: true, activePath: "/patients", ...accountLayoutOptions(session) });
}
function settingsPage(session) {
return layout("Settings", renderSettings(session), { authenticated: true, activePath: "/settings", ...accountLayoutOptions(session) });
}
function changePasswordPage(session) {
return layout("Change Password", renderChangePassword(session), {
authenticated: true,
activePath: "/settings/change-password",
...accountLayoutOptions(session),
});
}
function orderDetailPage(order, session = {}) {
return layout("Order Detail", renderOrderDetail(order), {
authenticated: true,
activePath: "/orders",
...accountLayoutOptions(session),
});
}
function resultDetailPage(result, session = {}) {
return layout("Result Detail", renderResultDetail(result), {
authenticated: true,
activePath: "/results",
...accountLayoutOptions(session),
});
}
function specialMessagePage(order, session = {}) {
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", ...accountLayoutOptions(session) },
);
}
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 displayName = user?.M_UserUsername || data?.M_UserUsername || data?.name || data?.display_name || "";
const loginUsername = data?.username || user?.username || data?.login_username || data?.user_name || "";
const doctorId = user?.M_UserM_DoctorID || data?.doctor_id || data?.doctorId || "";
const doctorCode = user?.M_UserM_DoctorCode || data?.doctor_code || "";
const mouId = user?.M_UserM_MouID || data?.M_UserM_MouID || data?.mou_id || "";
const userId = user?.M_UserID || data?.M_UserID || data?.user_id || "";
return {
token,
displayName,
loginUsername,
username: displayName || loginUsername,
doctorId,
doctorCode,
mouId,
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 normalizeHomeOrder(raw, index = 0) {
const status = String(raw?.is_confirm || raw?.status || "").toUpperCase() === "Y" ? "Confirmed" : "Unconfirmed";
return {
id: raw?.order_id || raw?.id || raw?.order_patient_id || "",
patient: raw?.order_name || raw?.patient_name || raw?.name || "",
doctor: raw?.doctor_id || raw?.doctor_name || raw?.doctor || "",
updated: raw?.order_date || raw?.updated_at || raw?.updated || "",
status,
tone: statusClass(status),
mode: raw?.LabNumber && raw?.LabNumber !== "-" ? raw.LabNumber : raw?.order_qrcode || "",
age: String(raw?.age || raw?.patient_age || ""),
gender: raw?.gender || raw?.patient_gender || "",
tests: Array.isArray(raw?.details) ? raw.details.map((item) => item?.test_name || item?.name || "").filter(Boolean) : [],
diagnosis: raw?.order_diagnosa || raw?.diagnosis || "",
message: raw?.order_note || raw?.note || "",
orderDate: raw?.order_date || "",
orderNik: raw?.order_nik || "",
orderHp: raw?.order_hp || "",
orderAddress: raw?.order_address || "",
orderDob: raw?.order_dob || "",
};
}
function normalizeDesktopOrder(raw, index = 0) {
return {
id: raw?.order_id || raw?.id || raw?.order_patient_id || "",
patient: raw?.order_name || raw?.patient_name || raw?.name || "",
doctor: raw?.doctor_id || raw?.doctor_name || raw?.doctor || "",
updated: raw?.order_date || raw?.updated_at || raw?.updated || "",
qrcode: raw?.order_qrcode || raw?.qrcode || "",
orderNik: raw?.order_nik || "",
orderHp: raw?.order_hp || "",
orderAddress: raw?.order_address || "",
orderDob: raw?.order_dob || "",
diagnosis: raw?.order_diagnosa || raw?.diagnosis || "",
message: raw?.order_note || raw?.note || "",
status: String(raw?.is_confirm || raw?.status || "").toUpperCase() === "Y" ? "Confirmed" : "Unconfirmed",
};
}
function normalizePatientOrder(raw, index = 0) {
return {
id: raw?.order_id || raw?.id || raw?.order_patient_id || "",
patient: raw?.order_name || raw?.patient_name || raw?.name || "",
orderDob: raw?.order_dob || "",
orderNik: raw?.order_nik || "",
orderHp: raw?.order_hp || "",
orderAddress: raw?.order_address || "",
updated: raw?.order_date || raw?.updated_at || raw?.updated || "",
};
}
function normalizeResult(raw, index = 0) {
const status = raw?.status || raw?.result_status || raw?.order_status || "Pending";
const detailsSource = raw?.details || raw?.items || raw?.order_details || [];
const details = Array.isArray(detailsSource)
? detailsSource
.map((item) => {
if (typeof item === "string") return item;
return item?.test_name || item?.name || item?.detail_name || item?.exam || item?.label || "";
})
.filter(Boolean)
: [];
return {
id: raw?.result_id || raw?.order_id || raw?.id || raw?.hasil_id || "",
patient: raw?.order_name || raw?.patient_name || raw?.name || raw?.patient || "",
test: details.join(", ") || raw?.test_name || raw?.item_name || raw?.test || "",
status,
tone: statusClass(status),
date: raw?.order_date || raw?.date || raw?.created_at || raw?.updated_at || "",
summary: raw?.order_diagnosa || raw?.summary || raw?.note || raw?.result_summary || "",
value: raw?.order_note || raw?.value || raw?.result_value || raw?.result || "",
details,
orderCode: raw?.order_qrcode || "",
orderDob: raw?.order_dob || "",
orderAddress: raw?.order_address || "",
orderNik: raw?.order_nik || "",
orderHp: raw?.order_hp || "",
};
}
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 loadHomeOrders(session) {
try {
const now = new Date();
const payload = await apiPost(
"/order/home",
{
token: session.token,
month: String(now.getMonth() + 1),
year: String(now.getFullYear()),
},
session.token,
);
const rows = extractArray(payload) || payload?.data || [];
const items = Array.isArray(rows) ? rows.map((row, index) => normalizeHomeOrder(row, index)).filter((item) => item.id) : [];
const totals = {
total: Number(payload?.total_order || payload?.total || items.length || 0),
confirmed: Number(payload?.total_confirmed || items.filter((item) => item.status === "Confirmed").length || 0),
unconfirmed: Number(payload?.total_unconfirmed || items.filter((item) => item.status === "Unconfirmed").length || 0),
};
return { items, totals, month: String(now.getMonth() + 1), year: String(now.getFullYear()) };
} catch {
return { items: [], totals: { total: 0, confirmed: 0, unconfirmed: 0 }, month: "", year: "" };
}
}
async function loadDesktopOrders(session, { search = "", month = "", year = "", currentPage = 1 } = {}) {
try {
const now = new Date();
const resolvedMonth = String(month || now.getMonth() + 1);
const resolvedYear = String(year || now.getFullYear());
const resolvedPage = String(Math.max(1, Number(currentPage) || 1));
const payload = await apiPost(
"/order/search_order_pasien_by_doktorid_desktop",
{
token: session.token,
month: resolvedMonth,
year: resolvedYear,
search: String(search || ""),
current_page: resolvedPage,
OrderPatientM_DoctorID: session.doctorId || sampleLogin.doctorId,
},
session.token,
);
const rows = extractArray(payload) || payload?.data || [];
const items = Array.isArray(rows) ? rows.map((row, index) => normalizeDesktopOrder(row, index)).filter((item) => item.id) : [];
return {
items,
month: resolvedMonth,
year: resolvedYear,
currentPage: Number(resolvedPage),
search: String(search || ""),
hasNext: items.length > 0,
hasPrev: Number(resolvedPage) > 1,
};
} catch {
const now = new Date();
return {
items: [],
month: String(month || now.getMonth() + 1),
year: String(year || now.getFullYear()),
currentPage: Number(currentPage) || 1,
search: String(search || ""),
hasNext: false,
hasPrev: false,
};
}
}
async function loadPatientSearch(session, search = "") {
try {
const payload = await apiPost(
"/order/search_order_pasien_by_doktorid",
{
token: session.token,
OrderPatientM_DoctorID: session.doctorId || sampleLogin.doctorId,
search: String(search || ""),
},
session.token,
);
const rows = extractArray(payload) || [];
return Array.isArray(rows) ? rows.map((row, index) => normalizePatientOrder(row, index)).filter((item) => item.id || item.patient) : [];
} catch {
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 rawRows = extractArray(payload) || (payload?.data ? [payload.data] : []);
return rawRows.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?.data && typeof payload.data === "object") return normalizeResult(payload.data, 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 {
const related = await loadResults(session, resultId);
return related.find((item) => item.id === resultId) || related[0] || 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 loadFppCatalog(session) {
try {
const { doctorId, mouId } = resolveFppRouteIds(session);
const payload = await apiPost(`/Fpp/loadFPP/${doctorId}/${mouId}`, { token: session.token }, session.token);
const rows = extractArray(payload) || payload?.rows || [];
const headings = Array.isArray(rows) ? rows : [];
const tests = headings.flatMap((heading) =>
(heading?.details || []).flatMap((detail) =>
(detail?.tests || []).map((test) => ({
heading: heading?.heading || "FPP",
subcategory: detail?.subcategories || "",
testId: String(test?.testid || ""),
code: String(test?.code || ""),
name: String(test?.name || ""),
price: String(test?.price || "0"),
checked: Boolean(test?.checked),
doctortest: test?.doctortest !== false && test?.doctortest !== "false",
})),
),
);
return tests.filter((item) => item.name);
} catch {
return [];
}
}
async function loadFppPosterGroups(session) {
try {
const { doctorId, mouId } = resolveFppRouteIds(session);
const payload = await apiPost(`/Fpp/loadFPP/${doctorId}/${mouId}`, { token: session.token }, session.token);
const rows = extractArray(payload) || payload?.rows || [];
const headings = Array.isArray(rows) ? rows : [];
return headings
.map((heading) => ({
heading: heading?.heading || "FPP",
sections: (heading?.details || [])
.map((detail) => ({
title: detail?.subcategories || "",
tests: (detail?.tests || [])
.map((test) => ({
testId: String(test?.testid || ""),
code: String(test?.code || ""),
name: String(test?.name || ""),
price: String(test?.price || "0"),
checked: Boolean(test?.checked),
doctortest: test?.doctortest !== false && test?.doctortest !== "false",
}))
.filter((test) => test.name),
}))
.filter((section) => section.tests.length),
}))
.filter((group) => group.sections.length);
} catch {
return [];
}
}
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 = {}) {
const resolvedLocation = typeof location === "string" && location.startsWith("/") ? appPath(location) : location;
res.writeHead(302, { Location: resolvedLocation, ...headers });
res.end();
}
function html(res, status, body, headers = {}) {
const output = rewriteHtmlPaths(body);
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", ...headers });
res.end(output);
}
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) : {};
const flat = Object.fromEntries(new URLSearchParams(raw));
const details = [];
for (const [key, value] of Object.entries(flat)) {
const match = key.match(/^details\[(\d+)\]\[(\w+)\]$/);
if (!match) continue;
const index = Number(match[1]);
const field = match[2];
details[index] ||= {};
details[index][field] = value;
delete flat[key];
}
if (details.length) flat.details = details.filter(Boolean);
return flat;
}
function fragmentOrdersTable(search = "", month = "", year = "", currentPage = 1, selected = "", data = null) {
return renderOrdersTable({
...(data || {}),
search,
month,
year,
currentPage,
selectedOrderId: selected,
});
}
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(groups = [], group = "All") {
return renderFpp(groups, group);
}
async function fragmentOrderStep(session, step) {
const fppTests = await loadFppCatalog(session);
return renderOrderForm({}, step, fppTests, session?.mouId || "");
}
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 fragmentPatientSearch(session, search = "") {
const patients = await loadPatientSearch(session, search);
return `
<div class="modal-shell" role="dialog" aria-modal="true" aria-labelledby="patient-search-title">
<div class="modal-backdrop" data-modal-close></div>
<section class="modal-card panel">
${panelHeader("Search patient", "Pick an existing patient and the demographic fields will fill automatically.", `<button class="btn btn-secondary" type="button" data-modal-close>Close</button>`)}
<form class="card" hx-get="/fragments/modals/patient-search" hx-target="#modal-root" hx-swap="innerHTML">
<label class="field">
<span>Search name</span>
<input name="search" value="${escapeHtml(search)}" placeholder="Type patient name" />
</label>
<div class="topbar-actions" style="justify-content:flex-start; margin-top:12px">
<button class="btn btn-primary" type="submit">Search</button>
</div>
</form>
<div style="height:14px"></div>
${
patients.length
? `<div class="mini-list">${patients
.map(
(patient) => `
<button
class="mini-item"
type="button"
data-patient-pick="true"
data-patient-name="${escapeHtml(patient.patient || "")}"
data-patient-dob="${escapeHtml(patient.orderDob || "")}"
data-patient-nik="${escapeHtml(patient.orderNik || "")}"
data-patient-hp="${escapeHtml(patient.orderHp || "")}"
data-patient-address="${escapeHtml(patient.orderAddress || "")}"
>
<div>
<strong>${escapeHtml(patient.patient || "Unknown patient")}</strong>
<p>${patient.orderNik ? escapeHtml(patient.orderNik) : "NIK belum ada"}</p>
</div>
</button>
`,
)
.join("")}</div>`
: emptyState("No patients found", "Search by patient name to load matching records.")
}
</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 = stripBasePath(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 === "/logo.png") {
const logo = await readFile(new URL("./logo.png", import.meta.url));
res.writeHead(200, { "Content-Type": "image/png" });
res.end(logo);
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,
displayName: normalized.displayName || normalized.username || body.username || "",
loginUsername: normalized.loginUsername || body.username || "",
username: normalized.displayName || normalized.username || body.username || "",
doctorId: normalized.doctorId || body.doctor_id || "",
doctorCode: normalized.doctorCode || body.doctor_id || "",
mouId: normalized.mouId || body.M_MouID || "",
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.displayName || 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 {
const payload = await apiPost(
"/auth/change_password",
{
token: sessionData.token,
M_UserID: sessionData.userId || "",
username: body.login_username || sessionData.loginUsername || "",
doctor_id: sessionData.doctorCode || sessionData.doctorId || sampleLogin.doctorId,
new_password: body.new_password,
confirm_password: body.confirm_password,
},
sessionData.token,
);
if (payload?.status && payload.status !== "OK") {
throw new Error(payload?.message || "Change password failed");
}
redirect(res, "/login", { "Set-Cookie": deleteCookie(sessionKey) });
} catch (error) {
html(
res,
200,
layout(
"Change Password",
renderChangePassword(sessionData, error?.message || "Upstream change password call failed. Check API availability."),
{ authenticated: true, activePath: "/settings/change-password", ...accountLayoutOptions(sessionData) },
),
);
}
return;
}
if (path === "/orders/new" && isGet) {
if (!requireAuth(req, res)) return;
html(res, 200, await orderNewPage(session, path));
return;
}
if (path.startsWith("/orders/new/") && isGet) {
if (!requireAuth(req, res)) return;
html(res, 200, await orderNewPage(session, path));
return;
}
if (path === "/orders" && isGet) {
if (!requireAuth(req, res)) return;
const orders = await loadDesktopOrders(session, query);
html(res, 200, ordersPage({ ...orders, selectedOrderId: query.selected || orders.items[0]?.id || "" }, session));
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 || "" }, session));
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", ...accountLayoutOptions(session) }));
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", ...accountLayoutOptions(session) }));
return;
}
if (path === "/fpp" && isGet) {
if (!requireAuth(req, res)) return;
const groups = await loadFppPosterGroups(session);
html(res, 200, fppPage({ group: query.group || "All", groups }, session));
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: "/", ...accountLayoutOptions(session) }));
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", ...accountLayoutOptions(session) }));
return;
}
html(res, 200, specialMessagePage(order, session));
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", ...accountLayoutOptions(session) }));
return;
}
html(res, 200, orderDetailPage(order, session));
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", ...accountLayoutOptions(session) }));
return;
}
html(res, 200, resultDetailPage(result, session));
return;
}
if (path === "/fragments/orders/table" && isGet) {
if (!requireAuth(req, res)) return;
const orders = await loadDesktopOrders(session, query);
html(
res,
200,
fragmentOrdersTable(query.search || "", query.month || "", query.year || "", query.current_page || 1, query.selected || orders.items[0]?.id || "", 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 loadFppPosterGroups(session);
html(res, 200, fragmentFpp(groups, query.group || "All"));
return;
}
if (path.startsWith("/fragments/forms/order-step/") && isGet) {
if (!requireAuth(req, res)) return;
const step = path.split("/")[4] || "demografi";
html(res, 200, await fragmentOrderStep(session, step));
return;
}
if (path === "/fragments/modals/pesan-khusus" && isGet) {
if (!requireAuth(req, res)) return;
html(res, 200, fragmentPesanKhusus(query.order_id || ""));
return;
}
if (path === "/fragments/modals/patient-search" && isGet) {
if (!requireAuth(req, res)) return;
html(res, 200, await fragmentPatientSearch(session, query.search || ""));
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 {
const payload = await apiPost(
"/order/order_patient",
{
token: sessionData.token,
M_MouID: body.M_MouID || sessionData.mouId || sampleLogin.mouId || "",
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,
);
const saved = payload?.data || payload?.result || payload || {};
const normalizedSaved = {
id: saved.order_id || saved.id || saved.order_patient_id || "",
patient: saved.order_name || body.patient_name || "",
diagnosis: saved.order_diagnosa || body.patient_diagnosa || "",
message: saved.order_note || body.patient_note || "",
orderDate: saved.order_date || "",
orderCode: saved.order_qrcode || "",
doctorName: sessionData.username || sampleLogin.username,
details: Array.isArray(saved.details) ? saved.details : Array.isArray(body.details) ? body.details : [],
};
html(
res,
200,
layout("Order saved", renderOrderSaved(normalizedSaved), {
authenticated: true,
activePath: "/orders",
subtitle: "QR Code and review are shown after the order has been submitted successfully.",
}),
);
} 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", ...accountLayoutOptions(sessionData) },
),
);
}
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", ...accountLayoutOptions(sessionData) },
),
);
}
return;
}
html(res, 404, emptyRoute(url.pathname));
}
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}`);
});