Files
Dominic Gundel 6e59df22e5 Grotti 3.1.0: Bedarf-UI, Zeitfenster, HT/NT, Theme, Chrome-Fixes.
Bedarf in Sidebar/Material, Drucker-Zeitfenster und Tarif in Einstellungen, helles Theme; Legacy-Token und /api/drucker entfernt; SSE/SW-Fixes gegen hängende Navigation in Chrome.
2026-10-18 02:20:12 +02:00

141 lines
5.9 KiB
HTML

{% extends "layout.html" %}
{% block body %}
<div class="page-head">
<div>
<h1>Wartung</h1>
<p class="tiny">Laufstunden und Filament je Drucker — Verschleißteile im Blick.</p>
</div>
<div class="page-head-side">
<button type="button" class="ghost" id="wartung-refresh"><svg class="ico"><use href="#i-refresh"/></svg>Neu laden</button>
</div>
</div>
<div id="wartung-summary" class="kpi-row" hidden></div>
<div id="wartung-root"></div>
<p id="wartung-note"></p>
{% endblock %}
{% block script %}
<script>
function partRow(part) {
const overdue = part.overdue_hours || part.overdue_meters;
return `<tr class="${overdue ? "warn" : ""}">
<td>${escapeHtml(part.label || part.part_key)}</td>
<td class="num">${(part.hours_since || 0).toLocaleString("de-DE", { maximumFractionDigits: 1 })} h</td>
<td class="num">${Math.round(part.meters_since || 0).toLocaleString("de-DE")} mm</td>
<td class="num">${part.life_hours ? part.life_hours + " h" : "—"}</td>
<td class="num">${part.life_meters ? Math.round(part.life_meters) + " mm" : "—"}</td>
<td><button type="button" class="ghost tiny-btn" data-reset="${part.id}">Zurücksetzen</button></td>
</tr>`;
}
function printerCard(block) {
const c = block.counters || {};
const overdue = (block.parts || []).filter((p) => p.overdue_hours || p.overdue_meters).length;
const parts = (block.parts || []).map(partRow).join("")
|| '<tr><td colspan="6" class="empty">Noch keine Teile — unten anlegen.</td></tr>';
return `<section class="card wartung-card">
<div class="lib-head">
<h2>${escapeHtml(block.name || block.host)}</h2>
${overdue ? chipHtml("error", overdue + " fällig") : chipHtml("idle", "ok")}
</div>
<p class="hint">${escapeHtml(block.host)} · ${(c.hours || 0).toLocaleString("de-DE", { maximumFractionDigits: 1 })} h gesamt · ${Math.round(c.meters || 0).toLocaleString("de-DE")} mm · ${c.runs || 0} Läufe</p>
<div class="table-wrap"><table>
<thead><tr><th>Teil</th><th>Seit Reset</th><th>Filament</th><th>Intervall h</th><th>Intervall mm</th><th></th></tr></thead>
<tbody>${parts}</tbody>
</table></div>
<details class="mat-more section-gap">
<summary><svg class="ico caret"><use href="#i-down"/></svg><span class="grow">Teil anlegen</span></summary>
<form class="grid-2 wartung-add" data-host="${escapeHtml(block.host)}">
<div class="field"><label>Schlüssel</label><input name="part_key" required placeholder="nozzle"></div>
<div class="field"><label>Bezeichnung</label><input name="label" placeholder="Düse 0.4"></div>
<div class="field"><label>Intervall h</label><input name="life_hours" type="number" step="1" min="0" value="0"></div>
<div class="field"><label>Intervall mm</label><input name="life_meters" type="number" step="1000" min="0" value="0"></div>
<div class="field" style="grid-column:1/-1"><label>Notiz</label><input name="note"></div>
<div class="actions"><button type="submit">Teil anlegen</button></div>
</form>
</details>
</section>`;
}
function renderSummary(data) {
const box = document.getElementById("wartung-summary");
if (!data.length) {
box.hidden = true;
box.innerHTML = "";
return;
}
let hours = 0;
let meters = 0;
let overdue = 0;
data.forEach((block) => {
hours += Number((block.counters || {}).hours) || 0;
meters += Number((block.counters || {}).meters) || 0;
overdue += (block.parts || []).filter((p) => p.overdue_hours || p.overdue_meters).length;
});
box.hidden = false;
box.innerHTML = `
<div class="kpi"><div class="kpi-val">${data.length}</div><div class="kpi-label">Drucker</div></div>
<div class="kpi"><div class="kpi-val">${hours.toLocaleString("de-DE", { maximumFractionDigits: 0 })} h</div><div class="kpi-label">Laufzeit</div></div>
<div class="kpi"><div class="kpi-val">${Math.round(meters / 1000).toLocaleString("de-DE")} m</div><div class="kpi-label">Filament</div></div>
<div class="kpi"><div class="kpi-val">${overdue}</div><div class="kpi-label">Fällig</div></div>`;
}
async function load() {
const data = await api("/wartung");
renderSummary(data || []);
document.getElementById("wartung-root").innerHTML = (data || []).map(printerCard).join("")
|| `<div class="empty-state">
<svg class="ico"><use href="#i-nozzle"/></svg>
<p>Keine Drucker angelegt.</p>
<a class="ghost" href="/einstellungen">Einstellungen →</a>
</div>`;
}
document.getElementById("wartung-refresh")?.addEventListener("click", () => {
load().catch((error) => showNote("wartung-note", error));
});
document.getElementById("wartung-root").addEventListener("submit", async (event) => {
const form = event.target.closest("form.wartung-add");
if (!form) return;
event.preventDefault();
const fd = new FormData(form);
try {
await api("/wartung/teile", {
method: "POST",
json: {
printer_host: form.dataset.host,
part_key: fd.get("part_key"),
label: fd.get("label"),
life_hours: Number(fd.get("life_hours") || 0),
life_meters: Number(fd.get("life_meters") || 0),
note: fd.get("note"),
},
});
form.reset();
showNote("wartung-note", null, "Teil gespeichert.");
toast("Wartungsteil angelegt.");
load();
} catch (error) {
showNote("wartung-note", error);
}
});
document.getElementById("wartung-root").addEventListener("click", (event) => {
const btn = event.target.closest("[data-reset]");
if (!btn) return;
armButton(btn, "Wirklich zurücksetzen?", async () => {
try {
await api("/wartung/teile/" + btn.dataset.reset + "/reset", { method: "POST", json: {} });
showNote("wartung-note", null, "Teil zurückgesetzt.");
toast("Zähler zurückgesetzt.");
load();
} catch (error) {
showNote("wartung-note", error);
}
});
});
load().catch((error) => showNote("wartung-note", error));
</script>
{% endblock %}