Grotti 3.6.0: Lauf-Identität nach Filament-Pause, robustere Queue, Projekte/Aufträge list-first und API-Polish.

This commit is contained in:
Dominic Gundel committed 2026-10-25 08:24:24 +01:00
1 parent d1eb87ba25
commit 2b1e36634a
53 files changed
+4254 -636

No files matched your search

+226 -85
View File
@@ -7,9 +7,10 @@
</div>
<div class="actions">
<a class="ghost" id="lieferschein-link" href="#" hidden target="_blank" rel="noopener">Lieferschein</a>
<button type="button" class="btn-primary" id="order-new-head"><svg class="ico"><use href="#i-plus"/></svg>Neuer Auftrag</button>
</div>
</div>
<div class="split">
<div class="split edit-split is-idle" id="order-layout">
<section class="card">
<div class="field inline">
<label for="filter-status">Status</label>
@@ -32,9 +33,14 @@
<div class="pager" id="order-pager" hidden></div>
</div>
</section>
<section class="card">
<h2 id="order-title">Neuer Auftrag</h2>
<form id="order-form">
<section class="card edit-panel" id="order-panel" hidden>
<div class="mat-form-head">
<h2 id="order-title">Neuer Auftrag</h2>
<button type="button" class="ghost tiny-btn" id="order-edit-btn" hidden>Bearbeiten</button>
<button type="button" class="icon-btn" id="order-close" title="Schließen" aria-label="Schließen"></button>
</div>
<form id="order-form" hidden>
<input id="order-id" type="hidden">
<div class="field"><label for="order-title-in">Titel</label><input id="order-title-in" required></div>
<div class="grid-2">
@@ -57,43 +63,61 @@
<div class="field"><label for="order-notes">Notiz</label><textarea id="order-notes"></textarea></div>
<div class="actions">
<button type="submit" class="btn-primary">Speichern</button>
<button type="button" class="ghost" id="order-new">Neu</button>
<button type="button" class="ghost" id="order-cancel-edit" hidden>Abbrechen</button>
<button type="button" class="btn-danger" id="order-delete" hidden>Löschen</button>
</div>
</form>
<p id="order-note" class="tiny"></p>
<p class="tiny" id="order-progress"></p>
<p class="tiny" id="order-actuals"></p>
<h2 class="section-gap">Positionen</h2>
<div class="table-wrap">
<table>
<thead><tr><th>Datei</th><th class="num">Stk</th><th class="num">fertig</th><th></th></tr></thead>
<tbody id="item-rows"></tbody>
</table>
</div>
<form id="item-form" class="section-gap">
<div class="grid-2">
<div class="field"><label for="item-file">Datei</label><select id="item-file"><option value="">—</option></select></div>
<div class="field"><label for="item-qty">Stück</label><input id="item-qty" type="number" min="1" step="1" value="1"></div>
<div class="field"><label for="item-price">Preis/Stk €</label><input id="item-price" type="number" min="0" step="0.01" value="0"></div>
<div class="field"><label for="item-material">Material</label><input id="item-material"></div>
<div id="order-detail" hidden>
<p class="tiny" id="order-progress"></p>
<p class="tiny" id="order-actuals"></p>
<p class="tiny" id="order-notes-view"></p>
<h2 class="section-gap">Positionen</h2>
<div class="table-wrap">
<table>
<thead><tr><th>Datei</th><th class="num">Stk</th><th class="num">fertig</th><th></th></tr></thead>
<tbody id="item-rows"></tbody>
</table>
</div>
<div class="actions"><button type="submit">Position hinzufügen</button></div>
</form>
<details class="section-gap">
<summary>Kunde anlegen / bearbeiten</summary>
<form id="customer-form">
<input type="hidden" id="cust-id">
<div class="field"><label for="cust-name">Name</label><input id="cust-name" required></div>
<div class="field"><label for="cust-email">E-Mail</label><input id="cust-email" type="email"></div>
<form id="item-edit-form" class="section-gap card" hidden style="padding:0.75rem;margin-top:0.75rem">
<input type="hidden" id="item-edit-id">
<h3 style="margin:0 0 0.5rem;font-size:0.95rem">Position ändern</h3>
<div class="grid-2">
<div class="field"><label for="item-edit-qty">Stück</label><input id="item-edit-qty" type="number" min="1" step="1" value="1"></div>
<div class="field"><label for="item-edit-price">Preis/Stk €</label><input id="item-edit-price" type="number" min="0" step="0.01" value="0"></div>
</div>
<div class="actions">
<button type="submit" class="btn-primary">Kunde speichern</button>
<button type="button" class="ghost" id="cust-edit-current">Aktuellen laden</button>
<button type="button" class="btn-danger" id="cust-delete" hidden>Kunde löschen</button>
<button type="submit" class="btn-primary">Speichern</button>
<button type="button" class="ghost" id="item-edit-cancel">Abbrechen</button>
</div>
</form>
</details>
<form id="item-form" class="section-gap">
<div class="grid-2">
<div class="field"><label for="item-file">Datei</label><select id="item-file"><option value="">—</option></select></div>
<div class="field"><label for="item-qty">Stück</label><input id="item-qty" type="number" min="1" step="1" value="1"></div>
<div class="field"><label for="item-price">Preis/Stk €</label><input id="item-price" type="number" min="0" step="0.01" value="0"></div>
<div class="field"><label for="item-material">Material</label><input id="item-material"></div>
</div>
<div class="actions"><button type="submit">Position hinzufügen</button></div>
</form>
<details class="section-gap">
<summary>Kunde anlegen / bearbeiten</summary>
<form id="customer-form">
<input type="hidden" id="cust-id">
<div class="field"><label for="cust-name">Name</label><input id="cust-name" required></div>
<div class="field"><label for="cust-email">E-Mail</label><input id="cust-email" type="email"></div>
<div class="actions">
<button type="submit" class="btn-primary">Kunde speichern</button>
<button type="button" class="ghost" id="cust-edit-current">Aktuellen laden</button>
<button type="button" class="btn-danger" id="cust-delete" hidden>Kunde löschen</button>
</div>
</form>
</details>
</div>
</section>
</div>
{% endblock %}
@@ -107,6 +131,43 @@ let orders = [];
let customers = [];
let files = [];
let current = null;
/** idle | create | view | edit */
let panelMode = "idle";
let orderPage = 1;
document.getElementById("order-close").innerHTML = icon("x");
function openPanel() {
document.getElementById("order-panel").hidden = false;
document.getElementById("order-layout").classList.remove("is-idle");
document.getElementById("order-panel").scrollIntoView({ block: "nearest", behavior: "smooth" });
}
function closePanel() {
panelMode = "idle";
current = null;
document.getElementById("order-panel").hidden = true;
document.getElementById("order-layout").classList.add("is-idle");
document.getElementById("order-form").hidden = true;
document.getElementById("order-detail").hidden = true;
document.getElementById("order-edit-btn").hidden = true;
document.getElementById("order-cancel-edit").hidden = true;
document.getElementById("order-delete").hidden = true;
document.getElementById("lieferschein-link").hidden = true;
document.getElementById("item-edit-form").hidden = true;
document.getElementById("order-note").textContent = "";
history.replaceState(null, "", location.pathname);
renderList();
}
function setFormValues(order) {
document.getElementById("order-id").value = order ? order.id : "";
document.getElementById("order-title-in").value = order ? order.title : "";
document.getElementById("order-status").value = order ? order.status : "idee";
document.getElementById("order-due").value = order && order.due_at ? order.due_at.slice(0, 10) : "";
document.getElementById("order-customer").value = order && order.customer_id ? order.customer_id : "";
document.getElementById("order-notes").value = order ? (order.notes || "") : "";
}
function fillCustomers() {
const sel = document.getElementById("order-customer");
@@ -146,27 +207,64 @@ function paintActuals(order) {
el.textContent = bits.join(" · ");
}
function fillOrder(order) {
function showCreate() {
panelMode = "create";
current = null;
setFormValues(null);
document.getElementById("order-title").textContent = "Neuer Auftrag";
document.getElementById("order-form").hidden = false;
document.getElementById("order-detail").hidden = true;
document.getElementById("order-edit-btn").hidden = true;
document.getElementById("order-cancel-edit").hidden = true;
document.getElementById("order-delete").hidden = true;
document.getElementById("lieferschein-link").hidden = true;
document.getElementById("order-note").textContent = "";
document.getElementById("item-edit-form").hidden = true;
history.replaceState(null, "", location.pathname);
openPanel();
renderList();
document.getElementById("order-title-in").focus();
}
function showView(order) {
panelMode = "view";
current = order;
document.getElementById("order-id").value = order ? order.id : "";
document.getElementById("order-title-in").value = order ? order.title : "";
document.getElementById("order-status").value = order ? order.status : "idee";
document.getElementById("order-due").value = order && order.due_at ? order.due_at.slice(0, 10) : "";
document.getElementById("order-customer").value = order && order.customer_id ? order.customer_id : "";
document.getElementById("order-notes").value = order ? order.notes : "";
document.getElementById("order-title").textContent = order ? order.title : "Neuer Auftrag";
document.getElementById("order-delete").hidden = !order;
setFormValues(order);
document.getElementById("order-title").textContent = order.title || "Auftrag";
document.getElementById("order-form").hidden = true;
document.getElementById("order-detail").hidden = false;
document.getElementById("order-edit-btn").hidden = false;
document.getElementById("order-cancel-edit").hidden = true;
document.getElementById("order-delete").hidden = true;
document.getElementById("item-edit-form").hidden = true;
const link = document.getElementById("lieferschein-link");
if (order) {
link.href = "/auftraege/" + order.id + "/lieferschein";
link.hidden = false;
} else {
link.hidden = true;
}
const prog = order && order.progress;
link.href = "/auftraege/" + order.id + "/lieferschein";
link.hidden = false;
history.replaceState(null, "", "#" + order.id);
const prog = order.progress;
document.getElementById("order-progress").textContent = prog ? "Fortschritt: " + prog.label : "";
paintActuals(order || {});
renderItems(order && order.items ? order.items : []);
paintActuals(order);
const notes = (order.notes || "").trim();
document.getElementById("order-notes-view").textContent = notes ? "Notiz: " + notes : "";
renderItems(order.items || []);
openPanel();
renderList();
}
function showEdit(order) {
panelMode = "edit";
current = order;
setFormValues(order);
document.getElementById("order-title").textContent = "Bearbeiten";
document.getElementById("order-form").hidden = false;
document.getElementById("order-detail").hidden = true;
document.getElementById("order-edit-btn").hidden = true;
document.getElementById("order-cancel-edit").hidden = false;
document.getElementById("order-delete").hidden = false;
document.getElementById("item-edit-form").hidden = true;
openPanel();
renderList();
document.getElementById("order-title-in").focus();
}
function renderItems(items) {
@@ -179,7 +277,11 @@ function renderItems(items) {
items.forEach((item) => {
const tr = document.createElement("tr");
tr.innerHTML = "<td></td><td class=\"num\"></td><td class=\"num\"></td><td class=\"actions\"></td>";
tr.children[0].textContent = item.file_name || ("#" + (item.file_id || "—"));
if (item.file_id) {
tr.children[0].innerHTML = linkTo("datei", item.file_id, item.file_name || ("#" + item.file_id));
} else {
tr.children[0].textContent = item.file_name || "—";
}
tr.children[1].textContent = item.qty;
tr.children[2].textContent = item.done_ok + (item.done_bad ? " (−" + item.done_bad + ")" : "");
const tools = document.createElement("div");
@@ -194,7 +296,7 @@ function renderItems(items) {
edit.type = "button";
edit.className = "ghost tiny-btn";
edit.textContent = "Ändern";
edit.addEventListener("click", () => editItem(item));
edit.addEventListener("click", () => beginEditItem(item));
const del = document.createElement("button");
del.type = "button";
del.className = "btn-danger tiny-btn";
@@ -204,7 +306,7 @@ function renderItems(items) {
await api("/auftraege/positionen/" + item.id, { method: "DELETE" });
toast("Position gelöscht.");
const full = await api("/auftraege/" + current.id);
fillOrder(full);
showView(full);
});
});
tools.append(btn, edit, del);
@@ -213,22 +315,12 @@ function renderItems(items) {
});
}
async function editItem(item) {
const qty = prompt("Stückzahl", String(item.qty || 1));
if (qty == null) return;
const price = prompt("Preis/Stk €", String(item.unit_price || 0));
if (price == null) return;
try {
await api("/auftraege/positionen/" + item.id, {
method: "PATCH",
json: { qty: Number(qty) || 1, unit_price: Number(price) || 0 },
});
const full = await api("/auftraege/" + current.id);
fillOrder(full);
toast("Position gespeichert.");
} catch (error) {
showNote("order-note", error);
}
function beginEditItem(item) {
document.getElementById("item-edit-id").value = item.id;
document.getElementById("item-edit-qty").value = item.qty || 1;
document.getElementById("item-edit-price").value = item.unit_price || 0;
document.getElementById("item-edit-form").hidden = false;
document.getElementById("item-edit-qty").focus();
}
async function einreihen(itemId) {
@@ -240,42 +332,40 @@ async function einreihen(itemId) {
});
toast("In Warteschlange.");
const full = await api("/auftraege/" + current.id);
fillOrder(full);
showView(full);
await loadList();
} catch (error) {
showNote("order-note", error);
}
}
let orderPage = 1;
function renderList() {
const body = document.getElementById("order-rows");
body.innerHTML = "";
const pager = document.getElementById("order-pager");
if (!orders.length) {
body.innerHTML = '<tr><td colspan="3" class="empty"><div class="empty-state"><p>Noch kein Auftrag.</p><p><button type="button" class="ghost" id="order-empty-new">Ersten Auftrag anlegen →</button></p></div></td></tr>';
document.getElementById("order-empty-new")?.addEventListener("click", () => fillOrder(null));
body.innerHTML = '<tr><td colspan="3" class="empty"><div class="empty-state"><p>Noch kein Auftrag.</p><p><button type="button" class="btn-primary" id="order-empty-new">Ersten Auftrag anlegen</button></p></div></td></tr>';
document.getElementById("order-empty-new")?.addEventListener("click", showCreate);
renderPager(pager, null, () => {});
return;
}
const selected = document.getElementById("order-id").value;
const selected = current ? String(current.id) : "";
const paged = pageSlice(orders, orderPage, DEFAULT_PAGE_SIZE);
orderPage = paged.page;
paged.items.forEach((order) => {
const tr = document.createElement("tr");
tr.className = "pick" + (selected && String(order.id) === selected ? " on" : "");
tr.innerHTML = "<td></td><td></td><td></td>";
tr.innerHTML = "<td></td><td></td><td class=\"cell-path\"></td>";
tr.children[0].textContent = order.title;
const st = order.status || "";
const chipState = st === "druckt" ? "printing" : (st === "fertig" || st === "geliefert" || st === "bezahlt") ? "done" : st === "bestaetigt" ? "waiting" : "idle";
tr.children[1].innerHTML = chipHtml(chipState, STATUS_LABEL[st] || st);
tr.children[2].textContent = (order.progress && order.progress.label) || "";
tr.children[2].title = (order.progress && order.progress.label) || "";
tr.addEventListener("click", async () => {
try {
const full = await api("/auftraege/" + order.id);
fillOrder(full);
renderList();
showView(full);
} catch (error) {
showNote("order-note", error);
}
@@ -304,11 +394,28 @@ async function loadBase() {
fillCustomers();
fillFiles();
await loadList();
fillOrder(null);
}
document.getElementById("filter-status").addEventListener("change", () => loadList().catch((e) => showNote("order-note", e)));
document.getElementById("order-new-head").addEventListener("click", showCreate);
document.getElementById("order-close").addEventListener("click", closePanel);
document.getElementById("order-edit-btn").addEventListener("click", () => {
if (current) showEdit(current);
});
document.getElementById("order-cancel-edit").addEventListener("click", async () => {
if (!current) {
closePanel();
return;
}
try {
const full = await api("/auftraege/" + current.id);
showView(full);
} catch (error) {
showNote("order-note", error);
}
});
document.getElementById("order-form").addEventListener("submit", async (event) => {
event.preventDefault();
const id = document.getElementById("order-id").value;
@@ -324,22 +431,22 @@ document.getElementById("order-form").addEventListener("submit", async (event) =
? await api("/auftraege/" + id, { method: "PATCH", json: payload })
: await api("/auftraege", { method: "POST", json: payload });
showNote("order-note", null);
document.getElementById("order-note").textContent = "Gespeichert.";
toast("Gespeichert.", "good");
await loadList();
fillOrder(saved);
const full = await api("/auftraege/" + saved.id);
showView(full);
} catch (error) {
showNote("order-note", error);
}
});
document.getElementById("order-new").addEventListener("click", () => fillOrder(null));
document.getElementById("order-delete").addEventListener("click", (event) => {
const id = document.getElementById("order-id").value;
if (!id) return;
armButton(event.currentTarget, "Auftrag wirklich löschen?", async () => {
await api("/auftraege/" + id, { method: "DELETE" });
toast("Gelöscht.");
fillOrder(null);
closePanel();
loadList();
});
});
@@ -359,12 +466,36 @@ document.getElementById("item-form").addEventListener("submit", async (event) =>
try {
await api("/auftraege/" + current.id + "/positionen", { method: "POST", json: payload });
const full = await api("/auftraege/" + current.id);
fillOrder(full);
showView(full);
} catch (error) {
showNote("order-note", error);
}
});
document.getElementById("item-edit-form").addEventListener("submit", async (event) => {
event.preventDefault();
if (!current) return;
const itemId = document.getElementById("item-edit-id").value;
try {
await api("/auftraege/positionen/" + itemId, {
method: "PATCH",
json: {
qty: Number(document.getElementById("item-edit-qty").value) || 1,
unit_price: Number(document.getElementById("item-edit-price").value) || 0,
},
});
document.getElementById("item-edit-form").hidden = true;
const full = await api("/auftraege/" + current.id);
showView(full);
toast("Position gespeichert.");
} catch (error) {
showNote("order-note", error);
}
});
document.getElementById("item-edit-cancel").addEventListener("click", () => {
document.getElementById("item-edit-form").hidden = true;
});
document.getElementById("customer-form").addEventListener("submit", async (event) => {
event.preventDefault();
const id = document.getElementById("cust-id").value;
@@ -421,6 +552,16 @@ document.getElementById("cust-delete").addEventListener("click", (event) => {
});
});
loadBase().catch((error) => showNote("order-note", error));
loadBase().then(async () => {
const hash = (location.hash || "").replace(/^#/, "");
if (hash && /^\d+$/.test(hash)) {
try {
const full = await api("/auftraege/" + hash);
showView(full);
} catch (error) {
showNote("order-note", error);
}
}
}).catch((error) => showNote("order-note", error));
</script>
{% endblock %}