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 `
@@ -352,10 +337,15 @@ function topbar(activePath, subtitle) {

${escapeHtml(subtitle || fallbackSubtitle)}

- ${searchForm} - - Akun ${icon("plus")} New order + +
`; @@ -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 ? `
${results.map((item) => `
${escapeHtml(item.patient || "Unknown patient")}

${escapeHtml(item.test || "-")} · ${escapeHtml(item.date || "-")}

${statusBadge(item.status)}
`).join("")}
` : 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 ? `
${results.map((item) => `
${escapeHtml(item.patient || "Unknown patient")}

${escapeHtml(item.test || "-")} · ${escapeHtml(item.date || "-")}

${statusBadge(item.status)}
`).join("")}
` : 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;