From 24921e836c01e9865d5abdf28e712ebd37b5c75c Mon Sep 17 00:00:00 2001 From: "sas.fajri" Date: Mon, 13 Apr 2026 19:05:03 +0700 Subject: [PATCH] Move create order QR to saved state --- server.js | 221 +++++++++++++++++++++++++++++++++-------------------- styles.css | 38 +++++++++ 2 files changed, 177 insertions(+), 82 deletions(-) diff --git a/server.js b/server.js index 3416b9e..08238f4 100644 --- a/server.js +++ b/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); + } })(); @@ -330,6 +342,7 @@ function topbar(activePath, subtitle) {
${searchForm} + Akun ${icon("plus")} New order
@@ -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 ` +
+
+ ${panelHeader( + "Order saved", + "QR Code and review are shown after the order has been submitted successfully.", + ``, + )} +
+
+ QR Code +

${escapeHtml(qrText || "-")}

+
+ ${ + qrText + ? `QR code for order ${escapeHtml(qrText)}` + : `
QR code not available.
` + } +
+
+
+ Review +

Saved payload summary from the backend response.

+
+
Patient

${escapeHtml(order.patient || "-")}

+
Diagnosis

${escapeHtml(order.diagnosis || "-")}

+
Notes

${escapeHtml(order.message || "-")}

+
Saved at

${escapeHtml(order.orderDate || "-")}

+
+
+
+
+
+ ${panelHeader("Selected tests", "These items are the `details[]` sent to the backend on submit.")} + ${ + details.length + ? `
${details + .map( + (item) => ` +
+
+ ${escapeHtml(item.test_name || item.testName || "-")} +

Test ID ${escapeHtml(item.test_id || item.testId || "-")} ยท Rp ${escapeHtml(item.price || "0")}

+
+
+ `, + ) + .join("")}
` + : emptyState("No tests saved", "The saved order did not return any detail rows.") + } +
+
+ `; +} + 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: `
@@ -647,11 +723,6 @@ function renderOrderForm(step, stepKey = "demografi", fppTests = [], mouId = "") ${panelHeader("Mandatory", "These fields are required or expected by the backend before save.")}
- -
- Backend Mou ID -

Auto-filled from the logged-in session.

-
@@ -750,78 +821,45 @@ function renderOrderForm(step, stepKey = "demografi", fppTests = [], mouId = "")
`, - qrcode: ` -
-
- ${panelHeader("Mandatory", "QR flow is optional in the backend, but keep the scan control at the top if used.")} -
-
- Scan existing patient QR -

Use a QR code to pull patient and visit context instantly.

-
QR preview area
-
-
-
-
- ${panelHeader("Optional", "Manual fallback when scanning is not available.")} -
-
- Manual fallback -

Still allow manual entry if the scan is unavailable.

- -
-
-
-
- `, - review: ` -
-
Summary

All steps are merged into a final review before submit.

-
-
Patient name

patient_name

-
Diagnosis

patient_diagnosa

-
Note

patient_note

-
Details

details[]

-
-
- `, - }[stepKey]; + }[normalizedStep]; const prev = steps[activeIndex - 1]; const next = steps[activeIndex + 1]; return ` -
- ${panelHeader("Create new order", "The new project collapses the old step-heavy flow into a cleaner shell while keeping the same workflow.", 'Cancel')} -
- ${steps - .map( - (item, index) => ` - - ${escapeHtml(item[1])} - ${escapeHtml(item[2])} - - `, - ) - .join("")} -
-
-
-
-
- ${stepBody} -
- Back -
- Save draft - ${ - next - ? `Continue` - : `` - } -
-
+
+
+ ${panelHeader("Create new order", "The new project collapses the old step-heavy flow into a cleaner shell while keeping the same workflow.", 'Cancel')} +
+ ${steps + .map( + (item, index) => ` + + ${escapeHtml(item[1])} + ${escapeHtml(item[2])} + + `, + ) + .join("")}
- +
+
+
+ ${stepBody} +
+ Back +
+ Save draft + ${ + next + ? `Continue` + : `` + } +
+
+
+
+
+
`; } @@ -1102,7 +1140,6 @@ async function renderPatients(session) {

Jump straight into demographic capture.

Use the registration stepper when the patient is not yet in the system.
@@ -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", `
${renderOrderForm({}, step, fppTests, session?.mouId || "")}
`, { + return layout("Create Order", `
${renderOrderForm({}, normalizedStep, fppTests, session?.mouId || "")}
`, { 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, diff --git a/styles.css b/styles.css index ec18250..9dfd4d9 100644 --- a/styles.css +++ b/styles.css @@ -890,6 +890,44 @@ tr:last-child td { gap: 18px; } +.order-create-stack { + gap: 28px; +} + +.order-created-shell { + gap: 22px; +} + +.order-created-qr { + display: flex; + flex-direction: column; + gap: 12px; + align-items: center; + text-align: center; +} + +.qr-preview { + display: grid; + place-items: center; + width: 100%; + min-height: 260px; + padding: 14px; + border-radius: 20px; + background: rgba(255, 255, 255, 0.92); + border: 1px dashed rgba(185, 28, 28, 0.18); +} + +.image_qrcode { + display: block; + width: 220px; + height: 220px; + object-fit: contain; + border-radius: 18px; + background: white; + padding: 10px; + box-shadow: 0 14px 30px rgba(15, 23, 42, 0.08); +} + .form { display: grid; gap: 16px;