1127 lines
45 KiB
HTML
1127 lines
45 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>
|
||
|
||
<section class="card bedarf-card" id="bedarf-card" hidden>
|
||
<div class="lib-head">
|
||
<h2 id="bedarf">Bedarf aus Warteschlange</h2>
|
||
<span class="count" id="bedarf-short-count">0</span>
|
||
</div>
|
||
<p class="hint" id="bedarf-hint">Fehlmenge = Queue-Gewicht minus Spulenrest (gleiche Sorte/Farbe).</p>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead><tr><th>Sorte</th><th>Farbe</th><th class="num">Queue</th><th class="num">Rest</th><th class="num">Fehlt</th><th class="num">Jobs</th></tr></thead>
|
||
<tbody id="bedarf-rows"></tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<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="toolbar page-tools mat-tools">
|
||
<div class="fchips" 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>
|
||
<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>
|
||
<input id="spool-search" type="search" placeholder="Spule suchen…" autocomplete="off">
|
||
</div>
|
||
<div class="tools-end"><button type="button" id="spool-new" class="btn-primary"><svg class="ico"><use href="#i-plus"/></svg>Neue Spule</button></div>
|
||
</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="card-head lib-head">
|
||
<svg class="ico head-ico"><use href="#i-spool"/></svg>
|
||
<h2>Spulen</h2>
|
||
<span class="count-pill" 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>
|
||
<div class="pager" id="spool-pager" hidden></div>
|
||
</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-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-left">Rest gramm</label><input id="spool-left" type="number" step="0.1" value="1000"></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>
|
||
|
||
<details class="mat-more" id="spool-more">
|
||
<summary>
|
||
<svg class="ico caret"><use href="#i-down"/></svg>
|
||
<span class="grow">Weitere Angaben</span>
|
||
</summary>
|
||
<div class="mat-more-body">
|
||
<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 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 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-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>
|
||
</details>
|
||
|
||
<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>
|
||
</form>
|
||
<div id="spool-akte" class="spool-akte" hidden>
|
||
<h3>Akte</h3>
|
||
<div class="kpi-row akte-kpis" id="akte-kpis"></div>
|
||
<div class="akte-runs" id="akte-runs"></div>
|
||
</div>
|
||
<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" class="btn-primary">Speichern</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 spoolTypeFilter = "";
|
||
let spoolColorFilter = "";
|
||
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),
|
||
};
|
||
}
|
||
|
||
const akteStates = {
|
||
completed: "fertig", cancelled: "abgebrochen", error: "Fehler",
|
||
in_progress: "druckt", printing: "druckt", paused: "pausiert",
|
||
interrupted: "unterbrochen",
|
||
};
|
||
|
||
function akteWhen(iso) {
|
||
if (!iso) return "—";
|
||
const text = formatDateTime(iso, { year: undefined });
|
||
return text === "—" ? "—" : text.replace(/,\s*/, " ");
|
||
}
|
||
|
||
function akteFile(name) {
|
||
const base = (name || "").split("/").pop() || "";
|
||
if (!base) return "Druck";
|
||
if (base.length <= 42) return base;
|
||
return base.slice(0, 39) + "…";
|
||
}
|
||
|
||
function akteStatus(run) {
|
||
if (run.verdict === "ausschuss") return "Ausschuss";
|
||
if (run.verdict === "gut") return "gut";
|
||
return akteStates[run.state] || run.state || "";
|
||
}
|
||
|
||
function akteKpi(label, value) {
|
||
return '<div class="kpi"><div class="kpi-val">' + escapeHtml(value) + '</div><div class="kpi-label">' + escapeHtml(label) + "</div></div>";
|
||
}
|
||
|
||
async function loadAkte(spoolId) {
|
||
const box = document.getElementById("spool-akte");
|
||
if (!spoolId) {
|
||
box.hidden = true;
|
||
return;
|
||
}
|
||
try {
|
||
const data = await api("/spulen/" + spoolId + "/akte");
|
||
const stats = data.stats || {};
|
||
const fillPct = Math.round(Number(stats.fill_pct) || 0);
|
||
let leer = "—";
|
||
if (stats.empty_in_days == null) leer = "kein Verbrauch";
|
||
else if (stats.empty_in_days <= 0) leer = "leer";
|
||
else leer = "~" + Math.round(stats.empty_in_days) + " Tage";
|
||
document.getElementById("akte-kpis").innerHTML =
|
||
akteKpi("Verdruckt", grams(stats.total_g))
|
||
+ akteKpi("Ausschuss", grams(stats.scrap_g))
|
||
+ akteKpi("Kosten", euro(stats.total_cost))
|
||
+ akteKpi("Füllstand", fillPct + " %")
|
||
+ akteKpi("Leer-in", leer);
|
||
const list = document.getElementById("akte-runs");
|
||
const runs = data.runs || [];
|
||
if (!runs.length) {
|
||
list.innerHTML = '<p class="tiny">Noch kein Druck auf dieser Spule.</p>';
|
||
} else {
|
||
list.innerHTML = runs.slice(0, 12).map((run) => {
|
||
const href = "/protokoll/" + run.id;
|
||
const charge = run.chargen_nr
|
||
? '<a class="tag akte-charge" href="' + href + '">' + escapeHtml(run.chargen_nr) + "</a>"
|
||
: '<span class="akte-charge tiny">—</span>';
|
||
const file = '<a class="akte-file" href="' + href + '" title="' + escapeHtml((run.filename || "").split("/").pop() || "") + '">'
|
||
+ escapeHtml(akteFile(run.filename)) + "</a>";
|
||
const gramsTxt = grams(run.spool_weight_g) + (run.spool_is_estimate ? " ≈" : "");
|
||
return '<div class="akte-run">'
|
||
+ '<span class="akte-when tiny">' + escapeHtml(akteWhen(run.started_at)) + "</span>"
|
||
+ charge
|
||
+ file
|
||
+ '<span class="akte-g">' + escapeHtml(gramsTxt) + "</span>"
|
||
+ '<span class="akte-state tiny">' + escapeHtml(akteStatus(run)) + "</span>"
|
||
+ "</div>";
|
||
}).join("");
|
||
}
|
||
box.hidden = false;
|
||
} catch (error) {
|
||
box.hidden = true;
|
||
}
|
||
}
|
||
|
||
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
|
||
? Math.round((Number(spool.remaining_g) || 0) * 10) / 10
|
||
: 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("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();
|
||
renderSpools();
|
||
loadAkte(spool ? spool.id : null);
|
||
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 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,
|
||
spool.brand, ortLabel(spool),
|
||
].join(" ").toLowerCase();
|
||
return hay.includes(q);
|
||
});
|
||
}
|
||
|
||
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;
|
||
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;
|
||
}
|
||
|
||
let spoolPage = 1;
|
||
|
||
function renderSpools() {
|
||
document.querySelectorAll("#spool-filter .fchip").forEach((btn) => {
|
||
btn.classList.toggle("on", (btn.dataset.filter || "all") === spoolFilter);
|
||
});
|
||
fillSelects();
|
||
fillSpoolMetaFilters();
|
||
|
||
const body = document.getElementById("spool-rows");
|
||
body.innerHTML = "";
|
||
const shown = shownSpools();
|
||
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");
|
||
const pager = document.getElementById("spool-pager");
|
||
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>';
|
||
renderPager(pager, null, () => {});
|
||
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>';
|
||
renderPager(pager, null, () => {});
|
||
return;
|
||
}
|
||
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";
|
||
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");
|
||
});
|
||
const paged = pageSlice(ordered, spoolPage, DEFAULT_PAGE_SIZE);
|
||
spoolPage = paged.page;
|
||
let lastPlace = null;
|
||
paged.items.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" + (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;
|
||
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");
|
||
if (rest < 0) tr.children[5].title = "Überzogen — Rest korrigieren";
|
||
tr.addEventListener("click", () => fill(spool));
|
||
body.appendChild(tr);
|
||
});
|
||
renderPager(pager, paged, (p) => {
|
||
spoolPage = p;
|
||
renderSpools();
|
||
});
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
function renderBedarf(data) {
|
||
const card = document.getElementById("bedarf-card");
|
||
const shortfall = (data && data.shortfall) || [];
|
||
const needed = (data && data.needed) || [];
|
||
if (!needed.length && !shortfall.length) {
|
||
card.hidden = true;
|
||
return;
|
||
}
|
||
card.hidden = false;
|
||
document.getElementById("bedarf-short-count").textContent = shortfall.length
|
||
? shortfall.length + " Fehl"
|
||
: "ok";
|
||
document.getElementById("bedarf-hint").textContent = shortfall.length
|
||
? shortfall.length + " Sorten reichen nicht für die wartende Queue."
|
||
: "Queue hat Bedarf — Spulenrest reicht.";
|
||
const rows = (shortfall.length ? shortfall : needed).map((item) => {
|
||
const missing = item.shortfall_g != null ? grams(item.shortfall_g) : "—";
|
||
const rest = item.remaining_g != null
|
||
? grams(item.remaining_g)
|
||
: "—";
|
||
return `<tr class="${item.shortfall_g > 0 ? "warn" : ""}">
|
||
<td>${escapeHtml(item.material_type || "—")}</td>
|
||
<td>${escapeHtml(item.color || "—")}</td>
|
||
<td class="num">${grams(item.weight_g)}</td>
|
||
<td class="num">${rest}</td>
|
||
<td class="num">${missing}</td>
|
||
<td class="num">${item.jobs || 0}</td>
|
||
</tr>`;
|
||
}).join("");
|
||
document.getElementById("bedarf-rows").innerHTML = rows
|
||
|| '<tr><td class="empty" colspan="6">Kein Queue-Bedarf.</td></tr>';
|
||
}
|
||
|
||
async function load() {
|
||
const [spoolData, printerData, materialData, paletteData, bedarfData] = await Promise.all([
|
||
api("/spulen"),
|
||
api("/drucker?live=0"),
|
||
api("/materialien"),
|
||
api("/farben"),
|
||
api("/bedarf").catch(() => ({ needed: [], shortfall: [] })),
|
||
]);
|
||
spools = spoolData;
|
||
printers = printerData;
|
||
materials = materialData;
|
||
palette = paletteData;
|
||
render();
|
||
renderPalette();
|
||
renderBedarf(bedarfData);
|
||
}
|
||
|
||
window.onBedarfChanged = renderBedarf;
|
||
|
||
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) => {
|
||
spoolPage = 1;
|
||
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 || "";
|
||
spoolPage = 1;
|
||
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-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");
|
||
});
|
||
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("spool-filter-type")?.addEventListener("change", (event) => {
|
||
spoolPage = 1;
|
||
spoolTypeFilter = event.target.value || "";
|
||
renderSpools();
|
||
});
|
||
document.getElementById("spool-filter-color")?.addEventListener("change", (event) => {
|
||
spoolPage = 1;
|
||
spoolColorFilter = event.target.value || "";
|
||
renderSpools();
|
||
});
|
||
|
||
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) {
|
||
const found = await api("/spulen/by-code/" + encodeURIComponent(code));
|
||
if (found && found.kind === "lauf" && found.redirect) {
|
||
location.href = found.redirect;
|
||
return;
|
||
}
|
||
spool = found;
|
||
}
|
||
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 %}
|