612 lines
23 KiB
HTML
612 lines
23 KiB
HTML
{% extends "layout.html" %}
|
||
{% block body %}
|
||
<div class="page-head">
|
||
<div>
|
||
<h1>Projekte</h1>
|
||
<p class="tiny">Arbeitsmappen mit Ordner unter dem Slice-Pfad — G-code und Konstruktionsdaten.</p>
|
||
</div>
|
||
<div class="actions">
|
||
<div class="seg" id="proj-filter" role="group" aria-label="Filter">
|
||
<button type="button" class="on" data-filter="aktiv">Aktiv</button>
|
||
<button type="button" data-filter="fertig">Fertig</button>
|
||
<button type="button" data-filter="">Alle</button>
|
||
</div>
|
||
<button type="button" class="btn-primary" id="proj-new"><svg class="ico"><use href="#i-plus"/></svg>Neues Projekt</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="split edit-split is-idle" id="proj-layout">
|
||
<section class="card">
|
||
<div class="card-head lib-head">
|
||
<svg class="ico head-ico"><use href="#i-folder"/></svg>
|
||
<h2>Liste</h2>
|
||
<span class="count-pill" id="proj-count">0</span>
|
||
</div>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead><tr><th>Name</th><th>Status</th><th>Ordner</th><th class="num">Dateien</th><th class="num">Kosten</th></tr></thead>
|
||
<tbody id="proj-rows"></tbody>
|
||
</table>
|
||
<div class="pager" id="proj-pager" hidden></div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="card edit-panel" id="proj-panel" hidden>
|
||
<div class="mat-form-head">
|
||
<h2 id="proj-title">Neues Projekt</h2>
|
||
<button type="button" class="ghost tiny-btn" id="proj-edit-btn" hidden>Bearbeiten</button>
|
||
<button type="button" class="icon-btn" id="proj-close" title="Schließen" aria-label="Schließen"></button>
|
||
</div>
|
||
|
||
<form id="proj-form" novalidate hidden>
|
||
<input id="proj-id" type="hidden">
|
||
<div class="field"><label for="proj-name">Name</label><input id="proj-name" required autocomplete="off"></div>
|
||
<div class="grid-2">
|
||
<div class="field">
|
||
<label for="proj-status">Status</label>
|
||
<select id="proj-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="field">
|
||
<label for="proj-folder">Ordner <span class="tip" title="Relativ unter dem Slice-Ordner, z. B. Projekte/2609_002 — kein D:\…"><svg class="ico"><use href="#i-info"/></svg></span></label>
|
||
<input id="proj-folder" placeholder="Projekte/…" autocomplete="off">
|
||
<p class="field-help">Leer = automatisch <code>Projekte/<Name></code>. Nur Relativpfad, kein Laufwerk.</p>
|
||
</div>
|
||
</div>
|
||
<div class="field"><label for="proj-notes">Notiz</label><textarea id="proj-notes" rows="3"></textarea></div>
|
||
<div class="actions">
|
||
<button type="submit" class="btn-primary" id="proj-save">Speichern</button>
|
||
<button type="button" class="ghost" id="proj-cancel-edit" hidden>Abbrechen</button>
|
||
<button type="button" class="warn" id="proj-delete" hidden><svg class="ico"><use href="#i-trash"/></svg>Löschen</button>
|
||
</div>
|
||
</form>
|
||
<p id="proj-note" class="tiny"></p>
|
||
|
||
<div id="proj-detail" hidden>
|
||
<div class="actions" style="margin-bottom:0.5rem">
|
||
<a class="ghost" id="proj-files-link" href="/dateien" hidden>Dateien</a>
|
||
</div>
|
||
<div id="proj-stats" class="proj-stats"></div>
|
||
<p id="proj-suggest" class="tiny" hidden></p>
|
||
<p id="proj-meta" class="tiny"></p>
|
||
|
||
<div class="seg" id="proj-tabs" role="tablist" style="margin:0.75rem 0">
|
||
<button type="button" class="on" data-tab="files">Druckdateien</button>
|
||
<button type="button" data-tab="runs">Läufe</button>
|
||
<button type="button" data-tab="assets">Weitere Dateien</button>
|
||
</div>
|
||
|
||
<div id="proj-tab-files">
|
||
<div class="actions" style="margin-bottom:0.5rem">
|
||
<button type="button" class="btn-primary" id="proj-enqueue-all" hidden>Alle einreihen</button>
|
||
</div>
|
||
<div id="proj-gcodes" class="proj-file-list"></div>
|
||
</div>
|
||
|
||
<div id="proj-tab-runs" hidden>
|
||
<div id="proj-runs" class="proj-file-list"></div>
|
||
<div class="pager" id="proj-runs-pager" hidden></div>
|
||
</div>
|
||
|
||
<div id="proj-tab-assets" hidden>
|
||
<p class="tiny">STEP, STL, PDF, SolidWorks usw. im Projektordner — nicht in der Druck-Bibliothek.</p>
|
||
<div class="actions" style="margin-bottom:0.5rem">
|
||
<label class="button ghost" for="proj-asset-file"><svg class="ico"><use href="#i-upload"/></svg>Hochladen</label>
|
||
<input id="proj-asset-file" type="file" multiple hidden>
|
||
</div>
|
||
<div id="proj-assets" class="proj-file-list"></div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block script %}
|
||
<script>
|
||
const STATUS_LABEL = { idee: "Idee", bereit: "Bereit", druckt: "Druckt", fertig: "Fertig" };
|
||
let projects = [];
|
||
let selectedId = null;
|
||
let listFilter = "aktiv";
|
||
let detailTab = "files";
|
||
let runsPage = 0;
|
||
let runsTotal = 0;
|
||
const RUNS_PAGE = 20;
|
||
/** idle | create | view | edit */
|
||
let panelMode = "idle";
|
||
let folderTouched = false;
|
||
let projPage = 1;
|
||
|
||
document.getElementById("proj-close").innerHTML = icon("x");
|
||
|
||
function openPanel() {
|
||
document.getElementById("proj-panel").hidden = false;
|
||
document.getElementById("proj-layout").classList.remove("is-idle");
|
||
document.getElementById("proj-panel").scrollIntoView({ block: "nearest", behavior: "smooth" });
|
||
}
|
||
|
||
function closePanel() {
|
||
panelMode = "idle";
|
||
selectedId = null;
|
||
document.getElementById("proj-panel").hidden = true;
|
||
document.getElementById("proj-layout").classList.add("is-idle");
|
||
document.getElementById("proj-form").hidden = true;
|
||
document.getElementById("proj-detail").hidden = true;
|
||
document.getElementById("proj-edit-btn").hidden = true;
|
||
document.getElementById("proj-cancel-edit").hidden = true;
|
||
document.getElementById("proj-delete").hidden = true;
|
||
document.getElementById("proj-files-link").hidden = true;
|
||
document.getElementById("proj-note").textContent = "";
|
||
document.getElementById("proj-enqueue-all").hidden = true;
|
||
history.replaceState(null, "", location.pathname);
|
||
renderList();
|
||
}
|
||
|
||
function setFormValues(project) {
|
||
document.getElementById("proj-id").value = project ? project.id : "";
|
||
document.getElementById("proj-name").value = project ? (project.name || "") : "";
|
||
document.getElementById("proj-status").value = project ? (project.status || "idee") : "idee";
|
||
document.getElementById("proj-folder").value = project ? (project.folder || "") : "";
|
||
document.getElementById("proj-notes").value = project ? (project.notes || "") : "";
|
||
}
|
||
|
||
function showCreate() {
|
||
panelMode = "create";
|
||
selectedId = null;
|
||
folderTouched = false;
|
||
setFormValues(null);
|
||
document.getElementById("proj-title").textContent = "Neues Projekt";
|
||
document.getElementById("proj-form").hidden = false;
|
||
document.getElementById("proj-detail").hidden = true;
|
||
document.getElementById("proj-edit-btn").hidden = true;
|
||
document.getElementById("proj-cancel-edit").hidden = true;
|
||
document.getElementById("proj-delete").hidden = true;
|
||
document.getElementById("proj-files-link").hidden = true;
|
||
document.getElementById("proj-note").textContent = "";
|
||
document.getElementById("proj-enqueue-all").hidden = true;
|
||
openPanel();
|
||
renderList();
|
||
document.getElementById("proj-name").focus();
|
||
}
|
||
|
||
function showView(project) {
|
||
panelMode = "view";
|
||
selectedId = project.id;
|
||
setFormValues(project);
|
||
document.getElementById("proj-title").textContent = project.name || "Projekt";
|
||
document.getElementById("proj-form").hidden = true;
|
||
document.getElementById("proj-detail").hidden = false;
|
||
document.getElementById("proj-edit-btn").hidden = false;
|
||
document.getElementById("proj-cancel-edit").hidden = true;
|
||
document.getElementById("proj-delete").hidden = true;
|
||
const link = document.getElementById("proj-files-link");
|
||
link.hidden = false;
|
||
link.href = "/dateien?project=" + encodeURIComponent(project.id);
|
||
document.getElementById("proj-note").textContent = "";
|
||
openPanel();
|
||
renderList();
|
||
renderDetail(project);
|
||
}
|
||
|
||
function showEdit(project) {
|
||
panelMode = "edit";
|
||
selectedId = project.id;
|
||
folderTouched = true;
|
||
setFormValues(project);
|
||
document.getElementById("proj-title").textContent = "Bearbeiten";
|
||
document.getElementById("proj-form").hidden = false;
|
||
document.getElementById("proj-detail").hidden = true;
|
||
document.getElementById("proj-edit-btn").hidden = true;
|
||
document.getElementById("proj-cancel-edit").hidden = false;
|
||
document.getElementById("proj-delete").hidden = false;
|
||
document.getElementById("proj-files-link").hidden = true;
|
||
openPanel();
|
||
renderList();
|
||
document.getElementById("proj-name").focus();
|
||
}
|
||
|
||
function renderList() {
|
||
const body = document.getElementById("proj-rows");
|
||
const pager = document.getElementById("proj-pager");
|
||
document.getElementById("proj-count").textContent = String(projects.length);
|
||
if (!projects.length) {
|
||
body.innerHTML = '<tr><td class="empty" colspan="5"><div class="empty-state">' + icon("folder")
|
||
+ "<p>Noch kein Projekt.</p>"
|
||
+ '<p><button type="button" class="btn-primary" id="proj-empty-new">Erstes Projekt anlegen</button></p>'
|
||
+ "</div></td></tr>";
|
||
document.getElementById("proj-empty-new")?.addEventListener("click", showCreate);
|
||
renderPager(pager, null, () => {});
|
||
return;
|
||
}
|
||
body.innerHTML = "";
|
||
const paged = pageSlice(projects, projPage, DEFAULT_PAGE_SIZE);
|
||
projPage = paged.page;
|
||
paged.items.forEach((project) => {
|
||
const tr = document.createElement("tr");
|
||
tr.className = "pick" + (String(project.id) === String(selectedId) ? " on" : "");
|
||
tr.innerHTML = "<td></td><td></td><td class=\"cell-path\"></td><td class=\"num\"></td><td class=\"num\"></td>";
|
||
tr.children[0].textContent = project.name;
|
||
let status = STATUS_LABEL[project.status] || project.status || "";
|
||
if ((project.stats || {}).active_count > 0) status = STATUS_LABEL.druckt;
|
||
tr.children[1].textContent = status;
|
||
tr.children[2].textContent = project.folder || "—";
|
||
tr.children[2].title = project.folder || "";
|
||
tr.children[3].textContent = String(project.file_count ?? (project.files || []).length);
|
||
const cost = (project.stats || {}).cost;
|
||
tr.children[4].textContent = cost ? euro(cost) : "—";
|
||
tr.addEventListener("click", () => openProject(project.id));
|
||
body.appendChild(tr);
|
||
});
|
||
renderPager(pager, paged, (p) => {
|
||
projPage = p;
|
||
renderList();
|
||
});
|
||
}
|
||
|
||
function setTab(tab) {
|
||
detailTab = tab;
|
||
document.querySelectorAll("#proj-tabs [data-tab]").forEach((btn) => {
|
||
btn.classList.toggle("on", btn.dataset.tab === tab);
|
||
});
|
||
document.getElementById("proj-tab-files").hidden = tab !== "files";
|
||
document.getElementById("proj-tab-runs").hidden = tab !== "runs";
|
||
document.getElementById("proj-tab-assets").hidden = tab !== "assets";
|
||
if (tab === "runs" && selectedId) loadRuns(selectedId, 0);
|
||
if (tab === "assets" && selectedId) loadAssets(selectedId);
|
||
}
|
||
|
||
function renderDetail(project) {
|
||
const stats = project.stats || {};
|
||
const statsEl = document.getElementById("proj-stats");
|
||
const chips = [
|
||
[stats.run_count || 0, "Läufe"],
|
||
[hours(stats.duration_s) || "0 min", "Zeit"],
|
||
[grams(stats.weight_g || 0), "Filament"],
|
||
[euro(stats.cost || 0), "Kosten"],
|
||
];
|
||
if (stats.files_total) {
|
||
chips.push([stats.files_ok + "/" + stats.files_total, "Dateien ok"]);
|
||
}
|
||
statsEl.innerHTML = chips.map(([v, l]) =>
|
||
`<div class="proj-stat"><strong>${escapeHtml(String(v))}</strong><span class="tiny">${escapeHtml(l)}</span></div>`
|
||
).join("");
|
||
|
||
const meta = [];
|
||
if (project.folder) meta.push(project.folder);
|
||
if (project.created_at) meta.push("Angelegt " + formatDateTime(project.created_at));
|
||
if (stats.last_run) {
|
||
meta.push("Zuletzt " + formatDateTime(stats.last_run.started_at) + " · " + (stats.last_run.filename || ""));
|
||
}
|
||
document.getElementById("proj-meta").textContent = meta.join(" · ");
|
||
|
||
const suggest = document.getElementById("proj-suggest");
|
||
if (stats.suggest_fertig) {
|
||
suggest.hidden = false;
|
||
suggest.innerHTML = 'Alle Dateien mindestens einmal ok gedruckt. <button type="button" class="ghost" id="proj-mark-fertig">Als fertig markieren?</button>';
|
||
document.getElementById("proj-mark-fertig")?.addEventListener("click", async () => {
|
||
try {
|
||
await api("/projekte/" + project.id, { method: "PATCH", json: { status: "fertig" } });
|
||
toast("Projekt als fertig markiert.", "good");
|
||
await load();
|
||
await openProject(project.id);
|
||
} catch (error) {
|
||
showNote("proj-note", error);
|
||
}
|
||
});
|
||
} else {
|
||
suggest.hidden = true;
|
||
suggest.textContent = "";
|
||
}
|
||
|
||
const files = project.files || [];
|
||
document.getElementById("proj-enqueue-all").hidden = !files.length;
|
||
const gcodes = document.getElementById("proj-gcodes");
|
||
if (!files.length) {
|
||
gcodes.innerHTML = '<p class="tiny">Keine G-code/3MF-Dateien verknüpft. Dateien in den Projektordner legen oder unter Dateien verschieben.</p>';
|
||
} else {
|
||
gcodes.innerHTML = files.map((file) => {
|
||
const mats = (file.filaments || []).map((f) => f.type || f.material_type || f.name || "").filter(Boolean);
|
||
const mat = [...new Set(mats)].join(" / ");
|
||
const metaBits = [
|
||
mat,
|
||
file.weight_g ? Math.round(file.weight_g) + " g" : "",
|
||
file.printed ? file.printed + "× gedruckt" : "noch nie",
|
||
file.queued ? file.queued + " in Queue" : "",
|
||
file.ok_once ? "ok" : "",
|
||
].filter(Boolean).join(" · ");
|
||
const href = "/dateien?file=" + encodeURIComponent(file.id) + "&project=" + encodeURIComponent(project.id);
|
||
return `<div class="proj-file-row">
|
||
<a class="proj-file" href="${escapeHtml(href)}">${escapeHtml(file.name)}<span class="tiny">${escapeHtml(metaBits)}</span></a>
|
||
<button type="button" class="ghost" data-enqueue="${file.id}">Einreihen</button>
|
||
</div>`;
|
||
}).join("");
|
||
}
|
||
setTab(detailTab);
|
||
if (detailTab === "assets") loadAssets(project.id);
|
||
if (detailTab === "runs") loadRuns(project.id, runsPage);
|
||
}
|
||
|
||
async function loadRuns(projectId, offset) {
|
||
const box = document.getElementById("proj-runs");
|
||
const pager = document.getElementById("proj-runs-pager");
|
||
runsPage = Math.max(0, offset || 0);
|
||
try {
|
||
const resp = await fetch(withToken("/api/v1/projekte/" + projectId + "/laeufe?limit=" + RUNS_PAGE + "&offset=" + runsPage), {
|
||
credentials: "same-origin",
|
||
headers: { Accept: "application/json" },
|
||
});
|
||
if (!resp.ok) throw new Error("Läufe laden fehlgeschlagen.");
|
||
const rows = await resp.json();
|
||
runsTotal = Number(resp.headers.get("X-Total-Count") || rows.length);
|
||
if (!rows.length) {
|
||
box.innerHTML = '<p class="tiny">Noch keine Läufe.</p>';
|
||
pager.hidden = true;
|
||
return;
|
||
}
|
||
box.innerHTML = rows.map((run) => {
|
||
const state = (window.RUN_STATES && RUN_STATES[run.state]) || run.state || "";
|
||
const cost = Number(run.filament_cost || 0) + Number(run.energy_cost || 0);
|
||
const meta = [state, hours(run.duration_s), cost ? euro(cost) : "", formatDateTime(run.started_at)].filter(Boolean).join(" · ");
|
||
return `<a class="proj-file" href="/protokoll/${run.id}">${escapeHtml(run.filename || "Lauf #" + run.id)}<span class="tiny">${escapeHtml(meta)}</span></a>`;
|
||
}).join("");
|
||
const pages = Math.max(1, Math.ceil(runsTotal / RUNS_PAGE));
|
||
const page = Math.floor(runsPage / RUNS_PAGE) + 1;
|
||
renderPager(pager, {
|
||
items: rows, page, pages, total: runsTotal, pageSize: RUNS_PAGE,
|
||
from: runsTotal ? runsPage + 1 : 0,
|
||
to: Math.min(runsPage + rows.length, runsTotal),
|
||
}, (p) => loadRuns(projectId, (p - 1) * RUNS_PAGE));
|
||
} catch (error) {
|
||
box.innerHTML = '<p class="tiny">' + escapeHtml(error.message || String(error)) + "</p>";
|
||
}
|
||
}
|
||
|
||
async function loadAssets(projectId) {
|
||
const box = document.getElementById("proj-assets");
|
||
try {
|
||
const rows = await api("/projekte/" + projectId + "/assets");
|
||
if (!rows.length) {
|
||
box.innerHTML = '<p class="tiny">Noch keine weiteren Dateien.</p>';
|
||
return;
|
||
}
|
||
box.innerHTML = rows.map((item) => {
|
||
const href = withToken("/api/v1/projekte/" + projectId + "/assets/" + encodeURIComponent(item.rel).replace(/%2F/g, "/"));
|
||
return `<div class="proj-asset">
|
||
<a href="${escapeHtml(href)}" download>${escapeHtml(item.rel)}</a>
|
||
<span class="tiny">${escapeHtml(sizeText(item.size_bytes))}</span>
|
||
<button type="button" class="icon-btn" data-asset-del="${escapeHtml(item.rel)}" title="Löschen">${icon("trash")}</button>
|
||
</div>`;
|
||
}).join("");
|
||
} catch (error) {
|
||
box.innerHTML = '<p class="tiny">' + escapeHtml(error.message || String(error)) + "</p>";
|
||
}
|
||
}
|
||
|
||
function sizeText(bytes) {
|
||
const n = Number(bytes) || 0;
|
||
if (n < 1024) return n + " B";
|
||
if (n < 1024 * 1024) return (n / 1024).toFixed(1).replace(".", ",") + " KB";
|
||
return (n / (1024 * 1024)).toFixed(1).replace(".", ",") + " MB";
|
||
}
|
||
|
||
async function enqueueFiles(projectId, fileIds) {
|
||
const payload = { confirm: true };
|
||
if (fileIds && fileIds.length) payload.file_ids = fileIds;
|
||
const out = await api("/projekte/" + projectId + "/einreihen", { method: "POST", json: payload });
|
||
toast((out.count || 0) + " in die Warteschlange.", "good");
|
||
await openProject(projectId);
|
||
}
|
||
|
||
async function openProject(id) {
|
||
try {
|
||
const row = await api("/projekte/" + id);
|
||
showView(row);
|
||
history.replaceState(null, "", "#" + id);
|
||
} catch (error) {
|
||
showNote("proj-note", error);
|
||
}
|
||
}
|
||
|
||
async function load() {
|
||
const q = listFilter ? ("?status=" + encodeURIComponent(listFilter)) : "";
|
||
projects = await api("/projekte" + q);
|
||
renderList();
|
||
if (selectedId && (panelMode === "view" || panelMode === "edit")) {
|
||
const still = projects.find((p) => String(p.id) === String(selectedId));
|
||
if (still) {
|
||
if (panelMode === "edit") {
|
||
const row = await api("/projekte/" + selectedId);
|
||
showEdit(row);
|
||
} else {
|
||
await openProject(selectedId);
|
||
}
|
||
} else {
|
||
closePanel();
|
||
}
|
||
}
|
||
}
|
||
|
||
document.getElementById("proj-filter").addEventListener("click", (event) => {
|
||
const btn = event.target.closest("[data-filter]");
|
||
if (!btn) return;
|
||
listFilter = btn.dataset.filter || "";
|
||
document.querySelectorAll("#proj-filter [data-filter]").forEach((b) => b.classList.toggle("on", b === btn));
|
||
projPage = 1;
|
||
load().catch((error) => showNote("proj-note", error));
|
||
});
|
||
|
||
document.getElementById("proj-tabs").addEventListener("click", (event) => {
|
||
const btn = event.target.closest("[data-tab]");
|
||
if (!btn) return;
|
||
setTab(btn.dataset.tab);
|
||
});
|
||
|
||
document.getElementById("proj-new").addEventListener("click", () => {
|
||
history.replaceState(null, "", location.pathname);
|
||
showCreate();
|
||
});
|
||
document.getElementById("proj-close").addEventListener("click", closePanel);
|
||
document.getElementById("proj-edit-btn").addEventListener("click", async () => {
|
||
if (!selectedId) return;
|
||
try {
|
||
const row = await api("/projekte/" + selectedId);
|
||
showEdit(row);
|
||
} catch (error) {
|
||
showNote("proj-note", error);
|
||
}
|
||
});
|
||
document.getElementById("proj-cancel-edit").addEventListener("click", async () => {
|
||
if (!selectedId) {
|
||
closePanel();
|
||
return;
|
||
}
|
||
await openProject(selectedId);
|
||
});
|
||
|
||
document.getElementById("proj-folder").addEventListener("input", () => {
|
||
folderTouched = true;
|
||
});
|
||
document.getElementById("proj-name").addEventListener("input", () => {
|
||
if (folderTouched || document.getElementById("proj-id").value) return;
|
||
const name = document.getElementById("proj-name").value.trim();
|
||
const folderEl = document.getElementById("proj-folder");
|
||
if (!name) {
|
||
folderEl.value = "";
|
||
return;
|
||
}
|
||
const slug = name
|
||
.replace(/[^\w\- äöüÄÖÜß.]+/gi, "-")
|
||
.replace(/\s+/g, "-")
|
||
.replace(/-{2,}/g, "-")
|
||
.replace(/^[-.]+|[-.]+$/g, "")
|
||
.slice(0, 60) || "projekt";
|
||
folderEl.value = "Projekte/" + slug;
|
||
});
|
||
|
||
document.getElementById("proj-form").addEventListener("submit", async (event) => {
|
||
event.preventDefault();
|
||
const id = document.getElementById("proj-id").value;
|
||
const name = document.getElementById("proj-name").value.trim();
|
||
if (!name) {
|
||
toast("Name fehlt.", "bad");
|
||
document.getElementById("proj-name").focus();
|
||
return;
|
||
}
|
||
const folderRaw = document.getElementById("proj-folder").value.trim();
|
||
if (/^[a-zA-Z]:[\\/]/.test(folderRaw) || folderRaw.startsWith("\\\\") || folderRaw.includes(":")) {
|
||
toast("Ordner nur relativ unter dem Slice-Pfad — z. B. Projekte/2609_002, kein D:\\…", "bad");
|
||
document.getElementById("proj-folder").focus();
|
||
return;
|
||
}
|
||
const payload = {
|
||
name,
|
||
status: document.getElementById("proj-status").value,
|
||
notes: document.getElementById("proj-notes").value,
|
||
folder: folderRaw,
|
||
};
|
||
const saveBtn = document.getElementById("proj-save");
|
||
saveBtn.disabled = true;
|
||
try {
|
||
const saved = id
|
||
? await api("/projekte/" + id, { method: "PATCH", json: payload })
|
||
: await api("/projekte", { method: "POST", json: payload });
|
||
toast(id ? "Projekt gespeichert." : "Projekt angelegt.", "good");
|
||
folderTouched = true;
|
||
await load();
|
||
await openProject(saved.id);
|
||
} catch (error) {
|
||
showNote("proj-note", error);
|
||
} finally {
|
||
saveBtn.disabled = false;
|
||
}
|
||
});
|
||
|
||
document.getElementById("proj-delete").addEventListener("click", () => {
|
||
const id = document.getElementById("proj-id").value;
|
||
if (!id) return;
|
||
armButton(document.getElementById("proj-delete"), "Ordner inkl. Inhalt?", async () => {
|
||
try {
|
||
await api("/projekte/" + id + "?force=1", { method: "DELETE" });
|
||
toast("Projekt gelöscht.");
|
||
closePanel();
|
||
await load();
|
||
} catch (error) {
|
||
showNote("proj-note", error);
|
||
}
|
||
});
|
||
});
|
||
|
||
document.getElementById("proj-enqueue-all").addEventListener("click", () => {
|
||
const id = selectedId;
|
||
if (!id) return;
|
||
armButton(document.getElementById("proj-enqueue-all"), "Alle einreihen?", async () => {
|
||
try {
|
||
await enqueueFiles(id, null);
|
||
} catch (error) {
|
||
showNote("proj-note", error);
|
||
}
|
||
});
|
||
});
|
||
|
||
document.getElementById("proj-gcodes").addEventListener("click", (event) => {
|
||
const btn = event.target.closest("[data-enqueue]");
|
||
if (!btn) return;
|
||
event.preventDefault();
|
||
const id = selectedId;
|
||
const fileId = Number(btn.dataset.enqueue);
|
||
if (!id || !fileId) return;
|
||
armButton(btn, "einreihen?", async () => {
|
||
try {
|
||
await enqueueFiles(id, [fileId]);
|
||
} catch (error) {
|
||
showNote("proj-note", error);
|
||
}
|
||
});
|
||
});
|
||
|
||
document.getElementById("proj-asset-file").addEventListener("change", async (event) => {
|
||
const id = selectedId;
|
||
const files = [...(event.target.files || [])];
|
||
event.target.value = "";
|
||
if (!id || !files.length) return;
|
||
try {
|
||
for (const file of files) {
|
||
const body = new FormData();
|
||
body.append("file", file);
|
||
await api("/projekte/" + id + "/assets", { method: "POST", body });
|
||
}
|
||
toast(files.length === 1 ? "Datei hochgeladen." : files.length + " Dateien hochgeladen.");
|
||
await loadAssets(id);
|
||
} catch (error) {
|
||
showNote("proj-note", error);
|
||
}
|
||
});
|
||
|
||
document.getElementById("proj-assets").addEventListener("click", (event) => {
|
||
const btn = event.target.closest("[data-asset-del]");
|
||
if (!btn) return;
|
||
const id = selectedId;
|
||
const rel = btn.dataset.assetDel;
|
||
if (!id || !rel) return;
|
||
armButton(btn, "löschen?", async () => {
|
||
await api("/projekte/" + id + "/assets/" + encodeURIComponent(rel).replace(/%2F/g, "/"), { method: "DELETE" });
|
||
toast("Datei gelöscht.");
|
||
await loadAssets(id);
|
||
});
|
||
});
|
||
|
||
(async () => {
|
||
try {
|
||
await load();
|
||
const hash = (location.hash || "").replace(/^#/, "");
|
||
if (hash && /^\d+$/.test(hash)) await openProject(Number(hash));
|
||
} catch (error) {
|
||
showNote("proj-note", error);
|
||
}
|
||
})();
|
||
</script>
|
||
{% endblock %}
|