Refine dashboard and pending results
This commit is contained in:
101
server.js
101
server.js
@@ -267,26 +267,17 @@ function panelHeader(title, text, action = "") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function dashboardPage(session) {
|
async function dashboardPage(session) {
|
||||||
const [orders, results, fpp] = await Promise.all([
|
const [orders, results] = await Promise.all([
|
||||||
loadOrders(session, "", "All"),
|
loadOrders(session, "", "All"),
|
||||||
loadResults(session, ""),
|
loadResults(session, ""),
|
||||||
loadFpp(session, "All"),
|
|
||||||
]);
|
]);
|
||||||
const stats = [
|
const stats = [
|
||||||
{ label: "Orders today", value: String(orders.length), trend: "Live", hint: "From API response" },
|
{ label: "Orders today", value: String(orders.length), trend: "Live", hint: "" },
|
||||||
{ label: "Results pending", value: String(results.filter((item) => item.status !== "Released").length), trend: "Live", hint: "From API response" },
|
{ label: "Results pending", value: String(results.filter((item) => item.status !== "Released").length), trend: "Live", hint: "" },
|
||||||
{ label: "FPP items", value: String(fpp.items.length), trend: "Live", hint: "From API response" },
|
|
||||||
{ label: "Special messages", value: String(orders.filter((item) => Boolean(item.message)).length), trend: "Live", hint: "From API response" },
|
|
||||||
];
|
|
||||||
const shortcuts = [
|
|
||||||
{ title: "New order", desc: "Start registration flow", href: "/orders/new" },
|
|
||||||
{ title: "Pending results", desc: "Review unreleased items", href: "/results/pending" },
|
|
||||||
{ title: "FPP catalog", desc: "Inspect lab groups", href: "/fpp" },
|
|
||||||
{ title: "Change password", desc: "Update account security", href: "/settings/change-password" },
|
|
||||||
];
|
];
|
||||||
return `
|
return `
|
||||||
<div class="stack">
|
<div class="stack">
|
||||||
<section class="grid grid-4">
|
<section class="grid grid-2">
|
||||||
${stats
|
${stats
|
||||||
.map(
|
.map(
|
||||||
(item) => `
|
(item) => `
|
||||||
@@ -296,28 +287,12 @@ async function dashboardPage(session) {
|
|||||||
<span class="trend">${escapeHtml(item.trend)}</span>
|
<span class="trend">${escapeHtml(item.trend)}</span>
|
||||||
</div>
|
</div>
|
||||||
<strong>${escapeHtml(item.value)}</strong>
|
<strong>${escapeHtml(item.value)}</strong>
|
||||||
<span class="muted">${escapeHtml(item.hint)}</span>
|
${item.hint ? `<span class="muted">${escapeHtml(item.hint)}</span>` : ""}
|
||||||
</article>
|
</article>
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
.join("")}
|
.join("")}
|
||||||
</section>
|
</section>
|
||||||
<section class="panel">
|
|
||||||
${panelHeader("Quick actions", "Jump into the common flows without digging through nested screens.")}
|
|
||||||
<div class="grid grid-4">
|
|
||||||
${shortcuts
|
|
||||||
.map(
|
|
||||||
(item) => `
|
|
||||||
<a class="card" href="${item.href}">
|
|
||||||
<strong>${escapeHtml(item.title)}</strong>
|
|
||||||
<p class="muted">${escapeHtml(item.desc)}</p>
|
|
||||||
<span class="pill">${icon("arrow")} Open</span>
|
|
||||||
</a>
|
|
||||||
`,
|
|
||||||
)
|
|
||||||
.join("")}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="detail-grid">
|
<section class="detail-grid">
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
${panelHeader("Recent orders", "A compact snapshot of the latest patient orders and their state.", '<a class="btn btn-secondary" href="/orders">View all</a>')}
|
${panelHeader("Recent orders", "A compact snapshot of the latest patient orders and their state.", '<a class="btn btn-secondary" href="/orders">View all</a>')}
|
||||||
@@ -346,28 +321,6 @@ async function dashboardPage(session) {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel">
|
|
||||||
${panelHeader("Today’s notes", "Items that need attention now, not later.")}
|
|
||||||
<div class="mini-list">
|
|
||||||
${[
|
|
||||||
["FPP ready for release", "Review Hematology group before rounding."],
|
|
||||||
["Pending sample", "One fasting sample still waiting in the queue."],
|
|
||||||
["Password rotation", "Prompt user to refresh credentials after 90 days."],
|
|
||||||
]
|
|
||||||
.map(
|
|
||||||
([title, text]) => `
|
|
||||||
<div class="mini-item">
|
|
||||||
<div>
|
|
||||||
<strong>${escapeHtml(title)}</strong>
|
|
||||||
<p>${escapeHtml(text)}</p>
|
|
||||||
</div>
|
|
||||||
<span class="pill">${icon("arrow")}</span>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
)
|
|
||||||
.join("")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -713,10 +666,11 @@ function renderResultDetail(result) {
|
|||||||
</div>
|
</div>
|
||||||
<aside class="panel">
|
<aside class="panel">
|
||||||
<div class="mini-list">
|
<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>Date</strong><p>${escapeHtml(result.date || "-")}</p></div></div>
|
||||||
<div class="mini-item"><div><strong>Interpretation</strong><p>Borderline value requires doctor review.</p></div></div>
|
<div class="mini-item"><div><strong>Diagnosis</strong><p>${escapeHtml(result.summary || "-")}</p></div></div>
|
||||||
<div class="mini-item"><div><strong>Action</strong><p>Keep in pending state until confirmed.</p></div></div>
|
<div class="mini-item"><div><strong>Note</strong><p>${escapeHtml(result.value || "-")}</p></div></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>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -1222,16 +1176,31 @@ function normalizeOrder(raw, index = 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeResult(raw, index = 0) {
|
function normalizeResult(raw, index = 0) {
|
||||||
const status = raw?.status || raw?.result_status || "Released";
|
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 {
|
return {
|
||||||
id: raw?.result_id || raw?.order_id || raw?.id || raw?.hasil_id || "",
|
id: raw?.result_id || raw?.order_id || raw?.id || raw?.hasil_id || "",
|
||||||
patient: raw?.patient_name || raw?.name || raw?.patient || "",
|
patient: raw?.order_name || raw?.patient_name || raw?.name || raw?.patient || "",
|
||||||
test: raw?.test_name || raw?.item_name || raw?.test || "",
|
test: details.join(", ") || raw?.test_name || raw?.item_name || raw?.test || "",
|
||||||
status,
|
status,
|
||||||
tone: statusClass(status),
|
tone: statusClass(status),
|
||||||
date: raw?.date || raw?.created_at || raw?.updated_at || "",
|
date: raw?.order_date || raw?.date || raw?.created_at || raw?.updated_at || "",
|
||||||
summary: raw?.summary || raw?.note || raw?.result_summary || "",
|
summary: raw?.order_diagnosa || raw?.summary || raw?.note || raw?.result_summary || "",
|
||||||
value: raw?.value || raw?.result_value || raw?.result || "",
|
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 || "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1274,8 +1243,8 @@ async function loadResults(session, search = "") {
|
|||||||
{ token: session.token, order_id: orderId },
|
{ token: session.token, order_id: orderId },
|
||||||
session.token,
|
session.token,
|
||||||
);
|
);
|
||||||
const rows = extractArray(payload) || [];
|
const rawRows = extractArray(payload) || (payload?.data ? [payload.data] : []);
|
||||||
return rows.map((row, index) => normalizeResult(row, index)).filter((item) => {
|
return rawRows.map((row, index) => normalizeResult(row, index)).filter((item) => {
|
||||||
if (!item.id) return false;
|
if (!item.id) return false;
|
||||||
if (!term) return true;
|
if (!term) return true;
|
||||||
return [item.id, item.patient, item.test, item.status, item.summary].some((value) =>
|
return [item.id, item.patient, item.test, item.status, item.summary].some((value) =>
|
||||||
@@ -1301,11 +1270,13 @@ async function loadResultDetail(session, resultId) {
|
|||||||
);
|
);
|
||||||
const rows = extractArray(payload);
|
const rows = extractArray(payload);
|
||||||
if (rows?.length) return normalizeResult(rows[0], 0);
|
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);
|
if (payload && typeof payload === "object") return normalizeResult(payload, 0);
|
||||||
const related = await loadResults(session, resultId);
|
const related = await loadResults(session, resultId);
|
||||||
return related.find((item) => item.id === resultId) || null;
|
return related.find((item) => item.id === resultId) || null;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
const related = await loadResults(session, resultId);
|
||||||
|
return related.find((item) => item.id === resultId) || related[0] || null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user