Source order details from FPP catalog

This commit is contained in:
sas.fajri
2026-04-13 16:07:01 +07:00
parent 05b65b5c7a
commit f0bb1ced65

187
server.js
View File

@@ -11,6 +11,7 @@ const sessionKey = "doclink_session";
const sampleLogin = { const sampleLogin = {
username: "yogayogi", username: "yogayogi",
doctorId: "31010002", doctorId: "31010002",
mouId: "2773",
password: "123456", password: "123456",
}; };
@@ -461,7 +462,7 @@ function renderOrderDetail(order) {
`; `;
} }
function renderOrderForm(step, stepKey = "demografi") { function renderOrderForm(step, stepKey = "demografi", fppTests = [], mouId = "") {
const steps = [ const steps = [
["demografi", "Demografi", "Patient identity and contact details."], ["demografi", "Demografi", "Patient identity and contact details."],
["diagnosa", "Diagnosa", "Clinical indication and diagnosis."], ["diagnosa", "Diagnosa", "Clinical indication and diagnosis."],
@@ -472,46 +473,104 @@ function renderOrderForm(step, stepKey = "demografi") {
const activeIndex = Math.max(0, steps.findIndex((item) => item[0] === stepKey)); const activeIndex = Math.max(0, steps.findIndex((item) => item[0] === stepKey));
const stepBody = { const stepBody = {
demografi: ` demografi: `
<div class="grid grid-2"> <div class="stack">
<label class="field"><span>Patient name</span><input placeholder="Siti Amelia" /></label> <div class="panel" style="padding:0; box-shadow:none; background:transparent; border:0">
<label class="field"><span>Medical record no.</span><input placeholder="MRN-10011" /></label> ${panelHeader("Mandatory", "These fields are required or expected by the backend before save.")}
<label class="field"><span>Gender</span><select><option>Female</option><option>Male</option></select></label> <div class="grid grid-2">
<label class="field"><span>Date of birth</span><input type="date" /></label> <label class="field"><span>Patient name</span><input name="patient_name" placeholder="Fajar Anugrah" required /></label>
<label class="field" style="grid-column:1/-1"><span>Address</span><input placeholder="Bandung" /></label> <input type="hidden" name="M_MouID" value="${escapeHtml(mouId || sampleLogin.mouId)}" />
<div class="card">
<strong>Backend Mou ID</strong>
<p class="muted">Auto-filled from the logged-in session.</p>
</div>
</div>
</div>
<div class="panel" style="padding:0; box-shadow:none; background:transparent; border:0">
${panelHeader("Optional", "These fields can be filled when available.")}
<div class="grid grid-2">
<label class="field"><span>Date of birth</span><input name="patient_dob" type="date" /></label>
<label class="field"><span>NIK</span><input name="patient_nik" placeholder="16 digits if available" /></label>
<label class="field"><span>Phone</span><input name="patient_hp" placeholder="08xxxxxxxxxx" /></label>
<label class="field"><span>Address</span><input name="patient_address" placeholder="Bandung" /></label>
</div>
</div>
</div> </div>
`, `,
diagnosa: ` diagnosa: `
<div class="grid grid-2"> <div class="stack">
<label class="field"><span>Diagnosis</span><input placeholder="Kontrol hipertensi" /></label> <div class="panel" style="padding:0; box-shadow:none; background:transparent; border:0">
<label class="field"><span>Patient note</span><input placeholder="Fasting sample required" /></label> ${panelHeader("Mandatory", "Clinical indication is expected before save.")}
<label class="field" style="grid-column:1/-1"><span>Clinical complaint</span><textarea placeholder="Add symptoms and context"></textarea></label> <div class="grid grid-2">
<label class="field" style="grid-column:1/-1"><span>Diagnosis</span><input name="patient_diagnosa" placeholder="Demam" required /></label>
</div>
</div>
<div class="panel" style="padding:0; box-shadow:none; background:transparent; border:0">
${panelHeader("Optional", "Notes are useful for the lab or front office.")}
<div class="grid grid-2">
<label class="field" style="grid-column:1/-1"><span>Patient note</span><textarea name="patient_note" placeholder="sudah seminggu"></textarea></label>
</div>
</div>
</div> </div>
`, `,
pemeriksaan: ` pemeriksaan: `
<div class="pill-row"> <div class="stack">
${["Hematology", "Clinical Chemistry", "Urinalysis", "Immunology", "Microbiology"].map((item) => `<span class="pill">${escapeHtml(item)}</span>`).join("")} <div class="panel" style="padding:0; box-shadow:none; background:transparent; border:0">
</div> ${panelHeader("Mandatory", "The backend accepts a details array, so keep at least one item here when saving.")}
<div style="height:14px"></div> ${
<div class="grid grid-2"> fppTests.length
${["CBC", "Glucose", "Lipid Profile", "CRP"].map((item) => ` ? `<div class="grid grid-2">${fppTests.slice(0, 4).map((test, index) => `
<label class="card"> <label class="card">
<input type="checkbox" /> ${escapeHtml(item)} <span class="muted">${escapeHtml(test.heading || "FPP")}${test.subcategory ? ` · ${escapeHtml(test.subcategory)}` : ""}</span>
<p class="muted">Selectable test item</p> <strong style="display:block; margin-top:8px">${escapeHtml(test.name)}</strong>
</label> <input type="hidden" name="details[${index}][test_id]" value="${escapeHtml(test.testId)}" />
`).join("")} <input type="hidden" name="details[${index}][test_name]" value="${escapeHtml(test.name)}" />
<input type="hidden" name="details[${index}][price]" value="${escapeHtml(test.price)}" />
</label>
`).join("")}</div>`
: `<div class="note-box">FPP catalog not available yet.</div>`
}
</div>
<div class="panel" style="padding:0; box-shadow:none; background:transparent; border:0">
${panelHeader("Optional", "Add more test rows when needed.")}
<div class="grid grid-2">
${
fppTests.length > 4
? fppTests.slice(4, 8).map((test, index) => `
<label class="card">
<span class="muted">${escapeHtml(test.heading || "FPP")}${test.subcategory ? ` · ${escapeHtml(test.subcategory)}` : ""}</span>
<strong style="display:block; margin-top:8px">${escapeHtml(test.name)}</strong>
<input type="hidden" name="details[${index + 4}][test_id]" value="${escapeHtml(test.testId)}" />
<input type="hidden" name="details[${index + 4}][test_name]" value="${escapeHtml(test.name)}" />
<input type="hidden" name="details[${index + 4}][price]" value="${escapeHtml(test.price)}" />
</label>
`).join("")
: `<div class="note-box">No additional FPP rows available.</div>`
}
</div>
</div>
</div> </div>
`, `,
qrcode: ` qrcode: `
<div class="grid grid-2"> <div class="stack">
<div class="card"> <div class="panel" style="padding:0; box-shadow:none; background:transparent; border:0">
<strong>Scan existing patient QR</strong> ${panelHeader("Mandatory", "QR flow is optional in the backend, but keep the scan control at the top if used.")}
<p class="muted">Use a QR code to pull patient and visit context instantly.</p> <div class="grid grid-2">
<div class="note-box">QR preview area</div> <div class="card">
<strong>Scan existing patient QR</strong>
<p class="muted">Use a QR code to pull patient and visit context instantly.</p>
<div class="note-box">QR preview area</div>
</div>
</div>
</div> </div>
<div class="card"> <div class="panel" style="padding:0; box-shadow:none; background:transparent; border:0">
<strong>Manual fallback</strong> ${panelHeader("Optional", "Manual fallback when scanning is not available.")}
<p class="muted">Still allow manual entry if the scan is unavailable.</p> <div class="grid grid-2">
<label class="field"><span>QR token</span><input placeholder="DOCLINK-QR-0001" /></label> <div class="card">
<strong>Manual fallback</strong>
<p class="muted">Still allow manual entry if the scan is unavailable.</p>
<label class="field"><span>QR token</span><input placeholder="DOCLINK-QR-0001" /></label>
</div>
</div>
</div> </div>
</div> </div>
`, `,
@@ -519,10 +578,10 @@ function renderOrderForm(step, stepKey = "demografi") {
<div class="stack"> <div class="stack">
<div class="card"><strong>Summary</strong><p class="muted">All steps are merged into a final review before submit.</p></div> <div class="card"><strong>Summary</strong><p class="muted">All steps are merged into a final review before submit.</p></div>
<div class="grid grid-2"> <div class="grid grid-2">
<div class="mini-item"><div><strong>Patient</strong><p>Siti Amelia</p></div></div> <div class="mini-item"><div><strong>Patient name</strong><p>patient_name</p></div></div>
<div class="mini-item"><div><strong>Diagnosis</strong><p>Check-up rutin</p></div></div> <div class="mini-item"><div><strong>Diagnosis</strong><p>patient_diagnosa</p></div></div>
<div class="mini-item"><div><strong>Tests</strong><p>CBC, Glucose, Urine</p></div></div> <div class="mini-item"><div><strong>Note</strong><p>patient_note</p></div></div>
<div class="mini-item"><div><strong>Message</strong><p>Prioritize fasting sample</p></div></div> <div class="mini-item"><div><strong>Details</strong><p>details[]</p></div></div>
</div> </div>
</div> </div>
`, `,
@@ -947,9 +1006,10 @@ function problemLoginPage() {
); );
} }
function orderNewPage(path) { async function orderNewPage(session, path) {
const step = path.split("/").filter(Boolean)[2] || "demografi"; const step = path.split("/").filter(Boolean)[2] || "demografi";
return layout("Create Order", `<div id="order-step-fragment">${renderOrderForm({}, step)}</div>`, { const fppTests = await loadFppCatalog(session);
return layout("Create Order", `<div id="order-step-fragment">${renderOrderForm({}, step, fppTests, session?.mouId || "")}</div>`, {
authenticated: true, authenticated: true,
activePath: "/orders", activePath: "/orders",
subtitle: "The new project collapses the old step-heavy flow into a cleaner shell while keeping the same workflow.", subtitle: "The new project collapses the old step-heavy flow into a cleaner shell while keeping the same workflow.",
@@ -1130,12 +1190,14 @@ function normalizeSession(payload) {
const username = user?.M_UserUsername || data?.username || data?.M_UserUsername || data?.name || ""; const username = user?.M_UserUsername || data?.username || data?.M_UserUsername || data?.name || "";
const doctorId = user?.M_UserM_DoctorID || data?.doctor_id || data?.doctorId || ""; const doctorId = user?.M_UserM_DoctorID || data?.doctor_id || data?.doctorId || "";
const doctorCode = user?.M_UserM_DoctorCode || data?.doctor_code || ""; const doctorCode = user?.M_UserM_DoctorCode || data?.doctor_code || "";
const mouId = user?.M_UserM_MouID || data?.M_UserM_MouID || data?.mou_id || "";
const userId = user?.M_UserID || data?.M_UserID || data?.user_id || ""; const userId = user?.M_UserID || data?.M_UserID || data?.user_id || "";
return { return {
token, token,
username, username,
doctorId, doctorId,
doctorCode, doctorCode,
mouId,
userId, userId,
raw: payload, raw: payload,
}; };
@@ -1296,6 +1358,29 @@ async function loadFpp(session, group = "All") {
} }
} }
async function loadFppCatalog(session) {
try {
const payload = await apiPost("/Fpp/loadFPP/1/1", { token: session.token }, session.token);
const rows = extractArray(payload) || payload?.rows || [];
const headings = Array.isArray(rows) ? rows : [];
const tests = headings.flatMap((heading) =>
(heading?.details || []).flatMap((detail) =>
(detail?.tests || []).map((test) => ({
heading: heading?.heading || "FPP",
subcategory: detail?.subcategories || "",
testId: String(test?.testid || ""),
code: String(test?.code || ""),
name: String(test?.name || ""),
price: String(test?.price || "0"),
})),
),
);
return tests.filter((item) => item.name);
} catch {
return [];
}
}
async function loadOrderDetail(session, orderId) { async function loadOrderDetail(session, orderId) {
const [orders, saran, hasil] = await Promise.all([ const [orders, saran, hasil] = await Promise.all([
loadOrders(session, "", "All"), loadOrders(session, "", "All"),
@@ -1348,7 +1433,19 @@ async function readBody(req) {
const raw = Buffer.concat(chunks).toString("utf8"); const raw = Buffer.concat(chunks).toString("utf8");
const contentType = req.headers["content-type"] || ""; const contentType = req.headers["content-type"] || "";
if (contentType.includes("application/json")) return raw ? JSON.parse(raw) : {}; if (contentType.includes("application/json")) return raw ? JSON.parse(raw) : {};
return Object.fromEntries(new URLSearchParams(raw)); const flat = Object.fromEntries(new URLSearchParams(raw));
const details = [];
for (const [key, value] of Object.entries(flat)) {
const match = key.match(/^details\[(\d+)\]\[(\w+)\]$/);
if (!match) continue;
const index = Number(match[1]);
const field = match[2];
details[index] ||= {};
details[index][field] = value;
delete flat[key];
}
if (details.length) flat.details = details.filter(Boolean);
return flat;
} }
function fragmentOrdersTable(search = "", status = "All", ordersData = null) { function fragmentOrdersTable(search = "", status = "All", ordersData = null) {
@@ -1539,8 +1636,9 @@ function fragmentFpp(group = "All", itemsData = null) {
`; `;
} }
function fragmentOrderStep(step) { async function fragmentOrderStep(session, step) {
return renderOrderForm({}, step); const fppTests = await loadFppCatalog(session);
return renderOrderForm({}, step, fppTests, session?.mouId || "");
} }
function fragmentPesanKhusus(orderId) { function fragmentPesanKhusus(orderId) {
@@ -1627,6 +1725,7 @@ async function renderRoute(req, res, url) {
username: normalized.username || body.username || "", username: normalized.username || body.username || "",
doctorId: normalized.doctorId || body.doctor_id || "", doctorId: normalized.doctorId || body.doctor_id || "",
doctorCode: normalized.doctorCode || body.doctor_id || "", doctorCode: normalized.doctorCode || body.doctor_id || "",
mouId: normalized.mouId || body.M_MouID || "",
userId: normalized.userId || "", userId: normalized.userId || "",
raw: payload, raw: payload,
}); });
@@ -1693,13 +1792,13 @@ async function renderRoute(req, res, url) {
if (path === "/orders/new" && isGet) { if (path === "/orders/new" && isGet) {
if (!requireAuth(req, res)) return; if (!requireAuth(req, res)) return;
html(res, 200, orderNewPage(path)); html(res, 200, await orderNewPage(session, path));
return; return;
} }
if (path.startsWith("/orders/new/") && isGet) { if (path.startsWith("/orders/new/") && isGet) {
if (!requireAuth(req, res)) return; if (!requireAuth(req, res)) return;
html(res, 200, orderNewPage(path)); html(res, 200, await orderNewPage(session, path));
return; return;
} }
@@ -1822,7 +1921,7 @@ async function renderRoute(req, res, url) {
if (path.startsWith("/fragments/forms/order-step/") && isGet) { if (path.startsWith("/fragments/forms/order-step/") && isGet) {
if (!requireAuth(req, res)) return; if (!requireAuth(req, res)) return;
const step = path.split("/")[4] || "demografi"; const step = path.split("/")[4] || "demografi";
html(res, 200, fragmentOrderStep(step)); html(res, 200, await fragmentOrderStep(session, step));
return; return;
} }
@@ -1853,7 +1952,7 @@ async function renderRoute(req, res, url) {
"/order/order_patient", "/order/order_patient",
{ {
token: sessionData.token, token: sessionData.token,
M_MouID: body.M_MouID || sessionData.doctorId || "", M_MouID: body.M_MouID || sessionData.mouId || sampleLogin.mouId || "",
patient_name: body.patient_name || "", patient_name: body.patient_name || "",
patient_diagnosa: body.patient_diagnosa || "", patient_diagnosa: body.patient_diagnosa || "",
patient_address: body.patient_address || "", patient_address: body.patient_address || "",