Move create order QR to saved state

This commit is contained in:
sas.fajri
2026-04-13 19:05:03 +07:00
parent 0030cfb11a
commit 24921e836c
2 changed files with 177 additions and 82 deletions

221
server.js
View File

@@ -201,6 +201,15 @@ function layout(title, body, { authenticated = false, activePath = "/", subtitle
if (field.name) currentNames.add(field.name);
});
var mouId = form.getAttribute('data-mou-id') || '';
if (mouId && !currentNames.has('M_MouID')) {
var mouInput = document.createElement('input');
mouInput.type = 'hidden';
mouInput.name = 'M_MouID';
mouInput.value = mouId;
payload.appendChild(mouInput);
}
Object.keys(draft.fields || {}).forEach(function (name) {
if (currentNames.has(name)) return;
var value = draft.fields[name];
@@ -247,7 +256,7 @@ function layout(title, body, { authenticated = false, activePath = "/", subtitle
var form = event.target;
if (!(form instanceof HTMLFormElement) || !form.matches('[data-order-form]')) return;
var step = form.getAttribute('data-order-step') || '';
if (step !== 'review') {
if (step !== 'pemeriksaan') {
event.preventDefault();
return;
}
@@ -287,6 +296,9 @@ function layout(title, body, { authenticated = false, activePath = "/", subtitle
});
var orderForm = document.querySelector('[data-order-form]');
if (orderForm) restoreOrderForm(orderForm);
if (document.querySelector('[data-order-saved="true"]')) {
sessionStorage.removeItem(ORDER_DRAFT_KEY);
}
})();
</script>
</body>
@@ -330,6 +342,7 @@ function topbar(activePath, subtitle) {
<div class="topbar-actions">
${searchForm}
<button class="icon-btn" type="button" data-action="alert" title="Notifications">${icon("bell")}</button>
<a class="btn btn-secondary" href="/settings">Akun</a>
<a class="btn btn-primary" href="/orders/new">${icon("plus")} New order</a>
</div>
</header>
@@ -341,7 +354,6 @@ function desktopNav(activePath) {
["/", "Home", "Dashboard"],
["/orders", "Order", "Create & list"],
["/results", "Result", "History"],
["/fpp", "FPP", "Catalog"],
["/settings", "Akun", "Profile"],
];
const active = (href) => activePath === href || activePath.startsWith(`${href}/`);
@@ -393,7 +405,6 @@ function mobileNav(activePath) {
["/", "Home", "Dashboard"],
["/orders", "Order", "Create"],
["/results", "Result", "History"],
["/fpp", "FPP", "Catalog"],
["/settings", "Akun", "Profile"],
];
const active = (href) => activePath === href || activePath.startsWith(`${href}/`);
@@ -629,6 +640,72 @@ function renderOrderDetail(order) {
`;
}
function qrImageUrl(value, size = 240) {
const data = encodeURIComponent(String(value || ""));
return `https://api.qrserver.com/v1/create-qr-code/?size=${size}x${size}&data=${data}`;
}
function renderOrderSaved(order) {
const details = Array.isArray(order.details) ? order.details : [];
const qrText = order.orderCode || order.id || "";
return `
<div class="stack order-created-shell" data-order-saved="true">
<section class="panel">
${panelHeader(
"Order saved",
"QR Code and review are shown after the order has been submitted successfully.",
`<div class="pill-row">
<a class="btn btn-secondary" href="/orders/${escapeHtml(order.id || "")}">Open detail</a>
<a class="btn btn-primary" href="/orders/new">Create new order</a>
</div>`,
)}
<div class="detail-grid">
<div class="card order-created-qr">
<strong>QR Code</strong>
<p class="muted">${escapeHtml(qrText || "-")}</p>
<div class="qr-preview">
${
qrText
? `<img class="image_qrcode" src="${qrImageUrl(qrText)}" alt="QR code for order ${escapeHtml(qrText)}" loading="lazy" />`
: `<div class="note-box">QR code not available.</div>`
}
</div>
</div>
<div class="card">
<strong>Review</strong>
<p class="muted">Saved payload summary from the backend response.</p>
<div class="mini-list" style="margin-top:12px">
<div class="mini-item"><div><strong>Patient</strong><p>${escapeHtml(order.patient || "-")}</p></div></div>
<div class="mini-item"><div><strong>Diagnosis</strong><p>${escapeHtml(order.diagnosis || "-")}</p></div></div>
<div class="mini-item"><div><strong>Notes</strong><p>${escapeHtml(order.message || "-")}</p></div></div>
<div class="mini-item"><div><strong>Saved at</strong><p>${escapeHtml(order.orderDate || "-")}</p></div></div>
</div>
</div>
</div>
</section>
<section class="panel">
${panelHeader("Selected tests", "These items are the `details[]` sent to the backend on submit.")}
${
details.length
? `<div class="mini-list">${details
.map(
(item) => `
<div class="mini-item">
<div>
<strong>${escapeHtml(item.test_name || item.testName || "-")}</strong>
<p>Test ID ${escapeHtml(item.test_id || item.testId || "-")} · Rp ${escapeHtml(item.price || "0")}</p>
</div>
</div>
`,
)
.join("")}</div>`
: emptyState("No tests saved", "The saved order did not return any detail rows.")
}
</section>
</div>
`;
}
function renderOrderForm(step, stepKey = "demografi", fppTests = [], mouId = "") {
const fppGroups = groupFppTestsForSelection(fppTests);
const packedGroups = packFppGroupsIntoColumns(fppGroups, 5);
@@ -636,10 +713,9 @@ function renderOrderForm(step, stepKey = "demografi", fppTests = [], mouId = "")
["demografi", "Demografi", "Patient identity and contact details."],
["diagnosa", "Diagnosa", "Clinical indication and diagnosis."],
["pemeriksaan", "Pemeriksaan", "Choose examination groups."],
["qrcode", "QR Code", "Fast entry for existing records."],
["review", "Review", "Check before submit."],
];
const activeIndex = Math.max(0, steps.findIndex((item) => item[0] === stepKey));
const normalizedStep = steps.some((item) => item[0] === stepKey) ? stepKey : "demografi";
const activeIndex = Math.max(0, steps.findIndex((item) => item[0] === normalizedStep));
const stepBody = {
demografi: `
<div class="stack">
@@ -647,11 +723,6 @@ function renderOrderForm(step, stepKey = "demografi", fppTests = [], mouId = "")
${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 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">
@@ -750,78 +821,45 @@ function renderOrderForm(step, stepKey = "demografi", fppTests = [], mouId = "")
</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 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>
`,
}[stepKey];
}[normalizedStep];
const prev = steps[activeIndex - 1];
const next = steps[activeIndex + 1];
return `
<section class="panel">
${panelHeader("Create new order", "The new project collapses the old step-heavy flow into a cleaner shell while keeping the same workflow.", '<a class="btn btn-secondary" href="/orders">Cancel</a>')}
<div class="stepper">
${steps
.map(
(item, index) => `
<a class="step ${index === activeIndex ? "active" : ""}" href="/orders/new/${item[0]}" hx-get="/fragments/forms/order-step/${item[0]}" hx-target="#order-step-fragment" hx-push-url="true">
<strong>${escapeHtml(item[1])}</strong>
<span>${escapeHtml(item[2])}</span>
</a>
`,
)
.join("")}
</div>
</section>
<form class="stack" action="/orders" method="post" data-order-form data-order-step="${escapeHtml(stepKey)}">
<section class="panel">
<div class="stack">
${stepBody}
<div class="topbar-actions" style="justify-content: space-between; margin-top: 10px;">
<a class="btn btn-secondary" href="/orders/new/${prev ? prev[0] : "demografi"}">Back</a>
<div class="pill-row">
<a class="btn btn-ghost" href="/orders">Save draft</a>
${
next
? `<a class="btn btn-primary" href="/orders/new/${next[0]}" hx-get="/fragments/forms/order-step/${next[0]}" hx-target="#order-step-fragment" hx-push-url="true">Continue</a>`
: `<button class="btn btn-primary" type="submit">Submit order</button>`
}
</div>
</div>
<div class="stack order-create-stack">
<section class="panel order-create-panel">
${panelHeader("Create new order", "The new project collapses the old step-heavy flow into a cleaner shell while keeping the same workflow.", '<a class="btn btn-secondary" href="/orders">Cancel</a>')}
<div class="stepper">
${steps
.map(
(item, index) => `
<a class="step ${index === activeIndex ? "active" : ""}" href="/orders/new/${item[0]}" hx-get="/fragments/forms/order-step/${item[0]}" hx-target="#order-step-fragment" hx-push-url="true">
<strong>${escapeHtml(item[1])}</strong>
<span>${escapeHtml(item[2])}</span>
</a>
`,
)
.join("")}
</div>
</section>
</form>
<form class="stack" action="/orders" method="post" data-order-form data-order-step="${escapeHtml(normalizedStep)}" data-mou-id="${escapeHtml(mouId || sampleLogin.mouId)}">
<section class="panel">
<div class="stack">
${stepBody}
<div class="topbar-actions" style="justify-content: space-between; margin-top: 10px;">
<a class="btn btn-secondary" href="/orders/new/${prev ? prev[0] : "demografi"}">Back</a>
<div class="pill-row">
<a class="btn btn-ghost" href="/orders">Save draft</a>
${
next
? `<a class="btn btn-primary" href="/orders/new/${next[0]}" hx-get="/fragments/forms/order-step/${next[0]}" hx-target="#order-step-fragment" hx-push-url="true">Continue</a>`
: `<button class="btn btn-primary" type="submit">Submit order</button>`
}
</div>
</div>
</div>
</section>
</form>
</div>
`;
}
@@ -1102,7 +1140,6 @@ async function renderPatients(session) {
<p class="muted">Jump straight into demographic capture.</p>
<div class="pill-row" style="margin-top:12px">
<a class="pill" href="/orders/new/demografi">Demografi</a>
<a class="pill" href="/orders/new/qrcode">QR entry</a>
</div>
<div class="note-box" style="margin-top:14px">Use the registration stepper when the patient is not yet in the system.</div>
</div>
@@ -1262,9 +1299,11 @@ function problemLoginPage() {
}
async function orderNewPage(session, path) {
const allowedSteps = new Set(["demografi", "diagnosa", "pemeriksaan"]);
const step = path.split("/").filter(Boolean)[2] || "demografi";
const normalizedStep = allowedSteps.has(step) ? step : "demografi";
const fppTests = await loadFppCatalog(session);
return layout("Create Order", `<div id="order-step-fragment">${renderOrderForm({}, step, fppTests, session?.mouId || "")}</div>`, {
return layout("Create Order", `<div id="order-step-fragment">${renderOrderForm({}, normalizedStep, 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.",
@@ -2187,7 +2226,7 @@ async function renderRoute(req, res, url) {
if (!sessionData) return;
const body = await readBody(req);
try {
await apiPost(
const payload = await apiPost(
"/order/order_patient",
{
token: sessionData.token,
@@ -2203,7 +2242,25 @@ async function renderRoute(req, res, url) {
},
sessionData.token,
);
redirect(res, "/orders");
const saved = payload?.data || payload?.result || payload || {};
const normalizedSaved = {
id: saved.order_id || saved.id || saved.order_patient_id || "",
patient: saved.order_name || body.patient_name || "",
diagnosis: saved.order_diagnosa || body.patient_diagnosa || "",
message: saved.order_note || body.patient_note || "",
orderDate: saved.order_date || "",
orderCode: saved.order_qrcode || "",
details: Array.isArray(saved.details) ? saved.details : Array.isArray(body.details) ? body.details : [],
};
html(
res,
200,
layout("Order saved", renderOrderSaved(normalizedSaved), {
authenticated: true,
activePath: "/orders",
subtitle: "QR Code and review are shown after the order has been submitted successfully.",
}),
);
} catch {
html(
res,