Files
roast_command_center/public/js/plans.js
T
snowspeeder a72aec9f86
Test and deploy / test-and-deploy (push) Successful in 1m28s
Make plan deletion visible and reliable with in-page confirmation
2026-09-07 06:22:43 -04:00

205 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { api, protectedFetch } from "./api.js?v=__ASSET_VERSION__";
import { initSideNav, loadNavUser } from "./nav.js?v=__ASSET_VERSION__";
import { blankPlan } from "/shared/fields.js?v=__ASSET_VERSION__";
import { showToast } from "./toast.js?v=__ASSET_VERSION__";
const form = document.getElementById("plan-filters"),
body = document.getElementById("plans-body"),
status = document.getElementById("plans-status");
const initial = new URLSearchParams(location.search);
let page = Math.max(1, Number(initial.get("page")) || 1),
generation = 0,
total = 0;
for (const input of form.elements)
if (input.name && initial.has(input.name))
input.value = initial.get(input.name);
function node(tag, text, className = "") {
const el = document.createElement(tag);
el.textContent = text;
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));
params.set("view", "summary");
params.set("page", page);
params.set("limit", "25");
status.textContent = "Loading plans…";
try {
const data = await api("/api/plans?" + params);
if (current !== generation) return;
total = data.total;
if (page > 1 && (page - 1) * 25 >= total) {
page = Math.max(1, Math.ceil(total / 25));
return load();
}
const urlParams = new URLSearchParams(params);
urlParams.delete("view");
urlParams.delete("limit");
history.replaceState(null, "", "/plans?" + urlParams);
body.replaceChildren();
for (const plan of data.plans) {
const tr = document.createElement("tr"),
title = document.createElement("td"),
link = node("a", plan.title);
link.href = "/app?plan=" + encodeURIComponent(plan.id);
link.className = "plan-title-link";
title.append(link);
if (plan.coffee && plan.coffee !== plan.title)
title.append(node("div", plan.coffee, "muted"));
tr.append(
title,
...[
plan.origin,
plan.cultivar,
plan.process,
plan.level,
plan.batchG ? plan.batchG + " g" : "—",
new Date(plan.updated_at).toLocaleDateString(),
].map((v) => node("td", v || "—")),
);
const actions = document.createElement("td");
actions.className = "data-table-actions";
const clone = node("button", "Clone", "ghost-btn small");
clone.type = "button";
clone.setAttribute("aria-label", "Clone " + plan.title);
clone.onclick = async () => {
clone.disabled = true;
try {
const result = await api(`/api/plans/${plan.id}/clone`, {method:"POST"});
location.assign("/app?plan=" + encodeURIComponent(result.plan.id));
} catch (error) {
showToast(error.message, "fail");
clone.disabled = false;
}
};
const rename = node("button", "Rename", "ghost-btn small");
rename.type = "button";
rename.setAttribute("aria-label", "Rename " + plan.title);
rename.onclick = async () => {
const name = prompt("Plan name", plan.name || plan.title);
if (name === null) return;
try {
await api("/api/plans/" + plan.id, {
method: "PUT",
body: JSON.stringify({ name }),
});
await load();
} catch (e) {
showToast(e.message, "fail");
}
};
const remove = node("button", "Delete", "ghost-btn small");
remove.type = "button";
remove.setAttribute("aria-label", "Delete " + plan.title);
remove.onclick = () => {
pendingDelete = plan;
deleteName.textContent = plan.title;
deleteError.textContent = "";
deleteDialog.showModal();
};
actions.append(clone, rename, remove);
tr.append(actions);
body.append(tr);
}
if (!data.plans.length) {
const tr = document.createElement("tr"),
td = node("td", "No plans match these filters.", "empty-state");
td.colSpan = 8;
tr.append(td);
body.append(tr);
}
status.textContent = total
? `${(page - 1) * 25 + 1}–${Math.min(page * 25, total)} of ${total} plans`
: "0 plans";
document.getElementById("plans-prev").disabled = page <= 1;
document.getElementById("plans-next").disabled = page * 25 >= total;
} catch (e) {
if (current === generation) {
status.textContent = "Could not load plans. " + e.message;
body.replaceChildren();
}
}
}
let timer;
form.addEventListener("input", () => {
clearTimeout(timer);
page = 1;
generation++;
timer = setTimeout(load, 250);
});
form.addEventListener("submit", (e) => {
e.preventDefault();
clearTimeout(timer);
page = 1;
load();
});
document.getElementById("plans-clear").onclick = () => {
form.reset();
page = 1;
load();
};
document.getElementById("plans-prev").onclick = () => {
page--;
load();
};
document.getElementById("plans-next").onclick = () => {
page++;
load();
};
document.getElementById("plans-new").onclick = async (e) => {
e.currentTarget.disabled = true;
try {
const result = await api("/api/plans", {
method: "POST",
body: JSON.stringify({ plan: blankPlan() }),
});
location.assign("/app?plan=" + result.plan.id);
} catch (error) {
showToast(error.message, "fail");
document.getElementById("plans-new").disabled = false;
}
};
document.getElementById("btn-logout").addEventListener("click", async () => {
await protectedFetch("/api/auth/logout", { method: "POST" });
location.assign("/");
});
initSideNav();
await loadNavUser();
await load();