diff --git a/schadenprotokoll/api/app.py b/schadenprotokoll/api/app.py index 54ef662..58d06d8 100644 --- a/schadenprotokoll/api/app.py +++ b/schadenprotokoll/api/app.py @@ -1,8 +1,4 @@ #!/usr/bin/env python3 -""" -Schadenprotokoll API - Flask Backend -Endpunkte: /generate, /analyze, /vorbericht, /templates -""" from flask import Flask, request, jsonify, send_file from flask_cors import CORS import tempfile @@ -12,126 +8,92 @@ from processors import parse_laufzettel, fill_pdf, analyze_pdf, generate_vorberi app = Flask(__name__) CORS(app) -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") +TEMPLATES_DIR = '/opt/stacks/spa-hosting/html/schadenprotokoll/templates' -@app.route("/health", methods=["GET"]) +@app.route('/health', methods=['GET']) def health(): - return jsonify({"status": "ok"}) + 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"]) +@app.route('/generate', methods=['POST']) def generate(): - """Laufzettel.docx -> vorausgefuelltes Protokoll.pdf""" - if "file" not in request.files: - return jsonify({"error": "Keine Datei hochgeladen"}), 400 - - file = request.files["file"] - if not file.filename.endswith(".docx"): - return jsonify({"error": "Nur .docx Dateien erlaubt"}), 400 - - 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") - except Exception as e: - os.unlink(tmp_in.name) - return jsonify({"error": str(e)}), 500 + # Akzeptiert template (PDF) und laufzettel (DOCX) + if 'template' not in request.files or 'laufzettel' not in request.files: + return jsonify({'error': 'template und laufzettel erforderlich'}), 400 + + template_file = request.files['template'] + laufzettel_file = request.files['laufzettel'] + + if not template_file.filename.endswith('.pdf'): + return jsonify({'error': 'Template muss PDF sein'}), 400 + if not laufzettel_file.filename.endswith('.docx'): + return jsonify({'error': 'Laufzettel muss DOCX sein'}), 400 + + with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp_tpl: + template_file.save(tmp_tpl.name) + with tempfile.NamedTemporaryFile(suffix='.docx', delete=False) as tmp_lz: + laufzettel_file.save(tmp_lz.name) + try: + data = parse_laufzettel(tmp_lz.name) + with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp_out: + fill_pdf(tmp_tpl.name, tmp_out.name, data) + os.unlink(tmp_tpl.name) + os.unlink(tmp_lz.name) + return send_file(tmp_out.name, mimetype='application/pdf', + as_attachment=True, download_name='Schadenprotokoll_vorbefuellt.pdf') + except Exception as e: + os.unlink(tmp_tpl.name) + os.unlink(tmp_lz.name) + return jsonify({'error': str(e)}), 500 -@app.route("/analyze", methods=["POST"]) +@app.route('/analyze', methods=['POST']) def analyze(): - """Ausgefuelltes PDF -> JSON mit allen Feldern""" - if "file" not in request.files: - return jsonify({"error": "Keine Datei hochgeladen"}), 400 - - file = request.files["file"] - if not file.filename.endswith(".pdf"): - return jsonify({"error": "Nur .pdf Dateien erlaubt"}), 400 - - with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp: + if 'file' not in request.files: + return jsonify({'error': 'Keine Datei'}), 400 + file = request.files['file'] + with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp: file.save(tmp.name) try: result = analyze_pdf(tmp.name) os.unlink(tmp.name) - return jsonify({"success": True, "data": result}) + return jsonify({'success': True, 'data': result}) except Exception as e: os.unlink(tmp.name) - return jsonify({"error": str(e)}), 500 + return jsonify({'error': str(e)}), 500 -@app.route("/vorbericht", methods=["POST"]) +@app.route('/vorbericht', methods=['POST']) def vorbericht(): - """Ausgefuelltes PDF -> Vorbericht.docx""" - if "file" not in request.files: - return jsonify({"error": "Keine Datei hochgeladen"}), 400 - - file = request.files["file"] - if not file.filename.endswith(".pdf"): - return jsonify({"error": "Nur .pdf Dateien erlaubt"}), 400 - - with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp_in: + if 'file' not in request.files: + return jsonify({'error': 'Keine Datei'}), 400 + file = request.files['file'] + template_docx = os.path.join(TEMPLATES_DIR, 'vorbericht.docx') + 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) + 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, - mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document", - as_attachment=True, download_name="Vorbericht.docx") + mimetype='application/vnd.openxmlformats-officedocument.wordprocessingml.document', + as_attachment=True, download_name='Vorbericht.docx') except Exception as e: os.unlink(tmp_in.name) - return jsonify({"error": str(e)}), 500 + return jsonify({'error': str(e)}), 500 -if __name__ == "__main__": - app.run(host="0.0.0.0", port=5050, debug=False) +@app.route('/templates', methods=['GET']) +def list_templates(): + templates = [] + for f in os.listdir(TEMPLATES_DIR): + if not f.endswith('.bak'): + path = os.path.join(TEMPLATES_DIR, f) + templates.append({'name': f, 'size': os.path.getsize(path)}) + return jsonify({'templates': templates}) + + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5050, debug=False) diff --git a/schadenprotokoll/app.js b/schadenprotokoll/app.js index ed59170..04ac15b 100644 --- a/schadenprotokoll/app.js +++ b/schadenprotokoll/app.js @@ -1,171 +1,142 @@ -// Tab Navigation -document.querySelectorAll(".tab").forEach(tab => { - tab.addEventListener("click", () => { - document.querySelectorAll(".tab").forEach(t => t.classList.remove("active")); - document.querySelectorAll(".tab-content").forEach(c => c.classList.remove("active")); - tab.classList.add("active"); - document.getElementById(tab.dataset.tab).classList.add("active"); - if (tab.dataset.tab === "admin") loadTemplates(); - }); -}); - -function setupUploadZone(zoneId, inputId, infoId, btnId) { - const zone = document.getElementById(zoneId); - const input = document.getElementById(inputId); - const info = document.getElementById(infoId); - const btn = document.getElementById(btnId); - if (!zone || !input) return; - - 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; - updateFileInfo(input, info, btn); - } - }); - input.addEventListener("change", () => updateFileInfo(input, info, btn)); -} - -function updateFileInfo(input, info, btn) { - if (input.files.length && info && btn) { - const file = input.files[0]; - info.innerHTML = "OK " + file.name + " (" + (file.size/1024).toFixed(1) + " KB)"; - info.classList.add("visible"); - btn.disabled = false; - } -} - -function showStatus(id, type, message) { - const status = document.getElementById(id); - if (status) { - status.className = "status visible " + type; - status.textContent = message; - } -} - -const API_BASE = "/schadenprotokoll/api"; - -setupUploadZone("laufzettel-zone", "laufzettel-input", "laufzettel-info", "generate-btn"); -setupUploadZone("pdf-zone", "pdf-input", "pdf-info", "analyze-btn"); -setupUploadZone("vorbericht-zone", "vorbericht-input", "vorbericht-info", "vorbericht-btn"); - -document.getElementById("generate-btn").addEventListener("click", async () => { - showStatus("generate-status", "loading", "Verarbeite Laufzettel..."); - const file = document.getElementById("laufzettel-input").files[0]; - const formData = new FormData(); - formData.append("file", file); - - try { - const response = await fetch(API_BASE + "/generate", { method: "POST", body: formData }); - if (!response.ok) throw new Error((await response.json()).error); - const blob = await response.blob(); - const a = document.createElement("a"); - a.href = URL.createObjectURL(blob); - a.download = "Schadenprotokoll_vorbefuellt.pdf"; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - showStatus("generate-status", "success", "PDF erfolgreich generiert!"); - } catch (err) { - showStatus("generate-status", "error", "Fehler: " + err.message); - } -}); - -document.getElementById("analyze-btn").addEventListener("click", async () => { - showStatus("analyze-status", "loading", "Analysiere PDF..."); - const file = document.getElementById("pdf-input").files[0]; - const formData = new FormData(); - formData.append("file", file); - - try { - const response = await fetch(API_BASE + "/analyze", { method: "POST", body: formData }); - if (!response.ok) throw new Error((await response.json()).error); - const result = await response.json(); - displayAnalysisResult(result.data); - showStatus("analyze-status", "success", "Analyse abgeschlossen"); - } catch (err) { - showStatus("analyze-status", "error", "Fehler: " + err.message); - } -}); - -function displayAnalysisResult(data) { - const container = document.getElementById("analyze-data"); - let html = ""; - for (const [key, value] of Object.entries(data.textfields || {})) { - if (value) html += ""; - } - for (const [key, val] of Object.entries(data.dropdowns || {})) { - if (val.selected) html += ""; - } - container.innerHTML = html || "

Keine Felder gefunden.

"; - document.getElementById("analyze-result").classList.add("visible"); -} - -document.getElementById("vorbericht-btn").addEventListener("click", async () => { - showStatus("vorbericht-status", "loading", "Erstelle Vorbericht..."); - const file = document.getElementById("vorbericht-input").files[0]; - const formData = new FormData(); - formData.append("file", file); - - try { - const response = await fetch(API_BASE + "/vorbericht", { method: "POST", body: formData }); - if (!response.ok) throw new Error((await response.json()).error); - const blob = await response.blob(); - const a = document.createElement("a"); - a.href = URL.createObjectURL(blob); - a.download = "Vorbericht.docx"; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - showStatus("vorbericht-status", "success", "Vorbericht erstellt!"); - } catch (err) { - showStatus("vorbericht-status", "error", "Fehler: " + err.message); - } -}); - -function setupTemplateUpload(zoneId, inputId, templateType) { - const zone = document.getElementById(zoneId); - const input = document.getElementById(inputId); - if (!zone || !input) return; - 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) uploadTemplate(e.dataTransfer.files[0], templateType); - }); - input.addEventListener("change", () => { if (input.files.length) uploadTemplate(input.files[0], templateType); }); -} - -async function loadTemplates() { - const container = document.getElementById("templates-container"); - if (!container) return; - try { - const response = await fetch(API_BASE + "/templates"); - const data = await response.json(); - container.innerHTML = data.templates.map(t => - "
" + t.name + "" + (t.size/1024).toFixed(0) + " KB
" - ).join(""); - } catch (err) { container.innerHTML = "Fehler"; } -} - -async function uploadTemplate(file, type) { - showStatus("admin-status", "loading", "Lade 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) throw new Error((await response.json()).error); - showStatus("admin-status", "success", "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"); +document.querySelectorAll('.tab').forEach(tab => { + tab.addEventListener('click', () => { + document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); + document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); + tab.classList.add('active'); + document.getElementById(tab.dataset.tab).classList.add('active'); + }); +}); + +const API_BASE = '/schadenprotokoll/api'; +let templateFile = null; +let laufzettelFile = null; + +function showStatus(id, type, msg) { + const el = document.getElementById(id); + if (el) { el.className = 'status visible ' + type; el.textContent = msg; } +} + +function checkGenerateReady() { + document.getElementById('generate-btn').disabled = !(templateFile && laufzettelFile); +} + +const templateZone = document.getElementById('template-zone'); +const templateInput = document.getElementById('template-input'); +templateZone.addEventListener('click', () => templateInput.click()); +templateZone.addEventListener('dragover', e => { e.preventDefault(); templateZone.classList.add('dragover'); }); +templateZone.addEventListener('dragleave', () => templateZone.classList.remove('dragover')); +templateZone.addEventListener('drop', e => { + e.preventDefault(); templateZone.classList.remove('dragover'); + if (e.dataTransfer.files.length) { templateInput.files = e.dataTransfer.files; handleTemplateSelect(); } +}); +templateInput.addEventListener('change', handleTemplateSelect); + +function handleTemplateSelect() { + if (templateInput.files.length) { + templateFile = templateInput.files[0]; + document.getElementById('template-info').innerHTML = 'OK: ' + templateFile.name + ''; + document.getElementById('template-info').classList.add('visible'); + checkGenerateReady(); + } +} + +const laufzettelZone = document.getElementById('laufzettel-zone'); +const laufzettelInput = document.getElementById('laufzettel-input'); +laufzettelZone.addEventListener('click', () => laufzettelInput.click()); +laufzettelZone.addEventListener('dragover', e => { e.preventDefault(); laufzettelZone.classList.add('dragover'); }); +laufzettelZone.addEventListener('dragleave', () => laufzettelZone.classList.remove('dragover')); +laufzettelZone.addEventListener('drop', e => { + e.preventDefault(); laufzettelZone.classList.remove('dragover'); + if (e.dataTransfer.files.length) { laufzettelInput.files = e.dataTransfer.files; handleLaufzettelSelect(); } +}); +laufzettelInput.addEventListener('change', handleLaufzettelSelect); + +function handleLaufzettelSelect() { + if (laufzettelInput.files.length) { + laufzettelFile = laufzettelInput.files[0]; + document.getElementById('laufzettel-info').innerHTML = 'OK: ' + laufzettelFile.name + ''; + document.getElementById('laufzettel-info').classList.add('visible'); + checkGenerateReady(); + } +} + +document.getElementById('generate-btn').addEventListener('click', async () => { + showStatus('generate-status', 'loading', 'Verarbeite...'); + const formData = new FormData(); + formData.append('template', templateFile); + formData.append('laufzettel', laufzettelFile); + + try { + const response = await fetch(API_BASE + '/generate', { method: 'POST', body: formData }); + if (!response.ok) throw new Error((await response.json()).error); + const blob = await response.blob(); + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = 'Schadenprotokoll_vorbefuellt.pdf'; + document.body.appendChild(a); a.click(); document.body.removeChild(a); + showStatus('generate-status', 'success', 'PDF erfolgreich generiert!'); + } catch (err) { + showStatus('generate-status', 'error', 'Fehler: ' + err.message); + } +}); + +function setupSimpleUpload(zoneId, inputId, infoId, btnId) { + const zone = document.getElementById(zoneId); + const input = document.getElementById(inputId); + if (!zone || !input) return; + 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; updateInfo(); } + }); + input.addEventListener('change', updateInfo); + function updateInfo() { + if (input.files.length) { + document.getElementById(infoId).innerHTML = 'OK: ' + input.files[0].name + ''; + document.getElementById(infoId).classList.add('visible'); + document.getElementById(btnId).disabled = false; + } + } +} + +setupSimpleUpload('pdf-zone', 'pdf-input', 'pdf-info', 'analyze-btn'); +setupSimpleUpload('vorbericht-zone', 'vorbericht-input', 'vorbericht-info', 'vorbericht-btn'); + +document.getElementById('analyze-btn').addEventListener('click', async () => { + showStatus('analyze-status', 'loading', 'Analysiere...'); + const formData = new FormData(); + formData.append('file', document.getElementById('pdf-input').files[0]); + try { + const response = await fetch(API_BASE + '/analyze', { method: 'POST', body: formData }); + if (!response.ok) throw new Error((await response.json()).error); + const result = await response.json(); + let html = ''; + for (const [k, v] of Object.entries(result.data.textfields || {})) { + if (v) html += ''; + } + for (const [k, d] of Object.entries(result.data.dropdowns || {})) { + if (d.selected) html += ''; + } + document.getElementById('analyze-data').innerHTML = html || 'Keine Daten'; + document.getElementById('analyze-result').classList.add('visible'); + showStatus('analyze-status', 'success', 'Fertig'); + } catch (err) { showStatus('analyze-status', 'error', 'Fehler: ' + err.message); } +}); + +document.getElementById('vorbericht-btn').addEventListener('click', async () => { + showStatus('vorbericht-status', 'loading', 'Erstelle Vorbericht...'); + const formData = new FormData(); + formData.append('file', document.getElementById('vorbericht-input').files[0]); + try { + const response = await fetch(API_BASE + '/vorbericht', { method: 'POST', body: formData }); + if (!response.ok) throw new Error((await response.json()).error); + const blob = await response.blob(); + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = 'Vorbericht.docx'; + document.body.appendChild(a); a.click(); document.body.removeChild(a); + showStatus('vorbericht-status', 'success', 'Vorbericht erstellt!'); + } catch (err) { showStatus('vorbericht-status', 'error', 'Fehler: ' + err.message); } +}); diff --git a/schadenprotokoll/index.html b/schadenprotokoll/index.html index e6df99d..d53c034 100644 --- a/schadenprotokoll/index.html +++ b/schadenprotokoll/index.html @@ -17,23 +17,33 @@ -
-

Schadenlaufzettel → Protokoll PDF

-

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

+

Schadenlaufzettel + PDF Template = Ausgefuelltes Protokoll

+

Waehle beide Dateien aus und klicke auf Generieren.

-
- -

📄 Schadenlaufzettel.docx hochladen

-

Datei hierher ziehen oder klicken

+ +
+
+ +

1. PDF Template

+

Schadenprotokoll-Vorlage.pdf

+
+
+ +

2. Laufzettel

+

Schadenlaufzettel.docx

+
+ +
+
- +
@@ -42,16 +52,16 @@

Ausgefuelltes PDF analysieren

-

Liest alle Formularfelder und Dropdown-Auswahlen aus dem Schadenprotokoll.

+

Liest alle Formularfelder und Dropdown-Auswahlen aus.

-

📋 Schadenprotokoll.pdf hochladen

+

Schadenprotokoll.pdf

Das ausgefuellte PDF-Formular

- +
@@ -63,53 +73,23 @@
-

Protokoll → Vorbericht Word

-

Generiert einen strukturierten Vorbericht aus dem ausgefuellten Schadenprotokoll.

+

Protokoll zu Vorbericht Word

+

Generiert einen strukturierten Vorbericht aus dem ausgefuellten PDF.

-

📋 Ausgefuelltes Schadenprotokoll.pdf

-

Das vollstaendig ausgefuellte PDF-Formular

+

Ausgefuelltes Schadenprotokoll.pdf

+

Das vollstaendig ausgefuellte PDF

- +
- -
-
-

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/templates/protokoll.pdf b/schadenprotokoll/templates/protokoll.pdf index af1c2e6..744e5e8 100644 Binary files a/schadenprotokoll/templates/protokoll.pdf and b/schadenprotokoll/templates/protokoll.pdf differ