Files
grotti/web/templates/projekte.html
T

312 lines
12 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">
<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">
<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></tr></thead>
<tbody id="proj-rows"></tbody>
</table>
</div>
</section>
<section class="card" id="proj-panel">
<h2 id="proj-title">Neues Projekt</h2>
<form id="proj-form" novalidate>
<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/&lt;Name&gt;</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>
<a class="ghost" id="proj-files-link" href="/dateien" hidden>Dateien</a>
<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>
<h3 class="section-gap">Druckdateien</h3>
<div id="proj-gcodes" class="proj-file-list"></div>
<h3 class="section-gap">Weitere Dateien</h3>
<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>
</section>
</div>
{% endblock %}
{% block script %}
<script>
const STATUS_LABEL = { idee: "Idee", bereit: "bereit", druckt: "druckt", fertig: "fertig" };
let projects = [];
let selectedId = null;
function fillBlank() {
selectedId = null;
document.getElementById("proj-id").value = "";
document.getElementById("proj-name").value = "";
document.getElementById("proj-status").value = "idee";
document.getElementById("proj-folder").value = "";
document.getElementById("proj-notes").value = "";
document.getElementById("proj-title").textContent = "Neues Projekt";
document.getElementById("proj-delete").hidden = true;
document.getElementById("proj-files-link").hidden = true;
document.getElementById("proj-detail").hidden = true;
document.getElementById("proj-note").textContent = "";
renderList();
}
function fill(project) {
selectedId = project.id;
document.getElementById("proj-id").value = project.id;
document.getElementById("proj-name").value = project.name || "";
document.getElementById("proj-status").value = project.status || "idee";
document.getElementById("proj-folder").value = project.folder || "";
document.getElementById("proj-notes").value = project.notes || "";
document.getElementById("proj-title").textContent = project.name || "Projekt";
document.getElementById("proj-delete").hidden = false;
const link = document.getElementById("proj-files-link");
link.hidden = false;
link.href = "/dateien";
document.getElementById("proj-detail").hidden = false;
document.getElementById("proj-note").textContent = "";
renderList();
renderDetail(project);
}
function renderList() {
const body = document.getElementById("proj-rows");
document.getElementById("proj-count").textContent = String(projects.length);
if (!projects.length) {
body.innerHTML = '<tr><td class="empty" colspan="4"><div class="empty-state">' + icon("folder") + "<p>Noch kein Projekt.</p></div></td></tr>";
return;
}
body.innerHTML = "";
projects.forEach((project) => {
const tr = document.createElement("tr");
tr.className = "pick" + (String(project.id) === String(selectedId) ? " on" : "");
tr.innerHTML = "<td></td><td></td><td></td><td class=\"num\"></td>";
tr.children[0].textContent = project.name;
tr.children[1].textContent = STATUS_LABEL[project.status] || project.status || "";
tr.children[2].textContent = project.folder || "—";
tr.children[2].title = project.folder || "";
tr.children[3].textContent = String(project.file_count ?? (project.files || []).length);
tr.addEventListener("click", () => openProject(project.id));
body.appendChild(tr);
});
}
function renderDetail(project) {
const gcodes = document.getElementById("proj-gcodes");
const files = project.files || [];
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 meta = [mat, file.weight_g ? Math.round(file.weight_g) + " g" : ""].filter(Boolean).join(" · ");
return `<a class="proj-file" href="/dateien">${escapeHtml(file.name)}<span class="tiny">${escapeHtml(meta)}</span></a>`;
}).join("");
}
loadAssets(project.id);
}
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 openProject(id) {
try {
const row = await api("/projekte/" + id);
fill(row);
} catch (error) {
showNote("proj-note", error);
}
}
async function load() {
projects = await api("/projekte");
renderList();
if (selectedId) {
const still = projects.find((p) => String(p.id) === String(selectedId));
if (still) await openProject(selectedId);
else fillBlank();
}
}
let folderTouched = false;
document.getElementById("proj-new").addEventListener("click", () => {
folderTouched = false;
fillBlank();
document.getElementById("proj-name").focus();
});
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.");
fillBlank();
await load();
} catch (error) {
showNote("proj-note", error);
}
});
});
document.getElementById("proj-asset-file").addEventListener("change", async (event) => {
const id = document.getElementById("proj-id").value;
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 = document.getElementById("proj-id").value;
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);
});
});
load().catch((error) => showNote("proj-note", error));
</script>
{% endblock %}