Polish account header and sidebar logo

This commit is contained in:
sas.fajri
2026-04-13 19:28:08 +07:00
parent 80b4f132d8
commit 0ed36b98af
3 changed files with 125 additions and 56 deletions

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

116
server.js
View File

@@ -65,10 +65,10 @@ function icon(name) {
return map[name] || "";
}
function layout(title, body, { authenticated = false, activePath = "/", subtitle = "", shell = true } = {}) {
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) : "";
const header = authenticated ? topbar(activePath, subtitle, accountName, accountMeta) : "";
return `<!doctype html>
<html lang="en">
<head>
@@ -317,7 +317,7 @@ function layout(title, body, { authenticated = false, activePath = "/", subtitle
</html>`;
}
function topbar(activePath, subtitle) {
function topbar(activePath, subtitle, accountName = "", accountMeta = "") {
const titleMap = {
"/": ["Dashboard", "Overview of orders, results, and work queues."],
"/orders": ["Orders", "Search, review, and create patient orders."],
@@ -327,23 +327,8 @@ function topbar(activePath, subtitle) {
"/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>
`;
const displayName = accountName || sampleLogin.username;
const displayMeta = accountMeta || `Doctor ID ${sampleLogin.doctorId}`;
return `
<header class="topbar">
<div class="topbar-main">
@@ -352,10 +337,15 @@ function topbar(activePath, subtitle) {
<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-secondary" href="/settings">Akun</a>
<a class="btn btn-primary" href="/orders/new">${icon("plus")} New order</a>
<button class="icon-btn" type="button" data-action="alert" title="Notifications">${icon("bell")}</button>
<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>
<small>${escapeHtml(displayMeta)}</small>
</span>
</a>
</div>
</header>
`;
@@ -366,7 +356,6 @@ function desktopNav(activePath) {
["/", "Home", "Dashboard"],
["/orders", "Order", "Create & list"],
["/results", "Result", "History"],
["/settings", "Akun", "Profile"],
];
const active = (href) => activePath === href || activePath.startsWith(`${href}/`);
return `
@@ -398,14 +387,9 @@ function desktopNav(activePath) {
<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 class="sidebar-footer sidebar-footer-logo">
<img src="/logo.png" alt="Pramita Lab" class="sidebar-logo-image" />
</div>
</div>
</aside>
@@ -417,7 +401,6 @@ function mobileNav(activePath) {
["/", "Home", "Dashboard"],
["/orders", "Order", "Create"],
["/results", "Result", "History"],
["/settings", "Akun", "Profile"],
];
const active = (href) => activePath === href || activePath.startsWith(`${href}/`);
return `
@@ -457,6 +440,13 @@ function resolveFppRouteIds(session) {
};
}
function accountLayoutOptions(session = {}) {
return {
accountName: session?.username || sampleLogin.username,
accountMeta: session?.doctorId ? `Doctor ID ${session.doctorId}` : `Doctor ID ${sampleLogin.doctorId}`,
};
}
async function dashboardPage(session) {
const [orders, results] = await Promise.all([
loadOrders(session, "", "All"),
@@ -1361,60 +1351,67 @@ async function orderNewPage(session, path) {
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({ query = {}, orders = [], selectedOrderId = "" } = {}) {
function ordersPage({ query = {}, orders = [], selectedOrderId = "" } = {}, session = {}) {
return layout("Orders", `<div id="orders-fragment">${renderOrdersTable(orders, selectedOrderId, query.status || "All")}</div>`, {
authenticated: true,
activePath: "/orders",
...accountLayoutOptions(session),
});
}
function resultsPage({ query = {}, results = [], selectedResultId = "" } = {}) {
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 = [] } = {}) {
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" });
return layout("Patients", await renderPatients(session), { authenticated: true, activePath: "/patients", ...accountLayoutOptions(session) });
}
function settingsPage(session) {
return layout("Settings", renderSettings(session), { authenticated: true, activePath: "/settings" });
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) {
function orderDetailPage(order, session = {}) {
return layout("Order Detail", renderOrderDetail(order), {
authenticated: true,
activePath: "/orders",
...accountLayoutOptions(session),
});
}
function resultDetailPage(result) {
function resultDetailPage(result, session = {}) {
return layout("Result Detail", renderResultDetail(result), {
authenticated: true,
activePath: "/results",
...accountLayoutOptions(session),
});
}
function specialMessagePage(order) {
function specialMessagePage(order, session = {}) {
return layout(
"Pesan Khusus",
`
@@ -1441,7 +1438,7 @@ function specialMessagePage(order) {
</div>
</section>
`,
{ authenticated: true, activePath: "/orders" },
{ authenticated: true, activePath: "/orders", ...accountLayoutOptions(session) },
);
}
@@ -2028,6 +2025,13 @@ async function renderRoute(req, res, url) {
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;
@@ -2115,7 +2119,7 @@ async function renderRoute(req, res, url) {
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" },
{ authenticated: true, activePath: "/settings/change-password", ...accountLayoutOptions(sessionData) },
),
);
}
@@ -2137,35 +2141,35 @@ async function renderRoute(req, res, url) {
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 || "" }));
html(res, 200, ordersPage({ query, orders, selectedOrderId: orders[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 || "" }));
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" }));
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" }));
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 }));
html(res, 200, fppPage({ group: query.group || "All", groups }, session));
return;
}
@@ -2189,7 +2193,7 @@ async function renderRoute(req, res, url) {
if (path === "/" && isGet) {
if (!requireAuth(req, res)) return;
html(res, 200, layout("Dashboard", await dashboardPage(session), { authenticated: true, activePath: "/" }));
html(res, 200, layout("Dashboard", await dashboardPage(session), { authenticated: true, activePath: "/", ...accountLayoutOptions(session) }));
return;
}
@@ -2198,10 +2202,10 @@ async function renderRoute(req, res, url) {
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" }));
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));
html(res, 200, specialMessagePage(order, session));
return;
}
@@ -2210,10 +2214,10 @@ async function renderRoute(req, res, url) {
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" }));
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));
html(res, 200, orderDetailPage(order, session));
return;
}
@@ -2222,10 +2226,10 @@ async function renderRoute(req, res, url) {
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" }));
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));
html(res, 200, resultDetailPage(result, session));
return;
}
@@ -2323,7 +2327,7 @@ async function renderRoute(req, res, url) {
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" },
{ authenticated: true, activePath: "/orders", ...accountLayoutOptions(sessionData) },
),
);
}
@@ -2353,7 +2357,7 @@ async function renderRoute(req, res, url) {
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" },
{ authenticated: true, activePath: "/orders", ...accountLayoutOptions(sessionData) },
),
);
}

View File

@@ -233,6 +233,24 @@ button {
border: 1px solid rgba(217, 119, 6, 0.12);
}
.sidebar-footer-logo {
display: grid;
gap: 12px;
padding: 14px;
background: linear-gradient(180deg, #d91f1f 0%, #c51f1f 100%);
border: 0;
color: white;
}
.sidebar-logo-image {
display: block;
width: 100%;
height: auto;
border-radius: 14px;
object-fit: cover;
background: white;
}
.sidebar-footer strong {
display: block;
margin-bottom: 6px;
@@ -303,6 +321,53 @@ button {
flex-wrap: wrap;
}
.account-chip {
padding-right: 18px;
min-width: 172px;
justify-content: flex-start;
text-align: left;
}
.account-avatar {
display: inline-grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 999px;
color: white;
font-size: 0.72rem;
font-weight: 800;
letter-spacing: 0.08em;
background:
radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.3), transparent 36%),
linear-gradient(135deg, var(--brand), var(--brand-strong));
box-shadow: 0 8px 16px rgba(185, 28, 28, 0.22);
}
.account-copy {
display: flex;
flex-direction: column;
gap: 2px;
line-height: 1.05;
min-width: 0;
}
.account-copy strong {
font-size: 0.9rem;
font-weight: 800;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.account-copy small {
color: inherit;
opacity: 0.78;
font-size: 0.72rem;
font-weight: 600;
white-space: nowrap;
}
.search {
display: flex;
align-items: center;