Save password
@@ -965,9 +934,9 @@ function loginPage({ error = "" } = {}) {
@@ -1033,37 +1002,37 @@ function orderNewPage(path) {
});
}
-function ordersPage({ query = {}, orders = mockOrders, selectedOrderId = mockOrders[0].id } = {}) {
+function ordersPage({ query = {}, orders = [], selectedOrderId = "" } = {}) {
return layout("Orders", `
${renderOrdersTable(orders, selectedOrderId, query.status || "All")}
`, {
authenticated: true,
activePath: "/orders",
});
}
-function resultsPage({ query = {}, results = mockResults, selectedResultId = mockResults[0].id } = {}) {
+function resultsPage({ query = {}, results = [], selectedResultId = "" } = {}) {
return layout("Results", `
${renderResultsTable(results, selectedResultId)}
`, {
authenticated: true,
activePath: "/results",
});
}
-function fppPage({ group = "All", groups = mockFppGroups } = {}) {
+function fppPage({ group = "All", groups = [] } = {}) {
return layout("FPP", `
${renderFpp({ items: groups, filter: group })}
`, {
authenticated: true,
activePath: "/fpp",
});
}
-function patientsPage() {
- return layout("Patients", renderPatients(), { authenticated: true, activePath: "/patients" });
+async function patientsPage(session) {
+ return layout("Patients", await renderPatients(session), { authenticated: true, activePath: "/patients" });
}
-function settingsPage() {
- return layout("Settings", renderSettings(), { authenticated: true, activePath: "/settings" });
+function settingsPage(session) {
+ return layout("Settings", renderSettings(session), { authenticated: true, activePath: "/settings" });
}
-function changePasswordPage() {
- return layout("Change Password", renderChangePassword(), {
+function changePasswordPage(session) {
+ return layout("Change Password", renderChangePassword(session), {
authenticated: true,
activePath: "/settings/change-password",
});
@@ -1168,7 +1137,15 @@ function requireAuth(req, res) {
async function fetchJson(url, options = {}) {
const response = await fetch(url, options);
const contentType = response.headers.get("content-type") || "";
- const body = contentType.includes("application/json") ? await response.json() : await response.text();
+ const text = await response.text();
+ let body = text;
+ if (contentType.includes("application/json") || /^\s*[\[{]/.test(text)) {
+ try {
+ body = JSON.parse(text);
+ } catch {
+ body = text;
+ }
+ }
if (!response.ok) {
const error = new Error(`Upstream error ${response.status}`);
error.status = response.status;
@@ -1194,13 +1171,18 @@ async function apiPost(path, payload, token = "") {
function normalizeSession(payload) {
const data = payload?.data || payload?.result || payload;
+ const user = data?.user || data?.data || {};
const token = data?.token || data?.access_token || data?.accessToken || payload?.token || "";
- const username = data?.username || data?.M_UserUsername || data?.name || "";
- const doctorId = data?.doctor_id || data?.M_UserID || data?.doctorId || "";
+ const username = user?.M_UserUsername || data?.username || data?.M_UserUsername || data?.name || "";
+ const doctorId = user?.M_UserM_DoctorID || data?.doctor_id || data?.doctorId || "";
+ const doctorCode = user?.M_UserM_DoctorCode || data?.doctor_code || "";
+ const userId = user?.M_UserID || data?.M_UserID || data?.user_id || "";
return {
token,
username,
doctorId,
+ doctorCode,
+ userId,
raw: payload,
};
}
@@ -1217,52 +1199,37 @@ function extractArray(payload) {
}
function normalizeOrder(raw, index = 0) {
- const testsSource = raw?.details || raw?.tests || raw?.items || raw?.order_details || [];
- const tests = Array.isArray(testsSource)
- ? testsSource
- .map((item) => {
- if (typeof item === "string") return item;
- return item?.name || item?.test_name || item?.detail_name || item?.exam || item?.label || "";
- })
- .filter(Boolean)
- : [];
const status = raw?.status || raw?.order_status || raw?.state || "Processing";
return {
- id:
- raw?.order_patient_id ||
- raw?.order_id ||
- raw?.id ||
- raw?.OrderPatientID ||
- raw?.OrderID ||
- `ORD-${String(index + 1).padStart(5, "0")}`,
- patient: raw?.patient_name || raw?.name || raw?.patient || raw?.patient_fullname || "Patient",
- doctor: raw?.doctor_name || raw?.doctor || raw?.M_UserUsername || mockUser.name,
- updated: raw?.updated_at || raw?.updated || raw?.created_at || "Today",
+ id: raw?.order_patient_id || raw?.order_id || raw?.id || raw?.OrderPatientID || raw?.OrderID || "",
+ patient: raw?.order_name || raw?.patient_name || raw?.name || raw?.patient || raw?.patient_fullname || "",
+ doctor: raw?.doctor_id || raw?.doctor_name || raw?.doctor || raw?.M_UserUsername || "",
+ updated: raw?.order_date || raw?.updated_at || raw?.updated || raw?.created_at || "",
status,
tone: statusClass(status),
- mode: raw?.mode || raw?.visit_type || raw?.patient_type || "Outpatient",
+ mode: raw?.mode || raw?.visit_type || raw?.patient_type || "",
age: String(raw?.age || raw?.patient_age || ""),
gender: raw?.gender || raw?.patient_gender || "",
- tests,
- diagnosis: raw?.patient_diagnosa || raw?.diagnosis || raw?.note || "",
- message: raw?.message || raw?.patient_note || "",
+ tests: [],
+ diagnosis: raw?.order_nik || raw?.patient_diagnosa || raw?.diagnosis || raw?.note || "",
+ message: raw?.order_address || raw?.message || raw?.patient_note || "",
+ orderDate: raw?.order_date || "",
+ orderNik: raw?.order_nik || "",
+ orderHp: raw?.order_hp || "",
+ orderAddress: raw?.order_address || "",
+ orderDob: raw?.order_dob || "",
};
}
function normalizeResult(raw, index = 0) {
const status = raw?.status || raw?.result_status || "Released";
return {
- id:
- raw?.result_id ||
- raw?.order_id ||
- raw?.id ||
- raw?.hasil_id ||
- `RES-${String(index + 1).padStart(4, "0")}`,
- patient: raw?.patient_name || raw?.name || raw?.patient || "Patient",
- test: raw?.test_name || raw?.item_name || raw?.test || "Result",
+ id: raw?.result_id || raw?.order_id || raw?.id || raw?.hasil_id || "",
+ patient: raw?.patient_name || raw?.name || raw?.patient || "",
+ test: raw?.test_name || raw?.item_name || raw?.test || "",
status,
tone: statusClass(status),
- date: raw?.date || raw?.created_at || raw?.updated_at || "Today",
+ date: raw?.date || raw?.created_at || raw?.updated_at || "",
summary: raw?.summary || raw?.note || raw?.result_summary || "",
value: raw?.value || raw?.result_value || raw?.result || "",
};
@@ -1275,13 +1242,14 @@ async function loadOrders(session, search = "", status = "All") {
"/order/search_order_pasien_by_doktorid",
{
token: session.token,
- OrderPatientM_DoctorID: session.doctorId || mockUser.doctorId,
+ OrderPatientM_DoctorID: session.doctorId || sampleLogin.doctorId,
search: term,
},
session.token,
);
const rows = extractArray(payload) || [];
- const orders = rows.map((row, index) => normalizeOrder(row, index)).filter((item) => {
+ return rows.map((row, index) => normalizeOrder(row, index)).filter((item) => {
+ if (!item.id) return false;
const matchesSearch =
!term ||
[item.id, item.patient, item.status, item.diagnosis, item.message].some((value) =>
@@ -1290,39 +1258,38 @@ async function loadOrders(session, search = "", status = "All") {
const matchesStatus = !status || status === "All" || item.status === status;
return matchesSearch && matchesStatus;
});
- if (orders.length) return orders;
} catch {
- // Fall through to mock data.
+ return [];
}
- return filterOrders(term, status);
+ return [];
}
async function loadResults(session, search = "") {
const term = String(search || "").trim();
try {
+ const orders = await loadOrders(session, "", "All");
+ const orderId = session.orderId || orders[0]?.id || "";
const payload = await apiPost(
"/order/hasil_belum_keluar_by_id",
- { token: session.token, order_id: session.orderId || "" },
+ { token: session.token, order_id: orderId },
session.token,
);
const rows = extractArray(payload) || [];
- const results = rows.map((row, index) => normalizeResult(row, index)).filter((item) => {
+ return rows.map((row, index) => normalizeResult(row, index)).filter((item) => {
+ if (!item.id) return false;
if (!term) return true;
return [item.id, item.patient, item.test, item.status, item.summary].some((value) =>
String(value).toLowerCase().includes(term.toLowerCase()),
);
});
- if (results.length) return results;
} catch {
- // Fall through to mock data.
+ return [];
}
- return filterResults(term);
+ return [];
}
async function loadResultDetail(session, resultId) {
- const fallback = mockResults.find((item) => item.id === resultId) || mockResults[0];
try {
- // Inferred from project-specs note about an additional result base path.
const payload = await apiPost(
"/result/getResult",
{
@@ -1334,17 +1301,12 @@ async function loadResultDetail(session, resultId) {
);
const rows = extractArray(payload);
if (rows?.length) return normalizeResult(rows[0], 0);
- if (payload && typeof payload === "object") {
- return {
- ...fallback,
- ...normalizeResult(payload, 0),
- id: resultId || fallback.id,
- };
- }
+ if (payload && typeof payload === "object") return normalizeResult(payload, 0);
+ const related = await loadResults(session, resultId);
+ return related.find((item) => item.id === resultId) || null;
} catch {
- // Fall through to local seed data when the upstream result API is unavailable.
+ return null;
}
- return fallback;
}
async function loadFpp(session, group = "All") {
@@ -1356,38 +1318,35 @@ async function loadFpp(session, group = "All") {
count: Number(row?.count || row?.total || row?.qty || 1),
desc: row?.description || row?.desc || "Laboratory reference item.",
}));
- if (items.length) {
- const filtered = group === "All" ? items : items.filter((item) => item.group === group);
- return { items: filtered, filter: group };
- }
+ const filtered = group === "All" ? items : items.filter((item) => item.group === group);
+ return { items: filtered, filter: group };
} catch {
- // Fall through to mock data.
+ return { items: [], filter: group };
}
- const items = group === "All" ? mockFppGroups : mockFppGroups.filter((item) => item.group === group);
- return { items, filter: group };
}
async function loadOrderDetail(session, orderId) {
- const order = mockOrders.find((item) => item.id === orderId) || mockOrders[0];
- const detail = { ...order };
- try {
- const saran = await apiPost(
+ const [orders, saran, hasil] = await Promise.all([
+ loadOrders(session, "", "All"),
+ apiPost(
"/order/get_order_saran_by_order_patient_id",
{ token: session.token, order_patient_id: orderId },
session.token,
- );
- detail.apiSaran = extractArray(saran) || saran?.message || saran?.note || saran?.result || "";
- } catch {
- detail.apiSaran = "";
- }
- try {
- const hasil = await apiPost(
+ ).catch(() => null),
+ apiPost(
"/order/hasil_belum_keluar_by_id",
{ token: session.token, order_id: orderId },
session.token,
- );
- detail.apiHasil = extractArray(hasil) || hasil?.message || hasil?.note || hasil?.result || "";
+ ).catch(() => null),
+ ]);
+ const order = orders.find((item) => item.id === orderId) || orders[0] || null;
+ if (!order) return null;
+ const detail = { ...order };
+ try {
+ detail.apiSaran = saran ? extractArray(saran) || saran?.message || saran?.note || saran?.result || "" : "";
+ detail.apiHasil = hasil ? extractArray(hasil) || hasil?.message || hasil?.note || hasil?.result || "" : "";
} catch {
+ detail.apiSaran = "";
detail.apiHasil = "";
}
return detail;
@@ -1421,32 +1380,9 @@ async function readBody(req) {
return Object.fromEntries(new URLSearchParams(raw));
}
-function filterOrders(search, status) {
- const term = String(search || "").trim().toLowerCase();
- return mockOrders.filter((order) => {
- const matchesSearch =
- !term ||
- [order.id, order.patient, order.status, order.diagnosis, order.message].some((value) =>
- String(value).toLowerCase().includes(term),
- );
- const matchesStatus = !status || status === "All" || order.status === status;
- return matchesSearch && matchesStatus;
- });
-}
-
-function filterResults(search) {
- const term = String(search || "").trim().toLowerCase();
- return mockResults.filter((result) => {
- if (!term) return true;
- return [result.id, result.patient, result.test, result.status, result.summary].some((value) =>
- String(value).toLowerCase().includes(term),
- );
- });
-}
-
function fragmentOrdersTable(search = "", status = "All", ordersData = null) {
- const orders = ordersData || filterOrders(search, status);
- const selected = orders[0] || mockOrders[0];
+ const orders = ordersData || [];
+ const selected = orders[0] || null;
return `
@@ -1461,107 +1397,132 @@ function fragmentOrdersTable(search = "", status = "All", ordersData = null) {
)
.join("")}
-
-
-
- Patient Order ID Doctor Status Updated
-
-
- ${orders
- .map(
- (order) => `
-
- ${escapeHtml(order.patient)} ${escapeHtml(order.mode)}
- ${escapeHtml(order.id)}
- ${escapeHtml(order.doctor)}
- ${statusBadge(order.status)}
- ${escapeHtml(order.updated)}
-
- `,
- )
- .join("")}
-
-
-
+ ${
+ orders.length
+ ? `
+
+
+
+ Patient Order ID Doctor Status Updated
+
+
+ ${orders
+ .map(
+ (order) => `
+
+ ${escapeHtml(order.patient || "Unknown patient")} ${escapeHtml(order.mode || "-")}
+ ${escapeHtml(order.id)}
+ ${escapeHtml(order.doctor || "-")}
+ ${statusBadge(order.status)}
+ ${escapeHtml(order.updated || "-")}
+
+ `,
+ )
+ .join("")}
+
+
+
+ `
+ : emptyState("No orders returned", "The order endpoint did not return any rows for this filter.")
+ }
- ${panelHeader("Selected order", "Context stays visible while you review the table.", `Open detail `)}
-
-
-
${escapeHtml(selected.patient)}
-
${escapeHtml(selected.id)} · ${escapeHtml(selected.mode)}
-
${statusBadge(selected.status)}${escapeHtml(selected.age)} years ${escapeHtml(selected.gender)}
-
-
-
Diagnosis ${escapeHtml(selected.diagnosis)}
-
Requested tests ${escapeHtml(selected.tests.join(", "))}
-
Special note ${escapeHtml(selected.message)}
-
-
+ ${panelHeader("Selected order", "Context stays visible while you review the table.", selected ? `Open detail ` : "")}
+ ${
+ selected
+ ? `
+
+
+
${escapeHtml(selected.patient || "Unknown patient")}
+
${escapeHtml(selected.id || "-")} · ${escapeHtml(selected.mode || "-")}
+
${statusBadge(selected.status)}${escapeHtml(selected.age || "-")} years ${escapeHtml(selected.gender || "-")}
+
+
+
Diagnosis ${escapeHtml(selected.diagnosis || "-")}
+
Requested tests ${escapeHtml((selected.tests || []).join(", ") || "-")}
+
Special note ${escapeHtml(selected.message || "-")}
+
+
+ `
+ : emptyState("No selected order", "The API returned no rows for this search.")
+ }
`;
}
function fragmentResultsTable(search = "", resultsData = null) {
- const results = resultsData || filterResults(search);
- const selected = results[0] || mockResults[0];
+ const results = resultsData || [];
+ const selected = results[0] || null;
return `
${panelHeader("Result history", "Desktop shows table detail. Mobile collapses into stacked cards.")}
-
-
-
- Patient Result ID Test Status Date
-
-
- ${results
- .map(
- (result) => `
-
- ${escapeHtml(result.patient)} ${escapeHtml(result.summary)}
- ${escapeHtml(result.id)}
- ${escapeHtml(result.test)}
- ${statusBadge(result.status)}
- ${escapeHtml(result.date)}
-
- `,
- )
- .join("")}
-
-
+
Released ${results.filter((item) => item.status === "Released").length} items
+
Pending ${results.filter((item) => item.status === "Pending").length} items
+
Reviewed ${results.filter((item) => item.status === "Review").length} items
+ ${
+ results.length
+ ? `
+
+
+
+ Patient Result ID Test Status Date
+
+
+ ${results
+ .map(
+ (result) => `
+
+ ${escapeHtml(result.patient || "Unknown patient")} ${escapeHtml(result.summary || "-")}
+ ${escapeHtml(result.id)}
+ ${escapeHtml(result.test || "-")}
+ ${statusBadge(result.status)}
+ ${escapeHtml(result.date || "-")}
+
+ `,
+ )
+ .join("")}
+
+
+
+ `
+ : emptyState("No results returned", "The result endpoint did not return any rows for this search.")
+ }
`;
}
function fragmentFpp(group = "All", itemsData = null) {
- const items = itemsData || (group === "All" ? mockFppGroups : mockFppGroups.filter((item) => item.group === group));
+ const items = itemsData || [];
+ const groups = ["All", ...Array.from(new Set(items.map((item) => item.group)))];
return `
${panelHeader("FPP catalog", "Filter blocks and list cards on mobile, richer panel on desktop.")}
- ${["All", ...mockFppGroups.map((item) => item.group)]
+ ${groups
.map(
(item) => `
${escapeHtml(item)}
@@ -1571,33 +1532,37 @@ function fragmentFpp(group = "All", itemsData = null) {
- ${items
- .map(
- (item) => `
-
-
-
- ${["Filter", "Inspect", "Select", "Export"]
- .map(
- (action) => `
-
-
${escapeHtml(action)}
-
Workflow action for ${escapeHtml(item.group)}.
+ ${
+ items.length
+ ? items
+ .map(
+ (item) => `
+
+
-
- `,
- )
- .join("")}
+
${escapeHtml(item.count)} items
+
+
+ ${["Filter", "Inspect", "Select", "Export"]
+ .map(
+ (action) => `
+
+
${escapeHtml(action)}
+
Workflow action for ${escapeHtml(item.group)}.
+
+ `,
+ )
+ .join("")}
+
+
+ `,
+ )
+ .join("")
+ : `
${emptyState("No FPP items", "The API returned no catalog rows for this filter.")} `
+ }
`;
@@ -1608,7 +1573,9 @@ function fragmentOrderStep(step) {
}
function fragmentPesanKhusus(orderId) {
- const order = mockOrders.find((item) => item.id === orderId) || mockOrders[0];
+ const order = orderId
+ ? { id: orderId, patient: "", status: "Processing", message: "", apiSaran: "" }
+ : { id: "", patient: "", status: "Processing", message: "", apiSaran: "" };
return `
@@ -1616,15 +1583,15 @@ function fragmentPesanKhusus(orderId) {
${panelHeader("Pesan khusus", "Desktop opens this as a modal, mobile can still use it as a full sheet.", `
Close `)}
-
${escapeHtml(order.patient)}
+
${escapeHtml(order.patient || "Order detail")}
${escapeHtml(order.id)}
${statusBadge(order.status)}
-
${escapeHtml(order.message)}
+
${escapeHtml(order.message || "-")}