first commit
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
{% 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 an LexOffice 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 + ' an LexOffice 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 %}
|
||||
Reference in New Issue
Block a user