839 lines
33 KiB
HTML
839 lines
33 KiB
HTML
{% 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 %}
|