205 lines
6.5 KiB
JavaScript
205 lines
6.5 KiB
JavaScript
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();
|