Grotti 2.0.0: eigenes Repo, Umbenennung von Farm.

This commit is contained in:
Dominic Gundel committed 2026-10-15 08:45:56 +02:00
commit b2e6443c68
69 files changed
+18719

No files matched your search

+195
View File
@@ -0,0 +1,195 @@
{% 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 %}