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
@@ -160,6 +160,7 @@ function promptAbraeumen(hostLabel) {
|
||||
<div class="field"><label for="ab-ok">Stück gut</label><input id="ab-ok" type="number" min="0" step="1" value="0"></div>
|
||||
<div class="field"><label for="ab-bad">Stück schlecht</label><input id="ab-bad" type="number" min="0" step="1" value="1"></div>
|
||||
</div>
|
||||
<label class="check"><input type="checkbox" id="ab-reprint"> Gleich nochmal drucken</label>
|
||||
<div class="actions">
|
||||
<button type="submit" value="ok" class="btn-primary">Quittieren</button>
|
||||
<button type="button" class="ghost" id="ab-cancel">Abbrechen</button>
|
||||
@@ -193,6 +194,7 @@ function promptAbraeumen(hostLabel) {
|
||||
verdict_reason: verdict.value === "ausschuss" ? dlg.querySelector("#ab-reason").value : "",
|
||||
pieces_ok: Number(dlg.querySelector("#ab-ok").value) || 0,
|
||||
pieces_bad: Number(dlg.querySelector("#ab-bad").value) || 0,
|
||||
reprint: !!dlg.querySelector("#ab-reprint").checked,
|
||||
};
|
||||
dlg.remove();
|
||||
resolve(payload);
|
||||
@@ -201,6 +203,131 @@ function promptAbraeumen(hostLabel) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toast mit Rückgängig für umkehrbare Aktionen.
|
||||
* @param {string} message
|
||||
* @param {() => void | Promise<void>} onUndo
|
||||
* @param {number} [ttlMs]
|
||||
*/
|
||||
function undoToast(message, onUndo, ttlMs) {
|
||||
let host = document.querySelector(".toast-host");
|
||||
if (!host) {
|
||||
host = document.createElement("div");
|
||||
host.className = "toast-host";
|
||||
host.setAttribute("role", "status");
|
||||
host.setAttribute("aria-live", "polite");
|
||||
document.body.appendChild(host);
|
||||
}
|
||||
const node = document.createElement("div");
|
||||
node.className = "toast good undo-toast";
|
||||
node.innerHTML = '<div></div><button type="button">Rückgängig</button>';
|
||||
node.querySelector("div").textContent = String(message ?? "");
|
||||
let undone = false;
|
||||
const btn = node.querySelector("button");
|
||||
btn.addEventListener("click", async () => {
|
||||
if (undone) return;
|
||||
undone = true;
|
||||
node.remove();
|
||||
try { await onUndo(); } catch (error) { toast(error.message || String(error), "bad"); }
|
||||
});
|
||||
host.appendChild(node);
|
||||
setTimeout(() => {
|
||||
if (undone) return;
|
||||
node.classList.add("out");
|
||||
setTimeout(() => node.remove(), 260);
|
||||
}, ttlMs || 6000);
|
||||
}
|
||||
|
||||
/** Globale Suche Ctrl+K / Cmd+K. */
|
||||
function openCommandPalette() {
|
||||
const existing = document.getElementById("cmdk-dlg");
|
||||
if (existing) {
|
||||
existing.showModal();
|
||||
existing.querySelector("input")?.focus();
|
||||
return;
|
||||
}
|
||||
const dlg = document.createElement("dialog");
|
||||
dlg.id = "cmdk-dlg";
|
||||
dlg.className = "cmdk-dlg";
|
||||
dlg.innerHTML = `<div class="cmdk-wrap">
|
||||
<input type="search" placeholder="Drucker, Datei, Spule, Charge, Auftrag…" autocomplete="off">
|
||||
<ul class="cmdk-list"></ul>
|
||||
</div>`;
|
||||
document.body.appendChild(dlg);
|
||||
const input = dlg.querySelector("input");
|
||||
const list = dlg.querySelector(".cmdk-list");
|
||||
let items = [];
|
||||
let active = 0;
|
||||
let timer = 0;
|
||||
|
||||
function paint() {
|
||||
list.innerHTML = items.map((item, i) =>
|
||||
`<li class="${i === active ? "on" : ""}" data-i="${i}">
|
||||
<span>${escapeHtml(item.title)}</span>
|
||||
<span class="tiny">${escapeHtml(item.subtitle || "")}</span>
|
||||
</li>`
|
||||
).join("") || '<li class="tiny">Nichts gefunden.</li>';
|
||||
}
|
||||
|
||||
async function search(q) {
|
||||
q = (q || "").trim();
|
||||
if (!q) { items = []; paint(); return; }
|
||||
try {
|
||||
const data = await api("/suche?q=" + encodeURIComponent(q));
|
||||
items = data.items || [];
|
||||
} catch (error) {
|
||||
items = [];
|
||||
}
|
||||
active = 0;
|
||||
paint();
|
||||
}
|
||||
|
||||
function go(item) {
|
||||
if (!item || !item.href) return;
|
||||
dlg.close();
|
||||
location.href = item.href;
|
||||
}
|
||||
|
||||
input.addEventListener("input", () => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => search(input.value), 180);
|
||||
});
|
||||
input.addEventListener("keydown", (event) => {
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
active = Math.min(items.length - 1, active + 1);
|
||||
paint();
|
||||
} else if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
active = Math.max(0, active - 1);
|
||||
paint();
|
||||
} else if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
go(items[active]);
|
||||
} else if (event.key === "Escape") {
|
||||
dlg.close();
|
||||
}
|
||||
});
|
||||
list.addEventListener("click", (event) => {
|
||||
const li = event.target.closest("[data-i]");
|
||||
if (!li) return;
|
||||
go(items[Number(li.dataset.i)]);
|
||||
});
|
||||
dlg.addEventListener("close", () => { /* bleibt im DOM */ });
|
||||
dlg.showModal();
|
||||
input.focus();
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if ((event.ctrlKey || event.metaKey) && (event.key === "k" || event.key === "K")) {
|
||||
event.preventDefault();
|
||||
openCommandPalette();
|
||||
}
|
||||
});
|
||||
|
||||
window.undoToast = undoToast;
|
||||
window.openCommandPalette = openCommandPalette;
|
||||
|
||||
window.RUN_STATES = RUN_STATES;
|
||||
window.setConnectionBanner = setConnectionBanner;
|
||||
window.createPoller = createPoller;
|
||||
|
||||
Reference in new issue
Block a user