Add template upload functionality to Schadenprotokoll

- 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 <noreply@anthropic.com>
This commit is contained in:
2025-12-21 11:54:00 +00:00
parent c6f086a0a9
commit a9eaa81c37
4 changed files with 180 additions and 43 deletions

View File

@@ -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

View File

@@ -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 = "<p>Keine Templates vorhanden.</p>";
return;
}
container.innerHTML = data.templates.map(t => `
<div class="template-item">
<span class="name">${t.type === "pdf" ? "📄" : "📝"} ${t.name}</span>
<span class="size">${(t.size / 1024).toFixed(1)} KB</span>
</div>
`).join("");
} catch (err) {
document.getElementById("templates-container").innerHTML =
`<p style="color:red;">Fehler: ${err.message}</p>`;
}
}
// 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);

View File

@@ -17,26 +17,23 @@
<button class="tab active" data-tab="generate">1. Generieren</button>
<button class="tab" data-tab="analyze">2. Auswerten</button>
<button class="tab" data-tab="vorbericht">3. Vorbericht</button>
<button class="tab" data-tab="admin">⚙️ Templates</button>
</div>
<!-- Tab 1: Generieren -->
<div id="generate" class="tab-content active">
<div class="info-box">
<h4>Schadenlaufzettel → Protokoll PDF</h4>
<p>Lädt Daten aus dem Schadenlaufzettel.docx und füllt das Schadenprotokoll-PDF vor.</p>
<p>Laedt Daten aus dem Schadenlaufzettel.docx und fuellt das Schadenprotokoll-PDF vor.</p>
</div>
<div class="upload-zone" id="laufzettel-zone">
<input type="file" id="laufzettel-input" accept=".docx">
<h3>📄 Schadenlaufzettel.docx hochladen</h3>
<p>Datei hierher ziehen oder klicken zum Auswählen</p>
<p>Datei hierher ziehen oder klicken</p>
</div>
<div class="file-info" id="laufzettel-info"></div>
<div class="actions">
<button class="btn btn-primary" id="generate-btn" disabled>
⚙️ PDF generieren
</button>
<button class="btn btn-primary" id="generate-btn" disabled>⚙️ PDF generieren</button>
</div>
<div class="status" id="generate-status"></div>
</div>
@@ -44,21 +41,17 @@
<!-- Tab 2: Auswerten -->
<div id="analyze" class="tab-content">
<div class="info-box">
<h4>Ausgefülltes PDF analysieren</h4>
<h4>Ausgefuelltes PDF analysieren</h4>
<p>Liest alle Formularfelder und Dropdown-Auswahlen aus dem Schadenprotokoll.</p>
</div>
<div class="upload-zone" id="pdf-zone">
<input type="file" id="pdf-input" accept=".pdf">
<h3>📋 Schadenprotokoll.pdf hochladen</h3>
<p>Das ausgefüllte PDF-Formular</p>
<p>Das ausgefuellte PDF-Formular</p>
</div>
<div class="file-info" id="pdf-info"></div>
<div class="actions">
<button class="btn btn-primary" id="analyze-btn" disabled>
🔍 Analysieren
</button>
<button class="btn btn-primary" id="analyze-btn" disabled>🔍 Analysieren</button>
</div>
<div class="status" id="analyze-status"></div>
<div class="result" id="analyze-result">
@@ -71,24 +64,50 @@
<div id="vorbericht" class="tab-content">
<div class="info-box">
<h4>Protokoll → Vorbericht Word</h4>
<p>Generiert einen strukturierten Vorbericht aus dem ausgefüllten Schadenprotokoll.</p>
<p>Generiert einen strukturierten Vorbericht aus dem ausgefuellten Schadenprotokoll.</p>
</div>
<div class="upload-zone" id="vorbericht-zone">
<input type="file" id="vorbericht-input" accept=".pdf">
<h3>📋 Ausgefülltes Schadenprotokoll.pdf</h3>
<p>Das vollständig ausgefüllte PDF-Formular</p>
<h3>📋 Ausgefuelltes Schadenprotokoll.pdf</h3>
<p>Das vollstaendig ausgefuellte PDF-Formular</p>
</div>
<div class="file-info" id="vorbericht-info"></div>
<div class="actions">
<button class="btn btn-primary" id="vorbericht-btn" disabled>
📝 Vorbericht erstellen
</button>
<button class="btn btn-primary" id="vorbericht-btn" disabled>📝 Vorbericht erstellen</button>
</div>
<div class="status" id="vorbericht-status"></div>
</div>
<!-- Tab 4: Admin/Templates -->
<div id="admin" class="tab-content">
<div class="info-box">
<h4>Template-Verwaltung</h4>
<p>Hier kannst du die PDF- und Word-Templates hochladen, die fuer die Generierung verwendet werden.</p>
</div>
<div class="template-list" id="template-list">
<h4>Aktuelle Templates:</h4>
<div id="templates-container">Lade...</div>
</div>
<div class="template-upload">
<h4>Neues Template hochladen:</h4>
<div class="upload-row">
<div class="upload-zone small" id="pdf-template-zone">
<input type="file" id="pdf-template-input" accept=".pdf">
<h3>📄 PDF Template</h3>
<p>Schadenprotokoll-Vorlage</p>
</div>
<div class="upload-zone small" id="docx-template-zone">
<input type="file" id="docx-template-input" accept=".docx">
<h3>📝 Word Template</h3>
<p>Vorbericht-Vorlage</p>
</div>
</div>
</div>
<div class="status" id="admin-status"></div>
</div>
<footer>
<p>Schadenprotokoll Tool v1.0 | artetui.de</p>
</footer>

View File

@@ -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; }