Fix report pagination and rebuild saved plan navigation
Test and deploy / test-and-deploy (push) Successful in 1m43s

This commit is contained in:
snowspeeder committed 2026-09-05 15:47:44 -04:00
1 parent 824a964f9d
commit 5f3a015ea2
20 files changed
+1480 -809

No files matched your search

+159
View File
@@ -0,0 +1,159 @@
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 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(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();