/** * Finanz Flow Mobile - Rechnungs-Scanner App * PWA mit Kamera-Zugriff und OCR-Integration */ // API Konfiguration let API_URL = localStorage.getItem('apiUrl') || 'http://192.168.0.141/api'; // DOM Elemente const video = document.getElementById('video'); const canvas = document.getElementById('canvas'); const preview = document.getElementById('preview'); const captureBtn = document.getElementById('capture-btn'); const retakeBtn = document.getElementById('retake-btn'); const scannerCard = document.getElementById('scanner-card'); const ocrCard = document.getElementById('ocr-card'); const loading = document.getElementById('loading'); const successMessage = document.getElementById('success-message'); // Aktueller OCR-Daten let currentOcrData = null; let currentImageBlob = null; // Service Worker Registration if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js') .then(reg => console.log('SW registered:', reg)) .catch(err => console.log('SW registration failed:', err)); } // Initialisierung document.addEventListener('DOMContentLoaded', () => { document.getElementById('api-url').value = API_URL; loadRecentDocuments(); initCamera(); }); // API Config speichern function saveApiConfig() { API_URL = document.getElementById('api-url').value.trim(); localStorage.setItem('apiUrl', API_URL); alert('✅ API-URL gespeichert: ' + API_URL); } // Kamera initialisieren async function initCamera() { try { const constraints = { video: { facingMode: 'environment', // Rückkamera bevorzugen width: { ideal: 1920 }, height: { ideal: 1080 } }, audio: false }; const stream = await navigator.mediaDevices.getUserMedia(constraints); video.srcObject = stream; } catch (err) { console.error('Kamera-Zugriff fehlgeschlagen:', err); // Fallback: Nur Datei-Upload zeigen document.getElementById('camera-container').style.display = 'none'; captureBtn.style.display = 'none'; alert('⚠️ Kamera nicht verfügbar. Bitte Datei-Upload nutzen.'); } } // Foto aufnehmen function capturePhoto() { const ctx = canvas.getContext('2d'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; ctx.drawImage(video, 0, 0); // Canvas zu Blob konvertieren canvas.toBlob((blob) => { currentImageBlob = blob; const url = URL.createObjectURL(blob); preview.src = url; preview.classList.remove('hidden'); video.parentElement.classList.add('hidden'); captureBtn.classList.add('hidden'); retakeBtn.classList.remove('hidden'); // OCR starten performOCR(blob); }, 'image/jpeg', 0.9); } // Neu aufnehmen function retakePhoto() { preview.classList.add('hidden'); preview.src = ''; video.parentElement.classList.remove('hidden'); captureBtn.classList.remove('hidden'); retakeBtn.classList.add('hidden'); ocrCard.classList.add('hidden'); currentOcrData = null; currentImageBlob = null; } // Datei-Upload verarbeiten function handleFileSelect(event) { const file = event.target.files[0]; if (!file) return; if (file.type === 'application/pdf') { // PDF-Verarbeitung currentImageBlob = file; uploadDocument(file, 'application/pdf'); } else { // Bild-Verarbeitung currentImageBlob = file; const url = URL.createObjectURL(file); preview.src = url; preview.classList.remove('hidden'); video.parentElement.classList.add('hidden'); captureBtn.classList.add('hidden'); retakeBtn.classList.remove('hidden'); performOCR(file); } } // OCR durchführen (simuliert - Backend würde echtes OCR machen) async function performOCR(imageBlob) { loading.classList.add('active'); try { // Im echten Betrieb: Bild zum Backend senden für OCR // Simulierte OCR-Ergebnisse für Demo await new Promise(resolve => setTimeout(resolve, 1500)); // Demo-Daten (würde vom Backend kommen) currentOcrData = { haendler: 'ASUSTeK Computer Inc.', datum: new Date().toISOString().split('T')[0], brutto: '2799.00', mwst: '446.90', netto: '2352.10', mwst_satz: '19', belegnummer: 'RE-2026-' + Math.floor(Math.random() * 10000) }; // OCR-Ergebnisse anzeigen document.getElementById('ocr-händler').textContent = currentOcrData.haendler; document.getElementById('ocr-datum').textContent = currentOcrData.datum; document.getElementById('ocr-brutto').textContent = formatCurrency(currentOcrData.brutto); document.getElementById('ocr-mwst').textContent = formatCurrency(currentOcrData.mwst); document.getElementById('ocr-netto').textContent = formatCurrency(currentOcrData.netto); // Kategorie vorschlagen basierend auf OCR suggestCategory(currentOcrData); ocrCard.classList.remove('hidden'); } catch (err) { console.error('OCR Fehler:', err); alert('❌ OCR fehlgeschlagen: ' + err.message); } finally { loading.classList.remove('active'); } } // Kategorie vorschlagen function suggestCategory(ocrData) { const kategorieSelect = document.getElementById('kategorie'); const haendler = ocrData.haendler.toLowerCase(); // Einfache Heuristik if (haendler.includes('asus') || haendler.includes('computer') || haendler.includes('hardware')) { kategorieSelect.value = 'hardware'; } else if (haendler.includes('amazon') || haendler.includes('büro')) { kategorieSelect.value = 'büro'; } else if (haendler.includes('werkzeug') || haendler.includes('bau')) { kategorieSelect.value = 'werkzeug'; } } // Dokument speichern async function saveDocument() { if (!currentOcrData || !currentImageBlob) { alert('⚠️ Bitte erst ein Bild aufnehmen'); return; } const kategorie = document.getElementById('kategorie').value; if (!kategorie) { alert('⚠️ Bitte Kategorie wählen'); return; } const notiz = document.getElementById('notiz').value; const formData = new FormData(); formData.append('file', currentImageBlob, 'rechnung.jpg'); formData.append('kategorie', kategorie); formData.append('betrag', currentOcrData.brutto); formData.append('datum', currentOcrData.datum); formData.append('haendler', currentOcrData.haendler); formData.append('mwst', currentOcrData.mwst); formData.append('notiz', notiz); try { const response = await fetch(`${API_URL}/documents`, { method: 'POST', body: formData }); if (!response.ok) { throw new Error('HTTP ' + response.status); } const result = await response.json(); console.log('Gespeichert:', result); // Erfolg anzeigen successMessage.classList.add('active'); setTimeout(() => successMessage.classList.remove('active'), 3000); // Reset retakePhoto(); document.getElementById('kategorie').value = ''; document.getElementById('notiz').value = ''; // Liste aktualisieren loadRecentDocuments(); } catch (err) { console.error('Speichern fehlgeschlagen:', err); // Demo-Modus: Lokal speichern saveLocally(); } } // Lokales Speichern (Demo-Modus) function saveLocally() { const documents = JSON.parse(localStorage.getItem('documents') || '[]'); documents.unshift({ id: Date.now(), ...currentOcrData, kategorie: document.getElementById('kategorie').value, notiz: document.getElementById('notiz').value, timestamp: new Date().toISOString() }); localStorage.setItem('documents', JSON.stringify(documents.slice(0, 50))); successMessage.classList.add('active'); setTimeout(() => successMessage.classList.remove('active'), 3000); retakePhoto(); document.getElementById('kategorie').value = ''; document.getElementById('notiz').value = ''; loadRecentDocuments(); } // Letzte Dokumente laden async function loadRecentDocuments() { // Versuche vom Backend zu laden try { const response = await fetch(`${API_URL}/documents?limit=5`); if (response.ok) { const documents = await response.json(); renderDocuments(documents); return; } } catch (err) { console.log('Backend nicht erreichbar, nutze lokale Daten'); } // Fallback: Lokale Daten const documents = JSON.parse(localStorage.getItem('documents') || '[]'); renderDocuments(documents.slice(0, 5)); } // Dokumente rendern function renderDocuments(documents) { const container = document.getElementById('recent-list'); if (documents.length === 0) { container.innerHTML = '

Noch keine Dokumente

'; return; } const html = documents.map(doc => `
${doc.haendler || 'Unbekannt'}
${doc.datum} • ${getKategorieLabel(doc.kategorie)}
${formatCurrency(doc.brutto || doc.betrag)}
`).join(''); container.innerHTML = html; } // Kategorie-Label function getKategorieLabel(value) { const labels = { hardware: 'Hardware', büro: 'Büro', fahrzeug: 'Fahrzeug', werkzeug: 'Werkzeug', software: 'Software', dienstleistung: 'Dienstleistung', material: 'Material', sonstiges: 'Sonstiges' }; return labels[value] || value; } // Währung formatieren function formatCurrency(value) { const num = parseFloat(value); if (isNaN(num)) return '-'; return num.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' }); } // Tab-Navigation function showTab(tab) { document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active')); event.currentTarget.classList.add('active'); // Hier könnte man zwischen verschiedenen Views wechseln if (tab === 'scan') { document.getElementById('scanner-card').classList.remove('hidden'); document.getElementById('ocr-card').classList.add('hidden'); } } // Touch-Gesten für schnelles Scannen document.addEventListener('touchstart', handleTouchStart, false); document.addEventListener('touchend', handleTouchEnd, false); let touchStartY = 0; function handleTouchStart(e) { touchStartY = e.touches[0].clientY; } function handleTouchEnd(e) { const touchEndY = e.changedTouches[0].clientY; const diff = touchStartY - touchEndY; // Nach unten wischen = Neuladen if (diff < -100) { loadRecentDocuments(); } }