Grotti 2.0.0: eigenes Repo, Umbenennung von Farm.

This commit is contained in:
Dominic Gundel committed 2026-10-15 08:45:56 +02:00
commit b2e6443c68
69 files changed
+18719

No files matched your search

+838
View File
@@ -0,0 +1,838 @@
{% extends "layout.html" %}
{% block body %}
<div class="page-head">
<div>
<h1>Material</h1>
<p class="tiny">Spulen am Drucker und im Regal. Sorten sind die Profile dazu.</p>
</div>
<div class="page-head-side">
<label class="scan-inline" for="scan-code">
<svg class="ico" aria-hidden="true"><use href="#i-scan"/></svg>
<input id="scan-code" autocomplete="off" placeholder="Code oder Charge · Enter" title="USB-Scanner oder Charge tippen">
</label>
<p id="scan-note" class="scan-note" hidden></p>
</div>
</div>
<div class="tabs mat-tabs" role="tablist">
<button type="button" class="on" data-view="spools" role="tab"><svg class="ico"><use href="#i-spool"/></svg>Spulen</button>
<button type="button" data-view="sorten" role="tab"><svg class="ico"><use href="#i-layers"/></svg>Sorten</button>
</div>
<div id="view-spools">
<div class="page-tools mat-tools">
<div class="filter-bar" id="spool-filter" role="group" aria-label="Ort">
<button type="button" class="fchip on" data-filter="all">Alle</button>
<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>
</div>
<input id="spool-search" type="search" placeholder="Spule suchen…" autocomplete="off">
<button type="button" id="spool-new"><svg class="ico"><use href="#i-spool"/></svg>Neue Spule</button>
</div>
<p id="spool-list-note" class="tiny"></p>
<div class="split mat-split is-idle" id="spool-layout">
<section class="card table-wrap mat-list">
<div class="lib-head">
<h2>Spulen</h2>
<span class="count" id="spool-count">0</span>
</div>
<table>
<thead><tr><th>Spule</th><th>Charge</th><th>Sorte</th><th>Farbe</th><th>Ort</th><th class="num">Rest</th></tr></thead>
<tbody id="spool-rows"></tbody>
</table>
</section>
<section class="card mat-form" id="spool-panel" hidden>
<div class="mat-form-head">
<h2 id="spool-title">Neue Spule</h2>
<button type="button" class="icon-btn" id="spool-close" title="Schließen" aria-label="Schließen"></button>
</div>
<form id="spool-form">
<input id="spool-id" type="hidden">
<div class="field">
<label for="spool-material">Sorte <span class="tip" title="Profil mit Dichte, Preis und Volumen. Verwalten unter Reiter Sorten."><svg class="ico"><use href="#i-info"/></svg></span></label>
<select id="spool-material"></select>
<p id="spool-hint" class="field-help"></p>
</div>
<div class="field">
<label for="spool-name">Name</label>
<input id="spool-name" required>
</div>
<div class="grid-2">
<div class="field"><label for="spool-brand">Marke</label><input id="spool-brand"></div>
<div class="field"><label for="spool-type">Typ</label><input id="spool-type" placeholder="PLA"></div>
</div>
<div class="grid-2">
<div class="field">
<label for="spool-chargen">Grotti-Charge</label>
<input id="spool-chargen" readonly placeholder="wird vergeben">
</div>
<div class="field">
<label for="spool-hersteller">Hersteller-Charge</label>
<input id="spool-hersteller" placeholder="vom Rollenetikett">
</div>
</div>
<details class="mat-color" id="color-box">
<summary>
<svg class="ico caret"><use href="#i-down"/></svg>
<span class="grow">Farbe</span>
<span class="tiny" id="color-sum">noch keine</span>
</summary>
<div class="mat-color-body">
<div id="color-picked" class="picked"></div>
<div id="color-solids" class="color-grid"></div>
<div id="color-multi" class="color-grid"></div>
<label class="mix check"><input id="color-mix" type="checkbox"> Eigene Mischung</label>
</div>
</details>
<div class="grid-2" style="margin-top:0.55rem">
<div class="field">
<label for="spool-printer">Drucker</label>
<select id="spool-printer"></select>
</div>
<div class="field">
<label for="spool-slot">ACE-Platz <span class="tip" title="ACE 1 = Plätze 1–4, ACE 2 = 5–8. Leer = externe Spule am Drucker. Ohne Drucker = Regal."><svg class="ico"><use href="#i-info"/></svg></span></label>
<select id="spool-slot">
<option value="">nicht im ACE</option>
<option value="1">ACE 1 · Platz 1</option>
<option value="2">ACE 1 · Platz 2</option>
<option value="3">ACE 1 · Platz 3</option>
<option value="4">ACE 1 · Platz 4</option>
<option value="5">ACE 2 · Platz 1</option>
<option value="6">ACE 2 · Platz 2</option>
<option value="7">ACE 2 · Platz 3</option>
<option value="8">ACE 2 · Platz 4</option>
</select>
</div>
<div class="field"><label for="spool-price">€ pro kg</label><input id="spool-price" type="number" min="0" step="0.01" value="0"></div>
<div class="field"><label for="spool-left">Rest gramm</label><input id="spool-left" type="number" step="1" value="1000"></div>
<div class="field"><label for="spool-density">Dichte g/cm³</label><input id="spool-density" type="number" min="0.1" step="0.01" value="1.24"></div>
<div class="field"><label for="spool-vol">Volumen mm³/s</label><input id="spool-vol" type="number" min="0.1" step="0.1" value="12"></div>
</div>
<div class="actions">
<button type="submit">Merken</button>
<button type="button" class="ghost" id="spool-shelf" hidden>Ins Regal</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>
</form>
<p id="spool-note"></p>
</section>
</div>
</div>
<div id="view-sorten" hidden>
<div class="page-tools mat-tools">
<input id="sorten-search" type="search" placeholder="Sorte suchen…" autocomplete="off">
<button type="button" id="sorten-new"><svg class="ico"><use href="#i-layers"/></svg>Neue Sorte</button>
</div>
<p id="sorten-list-note" class="tiny"></p>
<div class="split mat-split is-idle" id="sorten-layout">
<section class="card table-wrap mat-list">
<div class="lib-head">
<h2>Sorten</h2>
<span class="count" id="sorten-count">0</span>
</div>
<table>
<thead>
<tr>
<th>Sorte</th>
<th>Typ</th>
<th>Drucker</th>
<th class="num">Dichte</th>
<th class="num">mm³/s</th>
<th class="num">€/kg</th>
</tr>
</thead>
<tbody id="sorten-rows"></tbody>
</table>
</section>
<section class="card mat-form" id="sorten-panel" hidden>
<div class="mat-form-head">
<h2 id="sorten-title">Neue Sorte</h2>
<button type="button" class="icon-btn" id="sorten-close" title="Schließen"></button>
</div>
<form id="sorten-form">
<input id="sorten-id" type="hidden">
<div class="field">
<label for="sorten-name">Name</label>
<input id="sorten-name" required placeholder="z. B. Sunlu PLA+">
</div>
<div class="grid-2">
<div class="field"><label for="sorten-brand">Marke</label><input id="sorten-brand"></div>
<div class="field"><label for="sorten-type">Typ</label><input id="sorten-type" placeholder="PLA"></div>
</div>
<div class="field">
<label for="sorten-machines">Maschinen</label>
<select id="sorten-machines">
<option value="beide">beide</option>
<option value="kobra_3">nur Kobra 3</option>
<option value="kobra_s1">nur Kobra S1</option>
</select>
</div>
<div class="grid-2">
<div class="field"><label for="sorten-density">Dichte g/cm³</label><input id="sorten-density" type="number" min="0.1" step="0.01" value="1.24"></div>
<div class="field"><label for="sorten-price">€ pro kg</label><input id="sorten-price" type="number" min="0" step="0.01" value="0"></div>
<div class="field"><label for="sorten-vol">Volumen mm³/s</label><input id="sorten-vol" type="number" min="0.1" step="0.1" value="12"></div>
<div class="field"><label for="sorten-vol-s1">Volumen S1 <span class="tip" title="Leer = wie oben. Nur setzen, wenn S1 anders ist."><svg class="ico"><use href="#i-info"/></svg></span></label><input id="sorten-vol-s1" type="number" min="0.1" step="0.1" placeholder="wie oben"></div>
</div>
<div class="field">
<label for="sorten-note">Notiz</label>
<input id="sorten-note" placeholder="optional">
</div>
<div class="actions">
<button type="submit">Merken</button>
<button type="button" class="ghost" id="sorten-use">Spule damit anlegen</button>
<button type="button" class="warn" id="sorten-delete" hidden><svg class="ico"><use href="#i-trash"/></svg>Löschen</button>
</div>
</form>
<p id="sorten-note-msg"></p>
</section>
</div>
</div>
{% endblock %}
{% block script %}
<script>
let spools = [];
let printers = [];
let materials = [];
let palette = { solids: [], multi: [] };
let picked = [];
let colorLabel = "";
let autoName = "";
let spoolFilter = "all";
let spoolSearch = "";
let sortenSearch = "";
let view = "spools";
let spoolPanelOpen = false;
let sortenPanelOpen = false;
document.getElementById("spool-close").innerHTML = icon("x");
document.getElementById("sorten-close").innerHTML = icon("x");
function dots(colors) {
const wrap = document.createElement("span");
wrap.className = "dots";
(colors || []).forEach((color) => {
const dot = document.createElement("span");
dot.className = "dot";
dot.style.background = color.hex;
dot.title = color.name;
wrap.appendChild(dot);
});
return wrap;
}
function slotLabel(slot) {
if (!slot) return "";
if (slot <= 4) return "ACE 1 · " + slot;
return "ACE 2 · " + (slot - 4);
}
function printerName(host) {
return (printers.find((p) => p.host === host) || {}).name || host || "";
}
function ortLabel(spool) {
if (!spool.printer_host) return "Regal";
const name = printerName(spool.printer_host);
if (spool.ace_slot) return name + " · " + slotLabel(spool.ace_slot);
return name + " · extern";
}
function selectedMaterial() {
const id = document.getElementById("spool-material").value;
return materials.find((row) => String(row.id) === id) || null;
}
function volumeOf(material) {
const host = document.getElementById("spool-printer").value;
const printer = printers.find((row) => row.host === host);
if (printer && printer.family === "kobra_s1" && material.volumetric_s1 != null) return material.volumetric_s1;
return material.volumetric;
}
function showHint() {
const material = selectedMaterial();
const node = document.getElementById("spool-hint");
if (!material) {
node.textContent = "";
return;
}
const host = document.getElementById("spool-printer").value;
const printer = printers.find((row) => row.host === host);
const parts = [];
if (printer && material.machines !== "beide" && material.machines !== printer.family) {
parts.push("Diese Sorte läuft bei dir " + material.machines_label + ".");
} else if (material.machines !== "beide") {
parts.push(material.machines_label.charAt(0).toUpperCase() + material.machines_label.slice(1) + ".");
}
if (material.note) parts.push(material.note);
node.textContent = parts.join(" ");
}
function applyMaterial(material, rename) {
if (!material) return;
document.getElementById("spool-material").value = String(material.id);
document.getElementById("spool-brand").value = material.brand;
document.getElementById("spool-type").value = material.material_type;
document.getElementById("spool-density").value = material.density;
document.getElementById("spool-price").value = material.price_per_kg;
document.getElementById("spool-vol").value = volumeOf(material);
const name = document.getElementById("spool-name");
if (rename && (!name.value || name.value === autoName)) {
autoName = material.name;
name.value = material.name;
}
showHint();
}
function colorSummary() {
if (!picked.length) return "noch keine";
return colorLabel || picked.map((c) => c.name).join(" / ");
}
function renderPicked() {
const node = document.getElementById("color-picked");
node.innerHTML = "";
const sum = document.getElementById("color-sum");
if (!picked.length) {
node.textContent = "Farbe tippen — einfarbig oder Mehrton.";
sum.textContent = "noch keine";
return;
}
node.appendChild(dots(picked));
const label = document.createElement("span");
label.textContent = colorLabel || picked.map((color) => color.name).join(" / ");
node.appendChild(label);
sum.textContent = colorSummary();
}
function renderPalette() {
const mix = document.getElementById("color-mix").checked;
const solids = document.getElementById("color-solids");
const multi = document.getElementById("color-multi");
solids.innerHTML = "";
multi.innerHTML = "";
palette.solids.forEach((item) => {
const button = document.createElement("button");
button.type = "button";
button.className = "swatch";
const on = mix
? picked.some((color) => color.hex === item.colors[0].hex)
: picked.length === 1 && picked[0].hex === item.colors[0].hex && colorLabel === item.name;
if (on) button.classList.add("on");
button.appendChild(dots(item.colors));
button.appendChild(document.createTextNode(item.name));
button.addEventListener("click", () => chooseSolid(item));
solids.appendChild(button);
});
palette.multi.forEach((item) => {
const button = document.createElement("button");
button.type = "button";
button.className = "swatch";
if (colorLabel === item.name) button.classList.add("on");
button.appendChild(dots(item.colors));
button.appendChild(document.createTextNode(item.name));
button.addEventListener("click", () => chooseMulti(item));
multi.appendChild(button);
});
renderPicked();
}
function chooseSolid(item) {
const color = item.colors[0];
if (document.getElementById("color-mix").checked) {
const index = picked.findIndex((row) => row.hex === color.hex);
if (index >= 0) picked.splice(index, 1);
else if (picked.length < 8) picked.push(color);
colorLabel = picked.map((row) => row.name).join(" / ");
} else {
picked = [color];
colorLabel = item.name;
}
renderPalette();
}
function chooseMulti(item) {
picked = item.colors.map((color) => ({ name: color.name, hex: color.hex }));
colorLabel = item.name;
document.getElementById("color-mix").checked = false;
renderPalette();
}
function setView(next) {
view = next;
document.querySelectorAll(".mat-tabs [data-view]").forEach((btn) => {
btn.classList.toggle("on", btn.dataset.view === view);
});
document.getElementById("view-spools").hidden = view !== "spools";
document.getElementById("view-sorten").hidden = view !== "sorten";
}
function openSpoolPanel() {
spoolPanelOpen = true;
document.getElementById("spool-panel").hidden = false;
document.getElementById("spool-layout").classList.remove("is-idle");
}
function closeSpoolPanel() {
spoolPanelOpen = false;
document.getElementById("spool-panel").hidden = true;
document.getElementById("spool-layout").classList.add("is-idle");
document.getElementById("spool-id").value = "";
document.getElementById("spool-note").textContent = "";
renderSpools();
}
function openSortenPanel() {
sortenPanelOpen = true;
document.getElementById("sorten-panel").hidden = false;
document.getElementById("sorten-layout").classList.remove("is-idle");
}
function closeSortenPanel() {
sortenPanelOpen = false;
document.getElementById("sorten-panel").hidden = true;
document.getElementById("sorten-layout").classList.add("is-idle");
document.getElementById("sorten-id").value = "";
document.getElementById("sorten-note-msg").textContent = "";
renderSorten();
}
function payload() {
return {
name: document.getElementById("spool-name").value,
brand: document.getElementById("spool-brand").value,
material_type: document.getElementById("spool-type").value,
material_id: document.getElementById("spool-material").value || null,
hersteller_charge: document.getElementById("spool-hersteller").value,
color: colorLabel,
colors: picked,
printer_host: document.getElementById("spool-printer").value,
ace_slot: document.getElementById("spool-slot").value || null,
price_per_kg: Number(document.getElementById("spool-price").value),
remaining_g: Number(document.getElementById("spool-left").value),
density: Number(document.getElementById("spool-density").value),
volumetric: Number(document.getElementById("spool-vol").value),
};
}
function fill(spool) {
openSpoolPanel();
document.getElementById("spool-id").value = spool ? spool.id : "";
document.getElementById("spool-name").value = spool ? spool.name : "";
document.getElementById("spool-brand").value = spool ? spool.brand : "";
document.getElementById("spool-type").value = spool ? spool.material_type : "";
document.getElementById("spool-chargen").value = spool ? (spool.chargen_nr || spool.code || "") : "";
document.getElementById("spool-hersteller").value = spool ? (spool.hersteller_charge || "") : "";
document.getElementById("spool-printer").value = spool ? spool.printer_host : "";
document.getElementById("spool-slot").value = spool && spool.ace_slot ? String(spool.ace_slot) : "";
document.getElementById("spool-price").value = spool ? spool.price_per_kg : 0;
document.getElementById("spool-left").value = spool ? spool.remaining_g : 1000;
document.getElementById("spool-density").value = spool ? spool.density : 1.24;
document.getElementById("spool-vol").value = spool ? spool.volumetric : 12;
document.getElementById("spool-material").value = spool && spool.material_id ? String(spool.material_id) : "";
picked = spool && spool.colors ? spool.colors.map((color) => ({ name: color.name, hex: color.hex })) : [];
colorLabel = spool ? spool.color : "";
document.getElementById("color-mix").checked = false;
autoName = "";
document.getElementById("spool-title").textContent = spool ? spool.name : "Neue Spule";
document.getElementById("spool-delete").hidden = !spool;
document.getElementById("spool-label").hidden = !spool;
document.getElementById("spool-shelf").hidden = !spool || !(spool.printer_host);
document.getElementById("color-box").open = !spool || !picked.length;
showHint();
renderPalette();
renderSpools();
document.getElementById("spool-panel").scrollIntoView({ block: "nearest", behavior: "smooth" });
}
function fillSorten(material) {
openSortenPanel();
document.getElementById("sorten-id").value = material ? material.id : "";
document.getElementById("sorten-name").value = material ? material.name : "";
document.getElementById("sorten-brand").value = material ? material.brand : "";
document.getElementById("sorten-type").value = material ? material.material_type : "PLA";
document.getElementById("sorten-machines").value = material ? material.machines : "beide";
document.getElementById("sorten-density").value = material ? material.density : 1.24;
document.getElementById("sorten-price").value = material ? material.price_per_kg : 0;
document.getElementById("sorten-vol").value = material ? material.volumetric : 12;
document.getElementById("sorten-vol-s1").value =
material && material.volumetric_s1 != null ? material.volumetric_s1 : "";
document.getElementById("sorten-note").value = material ? (material.note || "") : "";
document.getElementById("sorten-title").textContent = material ? material.name : "Neue Sorte";
document.getElementById("sorten-delete").hidden = !material;
document.getElementById("sorten-use").hidden = !material;
renderSorten();
}
function shownSpools() {
const q = spoolSearch.trim().toLowerCase();
return spools.filter((spool) => {
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 (!q) return true;
const hay = [
spool.name, spool.chargen_nr, spool.code, spool.material_type, spool.color,
spool.brand, ortLabel(spool),
].join(" ").toLowerCase();
return hay.includes(q);
});
}
function shownSorten() {
const q = sortenSearch.trim().toLowerCase();
if (!q) return materials;
return materials.filter((m) =>
[m.name, m.brand, m.material_type, m.machines_label, m.note].join(" ").toLowerCase().includes(q)
);
}
function fillSelects() {
const select = document.getElementById("spool-printer");
const current = select.value;
select.innerHTML = '<option value="">Regal</option>';
printers.forEach((printer) => {
const option = document.createElement("option");
option.value = printer.host;
option.textContent = printer.name;
select.appendChild(option);
});
select.value = current;
const materialSelect = document.getElementById("spool-material");
const currentMaterial = materialSelect.value;
materialSelect.innerHTML = '<option value="">Sorte wählen</option>';
materials.forEach((material) => {
const option = document.createElement("option");
option.value = material.id;
option.textContent = material.name;
materialSelect.appendChild(option);
});
materialSelect.value = currentMaterial;
}
function renderSpools() {
document.querySelectorAll("#spool-filter .fchip").forEach((btn) => {
btn.classList.toggle("on", (btn.dataset.filter || "all") === spoolFilter);
});
fillSelects();
const body = document.getElementById("spool-rows");
body.innerHTML = "";
const shown = shownSpools();
document.getElementById("spool-count").textContent = String(shown.length);
const note = document.getElementById("spool-list-note");
if (!spools.length) {
note.textContent = "";
body.innerHTML = '<tr><td class="empty" colspan="6"><div class="empty-state">' + icon("spool") + '<p>Noch keine Spule.</p><p class="tiny">„Neue Spule“ anlegen oder Code scannen.</p></div></td></tr>';
return;
}
if (!shown.length) {
note.textContent = "";
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;
const ordered = [...shown].sort((a, b) => {
const ah = a.printer_host || "zzz";
const bh = b.printer_host || "zzz";
return ah.localeCompare(bh, "de") || (Number(a.ace_slot) || 0) - (Number(b.ace_slot) || 0) || a.name.localeCompare(b.name, "de");
});
let lastPlace = null;
ordered.forEach((spool) => {
const group = spool.printer_host ? printerName(spool.printer_host) : "Regal";
if (group !== lastPlace) {
lastPlace = group;
const head = document.createElement("tr");
head.className = "mat-group";
head.innerHTML = `<td colspan="6">${escapeHtml(group)}</td>`;
body.appendChild(head);
}
const tr = document.createElement("tr");
tr.className = "pick";
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;
tr.children[1].textContent = spool.chargen_nr || spool.code || "";
tr.children[1].classList.add("nowrap");
tr.children[2].textContent = spool.material_type;
const colorCell = tr.children[3];
if (spool.colors && spool.colors.length) colorCell.appendChild(dots(spool.colors));
const caption = document.createElement("span");
caption.className = "mat-color-cap";
caption.textContent = spool.color ? " " + spool.color : "";
colorCell.appendChild(caption);
tr.children[4].textContent = ortLabel(spool);
tr.children[4].classList.add("nowrap");
tr.children[4].title = ortLabel(spool);
const rest = Number(spool.remaining_g) || 0;
tr.children[5].textContent = grams(rest);
if (rest < 100) tr.children[5].classList.add("msg");
tr.addEventListener("click", () => fill(spool));
body.appendChild(tr);
});
}
function renderSorten() {
fillSelects();
const body = document.getElementById("sorten-rows");
body.innerHTML = "";
const shown = shownSorten();
document.getElementById("sorten-count").textContent = String(shown.length);
const note = document.getElementById("sorten-list-note");
if (!materials.length) {
note.textContent = "";
body.innerHTML = '<tr><td class="empty" colspan="6"><div class="empty-state">' + icon("layers") + '<p>Noch keine Sorte.</p></div></td></tr>';
return;
}
if (!shown.length) {
note.textContent = "";
body.innerHTML = '<tr><td class="empty" colspan="6"><div class="empty-state">' + icon("search") + '<p>Keine Sorte passt.</p></div></td></tr>';
return;
}
note.textContent = shown.length === 1 ? "1 Sorte" : shown.length + " Sorten";
shown.forEach((material) => {
const tr = document.createElement("tr");
tr.className = "pick";
if (sortenPanelOpen && String(material.id) === document.getElementById("sorten-id").value) tr.classList.add("on");
const speeds = material.volumetric_s1 != null && material.volumetric_s1 !== material.volumetric
? material.volumetric + " · S1 " + material.volumetric_s1
: String(material.volumetric);
tr.innerHTML = "<td></td><td></td><td class=\"nowrap\"></td><td class=\"num\"></td><td class=\"num\"></td><td class=\"num\"></td>";
tr.children[0].textContent = material.name;
tr.children[1].textContent = material.material_type;
tr.children[2].textContent = material.machines_label;
tr.children[3].textContent = String(material.density).replace(".", ",");
tr.children[4].textContent = speeds;
tr.children[5].textContent = euro(material.price_per_kg);
tr.addEventListener("click", () => fillSorten(material));
body.appendChild(tr);
});
}
function render() {
renderSpools();
renderSorten();
}
async function load() {
[spools, printers, materials, palette] = await Promise.all([
api("/spulen"),
api("/drucker?live=0"),
api("/materialien"),
api("/farben"),
]);
render();
renderPalette();
}
function setScanNote(text, ok) {
const note = document.getElementById("scan-note");
if (!text) {
note.hidden = true;
note.textContent = "";
return;
}
note.hidden = false;
note.textContent = text;
note.className = "scan-note " + (ok ? "ok" : "msg");
}
document.querySelectorAll(".mat-tabs [data-view]").forEach((btn) => {
btn.addEventListener("click", () => setView(btn.dataset.view));
});
document.getElementById("spool-filter").addEventListener("click", (event) => {
const btn = event.target.closest("[data-filter]");
if (!btn) return;
spoolFilter = btn.dataset.filter || "all";
renderSpools();
});
document.getElementById("spool-search").addEventListener("input", (event) => {
spoolSearch = event.target.value || "";
renderSpools();
});
document.getElementById("sorten-search").addEventListener("input", (event) => {
sortenSearch = event.target.value || "";
renderSorten();
});
document.getElementById("spool-close").addEventListener("click", closeSpoolPanel);
document.getElementById("sorten-close").addEventListener("click", closeSortenPanel);
document.getElementById("spool-material").addEventListener("change", () => applyMaterial(selectedMaterial(), true));
document.getElementById("spool-printer").addEventListener("change", () => {
const material = selectedMaterial();
if (material) document.getElementById("spool-vol").value = volumeOf(material);
showHint();
});
document.getElementById("color-mix").addEventListener("change", renderPalette);
document.getElementById("spool-form").addEventListener("submit", async (event) => {
event.preventDefault();
const id = document.getElementById("spool-id").value;
const data = payload();
const host = data.printer_host;
const slot = data.ace_slot;
try {
const base = { ...data, printer_host: "", ace_slot: null };
let saved = id
? await api("/spulen/" + id, { method: "PATCH", json: base })
: await api("/spulen", { method: "POST", json: base });
if (host) {
saved = await api("/spulen/" + saved.id + "/einlegen", {
method: "POST",
json: { host, ace_slot: slot },
});
}
toast("Spule gemerkt.");
document.getElementById("spool-note").textContent = "Gemerkt.";
document.getElementById("spool-note").className = "ok";
await load();
fill(saved);
} catch (error) {
showNote("spool-note", error);
}
});
document.getElementById("spool-new").addEventListener("click", () => {
fill(null);
document.getElementById("spool-material").focus();
});
document.getElementById("spool-shelf").addEventListener("click", async () => {
const id = document.getElementById("spool-id").value;
if (!id) return;
try {
const saved = await api("/spulen/" + id, { method: "PATCH", json: { printer_host: "", ace_slot: null } });
toast("Spule liegt im Regal.");
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");
});
document.getElementById("spool-delete").addEventListener("click", () => {
const id = document.getElementById("spool-id").value;
if (!id) return;
armButton(document.getElementById("spool-delete"), "Löschen?", async () => {
await api("/spulen/" + id, { method: "DELETE" });
toast("Spule gelöscht.");
await load();
closeSpoolPanel();
});
});
document.getElementById("sorten-new").addEventListener("click", () => {
fillSorten(null);
document.getElementById("sorten-name").focus();
});
document.getElementById("sorten-form").addEventListener("submit", async (event) => {
event.preventDefault();
const id = document.getElementById("sorten-id").value;
const volS1 = document.getElementById("sorten-vol-s1").value;
const data = {
name: document.getElementById("sorten-name").value.trim(),
brand: document.getElementById("sorten-brand").value.trim(),
material_type: document.getElementById("sorten-type").value.trim() || "PLA",
machines: document.getElementById("sorten-machines").value,
density: Number(document.getElementById("sorten-density").value) || 1.24,
price_per_kg: Number(document.getElementById("sorten-price").value) || 0,
volumetric: Number(document.getElementById("sorten-vol").value) || 12,
volumetric_s1: volS1 === "" ? null : Number(volS1),
note: document.getElementById("sorten-note").value.trim(),
};
try {
const saved = id
? await api("/materialien/" + id, { method: "PATCH", json: data })
: await api("/materialien", { method: "POST", json: data });
toast("Sorte gemerkt.");
showNote("sorten-note-msg", null, "Gemerkt.");
await load();
fillSorten(saved);
} catch (error) {
showNote("sorten-note-msg", error);
}
});
document.getElementById("sorten-use").addEventListener("click", () => {
const id = document.getElementById("sorten-id").value;
const material = materials.find((row) => String(row.id) === id);
if (!material) return;
setView("spools");
fill(null);
applyMaterial(material, true);
document.getElementById("color-box").open = true;
});
document.getElementById("sorten-delete").addEventListener("click", () => {
const id = document.getElementById("sorten-id").value;
if (!id) return;
armButton(document.getElementById("sorten-delete"), "Löschen?", async () => {
try {
await api("/materialien/" + id, { method: "DELETE" });
toast("Sorte gelöscht.");
await load();
closeSortenPanel();
} catch (error) {
showNote("sorten-note-msg", error);
}
});
});
async function openByCode(raw) {
const code = (raw || "").trim();
if (!code) return;
try {
let spool = null;
if (/^\d+$/.test(code)) {
spool = spools.find((row) => String(row.id) === code) || null;
}
if (!spool && code.includes("spool=")) {
const match = code.match(/[?&]spool=(\d+)/);
if (match) spool = spools.find((row) => String(row.id) === match[1]) || null;
}
if (!spool) spool = await api("/spulen/by-code/" + encodeURIComponent(code));
setView("spools");
fill(spool);
setScanNote("Spule " + (spool.chargen_nr || spool.name) + " geöffnet.", true);
document.getElementById("scan-code").select();
} catch (error) {
setScanNote(error.message || String(error), false);
}
}
document.getElementById("scan-code").addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
openByCode(event.target.value);
}
});
load().then(() => {
closeSpoolPanel();
closeSortenPanel();
const params = new URLSearchParams(location.search);
const spoolId = params.get("spool");
if (spoolId) {
const spool = spools.find((row) => String(row.id) === spoolId);
if (spool) fill(spool);
else openByCode(spoolId);
}
}).catch((error) => showNote("spool-note", error));
</script>
{% endblock %}