Polish account header and sidebar logo
This commit is contained in:
116
server.js
116
server.js
@@ -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) },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
65
styles.css
65
styles.css
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user