Grotti 3.3.0: Projekte mit Ordner, Datei-Löschen/Verschieben, Material-Archiv, UI-Polish.

This commit is contained in:
Dominic Gundel committed 2026-10-18 20:10:32 +02:00
1 parent 4ddffbbeb1
commit 67a89d1794
45 files changed
+2856 -320

No files matched your search

+118 -5
View File
@@ -40,6 +40,11 @@
<button type="button" class="fchip" data-filter="loaded">Am Drucker</button>
<button type="button" class="fchip" data-filter="shelf">Regal</button>
<button type="button" class="fchip" data-filter="low">Wenig Rest</button>
<button type="button" class="fchip" data-filter="archive">Archiv</button>
</div>
<div class="mat-extra-filters">
<select id="spool-filter-type" title="Sorte"><option value="">Alle Sorten</option></select>
<select id="spool-filter-color" title="Farbe"><option value="">Alle Farben</option></select>
</div>
<div class="search-wrap">
<svg class="ico"><use href="#i-search"/></svg>
@@ -141,6 +146,9 @@
<div class="actions">
<button type="submit" class="btn-primary">Speichern</button>
<button type="button" class="ghost" id="spool-shelf" hidden>Ins Regal</button>
<button type="button" class="ghost" id="spool-empty" hidden title="Rest auf 0, Regal, archivieren">Leer</button>
<button type="button" class="ghost" id="spool-archive" hidden>Archivieren</button>
<button type="button" class="ghost" id="spool-restore" hidden>Wiederherstellen</button>
<button type="button" class="ghost" id="spool-label" hidden>Etikett</button>
<button type="button" class="warn" id="spool-delete" hidden><svg class="ico"><use href="#i-trash"/></svg>Löschen</button>
</div>
@@ -237,6 +245,8 @@ let picked = [];
let colorLabel = "";
let autoName = "";
let spoolFilter = "all";
let spoolTypeFilter = "";
let spoolColorFilter = "";
let spoolSearch = "";
let sortenSearch = "";
let view = "spools";
@@ -556,6 +566,9 @@ function fill(spool) {
document.getElementById("spool-delete").hidden = !spool;
document.getElementById("spool-label").hidden = !spool;
document.getElementById("spool-shelf").hidden = !spool || !(spool.printer_host);
document.getElementById("spool-empty").hidden = !spool || spool.archived;
document.getElementById("spool-archive").hidden = !spool || spool.archived;
document.getElementById("spool-restore").hidden = !spool || !spool.archived;
document.getElementById("color-box").open = !spool || !picked.length;
showHint();
renderPalette();
@@ -583,12 +596,30 @@ function fillSorten(material) {
renderSorten();
}
function isSpoolArchived(spool) {
return !!(spool && (spool.archived || spool.archived_at));
}
function isSpoolEmpty(spool) {
return (Number(spool.remaining_g) || 0) <= 0.5;
}
function shownSpools() {
const q = spoolSearch.trim().toLowerCase();
const type = spoolTypeFilter.trim().toLowerCase();
const color = spoolColorFilter.trim().toLowerCase();
return spools.filter((spool) => {
const archived = isSpoolArchived(spool) || isSpoolEmpty(spool);
if (spoolFilter === "archive") {
if (!archived) return false;
} else if (archived) {
return false;
}
if (spoolFilter === "loaded" && !spool.printer_host) return false;
if (spoolFilter === "shelf" && spool.printer_host) return false;
if (spoolFilter === "low" && (Number(spool.remaining_g) || 0) >= 100) return false;
if (type && (spool.material_type || "").toLowerCase() !== type) return false;
if (color && (spool.color || "").toLowerCase() !== color) return false;
if (!q) return true;
const hay = [
spool.name, spool.chargen_nr, spool.code, spool.material_type, spool.color,
@@ -598,6 +629,28 @@ function shownSpools() {
});
}
function fillSpoolMetaFilters() {
const typeSel = document.getElementById("spool-filter-type");
const colorSel = document.getElementById("spool-filter-color");
if (!typeSel || !colorSel) return;
const types = [...new Set(spools.map((s) => (s.material_type || "").trim()).filter(Boolean))]
.sort((a, b) => a.localeCompare(b, "de"));
const colors = [...new Set(spools.map((s) => (s.color || "").trim()).filter(Boolean))]
.sort((a, b) => a.localeCompare(b, "de"));
const curType = spoolTypeFilter;
const curColor = spoolColorFilter;
typeSel.innerHTML = '<option value="">Alle Sorten</option>' + types.map((t) =>
`<option value="${escapeHtml(t)}">${escapeHtml(t)}</option>`
).join("");
colorSel.innerHTML = '<option value="">Alle Farben</option>' + colors.map((c) =>
`<option value="${escapeHtml(c)}">${escapeHtml(c)}</option>`
).join("");
typeSel.value = types.includes(curType) ? curType : "";
colorSel.value = colors.includes(curColor) ? curColor : "";
spoolTypeFilter = typeSel.value;
spoolColorFilter = colorSel.value;
}
function shownSorten() {
const q = sortenSearch.trim().toLowerCase();
if (!q) return materials;
@@ -635,11 +688,15 @@ function renderSpools() {
btn.classList.toggle("on", (btn.dataset.filter || "all") === spoolFilter);
});
fillSelects();
fillSpoolMetaFilters();
const body = document.getElementById("spool-rows");
body.innerHTML = "";
const shown = shownSpools();
document.getElementById("spool-count").textContent = String(shown.length);
const activeCount = spools.filter((s) => !isSpoolArchived(s) && !isSpoolEmpty(s)).length;
document.getElementById("spool-count").textContent = String(
spoolFilter === "archive" ? shown.length : activeCount
);
const note = document.getElementById("spool-list-note");
if (!spools.length) {
note.textContent = "";
@@ -651,9 +708,11 @@ function renderSpools() {
body.innerHTML = '<tr><td class="empty" colspan="6"><div class="empty-state">' + icon("search") + '<p>Keine Spule passt.</p></div></td></tr>';
return;
}
note.textContent = shown.length === spools.length
? (shown.length === 1 ? "1 Spule" : shown.length + " Spulen")
: shown.length + " von " + spools.length;
note.textContent = spoolFilter === "archive"
? (shown.length === 1 ? "1 archivierte Spule" : shown.length + " archivierte Spulen")
: shown.length === activeCount
? (shown.length === 1 ? "1 Spule" : shown.length + " Spulen")
: shown.length + " von " + activeCount;
const ordered = [...shown].sort((a, b) => {
const ah = a.printer_host || "zzz";
@@ -671,7 +730,7 @@ function renderSpools() {
body.appendChild(head);
}
const tr = document.createElement("tr");
tr.className = "pick";
tr.className = "pick" + (isSpoolArchived(spool) || isSpoolEmpty(spool) ? " is-archived" : "");
if (spoolPanelOpen && String(spool.id) === document.getElementById("spool-id").value) tr.classList.add("on");
tr.innerHTML = "<td></td><td></td><td></td><td></td><td></td><td class=\"num\"></td>";
tr.children[0].textContent = spool.name;
@@ -876,6 +935,52 @@ document.getElementById("spool-shelf").addEventListener("click", async () => {
showNote("spool-note", error);
}
});
document.getElementById("spool-empty").addEventListener("click", () => {
const id = document.getElementById("spool-id").value;
if (!id) return;
armButton(document.getElementById("spool-empty"), "Als leer markieren?", async () => {
try {
const saved = await api("/spulen/" + id + "/leer", { method: "POST", json: {} });
toast("Spule als leer archiviert.");
await load();
fill(saved);
} catch (error) {
showNote("spool-note", error);
}
});
});
document.getElementById("spool-archive").addEventListener("click", async () => {
const id = document.getElementById("spool-id").value;
if (!id) return;
try {
const saved = await api("/spulen/" + id, {
method: "PATCH",
json: { archived: true, printer_host: "", ace_slot: null },
});
toast("Spule archiviert.");
await load();
fill(saved);
} catch (error) {
showNote("spool-note", error);
}
});
document.getElementById("spool-restore").addEventListener("click", async () => {
const id = document.getElementById("spool-id").value;
if (!id) return;
try {
const left = Number(document.getElementById("spool-left").value) || 0;
const saved = await api("/spulen/" + id, {
method: "PATCH",
json: { archived: false, remaining_g: left > 0.5 ? left : 1000 },
});
toast("Spule wiederhergestellt.");
spoolFilter = "all";
await load();
fill(saved);
} catch (error) {
showNote("spool-note", error);
}
});
document.getElementById("spool-label").addEventListener("click", () => {
const id = document.getElementById("spool-id").value;
if (id) window.open("/etikett/" + id, "_blank", "noopener");
@@ -890,6 +995,14 @@ document.getElementById("spool-delete").addEventListener("click", () => {
closeSpoolPanel();
});
});
document.getElementById("spool-filter-type")?.addEventListener("change", (event) => {
spoolTypeFilter = event.target.value || "";
renderSpools();
});
document.getElementById("spool-filter-color")?.addEventListener("change", (event) => {
spoolColorFilter = event.target.value || "";
renderSpools();
});
document.getElementById("sorten-new").addEventListener("click", () => {
fillSorten(null);