Files
grotti/web/templates/auftraege.html
T
Dominic Gundel 6e59df22e5 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.
2026-10-18 02:20:12 +02:00

327 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "layout.html" %}
{% block body %}
<div class="page-head">
<div>
<h1>Aufträge</h1>
<p class="tiny">Kunden, Angebote, Positionen und Fortschritt.</p>
</div>
<div class="actions">
<a class="ghost" id="lieferschein-link" href="#" hidden target="_blank" rel="noopener">Lieferschein</a>
</div>
</div>
<div class="split">
<section class="card">
<div class="field inline">
<label for="filter-status">Status</label>
<select id="filter-status">
<option value="">alle</option>
<option value="idee">Idee</option>
<option value="angebot">Angebot</option>
<option value="bestaetigt">bestätigt</option>
<option value="druckt">druckt</option>
<option value="fertig">fertig</option>
<option value="geliefert">geliefert</option>
<option value="bezahlt">bezahlt</option>
</select>
</div>
<div class="table-wrap">
<table>
<thead><tr><th>Titel</th><th>Status</th><th>Fortschritt</th></tr></thead>
<tbody id="order-rows"></tbody>
</table>
</div>
</section>
<section class="card">
<h2 id="order-title">Neuer Auftrag</h2>
<form id="order-form">
<input id="order-id" type="hidden">
<div class="field"><label for="order-title-in">Titel</label><input id="order-title-in" required></div>
<div class="grid-2">
<div class="field"><label for="order-status">Status</label>
<select id="order-status">
<option value="idee">Idee</option>
<option value="angebot">Angebot</option>
<option value="bestaetigt">bestätigt</option>
<option value="druckt">druckt</option>
<option value="fertig">fertig</option>
<option value="geliefert">geliefert</option>
<option value="bezahlt">bezahlt</option>
</select>
</div>
<div class="field"><label for="order-due">Fällig</label><input id="order-due" type="date"></div>
</div>
<div class="field"><label for="order-customer">Kunde</label>
<select id="order-customer"><option value="">— keiner —</option></select>
</div>
<div class="field"><label for="order-notes">Notiz</label><textarea id="order-notes"></textarea></div>
<div class="actions">
<button type="submit">Merken</button>
<button type="button" class="ghost" id="order-new">Neu</button>
<button type="button" class="warn" id="order-delete" hidden>Löschen</button>
</div>
</form>
<p id="order-note" class="tiny"></p>
<p class="tiny" id="order-progress"></p>
<p class="tiny" id="order-actuals"></p>
<h2 class="section-gap">Positionen</h2>
<div class="table-wrap">
<table>
<thead><tr><th>Datei</th><th class="num">Stk</th><th class="num">fertig</th><th></th></tr></thead>
<tbody id="item-rows"></tbody>
</table>
</div>
<form id="item-form" class="section-gap">
<div class="grid-2">
<div class="field"><label for="item-file">Datei</label><select id="item-file"><option value="">—</option></select></div>
<div class="field"><label for="item-qty">Stück</label><input id="item-qty" type="number" min="1" step="1" value="1"></div>
<div class="field"><label for="item-price">Preis/Stk €</label><input id="item-price" type="number" min="0" step="0.01" value="0"></div>
<div class="field"><label for="item-material">Material</label><input id="item-material"></div>
</div>
<div class="actions"><button type="submit">Position hinzufügen</button></div>
</form>
<details class="section-gap">
<summary>Kunde anlegen</summary>
<form id="customer-form">
<div class="field"><label for="cust-name">Name</label><input id="cust-name" required></div>
<div class="field"><label for="cust-email">E-Mail</label><input id="cust-email" type="email"></div>
<button type="submit">Kunde speichern</button>
</form>
</details>
</section>
</div>
{% endblock %}
{% block script %}
<script>
const STATUS_LABEL = {
idee: "Idee", angebot: "Angebot", bestaetigt: "bestätigt", druckt: "druckt",
fertig: "fertig", geliefert: "geliefert", bezahlt: "bezahlt",
};
let orders = [];
let customers = [];
let files = [];
let current = null;
function fillCustomers() {
const sel = document.getElementById("order-customer");
const val = sel.value;
sel.innerHTML = '<option value="">— keiner —</option>';
customers.forEach((c) => {
const o = document.createElement("option");
o.value = c.id;
o.textContent = c.name;
sel.appendChild(o);
});
if (val) sel.value = val;
}
function fillFiles() {
const sel = document.getElementById("item-file");
sel.innerHTML = '<option value="">—</option>';
files.slice(0, 400).forEach((f) => {
const o = document.createElement("option");
o.value = f.id;
o.textContent = f.name;
sel.appendChild(o);
});
}
function paintActuals(order) {
const el = document.getElementById("order-actuals");
const a = order.actuals;
if (!a) { el.textContent = ""; return; }
el.textContent = "Ist: " + euro(a.cost_total) + " · " + (hours(a.duration_s) || "0 min") + " · Ausschuss " + a.pieces_bad + " Stk";
}
function fillOrder(order) {
current = order;
document.getElementById("order-id").value = order ? order.id : "";
document.getElementById("order-title-in").value = order ? order.title : "";
document.getElementById("order-status").value = order ? order.status : "idee";
document.getElementById("order-due").value = order && order.due_at ? order.due_at.slice(0, 10) : "";
document.getElementById("order-customer").value = order && order.customer_id ? order.customer_id : "";
document.getElementById("order-notes").value = order ? order.notes : "";
document.getElementById("order-title").textContent = order ? order.title : "Neuer Auftrag";
document.getElementById("order-delete").hidden = !order;
const link = document.getElementById("lieferschein-link");
if (order) {
link.href = "/auftraege/" + order.id + "/lieferschein";
link.hidden = false;
} else {
link.hidden = true;
}
const prog = order && order.progress;
document.getElementById("order-progress").textContent = prog ? "Fortschritt: " + prog.label : "";
paintActuals(order || {});
renderItems(order && order.items ? order.items : []);
}
function renderItems(items) {
const body = document.getElementById("item-rows");
body.innerHTML = "";
if (!items.length) {
body.innerHTML = '<tr><td colspan="4" class="empty">Keine Positionen.</td></tr>';
return;
}
items.forEach((item) => {
const tr = document.createElement("tr");
tr.innerHTML = "<td></td><td class=\"num\"></td><td class=\"num\"></td><td class=\"actions\"></td>";
tr.children[0].textContent = item.file_name || ("#" + (item.file_id || "—"));
tr.children[1].textContent = item.qty;
tr.children[2].textContent = item.done_ok + (item.done_bad ? " (−" + item.done_bad + ")" : "");
const btn = document.createElement("button");
btn.type = "button";
btn.className = "ghost tiny-btn";
btn.textContent = "Einreihen";
btn.disabled = !item.file_id || !current;
btn.addEventListener("click", () => einreihen(item.id));
tr.children[3].appendChild(btn);
body.appendChild(tr);
});
}
async function einreihen(itemId) {
if (!current) return;
try {
await api("/auftraege/" + current.id + "/einreihen", {
method: "POST",
json: { item_id: itemId, when_free: true, confirm: true },
});
toast("In Warteschlange.");
const full = await api("/auftraege/" + current.id);
fillOrder(full);
await loadList();
} catch (error) {
showNote("order-note", error);
}
}
function renderList() {
const body = document.getElementById("order-rows");
body.innerHTML = "";
if (!orders.length) {
body.innerHTML = '<tr><td colspan="3" class="empty">Noch kein Auftrag.</td></tr>';
return;
}
const selected = document.getElementById("order-id").value;
orders.forEach((order) => {
const tr = document.createElement("tr");
tr.className = "pick" + (selected && String(order.id) === selected ? " on" : "");
tr.innerHTML = "<td></td><td></td><td></td>";
tr.children[0].textContent = order.title;
tr.children[1].textContent = STATUS_LABEL[order.status] || order.status;
tr.children[2].textContent = (order.progress && order.progress.label) || "";
tr.addEventListener("click", async () => {
try {
const full = await api("/auftraege/" + order.id);
fillOrder(full);
renderList();
} catch (error) {
showNote("order-note", error);
}
});
body.appendChild(tr);
});
}
async function loadList() {
const st = document.getElementById("filter-status").value;
const q = st ? "?status=" + encodeURIComponent(st) : "";
orders = await api("/auftraege" + q);
renderList();
}
async function loadBase() {
[customers, files] = await Promise.all([
api("/kunden").catch(() => []),
api("/dateien").catch(() => []),
]);
fillCustomers();
fillFiles();
await loadList();
fillOrder(null);
}
document.getElementById("filter-status").addEventListener("change", () => loadList().catch((e) => showNote("order-note", e)));
document.getElementById("order-form").addEventListener("submit", async (event) => {
event.preventDefault();
const id = document.getElementById("order-id").value;
const payload = {
title: document.getElementById("order-title-in").value,
status: document.getElementById("order-status").value,
notes: document.getElementById("order-notes").value,
due_at: document.getElementById("order-due").value || null,
customer_id: document.getElementById("order-customer").value || null,
};
try {
const saved = id
? await api("/auftraege/" + id, { method: "PATCH", json: payload })
: await api("/auftraege", { method: "POST", json: payload });
showNote("order-note", null);
document.getElementById("order-note").textContent = "Gemerkt.";
await loadList();
fillOrder(saved);
} catch (error) {
showNote("order-note", error);
}
});
document.getElementById("order-new").addEventListener("click", () => fillOrder(null));
document.getElementById("order-delete").addEventListener("click", (event) => {
const id = document.getElementById("order-id").value;
if (!id) return;
armButton(event.currentTarget, "Auftrag wirklich löschen?", async () => {
await api("/auftraege/" + id, { method: "DELETE" });
toast("Gelöscht.");
fillOrder(null);
loadList();
});
});
document.getElementById("item-form").addEventListener("submit", async (event) => {
event.preventDefault();
if (!current) {
showNote("order-note", new Error("Zuerst Auftrag speichern."));
return;
}
const payload = {
file_id: document.getElementById("item-file").value || null,
qty: Number(document.getElementById("item-qty").value),
unit_price: Number(document.getElementById("item-price").value),
material_type: document.getElementById("item-material").value,
};
try {
await api("/auftraege/" + current.id + "/positionen", { method: "POST", json: payload });
const full = await api("/auftraege/" + current.id);
fillOrder(full);
} catch (error) {
showNote("order-note", error);
}
});
document.getElementById("customer-form").addEventListener("submit", async (event) => {
event.preventDefault();
try {
const row = await api("/kunden", {
method: "POST",
json: {
name: document.getElementById("cust-name").value,
email: document.getElementById("cust-email").value,
},
});
customers.push(row);
fillCustomers();
document.getElementById("order-customer").value = row.id;
document.getElementById("cust-name").value = "";
document.getElementById("cust-email").value = "";
toast("Kunde angelegt.");
} catch (error) {
showNote("order-note", error);
}
});
loadBase().catch((error) => showNote("order-note", error));
</script>
{% endblock %}