From a9eaa81c37fccd71a27aee4f75ab4aeb4579ed96 Mon Sep 17 00:00:00 2001 From: Admin Date: Sun, 21 Dec 2025 11:54:00 +0000 Subject: [PATCH] Add template upload functionality to Schadenprotokoll MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New Templates tab for PDF and Word template management - API endpoints for listing and uploading templates - Automatic backup of old templates before replacement 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- schadenprotokoll/api/app.py | 70 ++++++++++++++++++++++++---------- schadenprotokoll/app.js | 76 +++++++++++++++++++++++++++++++++++++ schadenprotokoll/index.html | 63 +++++++++++++++++++----------- schadenprotokoll/styles.css | 14 +++++++ 4 files changed, 180 insertions(+), 43 deletions(-) diff --git a/schadenprotokoll/api/app.py b/schadenprotokoll/api/app.py index b1bbf84..54ef662 100644 --- a/schadenprotokoll/api/app.py +++ b/schadenprotokoll/api/app.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ Schadenprotokoll API - Flask Backend -Endpunkte: /generate, /analyze, /vorbericht +Endpunkte: /generate, /analyze, /vorbericht, /templates """ from flask import Flask, request, jsonify, send_file from flask_cors import CORS @@ -12,8 +12,9 @@ from processors import parse_laufzettel, fill_pdf, analyze_pdf, generate_vorberi app = Flask(__name__) CORS(app) -TEMPLATE_PDF = "/opt/stacks/spa-hosting/html/schadenprotokoll/templates/protokoll.pdf" -TEMPLATE_DOCX = "/opt/stacks/spa-hosting/html/schadenprotokoll/templates/vorbericht.docx" +TEMPLATES_DIR = "/opt/stacks/spa-hosting/html/schadenprotokoll/templates" +TEMPLATE_PDF = os.path.join(TEMPLATES_DIR, "protokoll.pdf") +TEMPLATE_DOCX = os.path.join(TEMPLATES_DIR, "vorbericht.docx") @app.route("/health", methods=["GET"]) @@ -21,6 +22,47 @@ def health(): return jsonify({"status": "ok"}) +@app.route("/templates", methods=["GET"]) +def list_templates(): + """Listet alle verfuegbaren Templates""" + templates = [] + for f in os.listdir(TEMPLATES_DIR): + path = os.path.join(TEMPLATES_DIR, f) + if os.path.isfile(path) and not f.endswith(".bak"): + templates.append({ + "name": f, + "size": os.path.getsize(path), + "type": "pdf" if f.endswith(".pdf") else "docx" + }) + return jsonify({"templates": templates}) + + +@app.route("/templates/upload", methods=["POST"]) +def upload_template(): + """Laedt ein neues Template hoch""" + if "file" not in request.files: + return jsonify({"error": "Keine Datei hochgeladen"}), 400 + + file = request.files["file"] + ttype = request.form.get("type", "pdf") + + if ttype == "pdf" and not file.filename.endswith(".pdf"): + return jsonify({"error": "PDF-Template muss .pdf sein"}), 400 + if ttype == "docx" and not file.filename.endswith(".docx"): + return jsonify({"error": "Word-Template muss .docx sein"}), 400 + + target = TEMPLATE_PDF if ttype == "pdf" else TEMPLATE_DOCX + backup = target + ".bak" + + if os.path.exists(target): + if os.path.exists(backup): + os.remove(backup) + os.rename(target, backup) + + file.save(target) + return jsonify({"success": True, "message": f"Template erfolgreich hochgeladen"}) + + @app.route("/generate", methods=["POST"]) def generate(): """Laufzettel.docx -> vorausgefuelltes Protokoll.pdf""" @@ -33,20 +75,13 @@ def generate(): with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp_in: file.save(tmp_in.name) - try: data = parse_laufzettel(tmp_in.name) - with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp_out: fill_pdf(TEMPLATE_PDF, tmp_out.name, data) - os.unlink(tmp_in.name) - return send_file( - tmp_out.name, - mimetype="application/pdf", - as_attachment=True, - download_name="Schadenprotokoll_vorbefuellt.pdf" - ) + return send_file(tmp_out.name, mimetype="application/pdf", + as_attachment=True, download_name="Schadenprotokoll_vorbefuellt.pdf") except Exception as e: os.unlink(tmp_in.name) return jsonify({"error": str(e)}), 500 @@ -64,7 +99,6 @@ def analyze(): with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp: file.save(tmp.name) - try: result = analyze_pdf(tmp.name) os.unlink(tmp.name) @@ -86,20 +120,14 @@ def vorbericht(): with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp_in: file.save(tmp_in.name) - try: pdf_data = analyze_pdf(tmp_in.name) - with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp_out: generate_vorbericht(pdf_data, TEMPLATE_DOCX, tmp_out.name) - os.unlink(tmp_in.name) - return send_file( - tmp_out.name, + return send_file(tmp_out.name, mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document", - as_attachment=True, - download_name="Vorbericht.docx" - ) + as_attachment=True, download_name="Vorbericht.docx") except Exception as e: os.unlink(tmp_in.name) return jsonify({"error": str(e)}), 500 diff --git a/schadenprotokoll/app.js b/schadenprotokoll/app.js index 6a99b30..f38ebfd 100644 --- a/schadenprotokoll/app.js +++ b/schadenprotokoll/app.js @@ -172,3 +172,79 @@ document.getElementById("vorbericht-btn").addEventListener("click", async () => showStatus("vorbericht-status", "error", `❌ Fehler: ${err.message}`); } }); + +// Admin Tab - Templates laden +async function loadTemplates() { + try { + const response = await fetch(`${API_BASE}/templates`); + const data = await response.json(); + const container = document.getElementById("templates-container"); + + if (data.templates.length === 0) { + container.innerHTML = "

Keine Templates vorhanden.

"; + return; + } + + container.innerHTML = data.templates.map(t => ` +
+ ${t.type === "pdf" ? "📄" : "📝"} ${t.name} + ${(t.size / 1024).toFixed(1)} KB +
+ `).join(""); + } catch (err) { + document.getElementById("templates-container").innerHTML = + `

Fehler: ${err.message}

`; + } +} + +// Template Upload +function setupTemplateUpload(zoneId, inputId, templateType) { + const zone = document.getElementById(zoneId); + const input = document.getElementById(inputId); + + zone.addEventListener("click", () => input.click()); + zone.addEventListener("dragover", (e) => { e.preventDefault(); zone.classList.add("dragover"); }); + zone.addEventListener("dragleave", () => zone.classList.remove("dragover")); + zone.addEventListener("drop", (e) => { + e.preventDefault(); + zone.classList.remove("dragover"); + if (e.dataTransfer.files.length) { + input.files = e.dataTransfer.files; + uploadTemplate(input.files[0], templateType); + } + }); + input.addEventListener("change", () => { + if (input.files.length) uploadTemplate(input.files[0], templateType); + }); +} + +async function uploadTemplate(file, type) { + showStatus("admin-status", "loading", `⏳ Lade ${type.toUpperCase()} Template hoch...`); + + const formData = new FormData(); + formData.append("file", file); + formData.append("type", type); + + try { + const response = await fetch(`${API_BASE}/templates/upload`, { + method: "POST", + body: formData + }); + + if (!response.ok) { + const err = await response.json(); + throw new Error(err.error); + } + + showStatus("admin-status", "success", "✓ Template erfolgreich hochgeladen"); + loadTemplates(); + } catch (err) { + showStatus("admin-status", "error", `❌ Fehler: ${err.message}`); + } +} + +setupTemplateUpload("pdf-template-zone", "pdf-template-input", "pdf"); +setupTemplateUpload("docx-template-zone", "docx-template-input", "docx"); + +// Templates beim Tab-Wechsel laden +document.querySelector([data-tab=admin]).addEventListener("click", loadTemplates); diff --git a/schadenprotokoll/index.html b/schadenprotokoll/index.html index 45e98a8..e6df99d 100644 --- a/schadenprotokoll/index.html +++ b/schadenprotokoll/index.html @@ -17,26 +17,23 @@ +

Schadenlaufzettel → Protokoll PDF

-

Lädt Daten aus dem Schadenlaufzettel.docx und füllt das Schadenprotokoll-PDF vor.

+

Laedt Daten aus dem Schadenlaufzettel.docx und fuellt das Schadenprotokoll-PDF vor.

-

📄 Schadenlaufzettel.docx hochladen

-

Datei hierher ziehen oder klicken zum Auswählen

+

Datei hierher ziehen oder klicken

-
- +
@@ -44,21 +41,17 @@
-

Ausgefülltes PDF analysieren

+

Ausgefuelltes PDF analysieren

Liest alle Formularfelder und Dropdown-Auswahlen aus dem Schadenprotokoll.

-

📋 Schadenprotokoll.pdf hochladen

-

Das ausgefüllte PDF-Formular

+

Das ausgefuellte PDF-Formular

-
- +
@@ -71,24 +64,50 @@

Protokoll → Vorbericht Word

-

Generiert einen strukturierten Vorbericht aus dem ausgefüllten Schadenprotokoll.

+

Generiert einen strukturierten Vorbericht aus dem ausgefuellten Schadenprotokoll.

-
-

📋 Ausgefülltes Schadenprotokoll.pdf

-

Das vollständig ausgefüllte PDF-Formular

+

📋 Ausgefuelltes Schadenprotokoll.pdf

+

Das vollstaendig ausgefuellte PDF-Formular

-
- +
+ +
+
+

Template-Verwaltung

+

Hier kannst du die PDF- und Word-Templates hochladen, die fuer die Generierung verwendet werden.

+
+ +
+

Aktuelle Templates:

+
Lade...
+
+ +
+

Neues Template hochladen:

+
+
+ +

📄 PDF Template

+

Schadenprotokoll-Vorlage

+
+
+ +

📝 Word Template

+

Vorbericht-Vorlage

+
+
+
+
+
+ diff --git a/schadenprotokoll/styles.css b/schadenprotokoll/styles.css index 30e7d59..7811a51 100644 --- a/schadenprotokoll/styles.css +++ b/schadenprotokoll/styles.css @@ -105,3 +105,17 @@ header p { color: var(--text-muted); } .field-list { display: grid; gap: 0.5rem; } footer { text-align: center; margin-top: 3rem; color: var(--text-muted); font-size: 0.875rem; } + +.template-list { margin-bottom: 2rem; } +.template-list h4 { margin-bottom: 1rem; } +.template-item { + display: flex; justify-content: space-between; align-items: center; + padding: 1rem; background: white; border: 1px solid var(--border); + border-radius: 0.5rem; margin-bottom: 0.5rem; +} +.template-item .name { font-weight: 500; } +.template-item .size { color: var(--text-muted); font-size: 0.875rem; } +.template-upload h4 { margin-bottom: 1rem; } +.upload-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; } +.upload-zone.small { padding: 1.5rem; } +.upload-zone.small h3 { font-size: 1rem; }