Grotti 3.2.0: Jetzt-Liste, Filament-Pause, Strom-Torte, UI-Polish.
Jetzt/Topbar und Material-Routing, Pause bei Filament-Ende, Druckbericht mit Stromanteilen, Design- und Kamera-Stream-Polish; Bedarf-Alert-Spam und Platte-belegt-Bugs behoben.
This commit is contained in:
1 parent
6e59df22e5
commit
4ddffbbeb1
68 files changed
+4348
-1215
No files matched your search
+106
-18
@@ -55,9 +55,9 @@
|
||||
</div>
|
||||
<div class="field"><label for="order-notes">Notiz</label><textarea id="order-notes"></textarea></div>
|
||||
<div class="actions">
|
||||
<button type="submit">Merken</button>
|
||||
<button type="submit" class="btn-primary">Speichern</button>
|
||||
<button type="button" class="ghost" id="order-new">Neu</button>
|
||||
<button type="button" class="warn" id="order-delete" hidden>Löschen</button>
|
||||
<button type="button" class="btn-danger" id="order-delete" hidden>Löschen</button>
|
||||
</div>
|
||||
</form>
|
||||
<p id="order-note" class="tiny"></p>
|
||||
@@ -81,11 +81,16 @@
|
||||
<div class="actions"><button type="submit">Position hinzufügen</button></div>
|
||||
</form>
|
||||
<details class="section-gap">
|
||||
<summary>Kunde anlegen</summary>
|
||||
<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>
|
||||
<button type="submit">Kunde speichern</button>
|
||||
<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>
|
||||
</section>
|
||||
@@ -130,7 +135,14 @@ function paintActuals(order) {
|
||||
const el = document.getElementById("order-actuals");
|
||||
const a = order.actuals;
|
||||
if (!a) { el.textContent = ""; return; }
|
||||
el.textContent = "Ist: " + euro(a.cost_total) + " · " + (hours(a.duration_s) || "0 min") + " · Ausschuss " + a.pieces_bad + " Stk";
|
||||
const bits = [
|
||||
"Ist: " + euro(a.cost_total),
|
||||
hours(a.duration_s) || "0 min",
|
||||
"Ausschuss " + a.pieces_bad + " Stk",
|
||||
];
|
||||
if (a.cost_per_part != null) bits.push(euro(a.cost_per_part) + "/Teil");
|
||||
if (a.yield_percent != null) bits.push("Yield " + Math.round(a.yield_percent) + " %");
|
||||
el.textContent = bits.join(" · ");
|
||||
}
|
||||
|
||||
function fillOrder(order) {
|
||||
@@ -169,17 +181,55 @@ function renderItems(items) {
|
||||
tr.children[0].textContent = item.file_name || ("#" + (item.file_id || "—"));
|
||||
tr.children[1].textContent = item.qty;
|
||||
tr.children[2].textContent = item.done_ok + (item.done_bad ? " (−" + item.done_bad + ")" : "");
|
||||
const tools = document.createElement("div");
|
||||
tools.className = "queue-tools";
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "ghost tiny-btn";
|
||||
btn.textContent = "Einreihen";
|
||||
btn.disabled = !item.file_id || !current;
|
||||
btn.addEventListener("click", () => einreihen(item.id));
|
||||
tr.children[3].appendChild(btn);
|
||||
const edit = document.createElement("button");
|
||||
edit.type = "button";
|
||||
edit.className = "ghost tiny-btn";
|
||||
edit.textContent = "Ändern";
|
||||
edit.addEventListener("click", () => editItem(item));
|
||||
const del = document.createElement("button");
|
||||
del.type = "button";
|
||||
del.className = "btn-danger tiny-btn";
|
||||
del.textContent = "Löschen";
|
||||
del.addEventListener("click", (event) => {
|
||||
armButton(event.currentTarget, "Position löschen?", async () => {
|
||||
await api("/auftraege/positionen/" + item.id, { method: "DELETE" });
|
||||
toast("Position gelöscht.");
|
||||
const full = await api("/auftraege/" + current.id);
|
||||
fillOrder(full);
|
||||
});
|
||||
});
|
||||
tools.append(btn, edit, del);
|
||||
tr.children[3].appendChild(tools);
|
||||
body.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
async function einreihen(itemId) {
|
||||
if (!current) return;
|
||||
try {
|
||||
@@ -200,7 +250,8 @@ function renderList() {
|
||||
const body = document.getElementById("order-rows");
|
||||
body.innerHTML = "";
|
||||
if (!orders.length) {
|
||||
body.innerHTML = '<tr><td colspan="3" class="empty">Noch kein Auftrag.</td></tr>';
|
||||
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));
|
||||
return;
|
||||
}
|
||||
const selected = document.getElementById("order-id").value;
|
||||
@@ -209,7 +260,9 @@ function renderList() {
|
||||
tr.className = "pick" + (selected && String(order.id) === selected ? " on" : "");
|
||||
tr.innerHTML = "<td></td><td></td><td></td>";
|
||||
tr.children[0].textContent = order.title;
|
||||
tr.children[1].textContent = STATUS_LABEL[order.status] || order.status;
|
||||
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.addEventListener("click", async () => {
|
||||
try {
|
||||
@@ -259,7 +312,7 @@ 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 = "Gemerkt.";
|
||||
document.getElementById("order-note").textContent = "Gespeichert.";
|
||||
await loadList();
|
||||
fillOrder(saved);
|
||||
} catch (error) {
|
||||
@@ -302,25 +355,60 @@ document.getElementById("item-form").addEventListener("submit", async (event) =>
|
||||
|
||||
document.getElementById("customer-form").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const id = document.getElementById("cust-id").value;
|
||||
const payload = {
|
||||
name: document.getElementById("cust-name").value,
|
||||
email: document.getElementById("cust-email").value,
|
||||
};
|
||||
try {
|
||||
const row = await api("/kunden", {
|
||||
method: "POST",
|
||||
json: {
|
||||
name: document.getElementById("cust-name").value,
|
||||
email: document.getElementById("cust-email").value,
|
||||
},
|
||||
});
|
||||
customers.push(row);
|
||||
const row = id
|
||||
? await api("/kunden/" + id, { method: "PATCH", json: payload })
|
||||
: await api("/kunden", { method: "POST", json: payload });
|
||||
if (id) {
|
||||
customers = customers.map((c) => (c.id === row.id ? row : c));
|
||||
} else {
|
||||
customers.push(row);
|
||||
}
|
||||
fillCustomers();
|
||||
document.getElementById("order-customer").value = row.id;
|
||||
document.getElementById("cust-id").value = "";
|
||||
document.getElementById("cust-name").value = "";
|
||||
document.getElementById("cust-email").value = "";
|
||||
toast("Kunde angelegt.");
|
||||
document.getElementById("cust-delete").hidden = true;
|
||||
toast(id ? "Kunde gespeichert." : "Kunde angelegt.");
|
||||
} catch (error) {
|
||||
showNote("order-note", error);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("cust-edit-current").addEventListener("click", () => {
|
||||
const id = document.getElementById("order-customer").value;
|
||||
const row = customers.find((c) => String(c.id) === String(id));
|
||||
if (!row) {
|
||||
toast("Zuerst einen Kunden wählen.", "bad");
|
||||
return;
|
||||
}
|
||||
document.getElementById("cust-id").value = row.id;
|
||||
document.getElementById("cust-name").value = row.name || "";
|
||||
document.getElementById("cust-email").value = row.email || "";
|
||||
document.getElementById("cust-delete").hidden = false;
|
||||
});
|
||||
|
||||
document.getElementById("cust-delete").addEventListener("click", (event) => {
|
||||
const id = document.getElementById("cust-id").value;
|
||||
if (!id) return;
|
||||
armButton(event.currentTarget, "Kunde löschen?", async () => {
|
||||
await api("/kunden/" + id, { method: "DELETE" });
|
||||
customers = customers.filter((c) => String(c.id) !== String(id));
|
||||
fillCustomers();
|
||||
document.getElementById("cust-id").value = "";
|
||||
document.getElementById("cust-name").value = "";
|
||||
document.getElementById("cust-email").value = "";
|
||||
document.getElementById("cust-delete").hidden = true;
|
||||
toast("Kunde gelöscht.");
|
||||
});
|
||||
});
|
||||
|
||||
loadBase().catch((error) => showNote("order-note", error));
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user