Move create order QR to saved state
This commit is contained in:
221
server.js
221
server.js
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user