Grotti 2.5.1: Druckbericht, Statistik, Spulenakte und Protokoll-Fixes.
This commit is contained in:
1 parent
b277ca48aa
commit
efd0801cd1
30 files changed
+3393
-178
No files matched your search
@@ -0,0 +1,214 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>Statistik</h1>
|
||||
<p class="tiny">Auslastung, Verbrauch und Erfolgsquote aus dem Protokoll.</p>
|
||||
</div>
|
||||
<div class="page-head-side">
|
||||
<a class="ghost" id="csv-link" href="/api/v1/statistik.csv">CSV</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-tools">
|
||||
<div class="segmented period-presets" id="period" role="group" aria-label="Zeitraum">
|
||||
<button type="button" data-period="all" class="on">Alles</button>
|
||||
<button type="button" data-period="day">Tag</button>
|
||||
<button type="button" data-period="week">Woche</button>
|
||||
<button type="button" data-period="month">Monat</button>
|
||||
<button type="button" data-period="yesterday">Gestern</button>
|
||||
<button type="button" data-period="last_week">Letzte Woche</button>
|
||||
<button type="button" data-period="last_month">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">
|
||||
<button type="button" id="reload">Aktualisieren</button>
|
||||
</div>
|
||||
<p id="stat-note"></p>
|
||||
<div id="stat" class="statistik"></div>
|
||||
{% endblock %}
|
||||
{% block script %}
|
||||
<script>
|
||||
function kpi(label, value, hint) {
|
||||
return `<div class="kpi"><div class="kpi-val">${value}</div><div class="kpi-label">${escapeHtml(label)}</div>${hint ? '<div class="tiny">' + escapeHtml(hint) + "</div>" : ""}</div>`;
|
||||
}
|
||||
|
||||
function isoDate(date) {
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const d = String(date.getDate()).padStart(2, "0");
|
||||
return y + "-" + m + "-" + d;
|
||||
}
|
||||
|
||||
function startOfWeek(date) {
|
||||
const day = date.getDay();
|
||||
const mondayOffset = day === 0 ? -6 : 1 - day;
|
||||
const start = new Date(date.getFullYear(), date.getMonth(), date.getDate() + mondayOffset);
|
||||
return start;
|
||||
}
|
||||
|
||||
function periodRange(key) {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
if (key === "all") return { since: "", until: "" };
|
||||
if (key === "day") return { since: isoDate(today), until: isoDate(today) };
|
||||
if (key === "yesterday") {
|
||||
const day = new Date(today);
|
||||
day.setDate(day.getDate() - 1);
|
||||
return { since: isoDate(day), until: isoDate(day) };
|
||||
}
|
||||
if (key === "week") {
|
||||
const start = startOfWeek(today);
|
||||
const end = new Date(start);
|
||||
end.setDate(end.getDate() + 6);
|
||||
return { since: isoDate(start), until: isoDate(end) };
|
||||
}
|
||||
if (key === "last_week") {
|
||||
const start = startOfWeek(today);
|
||||
start.setDate(start.getDate() - 7);
|
||||
const end = new Date(start);
|
||||
end.setDate(end.getDate() + 6);
|
||||
return { since: isoDate(start), until: isoDate(end) };
|
||||
}
|
||||
if (key === "month") {
|
||||
const start = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
const end = new Date(today.getFullYear(), today.getMonth() + 1, 0);
|
||||
return { since: isoDate(start), until: isoDate(end) };
|
||||
}
|
||||
if (key === "last_month") {
|
||||
const start = new Date(today.getFullYear(), today.getMonth() - 1, 1);
|
||||
const end = new Date(today.getFullYear(), today.getMonth(), 0);
|
||||
return { since: isoDate(start), until: isoDate(end) };
|
||||
}
|
||||
return { since: "", until: "" };
|
||||
}
|
||||
|
||||
function setPeriodActive(key) {
|
||||
document.querySelectorAll("#period [data-period]").forEach((btn) => {
|
||||
btn.classList.toggle("on", btn.dataset.period === key);
|
||||
});
|
||||
}
|
||||
|
||||
function clearPeriodActive() {
|
||||
document.querySelectorAll("#period [data-period]").forEach((btn) => btn.classList.remove("on"));
|
||||
}
|
||||
|
||||
function fillPrinters(rows) {
|
||||
const select = document.getElementById("host");
|
||||
const current = select.value;
|
||||
const known = new Set([...select.options].map((o) => o.value));
|
||||
(rows || []).forEach((row) => {
|
||||
if (!row.printer_host || known.has(row.printer_host)) return;
|
||||
const opt = document.createElement("option");
|
||||
opt.value = row.printer_host;
|
||||
opt.textContent = row.printer_name || row.printer_host;
|
||||
select.appendChild(opt);
|
||||
known.add(row.printer_host);
|
||||
});
|
||||
if ([...select.options].some((o) => o.value === current)) select.value = current;
|
||||
}
|
||||
|
||||
function render(data) {
|
||||
fillPrinters(data.by_printer);
|
||||
const o = data.overview || {};
|
||||
const jobs = o.jobs || 0;
|
||||
const success = jobs ? Math.round(100 * (o.completed || 0) / jobs) : 0;
|
||||
const scrap = jobs ? Math.round(100 * (o.scrap || 0) / jobs) : 0;
|
||||
const ratio = data.estimate_ratio != null
|
||||
? (data.estimate_ratio * 100).toFixed(0) + " %"
|
||||
: "—";
|
||||
|
||||
const printers = (data.by_printer || []).map((p) => `
|
||||
<tr>
|
||||
<td>${escapeHtml(p.printer_name || p.printer_host)}</td>
|
||||
<td class="num">${p.jobs}</td>
|
||||
<td class="num">${hours(p.duration_s)}</td>
|
||||
<td class="num">${grams(p.weight_g)}</td>
|
||||
<td class="num">${euro(p.cost)}</td>
|
||||
<td class="num">${p.completed}</td>
|
||||
</tr>`).join("") || '<tr><td class="empty" colspan="6">Keine Daten.</td></tr>';
|
||||
|
||||
const materials = (data.by_material || []).map((m) => `
|
||||
<tr>
|
||||
<td>${escapeHtml(m.material_type || "—")}</td>
|
||||
<td>${escapeHtml(m.brand || "")}</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>';
|
||||
|
||||
document.getElementById("stat").innerHTML = `
|
||||
<div class="kpi-row">
|
||||
${kpi("Läufe", String(jobs))}
|
||||
${kpi("Druckzeit", hours(o.duration_s))}
|
||||
${kpi("Verbrauch", grams(o.weight_g))}
|
||||
${kpi("Kosten", euro(o.cost))}
|
||||
${kpi("Erfolg", success + " %", (o.completed || 0) + " fertig · " + (o.failed || 0) + " abgebrochen")}
|
||||
${kpi("Ausschuss", scrap + " %", (o.scrap || 0) + " mit Urteil Ausschuss")}
|
||||
${kpi("Schätztreue", ratio, data.estimate_count ? data.estimate_count + " Läufe mit Soll-Zeit" : "keine Soll-Zeit")}
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Nach Drucker</h2>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>Drucker</th><th>Läufe</th><th>Zeit</th><th>Gramm</th><th>Kosten</th><th>Fertig</th></tr></thead>
|
||||
<tbody>${printers}</tbody>
|
||||
</table></div>
|
||||
</div>
|
||||
<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>
|
||||
<tbody>${materials}</tbody>
|
||||
</table></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const params = new URLSearchParams();
|
||||
const since = document.getElementById("since").value;
|
||||
const until = document.getElementById("until").value;
|
||||
const host = document.getElementById("host").value;
|
||||
const material = document.getElementById("material").value.trim();
|
||||
const brand = document.getElementById("brand").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);
|
||||
const q = params.toString();
|
||||
document.getElementById("csv-link").href = "/api/v1/statistik.csv" + (q ? "?" + q : "");
|
||||
const data = await api("/statistik" + (q ? "?" + q : ""));
|
||||
render(data);
|
||||
}
|
||||
|
||||
function applyPeriod(key) {
|
||||
const range = periodRange(key);
|
||||
document.getElementById("since").value = range.since;
|
||||
document.getElementById("until").value = range.until;
|
||||
setPeriodActive(key);
|
||||
load().catch((error) => showNote("stat-note", error));
|
||||
}
|
||||
|
||||
document.getElementById("period").addEventListener("click", (event) => {
|
||||
const button = event.target.closest("[data-period]");
|
||||
if (!button) return;
|
||||
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));
|
||||
});
|
||||
});
|
||||
document.getElementById("host").addEventListener("change", () => {
|
||||
load().catch((error) => showNote("stat-note", error));
|
||||
});
|
||||
load().catch((error) => showNote("stat-note", error));
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user