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:
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Schadenprotokoll API - Flask Backend
|
Schadenprotokoll API - Flask Backend
|
||||||
Endpunkte: /generate, /analyze, /vorbericht
|
Endpunkte: /generate, /analyze, /vorbericht, /templates
|
||||||
"""
|
"""
|
||||||
from flask import Flask, request, jsonify, send_file
|
from flask import Flask, request, jsonify, send_file
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
@@ -12,8 +12,9 @@ from processors import parse_laufzettel, fill_pdf, analyze_pdf, generate_vorberi
|
|||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
CORS(app)
|
CORS(app)
|
||||||
|
|
||||||
TEMPLATE_PDF = "/opt/stacks/spa-hosting/html/schadenprotokoll/templates/protokoll.pdf"
|
TEMPLATES_DIR = "/opt/stacks/spa-hosting/html/schadenprotokoll/templates"
|
||||||
TEMPLATE_DOCX = "/opt/stacks/spa-hosting/html/schadenprotokoll/templates/vorbericht.docx"
|
TEMPLATE_PDF = os.path.join(TEMPLATES_DIR, "protokoll.pdf")
|
||||||
|
TEMPLATE_DOCX = os.path.join(TEMPLATES_DIR, "vorbericht.docx")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/health", methods=["GET"])
|
@app.route("/health", methods=["GET"])
|
||||||
@@ -21,6 +22,47 @@ 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():
|
def generate():
|
||||||
"""Laufzettel.docx -> vorausgefuelltes Protokoll.pdf"""
|
"""Laufzettel.docx -> vorausgefuelltes Protokoll.pdf"""
|
||||||
@@ -33,20 +75,13 @@ def generate():
|
|||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp_in:
|
with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp_in:
|
||||||
file.save(tmp_in.name)
|
file.save(tmp_in.name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = parse_laufzettel(tmp_in.name)
|
data = parse_laufzettel(tmp_in.name)
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp_out:
|
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp_out:
|
||||||
fill_pdf(TEMPLATE_PDF, tmp_out.name, data)
|
fill_pdf(TEMPLATE_PDF, tmp_out.name, data)
|
||||||
|
|
||||||
os.unlink(tmp_in.name)
|
os.unlink(tmp_in.name)
|
||||||
return send_file(
|
return send_file(tmp_out.name, mimetype="application/pdf",
|
||||||
tmp_out.name,
|
as_attachment=True, download_name="Schadenprotokoll_vorbefuellt.pdf")
|
||||||
mimetype="application/pdf",
|
|
||||||
as_attachment=True,
|
|
||||||
download_name="Schadenprotokoll_vorbefuellt.pdf"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
os.unlink(tmp_in.name)
|
os.unlink(tmp_in.name)
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
@@ -64,7 +99,6 @@ def analyze():
|
|||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
|
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
|
||||||
file.save(tmp.name)
|
file.save(tmp.name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = analyze_pdf(tmp.name)
|
result = analyze_pdf(tmp.name)
|
||||||
os.unlink(tmp.name)
|
os.unlink(tmp.name)
|
||||||
@@ -86,20 +120,14 @@ def vorbericht():
|
|||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp_in:
|
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp_in:
|
||||||
file.save(tmp_in.name)
|
file.save(tmp_in.name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
pdf_data = analyze_pdf(tmp_in.name)
|
pdf_data = analyze_pdf(tmp_in.name)
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp_out:
|
with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp_out:
|
||||||
generate_vorbericht(pdf_data, TEMPLATE_DOCX, tmp_out.name)
|
generate_vorbericht(pdf_data, TEMPLATE_DOCX, tmp_out.name)
|
||||||
|
|
||||||
os.unlink(tmp_in.name)
|
os.unlink(tmp_in.name)
|
||||||
return send_file(
|
return send_file(tmp_out.name,
|
||||||
tmp_out.name,
|
|
||||||
mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
as_attachment=True,
|
as_attachment=True, download_name="Vorbericht.docx")
|
||||||
download_name="Vorbericht.docx"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
os.unlink(tmp_in.name)
|
os.unlink(tmp_in.name)
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|||||||
@@ -172,3 +172,79 @@ document.getElementById("vorbericht-btn").addEventListener("click", async () =>
|
|||||||
showStatus("vorbericht-status", "error", `❌ Fehler: ${err.message}`);
|
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);
|
||||||
|
|||||||
@@ -17,26 +17,23 @@
|
|||||||
<button class="tab active" data-tab="generate">1. Generieren</button>
|
<button class="tab active" data-tab="generate">1. Generieren</button>
|
||||||
<button class="tab" data-tab="analyze">2. Auswerten</button>
|
<button class="tab" data-tab="analyze">2. Auswerten</button>
|
||||||
<button class="tab" data-tab="vorbericht">3. Vorbericht</button>
|
<button class="tab" data-tab="vorbericht">3. Vorbericht</button>
|
||||||
|
<button class="tab" data-tab="admin">⚙️ Templates</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab 1: Generieren -->
|
<!-- Tab 1: Generieren -->
|
||||||
<div id="generate" class="tab-content active">
|
<div id="generate" class="tab-content active">
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<h4>Schadenlaufzettel → Protokoll PDF</h4>
|
<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>
|
||||||
|
|
||||||
<div class="upload-zone" id="laufzettel-zone">
|
<div class="upload-zone" id="laufzettel-zone">
|
||||||
<input type="file" id="laufzettel-input" accept=".docx">
|
<input type="file" id="laufzettel-input" accept=".docx">
|
||||||
<h3>📄 Schadenlaufzettel.docx hochladen</h3>
|
<h3>📄 Schadenlaufzettel.docx hochladen</h3>
|
||||||
<p>Datei hierher ziehen oder klicken zum Auswählen</p>
|
<p>Datei hierher ziehen oder klicken</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-info" id="laufzettel-info"></div>
|
<div class="file-info" id="laufzettel-info"></div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn btn-primary" id="generate-btn" disabled>
|
<button class="btn btn-primary" id="generate-btn" disabled>⚙️ PDF generieren</button>
|
||||||
⚙️ PDF generieren
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="status" id="generate-status"></div>
|
<div class="status" id="generate-status"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,21 +41,17 @@
|
|||||||
<!-- Tab 2: Auswerten -->
|
<!-- Tab 2: Auswerten -->
|
||||||
<div id="analyze" class="tab-content">
|
<div id="analyze" class="tab-content">
|
||||||
<div class="info-box">
|
<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>
|
<p>Liest alle Formularfelder und Dropdown-Auswahlen aus dem Schadenprotokoll.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="upload-zone" id="pdf-zone">
|
<div class="upload-zone" id="pdf-zone">
|
||||||
<input type="file" id="pdf-input" accept=".pdf">
|
<input type="file" id="pdf-input" accept=".pdf">
|
||||||
<h3>📋 Schadenprotokoll.pdf hochladen</h3>
|
<h3>📋 Schadenprotokoll.pdf hochladen</h3>
|
||||||
<p>Das ausgefüllte PDF-Formular</p>
|
<p>Das ausgefuellte PDF-Formular</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-info" id="pdf-info"></div>
|
<div class="file-info" id="pdf-info"></div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn btn-primary" id="analyze-btn" disabled>
|
<button class="btn btn-primary" id="analyze-btn" disabled>🔍 Analysieren</button>
|
||||||
🔍 Analysieren
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="status" id="analyze-status"></div>
|
<div class="status" id="analyze-status"></div>
|
||||||
<div class="result" id="analyze-result">
|
<div class="result" id="analyze-result">
|
||||||
@@ -71,24 +64,50 @@
|
|||||||
<div id="vorbericht" class="tab-content">
|
<div id="vorbericht" class="tab-content">
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<h4>Protokoll → Vorbericht Word</h4>
|
<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>
|
||||||
|
|
||||||
<div class="upload-zone" id="vorbericht-zone">
|
<div class="upload-zone" id="vorbericht-zone">
|
||||||
<input type="file" id="vorbericht-input" accept=".pdf">
|
<input type="file" id="vorbericht-input" accept=".pdf">
|
||||||
<h3>📋 Ausgefülltes Schadenprotokoll.pdf</h3>
|
<h3>📋 Ausgefuelltes Schadenprotokoll.pdf</h3>
|
||||||
<p>Das vollständig ausgefüllte PDF-Formular</p>
|
<p>Das vollstaendig ausgefuellte PDF-Formular</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-info" id="vorbericht-info"></div>
|
<div class="file-info" id="vorbericht-info"></div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn btn-primary" id="vorbericht-btn" disabled>
|
<button class="btn btn-primary" id="vorbericht-btn" disabled>📝 Vorbericht erstellen</button>
|
||||||
📝 Vorbericht erstellen
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="status" id="vorbericht-status"></div>
|
<div class="status" id="vorbericht-status"></div>
|
||||||
</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>
|
<footer>
|
||||||
<p>Schadenprotokoll Tool v1.0 | artetui.de</p>
|
<p>Schadenprotokoll Tool v1.0 | artetui.de</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -105,3 +105,17 @@ header p { color: var(--text-muted); }
|
|||||||
.field-list { display: grid; gap: 0.5rem; }
|
.field-list { display: grid; gap: 0.5rem; }
|
||||||
|
|
||||||
footer { text-align: center; margin-top: 3rem; color: var(--text-muted); font-size: 0.875rem; }
|
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; }
|
||||||
|
|||||||
Reference in New Issue
Block a user