diff --git a/logo.png b/logo.png
new file mode 100644
index 0000000..5bb3fd1
Binary files /dev/null and b/logo.png differ
diff --git a/server.js b/server.js
index 6b014e7..a7c0738 100644
--- a/server.js
+++ b/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 `
@@ -317,7 +317,7 @@ function layout(title, body, { authenticated = false, activePath = "/", subtitle
`;
}
-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"
- ? `
-
- `
- : activePath === "/results"
- ? `
-
- `
- : `
- Search orders
- `;
+ const displayName = accountName || sampleLogin.username;
+ const displayMeta = accountMeta || `Doctor ID ${sampleLogin.doctorId}`;
return `
`;
@@ -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) {
Change password
Security
-
- Problem login
- Fallback state
-
-
@@ -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", `${renderOrdersTable(orders, selectedOrderId, query.status || "All")}
`, {
authenticated: true,
activePath: "/orders",
+ ...accountLayoutOptions(session),
});
}
-function resultsPage({ query = {}, results = [], selectedResultId = "" } = {}) {
+function resultsPage({ query = {}, results = [], selectedResultId = "" } = {}, session = {}) {
return layout("Results", `${renderResultsTable(results, selectedResultId)}
`, {
authenticated: true,
activePath: "/results",
+ ...accountLayoutOptions(session),
});
}
-function fppPage({ group = "All", groups = [] } = {}) {
+function fppPage({ group = "All", groups = [] } = {}, session = {}) {
return layout("FPP", `${renderFpp(groups, group)}
`, {
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) {
`,
- { 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",
`${panelHeader("Change password", "Inline validation and a straightforward submit path.")}Upstream change password call failed. Check API availability.
`,
- { 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", `${panelHeader("Historical results", "A compact historic view that still fits the responsive shell.")}${results.length ? `` : emptyState("No results returned", "The API returned no historical result rows.")}`, { authenticated: true, activePath: "/results" }));
+ html(res, 200, layout("Historical results", `${panelHeader("Historical results", "A compact historic view that still fits the responsive shell.")}${results.length ? `` : emptyState("No results returned", "The API returned no historical result rows.")}`, { 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", `${panelHeader("Pending results", "Items that need attention before release.")}${results.length ? `${results.map((item) => `
${escapeHtml(item.patient || "Unknown patient")}${statusBadge(item.status)}
${escapeHtml(item.test || "-")} · ${escapeHtml(item.summary || "-")}
Open detail`).join("")}
` : emptyState("No pending results", "The API returned no unreleased rows.")}`, { authenticated: true, activePath: "/results" }));
+ html(res, 200, layout("Pending results", `${panelHeader("Pending results", "Items that need attention before release.")}${results.length ? `${results.map((item) => `
${escapeHtml(item.patient || "Unknown patient")}${statusBadge(item.status)}
${escapeHtml(item.test || "-")} · ${escapeHtml(item.summary || "-")}
Open detail`).join("")}
` : emptyState("No pending results", "The API returned no unreleased rows.")}`, { 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", `${emptyState("No order found", "The API returned no matching order for this ID.")}`, { authenticated: true, activePath: "/orders" }));
+ html(res, 200, layout("Pesan Khusus", `${emptyState("No order found", "The API returned no matching order for this ID.")}`, { 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", `${emptyState("No order found", "The API returned no matching order for this ID.")}`, { authenticated: true, activePath: "/orders" }));
+ html(res, 200, layout("Order Detail", `${emptyState("No order found", "The API returned no matching order for this ID.")}`, { 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", `${emptyState("No result found", "The API returned no matching result for this ID.")}`, { authenticated: true, activePath: "/results" }));
+ html(res, 200, layout("Result Detail", `${emptyState("No result found", "The API returned no matching result for this ID.")}`, { 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",
`${panelHeader("Create new order", "The new project collapses the old step-heavy flow into a cleaner shell while keeping the same workflow.")}Upstream order create call failed. Check API availability.
`,
- { authenticated: true, activePath: "/orders" },
+ { authenticated: true, activePath: "/orders", ...accountLayoutOptions(sessionData) },
),
);
}
@@ -2353,7 +2357,7 @@ async function renderRoute(req, res, url) {
layout(
"Pesan Khusus",
`${panelHeader("Pesan khusus", "Desktop can treat this as a modal-style panel; mobile reads it as a dedicated page.")}Upstream save failed. Check API availability.
`,
- { authenticated: true, activePath: "/orders" },
+ { authenticated: true, activePath: "/orders", ...accountLayoutOptions(sessionData) },
),
);
}
diff --git a/styles.css b/styles.css
index 14f0b5c..e414263 100644
--- a/styles.css
+++ b/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;