Janssen
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 => `