Files
2026-05-22 08:13:05 +02:00

115 lines
3.7 KiB
HTML

{% extends "base.html" %}
{% block content %}
<h1>Neuer Serienbrief</h1>
<p>
Vorlage und Empfänger-CSV auswählen. Die Spaltennamen der CSV müssen mit den
Platzhaltern der Vorlage übereinstimmen (erste Zeile = Spaltennamen).
Erstelle vorab eine <strong>Vorschau</strong>, um die Ausgabe mit der ersten
Datenzeile zu prüfen.
</p>
{% if preview_error %}
<div class="messages">
<ul><li>{{ preview_error }}</li></ul>
</div>
{% endif %}
<form id="job-form" method="post" action="{% url 'job-create' %}" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<div class="form-actions">
<button type="button" id="btn-preview" class="btn btn-secondary">
Vorschau (erste Zeile)
</button>
<button type="submit" class="btn">
Job starten
</button>
</div>
</form>
<section id="preview-section" hidden>
<h2>Vorschau</h2>
<p id="preview-status" class="muted"></p>
<iframe id="preview-frame"
title="Vorschau-PDF"
style="width:100%; height:70vh; border:1px solid #ddd; border-radius:4px;"></iframe>
</section>
<script>
(function () {
const form = document.getElementById("job-form");
const btn = document.getElementById("btn-preview");
const section = document.getElementById("preview-section");
const status = document.getElementById("preview-status");
const frame = document.getElementById("preview-frame");
const previewUrl = "{% url 'job-preview' %}";
const csrfToken = form.querySelector("[name=csrfmiddlewaretoken]").value;
let lastBlobUrl = null;
btn.addEventListener("click", async function () {
btn.disabled = true;
const originalText = btn.textContent;
btn.textContent = "Erzeuge Vorschau…";
section.hidden = false;
status.textContent = "Bitte warten, LibreOffice rendert die erste Zeile…";
status.classList.remove("error");
try {
const formData = new FormData(form);
const resp = await fetch(previewUrl, {
method: "POST",
body: formData,
headers: { "X-CSRFToken": csrfToken },
credentials: "same-origin",
});
if (!resp.ok) {
// Server hat das Form-Template zurückgegeben → Fehlertext extrahieren
const text = await resp.text();
const match = text.match(/<li>([\s\S]*?)<\/li>/);
const msg = match ? match[1].trim() : `Vorschau fehlgeschlagen (HTTP ${resp.status}).`;
status.textContent = msg;
status.classList.add("error");
frame.removeAttribute("src");
return;
}
const ct = resp.headers.get("Content-Type") || "";
if (!ct.includes("application/pdf")) {
status.textContent = "Unerwartete Antwort vom Server (kein PDF).";
status.classList.add("error");
return;
}
const blob = await resp.blob();
if (lastBlobUrl) URL.revokeObjectURL(lastBlobUrl);
lastBlobUrl = URL.createObjectURL(blob);
frame.src = lastBlobUrl;
const extra = resp.headers.get("X-Preview-Extra-Columns");
let hint = "Vorschau bereit. Wenn alles passt, oben auf »Job starten« klicken.";
if (extra && extra.trim().length > 0) {
hint += " Hinweis: CSV enthält Spalten, die im Template nicht verwendet werden: " + extra + ".";
}
status.textContent = hint;
} catch (err) {
status.textContent = "Netzwerk- oder Browser-Fehler: " + err.message;
status.classList.add("error");
} finally {
btn.disabled = false;
btn.textContent = originalText;
}
});
})();
</script>
<style>
.form-actions { display: flex; gap: 0.75rem; margin-top: 1rem; }
.btn-secondary { background: #6b7280; }
.muted { color: #6b7280; font-size: 0.95rem; }
.muted.error { color: #dc2626; }
</style>
{% endblock %}