242 lines
8.8 KiB
JavaScript
242 lines
8.8 KiB
JavaScript
/* 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 <input type="date">)
|
||
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 dateiwahl = dialog.querySelector('[data-qrdatei]');
|
||
|
||
// Die Meldung fliegt wie alle anderen oben ins Bild (FmMeldungen, app.js).
|
||
function melden(text, fehler) {
|
||
window.FmMeldungen.zeigen(text, fehler ? 'fehler' : '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();
|
||
}
|
||
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 = '';
|
||
}
|
||
});
|
||
})();
|