Polish order modal and stepper UI

This commit is contained in:
sas.fajri
2026-04-13 15:03:37 +07:00
parent 34f3aa6e72
commit 6e059c8115
2 changed files with 90 additions and 23 deletions

View File

@@ -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>
`; `;
} }

View File

@@ -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) {