194 lines
5.5 KiB
JavaScript
194 lines
5.5 KiB
JavaScript
const state = {
|
|
root: null,
|
|
nodes: new Map(),
|
|
selectedId: null,
|
|
};
|
|
|
|
const elements = {
|
|
status: document.querySelector("#status"),
|
|
tree: document.querySelector("#tree"),
|
|
selected: document.querySelector("#selected"),
|
|
refresh: document.querySelector("#refresh"),
|
|
createForm: document.querySelector("#create-form"),
|
|
createParent: document.querySelector("#create-parent"),
|
|
createName: document.querySelector("#create-name"),
|
|
createQuantity: document.querySelector("#create-quantity"),
|
|
updateForm: document.querySelector("#update-form"),
|
|
updateName: document.querySelector("#update-name"),
|
|
updateQuantity: document.querySelector("#update-quantity"),
|
|
moveForm: document.querySelector("#move-form"),
|
|
moveParent: document.querySelector("#move-parent"),
|
|
moveChildren: document.querySelector("#move-children"),
|
|
deleteButton: document.querySelector("#delete"),
|
|
};
|
|
|
|
async function api(path, options = {}) {
|
|
const response = await fetch(path, {
|
|
...options,
|
|
headers: options.body ? { "Content-Type": "application/json" } : undefined,
|
|
});
|
|
if (response.status === 204) {
|
|
return null;
|
|
}
|
|
const body = await response.json();
|
|
if (!response.ok) {
|
|
throw new Error(body.message || `Request failed with status ${response.status}`);
|
|
}
|
|
return body;
|
|
}
|
|
|
|
async function loadTree() {
|
|
try {
|
|
state.root = await api("/api/tree");
|
|
state.nodes.clear();
|
|
indexNode(state.root);
|
|
const requestedCode = new URLSearchParams(window.location.search).get("code")?.toUpperCase();
|
|
if (requestedCode) {
|
|
state.selectedId = [...state.nodes.values()].find((node) => node.lookupCode === requestedCode)?.id || null;
|
|
}
|
|
if (state.selectedId && !state.nodes.has(state.selectedId)) {
|
|
state.selectedId = null;
|
|
}
|
|
render();
|
|
setStatus("");
|
|
} catch (error) {
|
|
setStatus(error.message);
|
|
}
|
|
}
|
|
|
|
function indexNode(node) {
|
|
state.nodes.set(node.id, node);
|
|
for (const child of node.children) {
|
|
indexNode(child);
|
|
}
|
|
}
|
|
|
|
function render() {
|
|
elements.tree.replaceChildren(renderNode(state.root));
|
|
renderSelection();
|
|
}
|
|
|
|
function renderNode(node) {
|
|
const list = document.createElement("ul");
|
|
const item = document.createElement("li");
|
|
const button = document.createElement("button");
|
|
button.type = "button";
|
|
button.textContent = node.quantity === null ? node.name : `${node.name} (${node.quantity})`;
|
|
button.addEventListener("click", () => {
|
|
state.selectedId = node.id;
|
|
renderSelection();
|
|
});
|
|
item.append(button);
|
|
if (node.children.length > 0) {
|
|
for (const child of node.children) {
|
|
item.append(renderNode(child));
|
|
}
|
|
}
|
|
list.append(item);
|
|
return list;
|
|
}
|
|
|
|
function renderSelection() {
|
|
const node = state.nodes.get(state.selectedId);
|
|
const hasSelection = Boolean(node);
|
|
const protectedNode = node?.id === "root" || node?.id === "unsorted";
|
|
|
|
elements.selected.textContent = node ? `${node.name} [${node.lookupCode}]` : "None";
|
|
elements.createParent.textContent = `Parent: ${node?.name || "Unsorted"}`;
|
|
elements.updateName.value = node?.name || "";
|
|
elements.updateQuantity.value = node?.quantity ?? "";
|
|
|
|
for (const control of elements.updateForm.elements) {
|
|
control.disabled = !hasSelection || protectedNode;
|
|
}
|
|
for (const control of elements.moveForm.elements) {
|
|
control.disabled = !hasSelection || protectedNode;
|
|
}
|
|
elements.deleteButton.disabled = !hasSelection || protectedNode;
|
|
|
|
elements.moveParent.replaceChildren();
|
|
for (const candidate of state.nodes.values()) {
|
|
if (candidate.id === state.selectedId) {
|
|
continue;
|
|
}
|
|
const option = document.createElement("option");
|
|
option.value = candidate.id;
|
|
option.textContent = candidate.name;
|
|
elements.moveParent.append(option);
|
|
}
|
|
}
|
|
|
|
function quantityFrom(input) {
|
|
return input.value === "" ? null : Number(input.value);
|
|
}
|
|
|
|
elements.refresh.addEventListener("click", loadTree);
|
|
|
|
elements.createForm.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
const request = {
|
|
name: elements.createName.value,
|
|
quantity: quantityFrom(elements.createQuantity),
|
|
};
|
|
if (state.selectedId) {
|
|
request.parentId = state.selectedId;
|
|
}
|
|
await perform(async () => {
|
|
const node = await api("/api/nodes", {
|
|
method: "POST",
|
|
body: JSON.stringify(request),
|
|
});
|
|
state.selectedId = node.id;
|
|
elements.createForm.reset();
|
|
});
|
|
});
|
|
|
|
elements.updateForm.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
await perform(() => api(`/api/nodes/${state.selectedId}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({
|
|
name: elements.updateName.value,
|
|
quantity: quantityFrom(elements.updateQuantity),
|
|
}),
|
|
}));
|
|
});
|
|
|
|
elements.moveForm.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
await perform(() => api(`/api/nodes/${state.selectedId}/move`, {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
targetParentId: elements.moveParent.value,
|
|
childHandling: elements.moveChildren.value,
|
|
}),
|
|
}));
|
|
});
|
|
|
|
elements.deleteButton.addEventListener("click", async () => {
|
|
const node = state.nodes.get(state.selectedId);
|
|
if (!node || !window.confirm(`Delete ${node.name}?`)) {
|
|
return;
|
|
}
|
|
await perform(async () => {
|
|
await api(`/api/nodes/${node.id}`, { method: "DELETE" });
|
|
state.selectedId = null;
|
|
});
|
|
});
|
|
|
|
async function perform(action) {
|
|
try {
|
|
setStatus("");
|
|
await action();
|
|
await loadTree();
|
|
} catch (error) {
|
|
setStatus(error.message);
|
|
}
|
|
}
|
|
|
|
function setStatus(message) {
|
|
elements.status.textContent = message;
|
|
}
|
|
|
|
loadTree();
|