Add QR share and print actions
This commit is contained in:
75
server.js
75
server.js
@@ -268,6 +268,15 @@ function layout(title, body, { authenticated = false, activePath = "/", subtitle
|
||||
injectOrderDraftPayload(form);
|
||||
});
|
||||
document.addEventListener('click', function (event) {
|
||||
var printTrigger = event.target.closest && event.target.closest('[data-action="print-order"]');
|
||||
if (printTrigger) {
|
||||
event.preventDefault();
|
||||
document.body.classList.add('print-order');
|
||||
window.setTimeout(function () {
|
||||
window.print();
|
||||
}, 50);
|
||||
return;
|
||||
}
|
||||
var close = event.target.closest && event.target.closest('[data-modal-close]');
|
||||
if (!close) return;
|
||||
event.preventDefault();
|
||||
@@ -276,6 +285,9 @@ function layout(title, body, { authenticated = false, activePath = "/", subtitle
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Escape') closeModal();
|
||||
});
|
||||
window.addEventListener('afterprint', function () {
|
||||
document.body.classList.remove('print-order');
|
||||
});
|
||||
document.addEventListener('htmx:afterSwap', function (event) {
|
||||
if (event.detail && event.detail.target && event.detail.target.id === 'modal-root') {
|
||||
var root = document.getElementById('modal-root');
|
||||
@@ -645,15 +657,24 @@ function qrImageUrl(value, size = 240) {
|
||||
return `https://api.qrserver.com/v1/create-qr-code/?size=${size}x${size}&data=${data}`;
|
||||
}
|
||||
|
||||
function formatOrderDisplayDate(value) {
|
||||
const text = String(value || "").trim();
|
||||
const match = text.match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}:\d{2}:\d{2}))?$/);
|
||||
if (!match) return text;
|
||||
return `${match[3]}-${match[2]}-${match[1]}${match[4] ? ` ${match[4]}` : ""}`;
|
||||
}
|
||||
|
||||
function renderOrderSaved(order) {
|
||||
const details = Array.isArray(order.details) ? order.details : [];
|
||||
const qrText = order.orderCode || order.id || "";
|
||||
const printableDate = formatOrderDisplayDate(order.orderDate || "");
|
||||
const printableDoctor = order.doctorName || order.doctor || sampleLogin.username;
|
||||
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.",
|
||||
"QR Step",
|
||||
"Order is saved. Share it to WhatsApp or print the slip below.",
|
||||
`<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>
|
||||
@@ -661,14 +682,47 @@ function renderOrderSaved(order) {
|
||||
)}
|
||||
<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 class="qr-slip">
|
||||
<div class="qr-slip-header">
|
||||
<div class="qr-slip-label">Tanggal Order</div>
|
||||
<div class="qr-slip-value">${escapeHtml(printableDate || "-")}</div>
|
||||
<div class="qr-slip-label">Nama</div>
|
||||
<div class="qr-slip-value">${escapeHtml(order.patient || "-")}</div>
|
||||
<div class="qr-slip-label">Dokter</div>
|
||||
<div class="qr-slip-value">${escapeHtml(printableDoctor)}</div>
|
||||
</div>
|
||||
<div class="qr-slip-section-title">Pemeriksaan</div>
|
||||
<div class="qr-slip-tests">
|
||||
${
|
||||
details.length
|
||||
? details
|
||||
.map(
|
||||
(item) => `<div class="qr-slip-test">- ${escapeHtml(item.test_name || item.testName || "-")}</div>`,
|
||||
)
|
||||
.join("")
|
||||
: `<div class="qr-slip-test muted">-</div>`
|
||||
}
|
||||
</div>
|
||||
<div class="qr-preview">
|
||||
${
|
||||
qrText
|
||||
? `<img class="image_qrcode" src="${qrImageUrl(qrText, 320)}" alt="QR code for order ${escapeHtml(qrText)}" loading="lazy" />`
|
||||
: `<div class="note-box">QR code not available.</div>`
|
||||
}
|
||||
</div>
|
||||
<div class="qr-code-text">${escapeHtml(qrText || "-")}</div>
|
||||
<div class="pill-row qr-actions">
|
||||
${
|
||||
qrText
|
||||
? `<a class="btn btn-secondary" href="${escapeHtml(
|
||||
`https://wa.me/?text=${encodeURIComponent(
|
||||
`Order ${order.id || ""} | ${order.patient || ""} | QR ${qrText}`,
|
||||
)}`,
|
||||
)}" target="_blank" rel="noreferrer">Share WhatsApp</a>`
|
||||
: ""
|
||||
}
|
||||
<button class="btn btn-primary" type="button" data-action="print-order">Print</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
@@ -2250,6 +2304,7 @@ async function renderRoute(req, res, url) {
|
||||
message: saved.order_note || body.patient_note || "",
|
||||
orderDate: saved.order_date || "",
|
||||
orderCode: saved.order_qrcode || "",
|
||||
doctorName: sessionData.username || sampleLogin.username,
|
||||
details: Array.isArray(saved.details) ? saved.details : Array.isArray(body.details) ? body.details : [],
|
||||
};
|
||||
html(
|
||||
|
||||
146
styles.css
146
styles.css
@@ -906,6 +906,54 @@ tr:last-child td {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qr-slip {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.qr-slip-header {
|
||||
display: grid;
|
||||
grid-template-columns: 190px minmax(0, 1fr);
|
||||
gap: 2px 18px;
|
||||
align-items: start;
|
||||
text-align: left;
|
||||
font-size: 1.02rem;
|
||||
}
|
||||
|
||||
.qr-slip-label {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.qr-slip-value {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.qr-slip-section-title {
|
||||
margin-top: 8px;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.qr-slip-tests {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.qr-slip-test {
|
||||
font-size: 1.15rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.qr-code-text {
|
||||
font-size: 1.7rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.04em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qr-preview {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
@@ -928,6 +976,104 @@ tr:last-child td {
|
||||
box-shadow: 0 14px 30px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
body.print-order .bg-orb,
|
||||
body.print-order .bg-grid,
|
||||
body.print-order .sidebar,
|
||||
body.print-order .topbar,
|
||||
body.print-order .mobile-nav,
|
||||
body.print-order .order-created-shell .panel .pill-row,
|
||||
body.print-order .order-created-shell .qr-actions,
|
||||
body.print-order .order-created-shell .card:not(.order-created-qr),
|
||||
body.print-order .order-created-shell > .panel:last-child {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.print-order {
|
||||
background: white;
|
||||
}
|
||||
|
||||
body.print-order #app {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
body.print-order .page-shell {
|
||||
display: block;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
body.print-order .content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body.print-order .content-inner {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
body.print-order .order-created-shell {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
body.print-order .order-created-shell > .panel {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
background: white;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body.print-order .order-created-shell .panel-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
body.print-order .order-created-shell .detail-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
body.print-order .qr-slip {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body.print-order .qr-preview {
|
||||
border: none;
|
||||
background: transparent;
|
||||
min-height: 0;
|
||||
padding: 10px 0 0;
|
||||
}
|
||||
|
||||
body.print-order .image_qrcode {
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
body.print-order .qr-code-text {
|
||||
font-size: 1.9rem;
|
||||
}
|
||||
|
||||
@media print {
|
||||
@page {
|
||||
size: auto;
|
||||
margin: 12mm;
|
||||
}
|
||||
|
||||
body {
|
||||
background: white !important;
|
||||
}
|
||||
|
||||
body.print-order .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
body.print-order .topbar,
|
||||
body.print-order .sidebar,
|
||||
body.print-order .mobile-nav,
|
||||
body.print-order .bg-orb,
|
||||
body.print-order .bg-grid {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.form {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
|
||||
Reference in New Issue
Block a user