Polish order modal and stepper UI
This commit is contained in:
43
server.js
43
server.js
@@ -179,6 +179,30 @@ function layout(title, body, { authenticated = false, activePath = "/", subtitle
|
|||||||
</div>
|
</div>
|
||||||
` : body}
|
` : body}
|
||||||
</div>
|
</div>
|
||||||
|
<div id="modal-root"></div>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
function closeModal() {
|
||||||
|
var modalRoot = document.getElementById('modal-root');
|
||||||
|
if (modalRoot) modalRoot.innerHTML = '';
|
||||||
|
}
|
||||||
|
document.addEventListener('click', function (event) {
|
||||||
|
var close = event.target.closest && event.target.closest('[data-modal-close]');
|
||||||
|
if (!close) return;
|
||||||
|
event.preventDefault();
|
||||||
|
closeModal();
|
||||||
|
});
|
||||||
|
document.addEventListener('keydown', function (event) {
|
||||||
|
if (event.key === 'Escape') closeModal();
|
||||||
|
});
|
||||||
|
document.addEventListener('htmx:afterSwap', function (event) {
|
||||||
|
if (event.detail && event.detail.target && event.detail.target.id === 'modal-root') {
|
||||||
|
var root = document.getElementById('modal-root');
|
||||||
|
if (root) root.scrollTop = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
}
|
}
|
||||||
@@ -503,7 +527,11 @@ function renderOrderDetail(order) {
|
|||||||
return `
|
return `
|
||||||
<div class="stack">
|
<div class="stack">
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
${panelHeader(`${order.patient} · ${order.id}`, "Order detail with the same structure as the proposed master-detail workflow.", `<a class="btn btn-secondary" href="/orders/${order.id}/pesan-khusus">Pesan khusus</a>`)}
|
${panelHeader(
|
||||||
|
`${order.patient} · ${order.id}`,
|
||||||
|
"Order detail with the same structure as the proposed master-detail workflow.",
|
||||||
|
`<button class="btn btn-secondary" type="button" hx-get="/fragments/modals/pesan-khusus?order_id=${escapeHtml(order.id)}" hx-target="#modal-root" hx-swap="innerHTML">Pesan khusus</button>`,
|
||||||
|
)}
|
||||||
<div class="grid grid-3">
|
<div class="grid grid-3">
|
||||||
<div class="card"><span class="muted">Status</span><div style="margin-top:8px">${statusBadge(order.status)}</div></div>
|
<div class="card"><span class="muted">Status</span><div style="margin-top:8px">${statusBadge(order.status)}</div></div>
|
||||||
<div class="card"><span class="muted">Patient</span><strong style="display:block; margin-top:8px">${escapeHtml(order.patient)}</strong><span class="muted">${escapeHtml(order.mode)} · ${escapeHtml(order.age)} years</span></div>
|
<div class="card"><span class="muted">Patient</span><strong style="display:block; margin-top:8px">${escapeHtml(order.patient)}</strong><span class="muted">${escapeHtml(order.mode)} · ${escapeHtml(order.age)} years</span></div>
|
||||||
@@ -613,7 +641,7 @@ function renderOrderForm(step, stepKey = "demografi") {
|
|||||||
${steps
|
${steps
|
||||||
.map(
|
.map(
|
||||||
(item, index) => `
|
(item, index) => `
|
||||||
<a class="step ${index === activeIndex ? "active" : ""}" href="/orders/new/${item[0]}">
|
<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>
|
<strong>${escapeHtml(item[1])}</strong>
|
||||||
<span>${escapeHtml(item[2])}</span>
|
<span>${escapeHtml(item[2])}</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -973,7 +1001,7 @@ function problemLoginPage() {
|
|||||||
|
|
||||||
function orderNewPage(path) {
|
function orderNewPage(path) {
|
||||||
const step = path.split("/").filter(Boolean)[2] || "demografi";
|
const step = path.split("/").filter(Boolean)[2] || "demografi";
|
||||||
return layout("Create Order", renderOrderForm({}, step), {
|
return layout("Create Order", `<div id="order-step-fragment">${renderOrderForm({}, step)}</div>`, {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
activePath: "/orders",
|
activePath: "/orders",
|
||||||
subtitle: "The new project collapses the old step-heavy flow into a cleaner shell while keeping the same workflow.",
|
subtitle: "The new project collapses the old step-heavy flow into a cleaner shell while keeping the same workflow.",
|
||||||
@@ -1557,8 +1585,10 @@ function fragmentOrderStep(step) {
|
|||||||
function fragmentPesanKhusus(orderId) {
|
function fragmentPesanKhusus(orderId) {
|
||||||
const order = mockOrders.find((item) => item.id === orderId) || mockOrders[0];
|
const order = mockOrders.find((item) => item.id === orderId) || mockOrders[0];
|
||||||
return `
|
return `
|
||||||
<section class="panel">
|
<div class="modal-shell" role="dialog" aria-modal="true" aria-labelledby="pesan-khusus-title">
|
||||||
${panelHeader("Pesan khusus", "Desktop can treat this as a modal-style panel; mobile reads it as a dedicated page.", `<a class="btn btn-secondary" href="/orders/${order.id}">Back</a>`)}
|
<div class="modal-backdrop" data-modal-close></div>
|
||||||
|
<section class="modal-card panel">
|
||||||
|
${panelHeader("Pesan khusus", "Desktop opens this as a modal, mobile can still use it as a full sheet.", `<button class="btn btn-secondary" type="button" data-modal-close>Close</button>`)}
|
||||||
<div class="detail-grid">
|
<div class="detail-grid">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<strong>${escapeHtml(order.patient)}</strong>
|
<strong>${escapeHtml(order.patient)}</strong>
|
||||||
@@ -1573,11 +1603,12 @@ function fragmentPesanKhusus(orderId) {
|
|||||||
</label>
|
</label>
|
||||||
<div class="topbar-actions" style="justify-content:flex-start">
|
<div class="topbar-actions" style="justify-content:flex-start">
|
||||||
<button class="btn btn-primary" type="submit">Save message</button>
|
<button class="btn btn-primary" type="submit">Save message</button>
|
||||||
<a class="btn btn-secondary" href="/orders/${order.id}">Cancel</a>
|
<button class="btn btn-secondary" type="button" data-modal-close>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
36
styles.css
36
styles.css
@@ -798,6 +798,31 @@ tr:last-child td {
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-shell {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 40;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(15, 23, 42, 0.5);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-card {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: min(980px, 100%);
|
||||||
|
max-height: calc(100vh - 44px);
|
||||||
|
overflow: auto;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
.desktop-only {
|
.desktop-only {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -872,6 +897,17 @@ tr:last-child td {
|
|||||||
.auth-screen {
|
.auth-screen {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-shell {
|
||||||
|
padding: 0;
|
||||||
|
place-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-card {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 100vh;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 620px) {
|
@media (max-width: 620px) {
|
||||||
|
|||||||
Reference in New Issue
Block a user