Add event preview with two-step download flow
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
<title>PDF zu ICS (Web)</title>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
|
||||
@@ -13,7 +14,7 @@
|
||||
color: #1f2937;
|
||||
}
|
||||
.wrap {
|
||||
max-width: 520px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 14px 28px;
|
||||
}
|
||||
@@ -24,7 +25,7 @@
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 10px;
|
||||
margin: 0 0 6px;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
p {
|
||||
@@ -43,7 +44,6 @@
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.options {
|
||||
margin: 12px 0;
|
||||
@@ -68,8 +68,14 @@
|
||||
color: #fff;
|
||||
margin-top: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
button:hover { background: #1d4ed8; }
|
||||
button:active { transform: translateY(1px); }
|
||||
button:disabled {
|
||||
background: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.error {
|
||||
margin: 0 0 12px;
|
||||
padding: 10px;
|
||||
@@ -83,38 +89,282 @@
|
||||
font-size: 0.9rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
.hidden { display: none; }
|
||||
.preview-header {
|
||||
margin: 0 0 12px;
|
||||
padding: 12px;
|
||||
background: #f0f9ff;
|
||||
border-radius: 10px;
|
||||
border-left: 3px solid #2563eb;
|
||||
}
|
||||
.preview-meta {
|
||||
font-size: 0.9rem;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
.preview-meta-item {
|
||||
color: #4b5563;
|
||||
}
|
||||
.preview-meta-label {
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
.preview-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.9rem;
|
||||
margin: 12px 0;
|
||||
}
|
||||
.preview-table thead {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
.preview-table th, .preview-table td {
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
.preview-table th {
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
.preview-table tbody tr:hover {
|
||||
background: #fafafa;
|
||||
}
|
||||
.actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
.actions button {
|
||||
margin-top: 0;
|
||||
}
|
||||
.btn-secondary {
|
||||
background: #6b7280;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background: #4b5563;
|
||||
}
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-top-color: #2563eb;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="wrap">
|
||||
<section class="card">
|
||||
<h1>PDF zu ICS Konverter</h1>
|
||||
<p>PDF hochladen, konvertieren und die ICS-Datei direkt herunterladen.</p>
|
||||
<p>PDF hochladen, Vorschau prüfen und herunterladen.</p>
|
||||
|
||||
{% if error %}
|
||||
<div class="error">{{ error }}</div>
|
||||
{% endif %}
|
||||
<div id="error" class="error hidden"></div>
|
||||
|
||||
<form action="/convert" method="post" enctype="multipart/form-data">
|
||||
<!-- UPLOAD-FORMULAR -->
|
||||
<div id="step-init">
|
||||
<label for="file">Dienstplan-PDF</label>
|
||||
<input id="file" name="file" type="file" accept="application/pdf,.pdf" required />
|
||||
<input id="file" type="file" accept="application/pdf,.pdf" />
|
||||
|
||||
<div class="options">
|
||||
<label class="option">
|
||||
<input type="checkbox" name="exclude_rest" value="true" />
|
||||
<input id="exclude_rest" type="checkbox" />
|
||||
Ruhetage ausschließen
|
||||
</label>
|
||||
<label class="option">
|
||||
<input type="checkbox" name="exclude_vacation" value="true" />
|
||||
<input id="exclude_vacation" type="checkbox" />
|
||||
Urlaub ausschließen
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit">ICS erstellen</button>
|
||||
</form>
|
||||
<button type="button" onclick="uploadAndPreview()">
|
||||
Vorschau laden
|
||||
</button>
|
||||
|
||||
<div class="hint">Hinweis: Die Datei wird nur temporär für die Konvertierung verarbeitet.</div>
|
||||
<div class="hint">Hinweis: Die Datei wird nur temporär verarbeitet.</div>
|
||||
</div>
|
||||
|
||||
<!-- PREVIEW -->
|
||||
<div id="step2" class="hidden">
|
||||
<div class="preview-header">
|
||||
<div style="font-weight: 600; font-size: 0.95rem;" id="preview-name"></div>
|
||||
<div class="preview-meta">
|
||||
<div class="preview-meta-item">
|
||||
<div class="preview-meta-label">Personalnummer</div>
|
||||
<div id="preview-personalnummer">—</div>
|
||||
</div>
|
||||
<div class="preview-meta-item">
|
||||
<div class="preview-meta-label">Betriebshof</div>
|
||||
<div id="preview-betriebshof">—</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 8px; color: #4b5563;">
|
||||
<strong id="preview-count"></strong> Ereignisse gefunden
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="preview-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Dienstart</th>
|
||||
<th>Zeit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="preview-events"></tbody>
|
||||
</table>
|
||||
|
||||
<div class="actions">
|
||||
<button type="button" class="btn-secondary" onclick="resetForm()">Erneut hochladen</button>
|
||||
<button type="button" onclick="downloadICS()">ICS herunterladen</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
let currentFile = null;
|
||||
let currentFilename = null;
|
||||
|
||||
async function uploadAndPreview() {
|
||||
const fileInput = document.getElementById("file");
|
||||
currentFile = fileInput.files[0];
|
||||
currentFilename = currentFile?.name;
|
||||
|
||||
if (!currentFile) {
|
||||
showError("Bitte eine PDF-Datei auswählen.");
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = event.target;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> Lädt Vorschau...';
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("file", currentFile);
|
||||
|
||||
const response = await fetch("/preview", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
showError(data.error || "Fehler beim Hochladen");
|
||||
btn.disabled = false;
|
||||
btn.textContent = "Vorschau laden";
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.success) {
|
||||
showError(data.error || "Fehler beim Vorschau-Laden");
|
||||
btn.disabled = false;
|
||||
btn.textContent = "Vorschau laden";
|
||||
return;
|
||||
}
|
||||
|
||||
showPreview(data);
|
||||
hide("step-init");
|
||||
show("step2");
|
||||
} catch (error) {
|
||||
showError(`Fehler: ${error.message}`);
|
||||
btn.disabled = false;
|
||||
btn.textContent = "Vorschau laden";
|
||||
}
|
||||
}
|
||||
|
||||
function showPreview(data) {
|
||||
const { metadata, events } = data;
|
||||
|
||||
document.getElementById("preview-name").textContent = metadata.name;
|
||||
document.getElementById("preview-personalnummer").textContent = metadata.personalnummer;
|
||||
document.getElementById("preview-betriebshof").textContent = metadata.betriebshof;
|
||||
document.getElementById("preview-count").textContent = metadata.count;
|
||||
|
||||
const tbody = document.getElementById("preview-events");
|
||||
tbody.innerHTML = "";
|
||||
events.forEach((event) => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `<td>${event.date}</td><td>${event.service}</td><td>${event.time}</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadICS() {
|
||||
const btn = event.target;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> Download...';
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("file", currentFile);
|
||||
formData.append("exclude_rest", document.getElementById("exclude_rest").checked);
|
||||
formData.append("exclude_vacation", document.getElementById("exclude_vacation").checked);
|
||||
|
||||
const response = await fetch("/convert", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
showError("Fehler beim Download");
|
||||
btn.disabled = false;
|
||||
btn.textContent = "ICS herunterladen";
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = currentFilename.replace(/\.pdf$/i, ".ics");
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
|
||||
resetForm();
|
||||
} catch (error) {
|
||||
showError(`Fehler: ${error.message}`);
|
||||
btn.disabled = false;
|
||||
btn.textContent = "ICS herunterladen";
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
document.getElementById("file").value = "";
|
||||
document.getElementById("exclude_rest").checked = false;
|
||||
document.getElementById("exclude_vacation").checked = false;
|
||||
currentFile = null;
|
||||
currentFilename = null;
|
||||
|
||||
hide("step2");
|
||||
show("step-init");
|
||||
hide("error");
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const el = document.getElementById("error");
|
||||
el.textContent = message;
|
||||
show("error");
|
||||
}
|
||||
|
||||
function show(id) {
|
||||
document.getElementById(id).classList.remove("hidden");
|
||||
}
|
||||
|
||||
function hide(id) {
|
||||
document.getElementById(id).classList.add("hidden");
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user