284 lines
10 KiB
HTML
284 lines
10 KiB
HTML
{% extends "base.html" %}
|
|
{% set active_page = "scan" %}
|
|
|
|
{% block content %}
|
|
<div class="card">
|
|
<h2>Scan-Upload</h2>
|
|
<p class="text-muted" style="margin-bottom:1rem;">
|
|
Mehrseitige PDF hochladen. Trennseiten mit QR-Code werden automatisch erkannt und die einzelnen Dokumente gesendet.
|
|
</p>
|
|
|
|
<!-- Upload Zone -->
|
|
<div id="uploadZone" class="upload-zone" onclick="document.getElementById('fileInput').click()">
|
|
<div class="upload-icon">📄</div>
|
|
<div class="upload-text">PDF hierher ziehen oder klicken zum Auswählen</div>
|
|
<div class="upload-hint">Unterstützt große Dateien (1 GB+)</div>
|
|
<input type="file" id="fileInput" accept=".pdf,application/pdf" style="display:none;">
|
|
</div>
|
|
|
|
<!-- Upload Progress -->
|
|
<div id="uploadProgress" style="display:none;">
|
|
<div class="progress-header">
|
|
<span id="uploadFilename"></span>
|
|
<span id="uploadPercent">0%</span>
|
|
</div>
|
|
<div class="progress-bar-track">
|
|
<div id="uploadBar" class="progress-bar-fill" style="width:0%"></div>
|
|
</div>
|
|
<div id="uploadStatus" class="text-muted" style="margin-top:0.3rem;font-size:0.85rem;"></div>
|
|
</div>
|
|
|
|
<!-- Processing Progress -->
|
|
<div id="processProgress" style="display:none;">
|
|
<div class="progress-header">
|
|
<span>Verarbeitung</span>
|
|
<span id="processStage"></span>
|
|
</div>
|
|
<div class="progress-bar-track">
|
|
<div id="processBar" class="progress-bar-fill progress-bar-processing" style="width:0%"></div>
|
|
</div>
|
|
<div id="processStatus" class="text-muted" style="margin-top:0.3rem;font-size:0.85rem;"></div>
|
|
</div>
|
|
|
|
<!-- Result -->
|
|
<div id="resultArea" style="display:none;"></div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>Trennseiten</h2>
|
|
<p class="text-muted" style="margin-bottom:1rem;">
|
|
Trennseiten ausdrucken und zwischen die Dokumente legen, bevor der Stapel gescannt wird.
|
|
</p>
|
|
<a href="/api/separator-pdf" class="btn btn-secondary" download>Trennseiten-PDF herunterladen</a>
|
|
</div>
|
|
|
|
<script>
|
|
const CHUNK_SIZE = 10 * 1024 * 1024; // 10 MB
|
|
let currentUploadId = null;
|
|
|
|
// Drag & Drop
|
|
const zone = document.getElementById('uploadZone');
|
|
const fileInput = document.getElementById('fileInput');
|
|
|
|
zone.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
zone.classList.add('upload-zone-active');
|
|
});
|
|
|
|
zone.addEventListener('dragleave', () => {
|
|
zone.classList.remove('upload-zone-active');
|
|
});
|
|
|
|
zone.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
zone.classList.remove('upload-zone-active');
|
|
const files = e.dataTransfer.files;
|
|
if (files.length > 0) handleFile(files[0]);
|
|
});
|
|
|
|
fileInput.addEventListener('change', () => {
|
|
if (fileInput.files.length > 0) handleFile(fileInput.files[0]);
|
|
});
|
|
|
|
function formatBytes(bytes) {
|
|
if (bytes < 1024) return bytes + ' B';
|
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
|
}
|
|
|
|
function generateUUID() {
|
|
return crypto.randomUUID ? crypto.randomUUID() : 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
const r = Math.random() * 16 | 0;
|
|
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
|
});
|
|
}
|
|
|
|
async function handleFile(file) {
|
|
if (!file.name.toLowerCase().endsWith('.pdf')) {
|
|
showResult('Bitte eine PDF-Datei auswählen.', 'error');
|
|
return;
|
|
}
|
|
|
|
// Reset UI
|
|
document.getElementById('resultArea').style.display = 'none';
|
|
document.getElementById('processProgress').style.display = 'none';
|
|
zone.style.display = 'none';
|
|
|
|
const uploadId = generateUUID();
|
|
currentUploadId = uploadId;
|
|
|
|
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
|
|
const uploadProgress = document.getElementById('uploadProgress');
|
|
const uploadBar = document.getElementById('uploadBar');
|
|
const uploadPercent = document.getElementById('uploadPercent');
|
|
const uploadStatus = document.getElementById('uploadStatus');
|
|
const uploadFilename = document.getElementById('uploadFilename');
|
|
|
|
uploadFilename.textContent = file.name + ' (' + formatBytes(file.size) + ')';
|
|
uploadBar.style.width = '0%';
|
|
uploadPercent.textContent = '0%';
|
|
uploadStatus.textContent = 'Upload wird gestartet...';
|
|
uploadProgress.style.display = '';
|
|
|
|
// Chunked upload
|
|
try {
|
|
for (let i = 0; i < totalChunks; i++) {
|
|
const start = i * CHUNK_SIZE;
|
|
const end = Math.min(start + CHUNK_SIZE, file.size);
|
|
const chunk = file.slice(start, end);
|
|
|
|
const formData = new FormData();
|
|
formData.append('file', chunk);
|
|
formData.append('chunk_index', i);
|
|
formData.append('total_chunks', totalChunks);
|
|
formData.append('upload_id', uploadId);
|
|
formData.append('filename', file.name);
|
|
|
|
const resp = await fetch('/api/scan-upload-chunk', {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
|
|
if (!resp.ok) {
|
|
const errData = await resp.json().catch(() => ({}));
|
|
throw new Error(errData.error || 'Upload fehlgeschlagen (HTTP ' + resp.status + ')');
|
|
}
|
|
|
|
const pct = Math.round(((i + 1) / totalChunks) * 100);
|
|
uploadBar.style.width = pct + '%';
|
|
uploadPercent.textContent = pct + '%';
|
|
uploadStatus.textContent = 'Chunk ' + (i + 1) + '/' + totalChunks + ' (' + formatBytes(end) + ' von ' + formatBytes(file.size) + ')';
|
|
}
|
|
|
|
uploadStatus.textContent = 'Upload abgeschlossen. Starte Verarbeitung...';
|
|
startProcessing(uploadId);
|
|
|
|
} catch (e) {
|
|
showResult('Upload-Fehler: ' + e.message, 'error');
|
|
resetUploadZone();
|
|
}
|
|
}
|
|
|
|
async function startProcessing(uploadId) {
|
|
const processProgress = document.getElementById('processProgress');
|
|
const processBar = document.getElementById('processBar');
|
|
const processStage = document.getElementById('processStage');
|
|
const processStatus = document.getElementById('processStatus');
|
|
|
|
processBar.style.width = '0%';
|
|
processStage.textContent = '';
|
|
processStatus.textContent = 'Starte Verarbeitung...';
|
|
processProgress.style.display = '';
|
|
|
|
try {
|
|
const resp = await fetch('/api/scan-process', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ upload_id: uploadId }),
|
|
});
|
|
|
|
if (!resp.ok) {
|
|
const errData = await resp.json().catch(() => ({}));
|
|
throw new Error(errData.error || 'Verarbeitung konnte nicht gestartet werden');
|
|
}
|
|
|
|
// Subscribe to SSE for progress
|
|
listenProgress(uploadId);
|
|
|
|
} catch (e) {
|
|
showResult('Fehler: ' + e.message, 'error');
|
|
resetUploadZone();
|
|
}
|
|
}
|
|
|
|
function listenProgress(uploadId) {
|
|
const processBar = document.getElementById('processBar');
|
|
const processStage = document.getElementById('processStage');
|
|
const processStatus = document.getElementById('processStatus');
|
|
|
|
const evtSource = new EventSource('/api/scan-status/' + uploadId);
|
|
|
|
evtSource.addEventListener('scan', (e) => {
|
|
const data = JSON.parse(e.data);
|
|
const pct = Math.round((data.current / data.total) * 100);
|
|
processBar.style.width = pct + '%';
|
|
processStage.textContent = pct + '%';
|
|
processStatus.textContent = 'Seite ' + data.current + ' von ' + data.total + ' analysiert...';
|
|
});
|
|
|
|
evtSource.addEventListener('status', (e) => {
|
|
const data = JSON.parse(e.data);
|
|
processStatus.textContent = data.message || '';
|
|
});
|
|
|
|
evtSource.addEventListener('send', (e) => {
|
|
const data = JSON.parse(e.data);
|
|
const pct = Math.round((data.current / data.total) * 100);
|
|
processBar.style.width = pct + '%';
|
|
processStage.textContent = data.current + '/' + data.total;
|
|
processStatus.textContent = 'Dokument ' + data.current + ' von ' + data.total + ' wird gesendet...';
|
|
});
|
|
|
|
evtSource.addEventListener('done', (e) => {
|
|
evtSource.close();
|
|
const data = JSON.parse(e.data);
|
|
const result = data.result || {};
|
|
|
|
let msg = '';
|
|
if (result.error) {
|
|
msg = result.error;
|
|
showResult(msg, 'error');
|
|
} else {
|
|
msg = result.documents + ' Dokument(e) erkannt';
|
|
if (result.separator_pages > 0) {
|
|
msg += ' (' + result.separator_pages + ' Trennseite(n))';
|
|
}
|
|
msg += ', ' + result.sent + ' gesendet';
|
|
if (result.errors > 0) {
|
|
msg += ', ' + result.errors + ' Fehler';
|
|
showResult(msg, 'warning');
|
|
} else {
|
|
showResult(msg, 'success');
|
|
}
|
|
}
|
|
|
|
document.getElementById('processProgress').style.display = 'none';
|
|
resetUploadZone();
|
|
});
|
|
|
|
evtSource.addEventListener('error', (e) => {
|
|
// Check if it's an SSE error event from our backend
|
|
try {
|
|
const data = JSON.parse(e.data);
|
|
showResult('Verarbeitungsfehler: ' + (data.message || 'Unbekannter Fehler'), 'error');
|
|
} catch {
|
|
// Connection error
|
|
showResult('Verbindung zum Server verloren.', 'error');
|
|
}
|
|
evtSource.close();
|
|
document.getElementById('processProgress').style.display = 'none';
|
|
resetUploadZone();
|
|
});
|
|
}
|
|
|
|
function showResult(message, type) {
|
|
const area = document.getElementById('resultArea');
|
|
area.innerHTML = '<div class="alert alert-' + type + '">' + escapeHtml(message) + '</div>';
|
|
area.style.display = '';
|
|
document.getElementById('uploadProgress').style.display = 'none';
|
|
}
|
|
|
|
function resetUploadZone() {
|
|
zone.style.display = '';
|
|
fileInput.value = '';
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
const d = document.createElement('div');
|
|
d.textContent = str;
|
|
return d.innerHTML;
|
|
}
|
|
</script>
|
|
{% endblock %}
|