diff --git a/.gitignore b/.gitignore index 5770d5a..7cae4a4 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ kostenschaetzung/ schadendokumentation/ zeitwert/ .claude/ +__pycache__/ diff --git a/schadenprotokoll/api/app.py b/schadenprotokoll/api/app.py new file mode 100644 index 0000000..b1bbf84 --- /dev/null +++ b/schadenprotokoll/api/app.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +""" +Schadenprotokoll API - Flask Backend +Endpunkte: /generate, /analyze, /vorbericht +""" +from flask import Flask, request, jsonify, send_file +from flask_cors import CORS +import tempfile +import os +from processors import parse_laufzettel, fill_pdf, analyze_pdf, generate_vorbericht + +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" + + +@app.route("/health", methods=["GET"]) +def health(): + return jsonify({"status": "ok"}) + + +@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 + + +@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: + file.save(tmp.name) + + try: + result = analyze_pdf(tmp.name) + os.unlink(tmp.name) + return jsonify({"success": True, "data": result}) + except Exception as e: + os.unlink(tmp.name) + return jsonify({"error": str(e)}), 500 + + +@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: + 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, + 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 + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5050, debug=False) diff --git a/schadenprotokoll/api/processors.py b/schadenprotokoll/api/processors.py new file mode 100644 index 0000000..8eabd18 --- /dev/null +++ b/schadenprotokoll/api/processors.py @@ -0,0 +1,136 @@ +""" +Schadenprotokoll Processors - PDF/DOCX Verarbeitung +""" +from docx import Document +from docx.oxml.ns import qn +from pypdf import PdfReader, PdfWriter +import zipfile +import xml.etree.ElementTree as ET + + +def parse_laufzettel(docx_path: str) -> dict: + """Extrahiert Schadensdaten aus dem Laufzettel.docx""" + data = {} + + with zipfile.ZipFile(docx_path, "r") as z: + xml_content = z.read("word/document.xml") + root = ET.fromstring(xml_content) + + ns = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"} + + for sdt in root.findall(".//w:sdt", ns): + alias = sdt.find(".//w:alias", ns) + if alias is not None: + alias_val = alias.get(qn("w:val")) + texts = sdt.findall(".//w:t", ns) + content = " ".join([t.text for t in texts if t.text]).strip() + + field_map = { + "Schadennummer": "Schadennummer", + "Versicherungsnehmer": "Versicherungsnehmer:", + "Versicherer": "Versicherer:", + "Datum": "Datum:", + "Schadenort": "Schadenort:", + "Schadenart": "Schadenart:", + "e-Mail": "e-Mail VN:", + } + + if alias_val in field_map: + data[field_map[alias_val]] = content + elif alias_val == "Straße": + data["Adresse:"] = content + elif alias_val == "Ort": + if "Adresse:" in data: + data["Adresse:"] += ", " + content + else: + data["Adresse:"] = content + + return data + + +def fill_pdf(template_path: str, output_path: str, data: dict) -> str: + """Befuellt das PDF-Template mit den Schadensdaten""" + reader = PdfReader(template_path) + writer = PdfWriter() + + for page in reader.pages: + writer.add_page(page) + if "/Annots" in page: + writer.update_page_form_field_values( + writer.pages[-1], data, auto_regenerate=False + ) + + with open(output_path, "wb") as f: + writer.write(f) + + return output_path + + +def analyze_pdf(pdf_path: str) -> dict: + """Liest alle Formularfelder aus dem ausgefuellten PDF""" + reader = PdfReader(pdf_path) + result = {"textfields": {}, "dropdowns": {}, "checkboxes": {}} + + for page_num, page in enumerate(reader.pages): + if "/Annots" not in page: + continue + + for annot in page["/Annots"]: + obj = annot.get_object() + field_type = obj.get("/FT", "") + field_name = obj.get("/T", f"Feld_{page_num}") + + if field_type == "/Tx": + value = obj.get("/V", "") + result["textfields"][field_name] = str(value) if value else "" + + elif field_type == "/Ch": + value = obj.get("/V", "") + options = [] + if "/Opt" in obj: + for opt in obj["/Opt"]: + if isinstance(opt, list): + options.append(str(opt[1]) if len(opt) > 1 else str(opt[0])) + else: + options.append(str(opt)) + result["dropdowns"][field_name] = { + "selected": str(value) if value else "", + "options": options + } + + elif field_type == "/Btn": + value = obj.get("/V", "/Off") + result["checkboxes"][field_name] = value != "/Off" + + return result + + +def generate_vorbericht(pdf_data: dict, template_path: str, output_path: str) -> str: + """Generiert Vorbericht.docx aus PDF-Daten""" + doc = Document(template_path) + + tf = pdf_data.get("textfields", {}) + replacements = { + "Schadennummer": tf.get("Schadennummer", "xx"), + "Versicherer": tf.get("Versicherer:", "xx"), + "Versicherungsnehmer": tf.get("Versicherungsnehmer:", "xx"), + "Adresse": tf.get("Adresse:", "xx"), + } + + # Dropdown-Antworten fuer Sachverhalt sammeln + dropdowns = pdf_data.get("dropdowns", {}) + sachverhalt = [] + for key in sorted(dropdowns.keys()): + val = dropdowns[key].get("selected", "") + if val and val not in ["Was ist beschädigt?", "Schadenursache?", ""]: + sachverhalt.append(val) + + for para in doc.paragraphs: + text = para.text + for key, value in replacements.items(): + if key.lower() in text.lower() and "xx" in text: + para.text = text.replace("xx", value, 1) + break + + doc.save(output_path) + return output_path diff --git a/schadenprotokoll/app.js b/schadenprotokoll/app.js new file mode 100644 index 0000000..6a99b30 --- /dev/null +++ b/schadenprotokoll/app.js @@ -0,0 +1,174 @@ +// 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"); + }); +}); + +// Upload Zone Setup +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); + + 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) { + const file = input.files[0]; + info.innerHTML = `✓ ${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); + status.className = `status visible ${type}`; + status.textContent = message; +} + +// Setup all upload zones +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"); + +// API Base URL +const API_BASE = "/schadenprotokoll/api"; + +// Generate Button +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) { + const err = await response.json(); + throw new Error(err.error || "Unbekannter Fehler"); + } + + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "Schadenprotokoll_vorbefuellt.pdf"; + a.click(); + URL.revokeObjectURL(url); + + showStatus("generate-status", "success", "✓ PDF erfolgreich generiert und heruntergeladen"); + } catch (err) { + showStatus("generate-status", "error", `❌ Fehler: ${err.message}`); + } +}); + +// Analyze Button +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) { + const err = await response.json(); + throw new Error(err.error || "Unbekannter Fehler"); + } + + 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 = ""; + + // Textfelder + if (Object.keys(data.textfields).length > 0) { + html += `
Keine ausgefüllten Felder gefunden.
"; + document.getElementById("analyze-result").classList.add("visible"); +} + +// Vorbericht Button +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) { + const err = await response.json(); + throw new Error(err.error || "Unbekannter Fehler"); + } + + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "Vorbericht.docx"; + a.click(); + URL.revokeObjectURL(url); + + showStatus("vorbericht-status", "success", "✓ Vorbericht erfolgreich erstellt und heruntergeladen"); + } catch (err) { + showStatus("vorbericht-status", "error", `❌ Fehler: ${err.message}`); + } +}); diff --git a/schadenprotokoll/index.html b/schadenprotokoll/index.html new file mode 100644 index 0000000..a211a93 --- /dev/null +++ b/schadenprotokoll/index.html @@ -0,0 +1,99 @@ + + + + + +Automatisierte Verarbeitung von Schadenprotokollen
+Lädt Daten aus dem Schadenlaufzettel.docx und füllt das Schadenprotokoll-PDF vor.
+Datei hierher ziehen oder klicken zum Auswählen
+Liest alle Formularfelder und Dropdown-Auswahlen aus dem Schadenprotokoll.
+Das ausgefüllte PDF-Formular
+Generiert einen strukturierten Vorbericht aus dem ausgefüllten Schadenprotokoll.
+Das vollständig ausgefüllte PDF-Formular
+