Grotti 3.3.0: Projekte mit Ordner, Datei-Löschen/Verschieben, Material-Archiv, UI-Polish.
This commit is contained in:
1 parent
4ddffbbeb1
commit
67a89d1794
45 files changed
+2856
-320
No files matched your search
@@ -9,7 +9,7 @@
|
||||
<a class="ghost" id="csv-link" href="/api/v1/statistik.csv">CSV</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar page-tools">
|
||||
<div class="toolbar page-tools stat-tools">
|
||||
<div class="segmented period-presets fchips" id="period" role="group" aria-label="Zeitraum">
|
||||
<button type="button" data-period="all" class="fchip on">Alles</button>
|
||||
<button type="button" data-period="day" class="fchip">Tag</button>
|
||||
@@ -19,20 +19,24 @@
|
||||
<button type="button" data-period="last_week" class="fchip">Letzte Woche</button>
|
||||
<button type="button" data-period="last_month" class="fchip">Letzter Monat</button>
|
||||
</div>
|
||||
<input type="date" id="since" title="Von" class="date-field">
|
||||
<input type="date" id="until" title="Bis" class="date-field">
|
||||
<select id="host"><option value="">Alle Drucker</option></select>
|
||||
<input type="text" id="material" placeholder="Sorte" size="10">
|
||||
<input type="text" id="brand" placeholder="Marke" size="10">
|
||||
<div class="tools-end"><button type="button" id="reload" class="ghost"><svg class="ico"><use href="#i-refresh"/></svg>Aktualisieren</button></div>
|
||||
<div class="stat-filters">
|
||||
<input type="date" id="since" title="Von" class="date-field">
|
||||
<input type="date" id="until" title="Bis" class="date-field">
|
||||
<select id="host"><option value="">Alle Drucker</option></select>
|
||||
<input type="text" id="material" placeholder="Sorte" size="8" autocomplete="off">
|
||||
<input type="text" id="brand" placeholder="Marke" size="8" autocomplete="off">
|
||||
<input type="text" id="color" placeholder="Farbe" size="8" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<p id="stat-note"></p>
|
||||
<div id="stat" class="statistik"></div>
|
||||
{% endblock %}
|
||||
{% block script %}
|
||||
<script>
|
||||
function kpi(label, value, hint, ico) {
|
||||
return `<div class="kpi"><div class="kpi-label">${ico ? icon(ico) : ""}${escapeHtml(label)}</div><div class="kpi-value">${value}</div>${hint ? '<div class="kpi-sub">' + escapeHtml(hint) + "</div>" : ""}</div>`;
|
||||
function kpi(label, value, hint, ico, extraClass) {
|
||||
const empty = !value || value === "—" || value === "0 %";
|
||||
const cls = ["kpi", extraClass || "", empty ? "kpi-empty" : ""].filter(Boolean).join(" ");
|
||||
return `<div class="${cls}"><div class="kpi-label">${ico ? icon(ico) : ""}${escapeHtml(label)}</div><div class="kpi-value">${value}</div>${hint ? '<div class="kpi-sub">' + escapeHtml(hint) + "</div>" : ""}</div>`;
|
||||
}
|
||||
|
||||
function isoDate(date) {
|
||||
@@ -165,10 +169,11 @@ function render(data) {
|
||||
<tr>
|
||||
<td>${escapeHtml(m.material_type || "—")}</td>
|
||||
<td>${escapeHtml(m.brand || "")}</td>
|
||||
<td>${escapeHtml(m.color || "")}</td>
|
||||
<td class="num">${grams(m.weight_g)}</td>
|
||||
<td class="num">${euro(m.cost)}</td>
|
||||
<td class="num">${m.jobs}</td>
|
||||
</tr>`).join("") || '<tr><td class="empty" colspan="5">Kein Spulenabzug im Zeitraum.</td></tr>';
|
||||
</tr>`).join("") || '<tr><td class="empty" colspan="6">Kein Spulenabzug im Zeitraum.</td></tr>';
|
||||
|
||||
const chartPrinters = (data.by_printer || []).map((p) => ({
|
||||
...p,
|
||||
@@ -180,16 +185,18 @@ function render(data) {
|
||||
}));
|
||||
|
||||
document.getElementById("stat").innerHTML = `
|
||||
<div class="kpi-row">
|
||||
<div class="kpi-row kpi-primary">
|
||||
${kpi("Läufe", String(jobs), "", "layers")}
|
||||
${kpi("Druckzeit", hours(o.duration_s), "", "clock")}
|
||||
${kpi("Verbrauch", grams(o.weight_g), "", "weight")}
|
||||
${kpi("Kosten", euro(o.cost), "", "euro")}
|
||||
${kpi("Erfolg", success + " %", (o.completed || 0) + " fertig · " + (o.failed || 0) + " abgebrochen", "circle-check")}
|
||||
${kpi("Ausschuss", scrap + " %", (o.scrap || 0) + " mit Urteil Ausschuss", "ban")}
|
||||
${kpi("Stück-Yield", o.yield_percent != null ? Math.round(o.yield_percent) + " %" : "—", (o.pieces_ok || 0) + " gut · " + (o.pieces_bad || 0) + " schlecht", "percent")}
|
||||
${kpi("€ / Teil", o.cost_per_part != null ? euro(o.cost_per_part) : "—", "Material + Strom ÷ gut", "tag")}
|
||||
${kpi("Schätztreue", ratio, data.estimate_count ? data.estimate_count + " Läufe mit Soll-Zeit" : "keine Soll-Zeit", "gauge")}
|
||||
</div>
|
||||
<div class="kpi-row kpi-secondary">
|
||||
${kpi("Ausschuss", scrap + " %", (o.scrap || 0) + " mit Urteil Ausschuss", "ban", "kpi-sm")}
|
||||
${kpi("Stück-Yield", o.yield_percent != null ? Math.round(o.yield_percent) + " %" : "—", (o.pieces_ok || 0) + " gut · " + (o.pieces_bad || 0) + " schlecht", "percent", "kpi-sm")}
|
||||
${kpi("€ / Teil", o.cost_per_part != null ? euro(o.cost_per_part) : "—", "Material + Strom ÷ gut", "tag", "kpi-sm")}
|
||||
${kpi("Schätztreue", ratio, data.estimate_count ? data.estimate_count + " Läufe mit Soll-Zeit" : "keine Soll-Zeit", "gauge", "kpi-sm")}
|
||||
</div>
|
||||
<div class="split">
|
||||
<div class="card">
|
||||
@@ -198,7 +205,7 @@ function render(data) {
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Verbrauch je Material</h2>
|
||||
${barChart(chartMaterials, "weight_g", (m) => [m.material_type, m.brand].filter(Boolean).join(" · ") || "—")}
|
||||
${barChart(chartMaterials, "weight_g", (m) => [m.material_type, m.brand, m.color].filter(Boolean).join(" · ") || "—")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
@@ -218,7 +225,7 @@ function render(data) {
|
||||
<div class="card">
|
||||
<h2>Nach Material</h2>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Sorte</th><th>Marke</th><th>Gramm</th><th>Kosten</th><th>Läufe</th></tr></thead>
|
||||
<thead><tr><th>Sorte</th><th>Marke</th><th>Farbe</th><th>Gramm</th><th>Kosten</th><th>Läufe</th></tr></thead>
|
||||
<tbody>${materials}</tbody>
|
||||
</table></div>
|
||||
</div>`;
|
||||
@@ -231,17 +238,25 @@ async function load() {
|
||||
const host = document.getElementById("host").value;
|
||||
const material = document.getElementById("material").value.trim();
|
||||
const brand = document.getElementById("brand").value.trim();
|
||||
const color = document.getElementById("color").value.trim();
|
||||
if (since) params.set("since", since);
|
||||
if (until) params.set("until", until);
|
||||
if (host) params.set("host", host);
|
||||
if (material) params.set("material", material);
|
||||
if (brand) params.set("brand", brand);
|
||||
if (color) params.set("color", color);
|
||||
const q = params.toString();
|
||||
document.getElementById("csv-link").href = "/api/v1/statistik.csv" + (q ? "?" + q : "");
|
||||
const data = await api("/statistik" + (q ? "?" + q : ""));
|
||||
render(data);
|
||||
}
|
||||
|
||||
let filterTimer = null;
|
||||
function scheduleLoad() {
|
||||
clearTimeout(filterTimer);
|
||||
filterTimer = setTimeout(() => load().catch((error) => showNote("stat-note", error)), 280);
|
||||
}
|
||||
|
||||
function applyPeriod(key) {
|
||||
const range = periodRange(key);
|
||||
document.getElementById("since").value = range.since;
|
||||
@@ -256,28 +271,18 @@ document.getElementById("period").addEventListener("click", (event) => {
|
||||
applyPeriod(button.dataset.period);
|
||||
});
|
||||
|
||||
document.getElementById("reload").addEventListener("click", () => {
|
||||
load().catch((error) => showNote("stat-note", error));
|
||||
});
|
||||
["since", "until"].forEach((id) => {
|
||||
document.getElementById(id).addEventListener("change", () => {
|
||||
clearPeriodActive();
|
||||
load().catch((error) => showNote("stat-note", error));
|
||||
scheduleLoad();
|
||||
});
|
||||
});
|
||||
document.getElementById("host").addEventListener("change", () => {
|
||||
load().catch((error) => showNote("stat-note", error));
|
||||
});
|
||||
["material", "brand"].forEach((id) => {
|
||||
document.getElementById("host").addEventListener("change", scheduleLoad);
|
||||
["material", "brand", "color"].forEach((id) => {
|
||||
const node = document.getElementById(id);
|
||||
if (!node) return;
|
||||
node.addEventListener("change", () => load().catch((error) => showNote("stat-note", error)));
|
||||
node.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
load().catch((error) => showNote("stat-note", error));
|
||||
}
|
||||
});
|
||||
node.addEventListener("input", scheduleLoad);
|
||||
node.addEventListener("change", scheduleLoad);
|
||||
});
|
||||
load().catch((error) => showNote("stat-note", error));
|
||||
</script>
|
||||
|
||||
Reference in new issue
Block a user