/* QR-Import im Dialog „Überweisung erfassen“. * * Eine Rechnung oder ein Screenshot wird auf den Bereich neben „Empfänger“ * gezogen (oder per Klick im Dialog aus der Zwischenablage eingefügt). Das * Bild wird im Browser nach einem QR-Code durchsucht (jsQR, siehe jsQR.js); * enthält er einen EPC-QR (GiroCode) oder einen BezahlCode, füllt das Skript * die Felder der Überweisung. Das Bild verlässt den Browser nicht. */ (function () { 'use strict'; // ---- Formate ----------------------------------------------------------- function betragNormalisieren(text) { // „260,61“, „1.234,56“, „260.61“ → „260,61“ let zahl = text.replace(/[^\d.,]/g, ''); if (zahl.indexOf(',') >= 0) { zahl = zahl.replace(/\./g, ''); } else if (/\.\d{3}$/.test(zahl) && zahl.split('.').length > 2) { zahl = zahl.replace(/\./g, ''); } else { zahl = zahl.replace('.', ','); } return zahl; } function datumNormalisieren(text) { // „02.10.2026“ oder „2026-10-02“ → „2026-10-02“ (Wert für ) let treffer = /^(\d{2})\.(\d{2})\.(\d{4})$/.exec(text); if (treffer) { return treffer[3] + '-' + treffer[2] + '-' + treffer[1]; } return /^\d{4}-\d{2}-\d{2}$/.test(text) ? text : ''; } // EPC-QR: zeilenweise, Kennung „BCD“ (European Payments Council, EPC069-12). function epcLesen(text) { const zeilen = text.split(/\r?\n/); if (zeilen[0].trim() !== 'BCD' || zeilen.length < 7) { return null; } const betrag = /^[A-Z]{3}(\d+(?:\.\d{1,2})?)$/.exec((zeilen[7] || '').trim()); return { empfaenger: (zeilen[5] || '').trim(), iban: (zeilen[6] || '').replace(/\s+/g, ''), bic: (zeilen[4] || '').trim(), betrag: betrag ? betragNormalisieren(betrag[1]) : '', // Zeile 10 = Referenz, Zeile 11 = Text; je eines von beiden ist gefüllt. verwendungszweck: ((zeilen[10] || '') || (zeilen[9] || '')).trim(), ausfuehrung_am: '' }; } // BezahlCode: URI „bank://singlepaymentsepa?name=…&iban=…“. Die ältere // Variante „singlepayment“ trägt Kontonummer und BLZ statt IBAN und lässt // sich in eine SEPA-Überweisung nicht verlässlich umrechnen. function bezahlcodeLesen(text) { const treffer = /^bank:\/\/([a-z]+)\?(.*)$/i.exec(text.trim()); if (!treffer || treffer[1].toLowerCase() !== 'singlepaymentsepa') { return null; } const parameter = new URLSearchParams(treffer[2]); const wert = function (name) { return (parameter.get(name) || '').trim(); }; return { empfaenger: wert('name'), iban: wert('iban').replace(/\s+/g, ''), bic: wert('bic'), betrag: wert('amount') ? betragNormalisieren(wert('amount')) : '', verwendungszweck: [wert('reason'), wert('reason1')].filter(Boolean).join(' '), ausfuehrung_am: datumNormalisieren(wert('executiondate')) }; } function zahlungLesen(text) { return epcLesen(text) || bezahlcodeLesen(text); } // ---- Bild → Text ------------------------------------------------------- function qrAusBild(bild) { // Mehrere Maßstäbe: Ein kleiner Code in einem großen Screenshot wird // oft erst beim Vergrößern erkannt, ein sehr feiner Code beim Verkleinern. const faktoren = [1, 0.5, 2, 0.35]; for (let i = 0; i < faktoren.length; i += 1) { const breite = Math.round(bild.width * faktoren[i]); const hoehe = Math.round(bild.height * faktoren[i]); if (breite < 40 || hoehe < 40 || breite * hoehe > 36e6) { continue; } const leinwand = document.createElement('canvas'); leinwand.width = breite; leinwand.height = hoehe; const kontext = leinwand.getContext('2d', { willReadFrequently: true }); kontext.imageSmoothingEnabled = faktoren[i] < 1; kontext.drawImage(bild, 0, 0, breite, hoehe); const daten = kontext.getImageData(0, 0, breite, hoehe); const code = window.jsQR(daten.data, breite, hoehe); if (code && code.data) { return code.data; } } return null; } async function dateiAuslesen(datei) { if (!datei.type.startsWith('image/')) { throw new Error('Nur Bilder werden gelesen (PNG, JPG, WebP …) – bei einem PDF bitte einen Screenshot der Seite einfügen.'); } let bild; try { bild = await createImageBitmap(datei); } catch (fehler) { throw new Error('Das Bild lässt sich nicht öffnen.'); } const text = qrAusBild(bild); bild.close(); if (text === null) { throw new Error('Im Bild wurde kein QR-Code gefunden.'); } const zahlung = zahlungLesen(text); if (zahlung === null) { throw new Error('Der QR-Code ist weder ein EPC-QR (GiroCode) noch ein BezahlCode für eine SEPA-Überweisung.'); } return zahlung; } // ---- Oberfläche -------------------------------------------------------- const bereich = document.querySelector('[data-qrbereich]'); const dialog = document.getElementById('qrdialog'); if (!bereich || !dialog) { return; } const meldung = document.querySelector('[data-qrmeldung]'); const dateiwahl = dialog.querySelector('[data-qrdatei]'); function melden(text, fehler) { meldung.textContent = text; meldung.hidden = text === ''; meldung.className = 'qrmeldung meldung ' + (fehler ? 'meldung-fehler' : 'meldung-erfolg'); } function uebernehmen(zahlung) { const gesetzt = []; Object.keys(zahlung).forEach(function (name) { const feld = document.getElementById(name); if (feld && zahlung[name] !== '') { feld.value = zahlung[name]; feld.dispatchEvent(new Event('change', { bubbles: true })); gesetzt.push(name); } }); // Eine zuvor gewählte Adresse passt nach dem Import nicht mehr zum Namen. const adresse = document.getElementById('adresse'); if (adresse && adresse.value !== '') { adresse.value = ''; } melden('QR-Code gelesen – bitte die Felder prüfen.', false); } async function verarbeiten(datei) { if (dialog.open) { dialog.close(); } melden('Wird gelesen …', false); try { uebernehmen(await dateiAuslesen(datei)); } catch (fehler) { melden(fehler.message, true); } } function dateiAusListe(liste) { for (let i = 0; i < liste.length; i += 1) { const datei = liste[i].kind === undefined ? liste[i] : (liste[i].kind === 'file' ? liste[i].getAsFile() : null); if (datei) { return datei; } } return null; } function oeffnen() { if (!dialog.open) { dialog.showModal(); } } bereich.addEventListener('click', oeffnen); bereich.addEventListener('keydown', function (ereignis) { if (ereignis.key === 'Enter' || ereignis.key === ' ') { ereignis.preventDefault(); oeffnen(); } }); ['dragenter', 'dragover'].forEach(function (name) { bereich.addEventListener(name, function (ereignis) { ereignis.preventDefault(); bereich.classList.add('ueberfahren'); }); }); ['dragleave', 'drop'].forEach(function (name) { bereich.addEventListener(name, function () { bereich.classList.remove('ueberfahren'); }); }); bereich.addEventListener('drop', function (ereignis) { ereignis.preventDefault(); const datei = dateiAusListe(ereignis.dataTransfer.files); if (datei) { verarbeiten(datei); } }); // Einfügen: Strg+V im geöffneten Dialog. dialog.addEventListener('paste', function (ereignis) { const datei = dateiAusListe(ereignis.clipboardData.items); if (datei) { ereignis.preventDefault(); verarbeiten(datei); } }); // Der Dialog nimmt auch eine Datei per Ziehen oder Dateiauswahl. dialog.addEventListener('dragover', function (ereignis) { ereignis.preventDefault(); }); dialog.addEventListener('drop', function (ereignis) { ereignis.preventDefault(); const datei = dateiAusListe(ereignis.dataTransfer.files); if (datei) { verarbeiten(datei); } }); dateiwahl.addEventListener('change', function () { if (dateiwahl.files.length > 0) { verarbeiten(dateiwahl.files[0]); dateiwahl.value = ''; } }); })();