'use strict'; (function () { const connections = JSON.parse(document.getElementById('fints-data').dataset.connections); const csrfToken = (document.querySelector('meta[name="csrf-token"]') || {}).content || ''; // ── NEU-Modal ──────────────────────────────────────────────── const modalNew = document.getElementById('modal-new-connection'); const btnNew = document.getElementById('btn-new-connection'); const btnClose = document.getElementById('btn-modal-close'); const btnCancel = document.getElementById('btn-modal-cancel'); const selectNew = document.getElementById('fc-provider'); const apiNew = document.getElementById('api-fields'); const fintsNew = document.getElementById('fints-fields'); const csvNew = document.getElementById('csv-fields'); const newBankWrap = document.getElementById('fc-new-bank-wrap'); const newBankInput = document.getElementById('fc-new-bank-name'); function openNew() { modalNew.style.display = 'flex'; } function closeNew() { modalNew.style.display = 'none'; } if (btnNew) btnNew.addEventListener('click', openNew); if (btnClose) btnClose.addEventListener('click', closeNew); if (btnCancel) btnCancel.addEventListener('click', closeNew); modalNew.addEventListener('click', (e) => { if (e.target === modalNew) closeNew(); }); // ── Bankleitzahlen-Verzeichnis: Suche + Formular vorbefüllen (Fix #187) ── (function () { const blzField = document.getElementById('fc-blz'); const urlField = document.getElementById('fc-fints-url'); const searchInput = document.getElementById('fc-blz-search'); const resultsBox = document.getElementById('fc-blz-results'); if (!searchInput || !resultsBox) return; function esc(str) { return String(str ?? '').replace(/&/g,'&').replace(//g,'>'); } let debounceTimer = null; searchInput.addEventListener('input', () => { clearTimeout(debounceTimer); const q = searchInput.value.trim(); if (q.length < 2) { resultsBox.style.display = 'none'; resultsBox.innerHTML = ''; return; } debounceTimer = setTimeout(async () => { let rows; try { const res = await fetch('/bankinstitute/search?q=' + encodeURIComponent(q)); rows = await res.json(); } catch (_) { rows = []; } if (!Array.isArray(rows) || rows.length === 0) { resultsBox.innerHTML = '
Keine Treffer
'; resultsBox.style.display = 'block'; return; } resultsBox.innerHTML = rows.map((r, i) => `
${esc(r.blz)} ${esc(r.name)} · ${esc(r.ort)} ${r.pin_tan_url ? '' : ' (kein PIN/TAN-Zugang)'}
`).join(''); resultsBox.style.display = 'block'; resultsBox.querySelectorAll('.fc-blz-result').forEach((el) => { el.addEventListener('click', () => { const row = rows[parseInt(el.dataset.idx, 10)]; blzField.value = row.blz; urlField.value = row.pin_tan_url || ''; resultsBox.style.display = 'none'; searchInput.value = ''; }); }); }, 300); }); })(); function syncTypFields(typ) { apiNew.style.display = (typ === 'API') ? 'block' : 'none'; fintsNew.style.display = (typ === 'FinTS') ? 'block' : 'none'; } selectNew.addEventListener('change', () => { const opt = selectNew.options[selectNew.selectedIndex]; const isNew = opt.value === '__new__'; const isCsv = opt.value === '__csv__'; newBankWrap.style.display = isNew ? 'block' : 'none'; newBankInput.required = isNew; csvNew.style.display = isCsv ? 'block' : 'none'; if (isNew) { const selected = modalNew.querySelector('input[name="new_bank_typ"]:checked'); syncTypFields(selected ? selected.value : 'FinTS'); } else if (isCsv) { syncTypFields(''); } else { syncTypFields(opt.dataset.typ); } }); modalNew.querySelectorAll('input[name="new_bank_typ"]').forEach(radio => { radio.addEventListener('change', () => { if (selectNew.value === '__new__') { syncTypFields(radio.value); } }); }); // ── Bearbeiten-Modal ───────────────────────────────────────── const modalEdit = document.getElementById('modal-edit-connection'); const btnEditClose = document.getElementById('btn-edit-close'); const btnEditCancel= document.getElementById('btn-edit-cancel'); const formEdit = document.getElementById('form-edit-connection'); const formDelete = document.getElementById('form-edit-delete'); const btnEditDelete = document.getElementById('btn-edit-delete'); const editName = document.getElementById('edit-provider-name'); const editBez = document.getElementById('edit-bezeichnung'); const editApiFields = document.getElementById('edit-api-fields'); const editFintsFields = document.getElementById('edit-fints-fields'); const editAppName = document.getElementById('edit-app-name'); const editClientId = document.getElementById('edit-client-id'); const editSecret = document.getElementById('edit-secret'); const editSandbox = document.getElementById('edit-sandbox'); const editKuerzel = document.getElementById('edit-kuerzel'); const editFarbe = document.getElementById('edit-farbe'); const editBlz = document.getElementById('edit-blz'); const editBenutzer = document.getElementById('edit-benutzerkennung'); const editPin = document.getElementById('edit-pin'); const editFintsUrl = document.getElementById('edit-fints-url'); const editTanModeId = document.getElementById('edit-tan-mode-id'); const editAccountList = document.getElementById('edit-account-list'); const editResetAcc = document.getElementById('edit-reset-accounts'); const editFetchRangeApi = document.getElementById('edit-fetch-range-api'); const editFetchRangeFints= document.getElementById('edit-fetch-range-fints'); function closeEdit() { modalEdit.style.display = 'none'; } if (btnEditClose) btnEditClose.addEventListener('click', closeEdit); if (btnEditCancel) btnEditCancel.addEventListener('click', closeEdit); modalEdit.addEventListener('click', (e) => { if (e.target === modalEdit) closeEdit(); }); btnEditDelete.addEventListener('click', () => { if (!confirm('Verbindung wirklich löschen?')) return; formDelete.submit(); }); // ── Abruf-Modal ────────────────────────────────────────────── const modalRun = document.getElementById('modal-run-log'); const runLogArea = document.getElementById('run-log-area'); const runTitle = document.getElementById('run-log-title'); const runSpinner = document.getElementById('run-log-spinner'); const btnRunClose= document.getElementById('btn-run-close'); let activeEs = null; let runWasOk = false; function closeRun() { if (activeEs) { activeEs.close(); activeEs = null; } if (runWasOk) { window.location.href = '/bank'; return; } modalRun.style.display = 'none'; runLogArea.textContent = ''; } btnRunClose.addEventListener('click', closeRun); modalRun.addEventListener('click', (e) => { if (e.target === modalRun && !btnRunClose.disabled) closeRun(); }); // ── Konto-Auswahl-Modal ────────────────────────────────────── const modalAcc = document.getElementById('modal-account-select'); const accList = document.getElementById('account-list'); const accError = document.getElementById('account-error'); const btnAccSubmit = document.getElementById('btn-account-submit'); const btnAccCancel = document.getElementById('btn-account-cancel'); let currentAccToken = null; function openAccountModal(payload) { currentAccToken = payload.token; accList.innerHTML = ''; accError.style.display = 'none'; payload.accounts.forEach((acc) => { const row = document.createElement('label'); row.style.display = 'flex'; row.style.alignItems = 'center'; row.style.gap = '.5rem'; row.style.cursor = 'pointer'; row.style.padding = '.35rem .5rem'; row.style.border = '1px solid #e2e8f0'; row.style.borderRadius = 'var(--radius)'; const cb = document.createElement('input'); cb.type = 'checkbox'; cb.value = acc.id; cb.checked = true; cb.style.margin = '0'; const label = document.createElement('span'); label.textContent = acc.label; row.appendChild(cb); row.appendChild(label); accList.appendChild(row); }); modalAcc.style.display = 'flex'; } function closeAccountModal() { modalAcc.style.display = 'none'; currentAccToken = null; } btnAccCancel.addEventListener('click', () => { closeAccountModal(); if (activeEs) { activeEs.close(); activeEs = null; } runTitle.textContent = 'Abruf abgebrochen'; runSpinner.textContent = '✗ abgebrochen'; btnRunClose.disabled = false; }); btnAccSubmit.addEventListener('click', async () => { const ids = Array.from(accList.querySelectorAll('input[type=checkbox]:checked')).map(cb => cb.value); if (ids.length === 0) { accError.textContent = 'Mindestens ein Konto auswählen.'; accError.style.display = 'block'; return; } btnAccSubmit.disabled = true; try { const form = new FormData(); form.append('token', currentAccToken); form.append('ids', JSON.stringify(ids)); const res = await fetch('/bank/direkt/' + currentSourceId + '/accounts', { method: 'POST', body: form }); const data = await res.json(); if (!res.ok || data.error) { accError.textContent = data.error || 'Übermittlung fehlgeschlagen.'; accError.style.display = 'block'; return; } closeAccountModal(); } catch (e) { accError.textContent = 'Netzwerkfehler: ' + e.message; accError.style.display = 'block'; } finally { btnAccSubmit.disabled = false; } }); // ── TAN-Modal ──────────────────────────────────────────────── const modalTan = document.getElementById('modal-tan-input'); const tanContext = document.getElementById('tan-context'); const tanChallenge = document.getElementById('tan-challenge'); const tanImageWrap = document.getElementById('tan-image-wrap'); const tanMedium = document.getElementById('tan-medium'); const tanInput = document.getElementById('tan-input'); const tanError = document.getElementById('tan-error'); const btnTanSubmit = document.getElementById('btn-tan-submit'); const btnTanCancel = document.getElementById('btn-tan-cancel'); let currentSourceId = null; let currentTanToken = null; function openTanModal(payload) { currentTanToken = payload.token; tanContext.textContent = payload.context ? 'Konto: ' + payload.context : ''; tanChallenge.textContent = payload.challenge || ''; tanMedium.textContent = payload.mediumName ? 'TAN-Medium: ' + payload.mediumName : ''; tanImageWrap.innerHTML = ''; if (payload.imageData && payload.imageMime) { const img = new Image(); img.src = 'data:' + payload.imageMime + ';base64,' + payload.imageData; img.style.maxWidth = '320px'; tanImageWrap.appendChild(img); } tanInput.value = ''; tanError.style.display = 'none'; modalTan.style.display = 'flex'; setTimeout(() => tanInput.focus(), 50); } function closeTanModal() { modalTan.style.display = 'none'; currentTanToken = null; } btnTanCancel.addEventListener('click', () => { closeTanModal(); if (activeEs) { activeEs.close(); activeEs = null; } runTitle.textContent = 'Abruf abgebrochen'; runSpinner.textContent = '✗ abgebrochen'; btnRunClose.disabled = false; }); async function submitTan() { const tan = tanInput.value.trim(); if (!tan) { tanError.textContent = 'Bitte TAN eingeben.'; tanError.style.display = 'block'; return; } if (!currentSourceId || !currentTanToken) return; btnTanSubmit.disabled = true; try { const form = new FormData(); form.append('token', currentTanToken); form.append('tan', tan); const res = await fetch('/bank/direkt/' + currentSourceId + '/tan', { method: 'POST', body: form }); const data = await res.json(); if (!res.ok || data.error) { tanError.textContent = data.error || 'TAN-Übermittlung fehlgeschlagen.'; tanError.style.display = 'block'; return; } closeTanModal(); } catch (e) { tanError.textContent = 'Netzwerkfehler: ' + e.message; tanError.style.display = 'block'; } finally { btnTanSubmit.disabled = false; } } btnTanSubmit.addEventListener('click', submitTan); tanInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') submitTan(); }); // ── Drag & Drop für Kachel-Reihenfolge ────────────────────── const grid = document.getElementById('connection-grid'); let dragSrc = null; let didDrag = false; if (grid) { grid.querySelectorAll('.drag-handle').forEach(handle => { handle.addEventListener('dragstart', (e) => { dragSrc = handle.closest('.card'); didDrag = false; if (!dragSrc) return; e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', dragSrc.dataset.id || ''); // Komplette Kachel als Drag-Image, am Mauszeiger an der Klick-Position const rect = dragSrc.getBoundingClientRect(); e.dataTransfer.setDragImage(dragSrc, e.clientX - rect.left, e.clientY - rect.top); // Ursprung subtil markieren, erst nach Snapshot (sonst landet das Highlight im Ghost) setTimeout(() => { if (dragSrc) { dragSrc.style.outline = '2px dashed #6366f1'; dragSrc.style.outlineOffset = '2px'; } }, 0); }); handle.addEventListener('dragend', () => { if (dragSrc) { dragSrc.style.outline = ''; dragSrc.style.outlineOffset = ''; } grid.querySelectorAll('.drop-before, .drop-after').forEach(c => { c.classList.remove('drop-before', 'drop-after'); }); dragSrc = null; setTimeout(() => { didDrag = false; }, 50); }); }); function clearDropMarkers() { grid.querySelectorAll('.drop-before, .drop-after').forEach(c => { c.classList.remove('drop-before', 'drop-after'); }); } function findDropTarget(clientX, clientY) { const cards = [...grid.querySelectorAll('.card')].filter(c => c !== dragSrc); // Erst: Karte unter dem Cursor (bevorzugt) for (const card of cards) { const r = card.getBoundingClientRect(); if (clientX >= r.left && clientX <= r.right && clientY >= r.top && clientY <= r.bottom) { return { card, side: (clientX < r.left + r.width / 2) ? 'before' : 'after' }; } } // Fallback: nächste Karte in derselben Zeile (anhand Y) let best = null, bestDist = Infinity; for (const card of cards) { const r = card.getBoundingClientRect(); if (clientY >= r.top && clientY <= r.bottom) { const center = r.left + r.width / 2; const dist = Math.abs(clientX - center); if (dist < bestDist) { bestDist = dist; best = { card, side: (clientX < center) ? 'before' : 'after' }; } } } return best; } grid.addEventListener('dragover', (e) => { if (!dragSrc) return; e.preventDefault(); e.dataTransfer.dropEffect = 'move'; const target = findDropTarget(e.clientX, e.clientY); if (!target) { clearDropMarkers(); return; } const cls = target.side === 'before' ? 'drop-before' : 'drop-after'; // Nur neu setzen wenn nötig (vermeidet Flackern) if (target.card.classList.contains(cls)) return; clearDropMarkers(); target.card.classList.add(cls); }); grid.addEventListener('dragleave', (e) => { // Nur wenn der Grid komplett verlassen wird if (e.target === grid && !grid.contains(e.relatedTarget)) { clearDropMarkers(); } }); grid.addEventListener('drop', (e) => { if (!dragSrc) return; e.preventDefault(); e.stopPropagation(); const marker = grid.querySelector('.drop-before, .drop-after'); if (marker) { const before = marker.classList.contains('drop-before'); marker.parentNode.insertBefore(dragSrc, before ? marker : marker.nextSibling); didDrag = true; persistOrder(); } clearDropMarkers(); }); } async function persistOrder() { const ids = Array.from(grid.querySelectorAll('.card[data-id]')).map(c => c.dataset.id); const form = new FormData(); form.append('ids', JSON.stringify(ids)); try { await fetch('/bank/direkt/_order', { method: 'POST', body: form }); } catch (e) { /* still local */ } } document.querySelectorAll('.btn-run-connection').forEach(card => { card.addEventListener('click', (e) => { if (didDrag) { didDrag = false; return; } if (e.target.closest('.btn-edit-connection')) return; if (e.target.closest('.drag-handle')) return; const id = card.dataset.id; currentSourceId = id; runLogArea.textContent = ''; runTitle.textContent = 'Abruf läuft…'; runSpinner.textContent = '⏳ wird abgerufen…'; btnRunClose.disabled = true; runWasOk = false; modalRun.style.display = 'flex'; if (activeEs) activeEs.close(); activeEs = new EventSource('/bank/direkt/' + id + '/run?csrf_token=' + encodeURIComponent(csrfToken)); activeEs.addEventListener('log', (ev) => { runLogArea.textContent += ev.data + '\n'; runLogArea.scrollTop = runLogArea.scrollHeight; }); activeEs.addEventListener('tan-required', (ev) => { try { openTanModal(JSON.parse(ev.data)); } catch (e) { runLogArea.textContent += '\n[TAN-Anforderung konnte nicht geparst werden]\n'; } }); activeEs.addEventListener('accounts-required', (ev) => { try { openAccountModal(JSON.parse(ev.data)); } catch (e) { runLogArea.textContent += '\n[Konto-Auswahl konnte nicht geparst werden]\n'; } }); activeEs.addEventListener('done', (ev) => { activeEs.close(); activeEs = null; runWasOk = ev.data === 'ok'; runTitle.textContent = runWasOk ? 'Abruf abgeschlossen' : 'Abruf mit Fehlern beendet'; runSpinner.textContent = runWasOk ? '✓ fertig' : '⚠ Fehler'; btnRunClose.disabled = false; }); activeEs.addEventListener('error', () => { activeEs.close(); activeEs = null; runLogArea.textContent += '\n[Verbindung unterbrochen]\n'; runLogArea.scrollTop = runLogArea.scrollHeight; runTitle.textContent = 'Abruf unterbrochen'; runSpinner.textContent = '✗ unterbrochen'; btnRunClose.disabled = false; }); }); }); document.querySelectorAll('.btn-edit-connection').forEach(btn => { btn.addEventListener('click', () => { const id = parseInt(btn.dataset.id, 10); const conn = connections.find(c => c.id === id); if (!conn) return; formEdit.action = '/bank/direkt/' + id; formDelete.action = '/bank/direkt/' + id + '/delete'; editName.textContent = conn.provider_name; editBez.value = conn.bezeichnung; editKuerzel.value = conn.kuerzel || ''; editFarbe.value = conn.farbe || '#6366f1'; const isApi = conn.type === 'api'; const isFints = conn.type === 'fints'; editApiFields.style.display = isApi ? 'block' : 'none'; editFintsFields.style.display = isFints ? 'block' : 'none'; // fetch_range steht in beiden Type-Blöcken; nur der zum aktiven Typ // gehörige Select wird enabled (disabled Felder werden nicht submitted). const fetchVal = conn.fetch_range || 'last_fetched'; editFetchRangeApi.value = fetchVal; editFetchRangeFints.value = fetchVal; editFetchRangeApi.disabled = !isApi; editFetchRangeFints.disabled= !isFints; if (isApi) { editAppName.value = conn.app_name || ''; editClientId.value = ''; editSecret.value = ''; editSandbox.checked = conn.sandbox === true; } if (isFints) { editBlz.value = conn.blz || ''; editBenutzer.value = conn.benutzerkennung || ''; editPin.value = ''; editFintsUrl.value = conn.fints_url || ''; editTanModeId.value = conn.tan_mode_id != null ? conn.tan_mode_id : ''; editResetAcc.checked = false; // Konto-Checkboxen aus account_filter aufbauen editAccountList.innerHTML = ''; let accounts = []; if (conn.account_filter) { try { const arr = JSON.parse(conn.account_filter); if (Array.isArray(arr)) accounts = arr.map(String); } catch (e) { /* ignore */ } } const editAccountEmpty = document.getElementById('edit-account-empty'); if (accounts.length === 0) { editAccountList.style.display = 'none'; editAccountEmpty.style.display = 'block'; } else { editAccountList.style.display = 'flex'; editAccountEmpty.style.display = 'none'; accounts.forEach(id => { const row = document.createElement('label'); row.style.display = 'flex'; row.style.alignItems = 'center'; row.style.gap = '.5rem'; row.style.cursor = 'pointer'; row.style.fontWeight = 'normal'; row.style.margin = '0'; const cb = document.createElement('input'); cb.type = 'checkbox'; cb.name = 'active_accounts[]'; cb.value = id; cb.checked = true; cb.style.width = 'auto'; cb.style.margin = '0'; const span = document.createElement('span'); // IBAN lesbarer machen (4er-Gruppen), AN:-Identifier wie geliefert span.textContent = /^[A-Z]{2}\d{2}/.test(id) ? id.replace(/(.{4})/g, '$1 ').trim() : id; span.style.fontFamily = 'monospace'; span.style.fontSize = '.85rem'; row.appendChild(cb); row.appendChild(span); editAccountList.appendChild(row); }); } } modalEdit.style.display = 'flex'; }); }); })();