Source order details from FPP catalog
This commit is contained in:
161
server.js
161
server.js
@@ -11,6 +11,7 @@ const sessionKey = "doclink_session";
|
||||
const sampleLogin = {
|
||||
username: "yogayogi",
|
||||
doctorId: "31010002",
|
||||
mouId: "2773",
|
||||
password: "123456",
|
||||
};
|
||||
|
||||
@@ -461,7 +462,7 @@ function renderOrderDetail(order) {
|
||||
`;
|
||||
}
|
||||
|
||||
function renderOrderForm(step, stepKey = "demografi") {
|
||||
function renderOrderForm(step, stepKey = "demografi", fppTests = [], mouId = "") {
|
||||
const steps = [
|
||||
["demografi", "Demografi", "Patient identity and contact details."],
|
||||
["diagnosa", "Diagnosa", "Clinical indication and diagnosis."],
|
||||
@@ -472,57 +473,115 @@ function renderOrderForm(step, stepKey = "demografi") {
|
||||
const activeIndex = Math.max(0, steps.findIndex((item) => item[0] === stepKey));
|
||||
const stepBody = {
|
||||
demografi: `
|
||||
<div class="stack">
|
||||
<div class="panel" style="padding:0; box-shadow:none; background:transparent; border:0">
|
||||
${panelHeader("Mandatory", "These fields are required or expected by the backend before save.")}
|
||||
<div class="grid grid-2">
|
||||
<label class="field"><span>Patient name</span><input placeholder="Siti Amelia" /></label>
|
||||
<label class="field"><span>Medical record no.</span><input placeholder="MRN-10011" /></label>
|
||||
<label class="field"><span>Gender</span><select><option>Female</option><option>Male</option></select></label>
|
||||
<label class="field"><span>Date of birth</span><input type="date" /></label>
|
||||
<label class="field" style="grid-column:1/-1"><span>Address</span><input placeholder="Bandung" /></label>
|
||||
<label class="field"><span>Patient name</span><input name="patient_name" placeholder="Fajar Anugrah" required /></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>
|
||||
`,
|
||||
diagnosa: `
|
||||
<div class="stack">
|
||||
<div class="panel" style="padding:0; box-shadow:none; background:transparent; border:0">
|
||||
${panelHeader("Mandatory", "Clinical indication is expected before save.")}
|
||||
<div class="grid grid-2">
|
||||
<label class="field"><span>Diagnosis</span><input placeholder="Kontrol hipertensi" /></label>
|
||||
<label class="field"><span>Patient note</span><input placeholder="Fasting sample required" /></label>
|
||||
<label class="field" style="grid-column:1/-1"><span>Clinical complaint</span><textarea placeholder="Add symptoms and context"></textarea></label>
|
||||
<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>
|
||||
`,
|
||||
pemeriksaan: `
|
||||
<div class="pill-row">
|
||||
${["Hematology", "Clinical Chemistry", "Urinalysis", "Immunology", "Microbiology"].map((item) => `<span class="pill">${escapeHtml(item)}</span>`).join("")}
|
||||
</div>
|
||||
<div style="height:14px"></div>
|
||||
<div class="grid grid-2">
|
||||
${["CBC", "Glucose", "Lipid Profile", "CRP"].map((item) => `
|
||||
<div class="stack">
|
||||
<div class="panel" style="padding:0; box-shadow:none; background:transparent; border:0">
|
||||
${panelHeader("Mandatory", "The backend accepts a details array, so keep at least one item here when saving.")}
|
||||
${
|
||||
fppTests.length
|
||||
? `<div class="grid grid-2">${fppTests.slice(0, 4).map((test, index) => `
|
||||
<label class="card">
|
||||
<input type="checkbox" /> ${escapeHtml(item)}
|
||||
<p class="muted">Selectable test item</p>
|
||||
<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}][test_id]" value="${escapeHtml(test.testId)}" />
|
||||
<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("")}
|
||||
`).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>
|
||||
`,
|
||||
qrcode: `
|
||||
<div class="stack">
|
||||
<div class="panel" style="padding:0; box-shadow:none; background:transparent; border:0">
|
||||
${panelHeader("Mandatory", "QR flow is optional in the backend, but keep the scan control at the top if used.")}
|
||||
<div class="grid grid-2">
|
||||
<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 class="panel" style="padding:0; box-shadow:none; background:transparent; border:0">
|
||||
${panelHeader("Optional", "Manual fallback when scanning is not available.")}
|
||||
<div class="grid grid-2">
|
||||
<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>
|
||||
`,
|
||||
review: `
|
||||
<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="grid grid-2">
|
||||
<div class="mini-item"><div><strong>Patient</strong><p>Siti Amelia</p></div></div>
|
||||
<div class="mini-item"><div><strong>Diagnosis</strong><p>Check-up rutin</p></div></div>
|
||||
<div class="mini-item"><div><strong>Tests</strong><p>CBC, Glucose, Urine</p></div></div>
|
||||
<div class="mini-item"><div><strong>Message</strong><p>Prioritize fasting sample</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>patient_diagnosa</p></div></div>
|
||||
<div class="mini-item"><div><strong>Note</strong><p>patient_note</p></div></div>
|
||||
<div class="mini-item"><div><strong>Details</strong><p>details[]</p></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";
|
||||
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,
|
||||
activePath: "/orders",
|
||||
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 doctorId = user?.M_UserM_DoctorID || data?.doctor_id || data?.doctorId || "";
|
||||
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 || "";
|
||||
return {
|
||||
token,
|
||||
username,
|
||||
doctorId,
|
||||
doctorCode,
|
||||
mouId,
|
||||
userId,
|
||||
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) {
|
||||
const [orders, saran, hasil] = await Promise.all([
|
||||
loadOrders(session, "", "All"),
|
||||
@@ -1348,7 +1433,19 @@ async function readBody(req) {
|
||||
const raw = Buffer.concat(chunks).toString("utf8");
|
||||
const contentType = req.headers["content-type"] || "";
|
||||
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) {
|
||||
@@ -1539,8 +1636,9 @@ function fragmentFpp(group = "All", itemsData = null) {
|
||||
`;
|
||||
}
|
||||
|
||||
function fragmentOrderStep(step) {
|
||||
return renderOrderForm({}, step);
|
||||
async function fragmentOrderStep(session, step) {
|
||||
const fppTests = await loadFppCatalog(session);
|
||||
return renderOrderForm({}, step, fppTests, session?.mouId || "");
|
||||
}
|
||||
|
||||
function fragmentPesanKhusus(orderId) {
|
||||
@@ -1627,6 +1725,7 @@ async function renderRoute(req, res, url) {
|
||||
username: normalized.username || body.username || "",
|
||||
doctorId: normalized.doctorId || body.doctor_id || "",
|
||||
doctorCode: normalized.doctorCode || body.doctor_id || "",
|
||||
mouId: normalized.mouId || body.M_MouID || "",
|
||||
userId: normalized.userId || "",
|
||||
raw: payload,
|
||||
});
|
||||
@@ -1693,13 +1792,13 @@ async function renderRoute(req, res, url) {
|
||||
|
||||
if (path === "/orders/new" && isGet) {
|
||||
if (!requireAuth(req, res)) return;
|
||||
html(res, 200, orderNewPage(path));
|
||||
html(res, 200, await orderNewPage(session, path));
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.startsWith("/orders/new/") && isGet) {
|
||||
if (!requireAuth(req, res)) return;
|
||||
html(res, 200, orderNewPage(path));
|
||||
html(res, 200, await orderNewPage(session, path));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1822,7 +1921,7 @@ async function renderRoute(req, res, url) {
|
||||
if (path.startsWith("/fragments/forms/order-step/") && isGet) {
|
||||
if (!requireAuth(req, res)) return;
|
||||
const step = path.split("/")[4] || "demografi";
|
||||
html(res, 200, fragmentOrderStep(step));
|
||||
html(res, 200, await fragmentOrderStep(session, step));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1853,7 +1952,7 @@ async function renderRoute(req, res, url) {
|
||||
"/order/order_patient",
|
||||
{
|
||||
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_diagnosa: body.patient_diagnosa || "",
|
||||
patient_address: body.patient_address || "",
|
||||
|
||||
Reference in New Issue
Block a user