Files
roast_command_center/public/js/plans.js
T
snowspeeder a9358048ef
Test and deploy / test-and-deploy (push) Successful in 1m52s
Add independent roast plan cloning from the plans table
2026-09-06 16:58:41 -04:00

173 lines
5.2 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;
}
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 = async () => {
if (!confirm(`Delete “${plan.title}”?`)) return;
try {
await api("/api/plans/" + plan.id, { method: "DELETE" });
await load();
} catch (e) {
showToast(e.message, "fail");
}
};
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();