Grotti 3.2.0: Jetzt-Liste, Filament-Pause, Strom-Torte, UI-Polish.
Jetzt/Topbar und Material-Routing, Pause bei Filament-Ende, Druckbericht mit Stromanteilen, Design- und Kamera-Stream-Polish; Bedarf-Alert-Spam und Platte-belegt-Bugs behoben.
This commit is contained in:
1 parent
6e59df22e5
commit
4ddffbbeb1
68 files changed
+4348
-1215
No files matched your search
@@ -1,195 +0,0 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>Projekte</h1>
|
||||
<p class="tiny">Gruppiere Dateien und Läufe.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="split">
|
||||
<section class="card">
|
||||
<h2>Liste</h2>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Status</th><th class="num">Dateien</th></tr></thead>
|
||||
<tbody id="project-rows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<section class="card">
|
||||
<h2 id="project-title">Neu</h2>
|
||||
<form id="project-form">
|
||||
<input id="project-id" type="hidden">
|
||||
<div class="field"><label for="project-name">Name</label><input id="project-name" required></div>
|
||||
<div class="field"><label for="project-notes">Notiz</label><textarea id="project-notes"></textarea></div>
|
||||
<div class="field"><label for="project-status">Status</label>
|
||||
<select id="project-status">
|
||||
<option value="idee">Idee</option>
|
||||
<option value="bereit">Bereit</option>
|
||||
<option value="druckt">Druckt</option>
|
||||
<option value="fertig">Fertig</option>
|
||||
</select></div>
|
||||
<div class="actions">
|
||||
<button type="submit">Merken</button>
|
||||
<button type="button" class="ghost" id="project-new">Neu</button>
|
||||
<button type="button" class="warn" id="project-delete">Löschen</button>
|
||||
</div>
|
||||
</form>
|
||||
<p id="project-note"></p>
|
||||
<h2 class="section-gap">Kalkulation</h2>
|
||||
<label>Spule</label>
|
||||
<select id="quote-spool"></select>
|
||||
<div class="grid-2">
|
||||
<div><label>Teilegewicht g</label><input id="quote-weight" type="number" min="0" step="1" value="0"></div>
|
||||
<div><label>Stück</label><input id="quote-qty" type="number" min="1" step="1" value="1"></div>
|
||||
<div><label>Ausschuss %</label><input id="quote-waste" type="number" min="0" step="1" value="0"></div>
|
||||
<div><label>Auslastung %</label><input id="quote-util" type="number" min="1" max="100" step="1" value="50"></div>
|
||||
<div><label>Preis €</label><input id="quote-price" type="number" min="0" step="0.01" value="0"></div>
|
||||
<div>
|
||||
<label>Preis gilt</label>
|
||||
<select id="quote-mode">
|
||||
<option value="perPart">pro Stück</option>
|
||||
<option value="perKg">pro kg Teile</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions"><button type="button" id="quote-go">Rechnen</button></div>
|
||||
<div class="nums" id="quote-result"></div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block script %}
|
||||
<script>
|
||||
const statusLabel = { idee: "Idee", bereit: "Bereit", druckt: "Druckt", fertig: "Fertig" };
|
||||
let projects = [];
|
||||
let spools = [];
|
||||
|
||||
function fill(project) {
|
||||
document.getElementById("project-id").value = project ? project.id : "";
|
||||
document.getElementById("project-name").value = project ? project.name : "";
|
||||
document.getElementById("project-notes").value = project ? project.notes : "";
|
||||
document.getElementById("project-status").value = project ? project.status : "idee";
|
||||
document.getElementById("project-title").textContent = project ? project.name : "Neu";
|
||||
document.getElementById("project-delete").hidden = !project;
|
||||
const quote = project && project.quote && project.quote.inputs;
|
||||
if (quote) {
|
||||
document.getElementById("quote-weight").value = quote.weight_g;
|
||||
document.getElementById("quote-qty").value = quote.qty;
|
||||
document.getElementById("quote-waste").value = quote.waste_percent;
|
||||
document.getElementById("quote-util").value = quote.utilization;
|
||||
document.getElementById("quote-price").value = quote.price;
|
||||
document.getElementById("quote-mode").value = quote.price_mode || "perPart";
|
||||
}
|
||||
if (project && project.result) paint(project.result);
|
||||
else if (project && project.quote && project.quote.result) paint(project.quote.result);
|
||||
else document.getElementById("quote-result").innerHTML = "";
|
||||
}
|
||||
|
||||
function paint(result) {
|
||||
document.getElementById("quote-result").innerHTML = `
|
||||
<div class="num"><span>bleibt gesamt</span><b>${euro(result.margin)}</b></div>
|
||||
<div class="num"><span>bleibt pro Stunde</span><b>${euro(result.per_hour)}</b></div>
|
||||
<div class="num"><span>Filament</span><b>${euro(result.filament_cost)}</b></div>
|
||||
<div class="num"><span>Strom</span><b>${euro(result.energy_cost)}</b></div>
|
||||
<div class="num"><span>Zeit</span><b>${hours(result.hours * 3600) || "0 min"}</b></div>
|
||||
<div class="num"><span>Filamentmenge</span><b>${grams(result.filament_g)}</b></div>`;
|
||||
}
|
||||
|
||||
function render() {
|
||||
const body = document.getElementById("project-rows");
|
||||
body.innerHTML = "";
|
||||
if (!projects.length) {
|
||||
body.innerHTML = '<tr><td class="empty" colspan="3"><div class="empty-state">' + icon("folder") + '<p>Noch kein Projekt.</p></div></td></tr>';
|
||||
}
|
||||
projects.forEach((project) => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.className = "pick";
|
||||
tr.innerHTML = "<td></td><td></td><td></td>";
|
||||
tr.children[0].textContent = project.name;
|
||||
tr.children[1].textContent = statusLabel[project.status] || project.status;
|
||||
tr.children[2].textContent = project.file_count || 0;
|
||||
tr.addEventListener("click", async () => {
|
||||
const full = await api("/projekte/" + project.id);
|
||||
fill(full);
|
||||
if (full.files && full.files[0] && full.files[0].weight_g && !full.quote.inputs) {
|
||||
document.getElementById("quote-weight").value = full.files[0].weight_g;
|
||||
}
|
||||
});
|
||||
body.appendChild(tr);
|
||||
});
|
||||
const select = document.getElementById("quote-spool");
|
||||
select.innerHTML = '<option value="">ohne Spule</option>';
|
||||
spools.forEach((spool) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = spool.id;
|
||||
option.textContent = spool.name;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
async function load() {
|
||||
[projects, spools] = await Promise.all([api("/projekte"), api("/spulen")]);
|
||||
const settings = await api("/einstellungen");
|
||||
document.getElementById("quote-waste").value = settings.energy.waste_percent;
|
||||
document.getElementById("quote-util").value = settings.energy.utilization;
|
||||
render();
|
||||
fill(null);
|
||||
}
|
||||
|
||||
document.getElementById("project-form").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const id = document.getElementById("project-id").value;
|
||||
const payload = {
|
||||
name: document.getElementById("project-name").value,
|
||||
notes: document.getElementById("project-notes").value,
|
||||
status: document.getElementById("project-status").value,
|
||||
};
|
||||
try {
|
||||
const saved = id
|
||||
? await api("/projekte/" + id, { method: "PATCH", json: payload })
|
||||
: await api("/projekte", { method: "POST", json: payload });
|
||||
showNote("project-note", null);
|
||||
document.getElementById("project-note").textContent = "Gemerkt.";
|
||||
await load();
|
||||
fill(saved);
|
||||
} catch (error) {
|
||||
showNote("project-note", error);
|
||||
}
|
||||
});
|
||||
document.getElementById("project-new").addEventListener("click", () => fill(null));
|
||||
document.getElementById("project-delete").addEventListener("click", (event) => {
|
||||
const id = document.getElementById("project-id").value;
|
||||
if (!id) return;
|
||||
armButton(event.currentTarget, "Dateien bleiben — löschen?", async () => {
|
||||
try {
|
||||
await api("/projekte/" + id, { method: "DELETE" });
|
||||
toast("Projekt gelöscht.");
|
||||
load();
|
||||
} catch (error) {
|
||||
showNote("project-note", error);
|
||||
}
|
||||
});
|
||||
});
|
||||
document.getElementById("quote-go").addEventListener("click", async () => {
|
||||
try {
|
||||
const payload = await api("/kalkulation", {
|
||||
method: "POST",
|
||||
json: {
|
||||
project_id: document.getElementById("project-id").value || null,
|
||||
spool_id: document.getElementById("quote-spool").value || null,
|
||||
weight_g: Number(document.getElementById("quote-weight").value),
|
||||
qty: Number(document.getElementById("quote-qty").value),
|
||||
waste_percent: Number(document.getElementById("quote-waste").value),
|
||||
utilization: Number(document.getElementById("quote-util").value),
|
||||
price: Number(document.getElementById("quote-price").value),
|
||||
price_mode: document.getElementById("quote-mode").value,
|
||||
},
|
||||
});
|
||||
paint(payload.result);
|
||||
} catch (error) {
|
||||
showNote("project-note", error);
|
||||
}
|
||||
});
|
||||
load().catch((error) => showNote("project-note", error));
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user