Untitled
ESS Scriptunknown
javascript
a month ago
21 kB
10
Indexable
// ==UserScript==
// @name SAP Aktuell
// @namespace tampermonkey
// @version 4.3
// @description Produktivstunden automatisch buchen (kompletter Tag, Duplikatprüfung)
// @author Du
// @match *://*.cancom.de/*
// @match *://*.jetsapphr*/*
// @run-at document-idle
// @grant none
// ==/UserScript==
(function(){
'use strict';
const C = { farbe: '#0070f2', debug: true };
const log = (...a) => C.debug && console.log('[AutoBuchen]', ...a);
const host = location.hostname;
if (!host.includes('cancom.de') && !host.includes('jetsapphr')) return;
log('v4.3 GESTARTET');
const warte = ms => new Promise(r => setTimeout(r, ms));
// ─── DOM-Hilfsfunktionen ────────────────────────────────────────────────────
function alleDocs() {
const docs = new Set();
function sammle(win) {
try {
docs.add(win.document);
for (let i = 0; i < win.frames.length; i++) {
try { sammle(win.frames[i]); } catch(e) {}
}
} catch(e) {}
}
try { sammle(window.top); } catch(e) { sammle(window); }
return [...docs];
}
function getSapDoc() {
for (const doc of alleDocs()) {
if (doc.getElementById('WD9D')) return doc;
}
return null;
}
// ─── Zeitstempel lesen ─────────────────────────────────────────────────────
function leseStempel() {
const liste = [], gesehen = new Set();
const SELS = [
'[class*="sapMListTbl"] td',
'[class*="sapUiTable"] td',
'[class*="sapMLIB"]',
'[class*="sapMObjLAttr"]',
'[class*="sapMText"]',
'[class*="sapMLabel"]',
'td, li, span, div, label'
];
for (const doc of alleDocs()) {
let hit = false;
for (const sel of SELS) {
let els = [];
try { els = [...doc.querySelectorAll(sel)]; } catch(e) { continue; }
for (const el of els) {
if (el.children.length > 4) continue;
const txt = (el.innerText || el.textContent || '').trim();
if (!txt || txt.length > 80) continue;
const m = txt.match(/\b(\d{2}:\d{2})\b/);
if (!m) continue;
const zeit = m[1], low = txt.toLowerCase();
let typ = null;
if (low.includes('kommen')) typ = 'kommen';
else if (low.includes('beginn') && low.includes('pause')) typ = 'beginn_pause';
else if (low.includes('ende') && low.includes('pause')) typ = 'ende_pause';
else if (low.includes('gehen')) typ = 'gehen';
if (!typ && el.closest('tr')) {
const rt = (el.closest('tr').innerText || '').toLowerCase();
if (rt.includes('kommen')) typ = 'kommen';
else if (rt.includes('beginn') && rt.includes('pause')) typ = 'beginn_pause';
else if (rt.includes('ende') && rt.includes('pause')) typ = 'ende_pause';
else if (rt.includes('gehen')) typ = 'gehen';
}
if (typ) {
const key = `${typ}:${zeit}`;
if (!gesehen.has(key)) {
gesehen.add(key);
liste.push({ zeit, typ });
log('✔ Stempel erkannt:', typ, zeit);
hit = true;
}
}
}
if (hit && sel !== 'td, li, span, div, label') break;
}
}
return liste;
}
// ─── Blöcke berechnen (gesamter Tag) ──────────────────────────────────────
//
// Erlaubte Paare (in dieser Reihenfolge):
// kommen → beginn_pause (Arbeitszeit vor Pause)
// ende_pause → beginn_pause (Arbeitszeit zwischen zwei Pausen)
// ende_pause → gehen (Arbeitszeit nach letzter Pause)
//
// NICHT erlaubt / wird ignoriert:
// kommen → gehen (kein direktes Kommen→Gehen ohne Pause dazwischen)
// doppelte kommen / gehen ohne Gegenpart
function berechneBloecke(stempel) {
const bloecke = [];
const s = [...stempel].sort((a, b) => a.zeit.localeCompare(b.zeit));
log('📋 Sortierte Stempel:', s.map(x => `${x.typ}@${x.zeit}`).join(', '));
// Zustandsmaschine: welcher Stempel darf als "von" stehen
// von-Quellen: kommen, ende_pause
// bis-Quellen: beginn_pause, gehen
// Regel: nach kommen darf nur beginn_pause folgen (nicht gehen)
// nach ende_pause darf beginn_pause ODER gehen folgen
let vonZeit = null;
let vonTyp = null;
for (const x of s) {
const { typ, zeit } = x;
if (typ === 'kommen') {
// Startet einen neuen Arbeitsblock
if (vonZeit !== null) log(`⚠️ Neues 'kommen' (${zeit}) obwohl bereits von=${vonZeit} offen – verwerfe altes`);
vonZeit = zeit;
vonTyp = 'kommen';
} else if (typ === 'ende_pause') {
// Startet Arbeitsblock nach einer Pause
if (vonZeit !== null) log(`⚠️ 'ende_pause' (${zeit}) obwohl bereits von=${vonZeit} offen – überschreibe`);
vonZeit = zeit;
vonTyp = 'ende_pause';
} else if (typ === 'beginn_pause') {
// Schließt einen Arbeitsblock — erlaubt nach kommen UND nach ende_pause
if (vonZeit === null) {
log(`⚠️ 'beginn_pause' (${zeit}) ohne offenes Von – überspringe`);
continue;
}
bloecke.push({ von: vonZeit, bis: zeit });
log(`📦 Block [${vonTyp} → beginn_pause]: ${vonZeit} – ${zeit}`);
vonZeit = null;
vonTyp = null;
} else if (typ === 'gehen') {
// Schließt Tagesende — erlaubt NUR nach ende_pause, NICHT direkt nach kommen
if (vonZeit === null) {
log(`⚠️ 'gehen' (${zeit}) ohne offenes Von – überspringe`);
continue;
}
if (vonTyp === 'kommen') {
// kommen → gehen direkt: ungültig, da keine Pause erfasst wurde
log(`⚠️ 'gehen' (${zeit}) direkt nach 'kommen' (${vonZeit}) – kein Pausenstempel vorhanden, Block wird NICHT gebucht`);
vonZeit = null;
vonTyp = null;
continue;
}
// vonTyp === 'ende_pause' → gültig
bloecke.push({ von: vonZeit, bis: zeit });
log(`📦 Block [ende_pause → gehen]: ${vonZeit} – ${zeit}`);
vonZeit = null;
vonTyp = null;
}
}
if (vonZeit !== null) {
log(`⚠️ Offener Block ohne Ende: von=${vonZeit} (${vonTyp}) – wird nicht gebucht`);
}
log(`✅ ${bloecke.length} gültige Block(e) berechnet`);
return bloecke;
}
// ─── Bestehende Aufträge / Buchungen lesen ─────────────────────────────────
/**
* Erkennt bereits gebuchte Einträge zuverlässig anhand der "Stunden"-Spalte.
*
* Logik aus der SAP-Tabelle (siehe Screenshot):
* - Jede Zeile mit einem gefüllten "Stunden"-Feld (z.B. "1,08") ist eine Buchung
* - Stempel-Zeilen (Kommen, Pause, Gehen) haben KEIN Stunden-Feld → werden ignoriert
* - Von/Bis wird aus den beiden Zeitspalten derselben Zeile gelesen
*
* Spaltenreihenfolge laut Screenshot: Tag | Datum | bis | von | Stunden | Art/Auftrag | ...
*/
function leseBestehendeAuftraege() {
const eintraege = [], gesehen = new Set();
// HH:MM Zeitformat
const zeitRegex = /^\d{2}:\d{2}$/;
// Stunden-Erkennung: NUR "1,08" / "8,13" (Komma + 2 Stellen) ODER
// Ganzzahl 1–24. Schließt explizit aus:
// - Auftragsnummern (7+ Stellen, z.B. "4504398")
// - Datumsangaben (z.B. "02.03.2026")
// - Dezimalzahlen mit Punkt statt Komma (nicht SAP-Standard)
const istStunden = w => {
if (!w) return false;
if (/^\d{1,2},\d{2}$/.test(w)) return true; // z.B. 1,08 / 8,13
if (/^\d{1,2}$/.test(w)) { const n = parseInt(w,10); return n >= 1 && n <= 24; }
return false;
};
for (const doc of alleDocs()) {
// Selektoren von spezifisch → generisch, damit SAP-Varianten alle abgedeckt sind
// Wichtig: generisches "tr" als letzter Fallback, aber Stempel-Filterung
// erfolgt ausschließlich über das Fehlen der Stunden-Zelle
const selektoren = [
'[class*="sapUiTableRow"]',
'[class*="sapMListTbl"] tr',
'[class*="sapMLIB"]',
'tr', // generischer Fallback
];
// Alle Zeilen aus allen Selektoren, Duplikate per Set entfernen
const zeilenSet = new Set();
for (const sel of selektoren) {
try { doc.querySelectorAll(sel).forEach(el => zeilenSet.add(el)); } catch(e) {}
}
log(`🔍 Zeilen gefunden: ${zeilenSet.size} (Doc: ${doc.title || doc.URL || 'unbekannt'})`);
for (const zeile of zeilenSet) {
const zellen = [...zeile.querySelectorAll('td')];
if (zellen.length < 4) continue;
const werte = zellen.map(td => (td.innerText || td.textContent || '').trim());
// Stunden-Zelle MUSS vorhanden sein — das ist das einzige zuverlässige
// Unterscheidungsmerkmal zwischen Buchungszeile und Stempel-Zeile
const stundenIdx = werte.findIndex(w => istStunden(w));
if (stundenIdx === -1) {
log('⏭ Stempel-Zeile (kein Stunden-Feld):', werte.filter(Boolean).slice(0,5).join(' | '));
continue;
}
// Alle HH:MM Zeiten der Zeile, sortiert → kleinste = von, größte = bis
const zeiten = werte.filter(w => zeitRegex.test(w)).sort((a,b) => a.localeCompare(b));
if (zeiten.length < 2) {
log('⚠️ Stunden-Zeile hat < 2 Zeiten:', werte.filter(Boolean).slice(0,5).join(' | '));
continue;
}
const von = zeiten[0], bis = zeiten[zeiten.length - 1];
if (von === bis) continue;
const key = `${von}:${bis}`;
if (!gesehen.has(key)) {
gesehen.add(key);
eintraege.push({ von, bis });
log('📌 Gebuchter Auftrag (Stunden=' + werte[stundenIdx] + '):', von, '\u2013', bis);
}
}
}
log(`📌 Bestehende Aufträge gesamt: ${eintraege.length}`);
return eintraege;
}
/**
* Prüft ob ein Block bereits als Auftrag vorhanden ist.
* Toleranz: ±1 Minute
*/
function istBereitsGebucht(block, bestehende) {
const parse = z => { const [h,m] = z.split(':').map(Number); return h*60+m; };
const bVon = parse(block.von), bBis = parse(block.bis);
for (const e of bestehende) {
const eVon = parse(e.von), eBis = parse(e.bis);
if (Math.abs(eVon - bVon) <= 1 && Math.abs(eBis - bBis) <= 1) {
return true;
}
}
return false;
}
// ─── Zeitfeld befüllen ─────────────────────────────────────────────────────
async function fuelleZeitfeld(doc, feldId, wert) {
log(` ⌨️ Fülle #${feldId} = "${wert}"`);
const el = doc.getElementById(feldId);
if (!el) { log(` ❌ #${feldId} nicht gefunden`); return false; }
const ns = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value');
const setVal = v => { if (ns?.set) ns.set.call(el, v); else el.value = v; };
el.focus();
await warte(100);
el.select();
await warte(100);
setVal('');
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', keyCode: 46, bubbles: true }));
el.dispatchEvent(new Event('input', { bubbles: true }));
await warte(100);
for (const char of wert) {
const cur = el.value;
setVal(cur + char);
el.dispatchEvent(new KeyboardEvent('keydown', { key: char, keyCode: char.charCodeAt(0), bubbles: true, cancelable: true }));
el.dispatchEvent(new KeyboardEvent('keypress', { key: char, keyCode: char.charCodeAt(0), bubbles: true, cancelable: true }));
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new KeyboardEvent('keyup', { key: char, keyCode: char.charCodeAt(0), bubbles: true, cancelable: true }));
await warte(100);
}
el.dispatchEvent(new Event('change', { bubbles: true }));
await warte(100);
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', keyCode: 9, bubbles: true, cancelable: true }));
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'Tab', keyCode: 9, bubbles: true, cancelable: true }));
el.blur();
log(` ⏳ Warte auf SAP-Validierung...`);
await warte(2500);
log(` ✅ #${feldId} = "${el.value}"`);
return true;
}
// ─── Einzelnen Block buchen ────────────────────────────────────────────────
async function buche(block) {
const sapDoc = getSapDoc();
if (!sapDoc) { alert('❌ SAP-Dokument nicht gefunden.'); return false; }
let vonFeld = sapDoc.getElementById('WD6E');
const formularBereitsOffen = vonFeld && vonFeld.offsetParent !== null;
if (!formularBereitsOffen) {
const oeffner = sapDoc.getElementById('WD9D');
if (!oeffner) { alert('❌ WD9D nicht gefunden.'); return false; }
log('▶ Öffne Formular (WD9D klicken)...');
oeffner.click();
await warte(1000);
vonFeld = sapDoc.getElementById('WD6E');
if (!vonFeld || vonFeld.offsetParent === null) {
await warte(1000);
vonFeld = sapDoc.getElementById('WD6E');
}
} else {
log('ℹ️ Formular war bereits offen');
}
if (!vonFeld) { log('❌ WD6E nicht gefunden'); return false; }
log('✅ Formular offen, WD6E =', vonFeld.value);
await fuelleZeitfeld(sapDoc, 'WD6E', block.von);
await fuelleZeitfeld(sapDoc, 'WD70', block.bis);
await warte(500);
const von = sapDoc.getElementById('WD6E')?.value;
const bis = sapDoc.getElementById('WD70')?.value;
log(`🔍 Vor Submit: WD6E="${von}" WD70="${bis}"`);
if (von !== block.von) await fuelleZeitfeld(sapDoc, 'WD6E', block.von);
if (bis !== block.bis) await fuelleZeitfeld(sapDoc, 'WD70', block.bis);
await warte(500);
const submitBtn = sapDoc.getElementById('WD9D');
if (!submitBtn) { log('❌ WD9D für Submit nicht gefunden'); return false; }
log('💾 Speichern (WD9D klicken)...');
submitBtn.click();
await warte(2000);
log('✅ Block gespeichert:', block.von, '–', block.bis);
return true;
}
// ─── Hauptlogik: Gesamten Tag prüfen und buchen ────────────────────────────
async function bucheGesamtenTag(btn) {
btn.disabled = true;
btn.innerText = '⏳ Analysiere Tag...';
try {
// 1. Zeitstempel lesen
const stempel = leseStempel();
if (!stempel.length) {
alert('⚠️ Keine Zeitstempel erkannt!\n\nBitte stelle sicher, dass die Tagesansicht mit Kommen/Gehen-Stempeln geöffnet ist.');
return;
}
log(`📊 ${stempel.length} Stempel erkannt`);
// 2. Soll-Blöcke berechnen (ganzer Tag)
const alleBloecke = berechneBloecke(stempel);
if (!alleBloecke.length) {
alert('⚠️ Keine buchbaren Blöcke aus den Stempeln berechenbar.\n\nPrüfe ob Kommen/Gehen-Stempel vorhanden sind.');
return;
}
// 3. Bestehende Aufträge prüfen
btn.innerText = '⏳ Prüfe bestehende Aufträge...';
const bestehende = leseBestehendeAuftraege();
log(`📌 ${bestehende.length} bestehende Aufträge gefunden`);
// 4. Duplikate herausfiltern
const zuBuchen = alleBloecke.filter(b => !istBereitsGebucht(b, bestehende));
const uebersprungen = alleBloecke.filter(b => istBereitsGebucht(b, bestehende));
log(`✅ Zu buchen: ${zuBuchen.length}, übersprungen (bereits vorhanden): ${uebersprungen.length}`);
// 5. Zusammenfassung anzeigen
let zusammenfassung = `📋 Tagesanalyse abgeschlossen\n`;
zusammenfassung += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
zusammenfassung += `Erkannte Blöcke gesamt: ${alleBloecke.length}\n\n`;
if (uebersprungen.length) {
zusammenfassung += `✅ Bereits gebucht (${uebersprungen.length}):\n`;
uebersprungen.forEach(b => { zusammenfassung += ` • ${b.von} – ${b.bis}\n`; });
zusammenfassung += '\n';
}
if (!zuBuchen.length) {
zusammenfassung += `🎉 Alle Blöcke sind bereits als Auftrag vorhanden.\nNichts zu tun!`;
alert(zusammenfassung);
return;
}
zusammenfassung += `⏳ Noch zu buchen (${zuBuchen.length}):\n`;
zuBuchen.forEach(b => { zusammenfassung += ` • ${b.von} – ${b.bis}\n`; });
zusammenfassung += `\nJetzt alle ${zuBuchen.length} Block(e) buchen?`;
if (!confirm(zusammenfassung)) return;
// 6. Alle ausstehenden Blöcke nacheinander buchen
let erfolgreich = 0, fehlgeschlagen = 0;
for (let i = 0; i < zuBuchen.length; i++) {
const b = zuBuchen[i];
btn.innerText = `⏳ Buche ${i+1}/${zuBuchen.length}: ${b.von}–${b.bis}`;
log(`▶ Buche Block ${i+1}/${zuBuchen.length}: ${b.von} – ${b.bis}`);
const ok = await buche(b);
if (ok) {
erfolgreich++;
log(`✅ Block ${i+1} erfolgreich gebucht`);
} else {
fehlgeschlagen++;
log(`❌ Block ${i+1} fehlgeschlagen`);
const weiter = confirm(
`❌ Block ${i+1} (${b.von}–${b.bis}) fehlgeschlagen.\n\n` +
`${zuBuchen.length - i - 1} Block(e) verbleiben.\nMit nächstem Block fortfahren?`
);
if (!weiter) break;
}
// Kurze Pause zwischen Buchungen
if (i < zuBuchen.length - 1) await warte(500);
}
// 7. Abschlussbericht
let bericht = `📊 Buchung abgeschlossen\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
bericht += `✅ Erfolgreich gebucht: ${erfolgreich}\n`;
if (fehlgeschlagen) bericht += `❌ Fehlgeschlagen: ${fehlgeschlagen}\n`;
if (uebersprungen.length) bericht += `⏭ Bereits vorhanden: ${uebersprungen.length}\n`;
alert(bericht);
} catch(e) {
alert('❌ Fehler: ' + e.message);
log(e);
} finally {
btn.disabled = false;
btn.innerText = '⏱ Produktivstunden buchen';
}
}
// ─── Button einfügen ───────────────────────────────────────────────────────
function zeigeButton() {
const targetDoc = (window === window.top) ? document : window.top.document;
const targetBody = targetDoc.body;
if (!targetBody || targetDoc.getElementById('ab-btn')) return;
const btn = targetDoc.createElement('button');
btn.id = 'ab-btn';
btn.innerText = '⏱ Produktivstunden buchen';
Object.assign(btn.style, {
position: 'fixed',
bottom: '20px',
right: '20px',
zIndex: '2147483647',
background: C.farbe,
color: '#fff',
border: '2px solid #fff',
borderRadius: '10px',
padding: '13px 22px',
fontSize: '15px',
fontWeight: 'bold',
cursor: 'pointer',
fontFamily: 'Arial,sans-serif',
boxShadow: '0 6px 20px rgba(0,0,0,0.45)',
});
btn.onmouseenter = () => btn.style.background = '#004fa3';
btn.onmouseleave = () => btn.style.background = C.farbe;
btn.onclick = () => bucheGesamtenTag(btn);
targetBody.appendChild(btn);
log('✅ Button eingefügt');
}
// ─── Polling & SPA-Support ─────────────────────────────────────────────────
let n = 0;
const poll = setInterval(() => {
zeigeButton();
const d = (window === window.top) ? document : window.top.document;
if (d.getElementById('ab-btn') || ++n > 100) clearInterval(poll);
}, 400);
['pushState', 'replaceState'].forEach(m => {
const o = history[m].bind(history);
history[m] = (...a) => { o(...a); n = 0; };
});
window.addEventListener('hashchange', () => { n = 0; });
})();Editor is loading...
Leave a Comment