v2.0 · 2026
`); eingetragen++; } } } // Nicht gefundene Vorgänge melden Object.keys(summeProVorgang).forEach(vg => { const gefunden = Object.values(vorgangProZeile).some(v => v === vg || v.replace(/\s/g,'') === vg.replace(/\s/g,'') ); if (!gefunden) nichtGefunden.push(vg); }); // Geänderte sheet1.xml zurück in ZIP unzipped[sheetKey] = new TextEncoder().encode(sheetXml); // ZIP neu packen const rezipped = fflate.zipSync(unzipped, { level: 6 }); const blob = new Blob([rezipped], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); // Über Bridge in Originaldatei auf Netzlaufwerk schreiben btnText.textContent = '⏳ Schreibe Excel…'; try { const writeResp = await fetch(_BRIDGE_WRITE, { method: 'POST', headers: { 'Content-Type': 'application/octet-stream' }, body: rezipped, signal: AbortSignal.timeout(10000) }); if (!writeResp.ok) throw new Error('HTTP ' + writeResp.status); } catch (e) { btnText.textContent = '→ Excel Teilfertige'; btn.disabled = false; alert('Fehler beim Schreiben der Excel-Datei:\n' + e.message); return; } btnText.textContent = '→ Excel Teilfertige'; btn.disabled = false; // Meldung let msg = `✓ ${eingetragen} Vorgang${eingetragen !== 1 ? 'änge' : ''} direkt in\n„Bewertung_teilfertige_Arbeiten.xlsx"\neingetragen und gespeichert.`; if (nichtGefunden.length > 0) msg += `\n\n⚠ Diese Vorgänge haben zugeordnete Rechnungen, wurden aber nicht in der Excel gefunden:\n${nichtGefunden.join(', ')}`; alert(msg); } // ============================================================ // EXPORT // ============================================================ function exportCSV() { const data = laden(); const filterMonat = document.getElementById('filterMonatRechnungen').value; let rows = [['Datum','Rechnungsnummer','Lieferant','Pos','Bezeichnung','Menge','Einheit','Einzelpreis','Gesamtpreis','Vorgangsnummer']]; data.rechnungen .filter(r => monatMatch(r.datum, filterMonat)) .sort((a, b) => b.datum.localeCompare(a.datum)) .forEach(r => { r.positionen.forEach((p, idx) => { rows.push([r.datum, r.rechnungsnummer, r.lieferant, idx + 1, p.bezeichnung, p.menge, p.einheit, p.einzelpreis, posGesamtpreis(p), p.vorgangsnummer || '']); }); }); downloadCSV(rows, 'Eingangsrechnungen'); } function exportAuswertungCSV() { const data = laden(); const filterMonat = document.getElementById('filterMonatAuswertung').value; const filterVorgang = document.getElementById('filterVorgangAuswertung').value.trim().toLowerCase(); let rows = [['Vorgangsnummer','Datum','Rechnungsnummer','Lieferant','Bezeichnung','Menge','Einheit','Einzelpreis','Betrag']]; const map = {}; data.rechnungen.filter(r => monatMatch(r.datum, filterMonat)).forEach(r => { r.positionen.forEach(p => { if (!p.vorgangsnummer) return; if (filterVorgang && !p.vorgangsnummer.toLowerCase().includes(filterVorgang)) return; if (!map[p.vorgangsnummer]) map[p.vorgangsnummer] = []; map[p.vorgangsnummer].push({ r, p }); }); }); Object.keys(map).sort().forEach(vn => { map[vn].forEach(({ r, p }) => { rows.push([vn, r.datum, r.rechnungsnummer, r.lieferant, p.bezeichnung, p.menge, p.einheit, p.einzelpreis, posGesamtpreis(p)]); }); }); downloadCSV(rows, 'Auswertung_Vorgaenge'); } function downloadCSV(rows, name) { const bom = '\uFEFF'; const csv = bom + rows.map(r => r.map(c => `"${String(c).replace(/"/g,'""')}"`).join(';')).join('\r\n'); const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${name}_${new Date().toISOString().slice(0,10)}.csv`; a.click(); URL.revokeObjectURL(url); } // ============================================================ // INIT // ============================================================ document.addEventListener('DOMContentLoaded', async () => { document.getElementById('headerDate').textContent = new Date().toLocaleDateString('de-DE', { day: '2-digit', month: 'long', year: 'numeric' }); // Monatsfilter bewusst LEER lassen → alle Rechnungen werden angezeigt document.getElementById('filterMonatRechnungen').value = ''; document.getElementById('filterMonatZuordnung').value = ''; document.getElementById('filterMonatAuswertung').value = ''; // Lade-Overlay einblenden const overlay = document.getElementById('ladeOverlay'); if (overlay) overlay.style.display = 'flex'; // Daten von Supabase laden await ladenVonSupabase(); // Lade-Overlay ausblenden if (overlay) overlay.style.display = 'none'; // ===== PDF DRAG & DROP – Zone + Page-Wide ===== (function() { const zone = document.getElementById('pdfDndZone'); // Hilfsfunktion: PDFs aus DataTransfer holen function pdfsAusEvent(e) { return [...(e.dataTransfer.files || [])].filter(f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf') ); } // Drop-Zone (die Label-Karte unter der Toolbar) if (zone) { zone.addEventListener('dragover', e => { e.preventDefault(); e.stopPropagation(); zone.classList.add('drag-over'); }); zone.addEventListener('dragenter', e => { e.preventDefault(); zone.classList.add('drag-over'); }); zone.addEventListener('dragleave', e => { if (!zone.contains(e.relatedTarget)) zone.classList.remove('drag-over'); }); zone.addEventListener('drop', e => { e.preventDefault(); e.stopPropagation(); zone.classList.remove('drag-over'); const pdfs = pdfsAusEvent(e); if (pdfs.length) pdfGewaehlt({ files: pdfs, value: '' }); }); } // Page-Wide Overlay: zeigt sich, sobald etwas auf die Seite gezogen wird (wenn Rechnungen-Tab aktiv) // Overlay-Element dynamisch einfügen const overlay = document.createElement('div'); overlay.id = 'pdfPageDropOverlay'; overlay.innerHTML = `

PDFs loslassen zum Importieren

Auch direkt aus E-Mail-Anhängen möglich
`; document.body.appendChild(overlay); let dragCounter = 0; function rechnungenTabAktiv() { const tab = document.getElementById('tab-rechnungen'); return tab && tab.style.display !== 'none'; } document.addEventListener('dragenter', e => { if (!rechnungenTabAktiv()) return; const hatDateien = e.dataTransfer && e.dataTransfer.types && (e.dataTransfer.types.includes('Files') || e.dataTransfer.types.includes('application/x-moz-file')); if (!hatDateien) return; dragCounter++; overlay.classList.add('active'); overlay.style.pointerEvents = 'auto'; }); document.addEventListener('dragleave', e => { if (!rechnungenTabAktiv()) return; dragCounter--; if (dragCounter <= 0) { dragCounter = 0; overlay.classList.remove('active'); overlay.style.pointerEvents = 'none'; } }); document.addEventListener('dragover', e => { if (rechnungenTabAktiv()) e.preventDefault(); }); document.addEventListener('drop', e => { dragCounter = 0; overlay.classList.remove('active'); overlay.style.pointerEvents = 'none'; if (!rechnungenTabAktiv()) return; // Nur verarbeiten wenn nicht schon von Zone abgefangen const pdfs = pdfsAusEvent(e); if (pdfs.length) { e.preventDefault(); pdfGewaehlt({ files: pdfs, value: '' }); } }); })(); renderRechnungen(); }); // Modal-Overlay schließen document.getElementById('rechnungModal').addEventListener('click', function(e) { if (e.target === this) closeRechnungModal(); }); // PDF-Vorschau: Escape-Taste zum Schließen document.addEventListener('keydown', e => { if (e.key === 'Escape') { const vm = document.getElementById('pdfVorschauModal'); if (vm && vm.classList.contains('open')) schliessePdfVorschau(); } }); // ============================================================ // SCHNELL-SYNC im Rechnungen-Tab (Balken) // ============================================================ async function synchronisierenSchnell() { const btn = document.getElementById('syncBalkenBtn'); const icon = document.getElementById('syncBalkenIcon'); const status = document.getElementById('syncBalkenStatus'); btn.disabled = true; icon.style.animation = 'spin .8s linear infinite'; if (status) { status.style.display = ''; status.style.color = '#6b7280'; status.textContent = '⏳ Bestelldaten laden…'; } // Immer frisch aus Supabase laden (neueste Bestellungen) try { await ladenVonSupabase(); } catch(e) {} let data = laden(); const bs = data.bestellungen || []; if (bs.length === 0) { icon.style.animation = ''; icon.textContent = '⚠'; if (status) { status.style.display = ''; status.style.color = '#b45309'; status.textContent = 'Keine Bestelldaten – bitte im Tab „Bestellabgleich" synchronisieren.'; } btn.disabled = false; setTimeout(() => { icon.textContent = '🔄'; if(status) status.style.display='none'; }, 4000); return; } const alleBestellPosS = []; (data.bestellungen || []).forEach(b => (b.positionen||[]).forEach(p => alleBestellPosS.push(p))); // Alle Rechnungen abgleichen – Schritt 1: regelbasiert let totalNeu = 0, betroffene = 0; data.rechnungen.forEach(r => { const vorher = r.positionen.filter(p => p.vorgangsnummer).length; const erg = matcheInvoiceMitBestellungen(r.positionen.map(p => ({...p})), r.lieferant, data); const nachher = erg.positionen.filter(p => p.vorgangsnummer).length; const neu = nachher - vorher; if (neu > 0) { r.positionen = erg.positionen; erg.positionen.forEach(p => { if (p.vorgangsnummer && !data.vorgaenge.find(v => v.nr === p.vorgangsnummer)) data.vorgaenge.push({ nr: p.vorgangsnummer, bezeichnung: '' }); }); totalNeu += neu; betroffene++; } }); // Schritt 2: Gemini KI für verbleibende offene Positionen const apiKeyS = getApiKey(); if (apiKeyS && alleBestellPosS.filter(bp => bp.vorgangsnummer).length > 0) { if (status) { status.style.display=''; status.style.color='#1d4ed8'; status.textContent='🤖 KI analysiert offene Positionen…'; } try { for (const r of data.rechnungen) { const offen = r.positionen.filter(p => !p.vorgangsnummer); if (offen.length === 0) continue; const gemMatches = await matcheMitGeminiKI(r.positionen, alleBestellPosS, apiKeyS); let neuR = 0; gemMatches.forEach(m => { const pos = r.positionen[m.origIdx]; if (pos && !pos.vorgangsnummer && m.vorgangsnummer) { pos.vorgangsnummer = m.vorgangsnummer; pos._matchGrund = m.grund || 'KI'; if (!data.vorgaenge.find(v => v.nr === m.vorgangsnummer)) data.vorgaenge.push({ nr: m.vorgangsnummer, bezeichnung: '' }); neuR++; } }); if (neuR > 0) { totalNeu += neuR; betroffene++; } } } catch(e) { console.warn('Gemini-Matching fehlgeschlagen:', e); } } if (totalNeu > 0) { await speichern(data); renderRechnungen(); } icon.style.animation = ''; icon.textContent = totalNeu > 0 ? '✓' : '🔄'; if (status) { status.style.display = ''; status.style.color = totalNeu > 0 ? '#15803d' : '#6b7280'; status.textContent = totalNeu > 0 ? `✓ ${totalNeu} Position${totalNeu !== 1 ? 'en' : ''} in ${betroffene} Rechnung${betroffene !== 1 ? 'en' : ''} zugeordnet` : 'Keine neuen Zuordnungen gefunden'; } btn.disabled = false; setTimeout(() => { icon.textContent = '🔄'; }, 4000); } // ============================================================ // BESTELLUNGEN – SYNCHRONISIEREN (Supabase laden + alle Rechnungen abgleichen) // ============================================================ async function synchronisieren() { const btn = document.getElementById('syncBtn'); const btnIcon = document.getElementById('syncBtnIcon'); const btnText = document.getElementById('syncBtnText'); const statusEl = document.getElementById('syncStatus'); // Button deaktivieren + Spinner if (btn) btn.disabled = true; if (btnIcon) { btnIcon.style.animation = 'spin .8s linear infinite'; btnIcon.textContent = '🔄'; } if (btnText) btnText.textContent = 'Synchronisiere…'; if (statusEl) { statusEl.style.display = 'none'; statusEl.innerHTML = ''; } const log = []; let fehler = false; try { // ── Schritt 1: Frische Daten aus Supabase laden ── if (statusEl) { statusEl.style.display = ''; statusEl.innerHTML = '⏳ Lade Bestellungen aus dem Bestelltool…'; } await ladenVonSupabase(); const data = laden(); const bs = data.bestellungen || []; const anzPos = bs.reduce((s, b) => s + (b.positionen?.length || 0), 0); if (bs.length === 0) { log.push('⚠ Keine Bestellungen gefunden. Bitte im Bestelltool eine Auftragsbestätigung speichern.'); fehler = true; } else { log.push(`✓ ${bs.length} Bestellung${bs.length !== 1 ? 'en' : ''} mit ${anzPos} Position${anzPos !== 1 ? 'en' : ''} geladen.`); } // ── Schritt 2: Alle Rechnungen abgleichen ── if (bs.length > 0) { if (statusEl) statusEl.innerHTML = '⏳ Gleiche Rechnungen ab…'; let totalNeu = 0; let totalBereits = 0; let betroffeneRechnungen = 0; data.rechnungen.forEach(r => { const vorher = r.positionen.filter(p => p.vorgangsnummer).length; totalBereits += vorher; // Alle Positionen erneut matchen (auch bereits gematchte bleiben erhalten) const ergebnis = matcheInvoiceMitBestellungen( r.positionen.map(p => ({ ...p })), // Kopie, damit Originaldaten nicht mutiert werden r.lieferant, data ); const nachher = ergebnis.positionen.filter(p => p.vorgangsnummer).length; const neu = nachher - vorher; if (neu > 0) { r.positionen = ergebnis.positionen; ergebnis.positionen.forEach(p => { if (p.vorgangsnummer && !data.vorgaenge.find(v => v.nr === p.vorgangsnummer)) data.vorgaenge.push({ nr: p.vorgangsnummer, bezeichnung: '' }); }); totalNeu += neu; betroffeneRechnungen++; } }); // Ergebnis speichern await speichern(data); renderRechnungen(); renderBestellungen(); const gesamtPos = data.rechnungen.reduce((s, r) => s + r.positionen.length, 0); const zugeordnet = data.rechnungen.reduce((s, r) => s + r.positionen.filter(p => p.vorgangsnummer).length, 0); if (totalNeu > 0) { log.push(`⚡ ${totalNeu} neue Zuordnung${totalNeu !== 1 ? 'en' : ''} in ${betroffeneRechnungen} Rechnung${betroffeneRechnungen !== 1 ? 'en' : ''} automatisch ermittelt.`); } else { log.push(`ℹ Keine neuen Zuordnungen gefunden – Artikel-Nr. oder Preise passen nicht oder alle Positionen sind bereits zugeordnet.`); } log.push(`📊 Gesamt: ${zugeordnet} von ${gesamtPos} Rechnungspositionen haben eine Vorgangszuordnung.`); } } catch(e) { log.push(`✗ Fehler: ${e.message}`); fehler = true; } // Ergebnis anzeigen if (statusEl) { statusEl.style.display = ''; statusEl.style.background = fehler ? '#fef2f2' : '#dcfce7'; statusEl.style.border = `1px solid ${fehler ? '#fca5a5' : '#86efac'}`; statusEl.style.color = fehler ? '#991b1b' : '#14532d'; statusEl.innerHTML = log.join('
'); } // Button zurücksetzen if (btn) btn.disabled = false; if (btnIcon) { btnIcon.style.animation = ''; btnIcon.textContent = fehler ? '⚠' : '✓'; } if (btnText) btnText.textContent = 'Jetzt synchronisieren'; setTimeout(() => { if (btnIcon) btnIcon.textContent = '🔄'; }, 3000); } // ============================================================ // BESTELLUNGEN – RENDER (Positionstabelle) // ============================================================ function renderBestellungen() { const data = laden(); const bs = data.bestellungen || []; const filterLieferant = (document.getElementById('filterBestLieferant')?.value || '').toLowerCase(); const filterVorgang = (document.getElementById('filterBestVorgang')?.value || '').toLowerCase(); const body = document.getElementById('bestellungenBody'); if (!body) return; if (bs.length === 0) { body.innerHTML = 'Noch keine Bestellungen vorhanden. Im Bestelltool eine Auftragsbestätigung speichern – sie wird automatisch hier übertragen.'; aktualisiereAbgleichLieferanten(); return; } let html = ''; let gesamtGP = 0; let anyShown = false; // Sortiert: neueste Bestellung zuerst const sortedBs = [...bs].sort((a, b) => (b.datum||'').localeCompare(a.datum||'')); sortedBs.forEach(batch => { const filteredPos = (batch.positionen || []).filter((p, _i) => { if (filterLieferant && !( (p.lieferantNr||'').toLowerCase().includes(filterLieferant) || (p.lieferantName||'').toLowerCase().includes(filterLieferant) || (batch.lieferant||'').toLowerCase().includes(filterLieferant) )) return false; if (filterVorgang && !( (p.vorgangsnummer||'').toLowerCase().includes(filterVorgang) || (batch.vorgangsnr||'').toLowerCase().includes(filterVorgang) )) return false; return true; }); if (filteredPos.length === 0) return; anyShown = true; const batchSumme = filteredPos.reduce((s, p) => s + (parseFloat(p.gesamtpreis)||0), 0); gesamtGP += batchSumme; // ── Gruppenheader: Bestellung ── const abId = batch.abId || ''; const vgNr = batch.vorgangsnr || filteredPos[0]?.vorgangsnummer || '–'; html += ` 📦 ${batch.lieferant || '–'} |Vorgang: ${vgNr} |AB: ${batch.abNummer || '–'} ·${batch.datum || ''} ${fmt(batchSumme)} € `; // ── Positionen ── filteredPos.forEach(p => { const actualIdx = (batch.positionen || []).indexOf(p); html += ` ${p.vorgangsnummer || vgNr} ${p.vorgangsbezeichnung || '–'} ${p.lieferantNr||'–'} ${(p.lieferantName && p.lieferantName !== p.lieferantNr) ? p.lieferantName : ''} ${p.artikelnr || '–'} ${p.verwendung || '–'} ${fmt(p.menge)} ${p.einheit || '–'} ${fmt(p.einzelpreis)} ${fmt(p.gesamtpreis)} ${p.bestelldatum || '–'} `; }); }); if (!anyShown) { body.innerHTML = 'Keine Positionen für diesen Filter.'; aktualisiereAbgleichLieferanten(); return; } html += ` Gesamtsumme (gefiltert): ${fmt(gesamtGP)} € `; body.innerHTML = html; aktualisiereAbgleichLieferanten(); } async function bestellungLoeschen(abId) { if (!confirm('Diese Bestellung und alle ihre Positionen löschen?')) return; const data = laden(); const vorher = (data.bestellungen || []).find(b => b.abId === abId); data.bestellungen = (data.bestellungen || []).filter(b => b.abId !== abId); await speichern(data); renderBestellungen(); renderAbgleich(); } async function bestellpositionLoeschen(abId, posIdx) { if (!confirm('Diese Position aus der Bestellung entfernen?')) return; const data = laden(); const batch = (data.bestellungen || []).find(b => b.abId === abId); if (!batch || !batch.positionen) return; batch.positionen.splice(posIdx, 1); if (batch.positionen.length === 0) { data.bestellungen = data.bestellungen.filter(b => b.abId !== abId); } await speichern(data); renderBestellungen(); renderAbgleich(); } async function alleBestellungenLoeschen() { if (!confirm('Alle importierten Bestelldaten wirklich löschen?\nDies kann nicht rückgängig gemacht werden.')) return; const data = laden(); data.bestellungen = []; await speichern(data); renderBestellungen(); } // ============================================================ // RECHNUNGSABGLEICH // ============================================================ function aktualisiereAbgleichLieferanten() { const data = laden(); const set = new Set(); (data.bestellungen || []).forEach(b => { (b.positionen||[]).forEach(p => { if (p.lieferantNr && p.lieferantNr !== '–') set.add(p.lieferantNr); if (p.lieferantName && p.lieferantName !== p.lieferantNr && p.lieferantName !== '–') set.add(p.lieferantName); }); }); (data.rechnungen || []).forEach(r => { if (r.lieferant) set.add(r.lieferant); }); const dl = document.getElementById('abgleichLieferantenListe'); if (dl) dl.innerHTML = [...set].sort().map(l => `