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

161
server.js
View File

@@ -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 || "",