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.
This commit is contained in:
1 parent
8e88efc094
commit
6e59df22e5
23 files changed
+741
-171
No files matched your search
+70
-23
@@ -5,7 +5,11 @@
|
||||
<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 %}
|
||||
@@ -19,38 +23,77 @@ function partRow(part) {
|
||||
<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 sm" data-reset="${part.id}">Zurücksetzen</button></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.</td></tr>';
|
||||
return `<section class="card">
|
||||
<h2>${escapeHtml(block.name || block.host)}</h2>
|
||||
|| '<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>Seit Reset</th><th>Intervall h</th><th>Intervall mm</th><th></th></tr></thead>
|
||||
<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>
|
||||
<form class="grid-2 wartung-add" data-host="${escapeHtml(block.host)}" style="margin-top:0.75rem">
|
||||
<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 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("")
|
||||
|| '<p class="empty">Keine Drucker angelegt.</p>';
|
||||
|| `<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;
|
||||
@@ -70,24 +113,28 @@ document.getElementById("wartung-root").addEventListener("submit", async (event)
|
||||
});
|
||||
form.reset();
|
||||
showNote("wartung-note", null, "Teil gespeichert.");
|
||||
toast("Wartungsteil angelegt.");
|
||||
load();
|
||||
} catch (error) {
|
||||
showNote("wartung-note", error);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("wartung-root").addEventListener("click", async (event) => {
|
||||
document.getElementById("wartung-root").addEventListener("click", (event) => {
|
||||
const btn = event.target.closest("[data-reset]");
|
||||
if (!btn) return;
|
||||
try {
|
||||
await api("/wartung/teile/" + btn.dataset.reset + "/reset", { method: "POST", json: {} });
|
||||
showNote("wartung-note", null, "Teil zurückgesetzt.");
|
||||
load();
|
||||
} catch (error) {
|
||||
showNote("wartung-note", error);
|
||||
}
|
||||
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();
|
||||
load().catch((error) => showNote("wartung-note", error));
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user