diff --git a/public/app.css b/public/app.css index bf53f60..0906692 100644 --- a/public/app.css +++ b/public/app.css @@ -4422,3 +4422,9 @@ select.f { .planner-page .header-row-top { max-width: 100%; } + +.delete-plan-dialog { width: min(500px, 94vw); } +#delete-plan-name { font-weight: 700; overflow-wrap: anywhere; } +#delete-plan-description { line-height: 1.6; } +#delete-plan-error { color: #a32920; } +.delete-plan-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 24px; } diff --git a/public/js/plans.js b/public/js/plans.js index fc700e1..5f95426 100644 --- a/public/js/plans.js +++ b/public/js/plans.js @@ -18,6 +18,41 @@ function node(tag, text, className = "") { el.className = className; return el; } +const deleteDialog = document.getElementById("delete-plan-dialog"), + deleteName = document.getElementById("delete-plan-name"), + deleteError = document.getElementById("delete-plan-error"), + deleteConfirm = document.getElementById("delete-plan-confirm"), + deleteCancel = document.getElementById("delete-plan-cancel"); +let pendingDelete = null, deleting = false; +deleteCancel.onclick = () => deleteDialog.close(); +deleteDialog.addEventListener("cancel", (event) => { + if (deleting) event.preventDefault(); +}); +deleteDialog.addEventListener("close", () => { pendingDelete = null; }); +deleteConfirm.onclick = async () => { + if (!pendingDelete || deleting) return; + deleting = true; + deleteConfirm.disabled = deleteCancel.disabled = true; + deleteConfirm.textContent = "Deleting…"; + deleteError.textContent = ""; + try { + try { + await api("/api/plans/" + pendingDelete.id, { method: "DELETE" }); + } catch (error) { + // Another tab may already have removed this plan. + if (error.status !== 404) throw error; + } + deleteDialog.close(); + await load(); + showToast("Plan deleted."); + } catch (error) { + deleteError.textContent = "Could not delete this plan. " + error.message + " Try again."; + } finally { + deleting = false; + deleteConfirm.disabled = deleteCancel.disabled = false; + deleteConfirm.textContent = "Delete plan"; + } +}; async function load() { const current = ++generation; const params = new URLSearchParams(new FormData(form)); @@ -92,14 +127,11 @@ async function load() { const remove = node("button", "Delete", "ghost-btn small"); remove.type = "button"; remove.setAttribute("aria-label", "Delete " + plan.title); - remove.onclick = async () => { - if (!confirm(`Delete “${plan.title}”?`)) return; - try { - await api("/api/plans/" + plan.id, { method: "DELETE" }); - await load(); - } catch (e) { - showToast(e.message, "fail"); - } + remove.onclick = () => { + pendingDelete = plan; + deleteName.textContent = plan.title; + deleteError.textContent = ""; + deleteDialog.showModal(); }; actions.append(clone, rename, remove); tr.append(actions); diff --git a/public/plans.html b/public/plans.html index 550b559..d5b8bf2 100644 --- a/public/plans.html +++ b/public/plans.html @@ -151,6 +151,16 @@ + + Delete this plan? + + This removes the saved plan and its revision history. Completed roast records and inventory history will be kept. This cannot be undone. + + + Cancel + Delete plan + +
This removes the saved plan and its revision history. Completed roast records and inventory history will be kept. This cannot be undone.