Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name LexOffice Umsatz-Highlighter + Schwarzer Text (Robust, neue Identifier & DOM-Struktur)
- // @namespace http://tampermonkey.net/
- // @version 2.1
- // @description Zeilen einfärben + schwarzen Text erzwingen. Unterstützt verschiedene Layouts (mehrzeilig/einzeilig), OI-43232 oder reine Nummern, Betragsabgleich und Stripe-/Porto-Regeln.
- // @match https://app.lexware.de/*
- // @grant none
- // ==/UserScript==
- (function() {
- 'use strict';
- // --------- 0) Kleines Utility: sichere .innerText-Lese-Funktion ----------
- function textOf(el) {
- try { return el ? el.innerText.trim() : ''; } catch(e){ return ''; }
- }
- // ----------------------------- 1) GLOBAL CSS --------------------------------
- const css = `
- /* alle DataGrid-Zellen und Typography-Boxen auf schwarz zwingen */
- .MuiDataGrid-root .MuiDataGrid-cell,
- .MuiDataGrid-root .MuiTypography-root,
- .MuiDataGrid-root .MuiBox-root {
- color: #000000 !important;
- }
- /* kleine optische Verbesserung für Legende */
- #tm-legend p, #tm-legend li { margin: 4px 0; }
- `;
- const style = document.createElement('style');
- style.textContent = css;
- document.head.appendChild(style);
- // ----------------------------- 2) FARBEN -------------------------------------
- const COLORS = {
- red: '#edaaaa',
- yellow: '#fffc97',
- purple: '#ffa9f8',
- green: '#92ff99',
- blue: '#a7c9ff',
- orange: '#ffd59e',
- teal: '#80e5ff'
- };
- // ----------------------------- 3) ZAHLPARSER --------------------------------
- function parseAmount(str) {
- if (!str) return null;
- const cleaned = String(str)
- .replace(/\u00A0/g, '') // NBSP entfernen
- .replace(/[^\d,.\-€]/g, '') // nur relevante Zeichen behalten
- .replace(/\.(?=\d{3}(\D|$))/g, '') // Tausenderpunkte entfernen
- .replace(',', '.')
- .replace('€','').trim();
- const n = parseFloat(cleaned);
- return Number.isFinite(n) ? n : null;
- }
- // ----------------------------- 4) LEGENDE -----------------------------------
- function injectLegend() {
- if (document.getElementById('tm-legend')) return;
- const legend = document.createElement('div');
- legend.id = 'tm-legend';
- Object.assign(legend.style, {
- position: 'fixed',
- top: '100px',
- right: '20px',
- background: 'white',
- border: '1px solid #ccc',
- padding: '10px 12px',
- borderRadius: '6px',
- boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
- fontFamily: 'Arial, sans-serif',
- fontSize: '12px',
- color: '#000000',
- zIndex: 99999,
- cursor: 'move',
- userSelect: 'none'
- });
- legend.innerHTML = `
- <div id="tm-legend-header" style="display:flex; justify-content:space-between; align-items:center; font-weight:600;">
- <span>Legende</span>
- <button id="tm-legend-toggle" style="margin-left:10px;background:#eee;border:none;padding:0 6px;cursor:pointer;border-radius:4px;">✕</button>
- </div>
- <ul id="tm-legend-body" style="list-style:none; padding:6px 0 0 0; margin:0;">
- <li><span style="display:inline-block;width:12px;height:12px;background:${COLORS.purple};margin-right:8px;"></span>Gebühren / Zinsen</li>
- <li><span style="display:inline-block;width:12px;height:12px;background:${COLORS.yellow};margin-right:8px;"></span>Deutsche Post / DHL / Porto</li>
- <li><span style="display:inline-block;width:12px;height:12px;background:${COLORS.green};margin-right:8px;"></span>Invoice-ID + Betrag match</li>
- <li><span style="display:inline-block;width:12px;height:12px;background:${COLORS.blue};margin-right:8px;"></span>Stripe Servicegebühr</li>
- <li><span style="display:inline-block;width:12px;height:12px;background:${COLORS.orange};margin-right:8px;"></span>Stripe Bestellgebühr</li>
- <li><span style="display:inline-block;width:12px;height:12px;background:${COLORS.teal};margin-right:8px;"></span>Stripe Auszahlung</li>
- <li><span style="display:inline-block;width:12px;height:12px;background:${COLORS.red};margin-right:8px;"></span>Sonstiges / Manual</li>
- </ul>
- `;
- document.body.appendChild(legend);
- // Drag & Drop
- const header = legend.querySelector('#tm-legend-header');
- let dragging = false, offX = 0, offY = 0;
- header.addEventListener('mousedown', e => {
- dragging = true;
- offX = e.clientX - legend.getBoundingClientRect().left;
- offY = e.clientY - legend.getBoundingClientRect().top;
- document.body.style.userSelect = 'none';
- });
- document.addEventListener('mousemove', e => {
- if (!dragging) return;
- legend.style.left = `${e.clientX - offX}px`;
- legend.style.top = `${e.clientY - offY}px`;
- legend.style.right = 'auto';
- });
- document.addEventListener('mouseup', () => {
- dragging = false;
- document.body.style.userSelect = '';
- });
- // Toggle body
- const toggle = legend.querySelector('#tm-legend-toggle');
- const body = legend.querySelector('#tm-legend-body');
- toggle.addEventListener('click', () => {
- if (body.style.display === 'none') {
- body.style.display = 'block'; toggle.textContent = '✕';
- } else {
- body.style.display = 'none'; toggle.textContent = '☰';
- }
- });
- }
- // ----------------------------- 5) ROW PARSER --------------------------------
- function extractFromRecipientCell(recCell) {
- // robust: sammle Zeilen / Tokens aus DOM (unterstützt sowohl einzeilige als auch mehrzeilige Darstellung)
- const raw = textOf(recCell);
- // lines: split by newlines (falls vorhanden) oder multiple spaces
- const lines = raw.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
- // Fallback: wenn keine neuen Zeilen existieren, splitte nach doppelten spaces
- if (lines.length === 1) {
- const maybe = lines[0].split(/\s{2,}/).map(s => s.trim()).filter(Boolean);
- if (maybe.length > 1) lines.splice(0,1, ...maybe);
- }
- // Versuche: Name = alles vor Datum (dd.mm.yyyy). Datum finden und splitten
- const joined = lines.join(' ');
- const dateMatch = joined.match(/\b\d{2}\.\d{2}\.\d{4}\b/);
- let name = '';
- let afterDate = '';
- if (dateMatch) {
- const idx = joined.indexOf(dateMatch[0]);
- name = joined.substring(0, idx).trim();
- afterDate = joined.substring(idx + dateMatch[0].length).trim();
- } else {
- // Keine Datumserkennung -> erste Token als Name (Fallback)
- name = lines[0] || '';
- afterDate = lines.slice(1).join(' ');
- }
- // Invoice-ID: OI-12345 oder reine Nummer (mind. 3 Ziffern)
- const oiMatch = afterDate.match(/\bOI[-\s]?(\d{3,})\b/i);
- const bareMatch = afterDate.match(/\b(\d{3,})\b/);
- let invNum = null;
- if (oiMatch) invNum = oiMatch[1];
- else if (bareMatch) invNum = bareMatch[1];
- // Versuch Betrag in recipient cell zu finden (falls vorhanden)
- const amtMatch = afterDate.match(/-?[\d\.,]+\s*€/);
- const recAmount = amtMatch ? parseAmount(amtMatch[0]) : null;
- return { raw, lines, name, invNum, recAmount };
- }
- function extractFromAssignmentCell(assCell) {
- const raw = textOf(assCell);
- // Suche nach Invoice-ID / Nummer auch hier (falls dort)
- const oiMatch = raw.match(/\bOI[-\s]?(\d{3,})\b/i);
- const bareMatch = raw.match(/\b(\d{3,})\b/);
- const invNum = oiMatch ? oiMatch[1] : (bareMatch ? bareMatch[1] : null);
- // Suche nach Beträgen
- const amtMatches = raw.match(/-?[\d\.,]+\s*€/g) || [];
- const lastAmt = amtMatches.length ? parseAmount(amtMatches[amtMatches.length-1]) : null;
- // kurze Kategorie-Extraktion (falls vorhanden in subtitle)
- const subtitleEl = assCell.querySelector('.MuiTypography-lxSubtitle2');
- const category = textOf(subtitleEl) || '';
- return { raw, invNum, lastAmt, category };
- }
- function updateRows() {
- injectLegend();
- document.querySelectorAll('.MuiDataGrid-row').forEach(row => {
- // prevent rows that are not fully loaded yet
- const recCell = row.querySelector('[data-field="recipientOrSenderName"]');
- const assCell = row.querySelector('[data-field="assignment"]');
- const amtCell = row.querySelector('[data-field="amount"]');
- if (!recCell || !assCell || !amtCell) return;
- const amtText = textOf(amtCell);
- const mainAmt = parseAmount(amtText);
- const rec = extractFromRecipientCell(recCell);
- const ass = extractFromAssignmentCell(assCell);
- const assText = ass.raw || '';
- // Bestimme Farbe
- let bg = COLORS.red; // default
- // 1) Gebühren / Zinsen
- if (/Zinsen|Gebühren|Gebühr/i.test(ass.raw) || ass.category === 'Zinsen/Gebühren') {
- bg = COLORS.purple;
- }
- // 2) Post / DHL / Porto (Name-Check + Kategorie)
- else if (/(Deutsche Post|DHL)/i.test(rec.raw) || /Porto/i.test(ass.raw) || /Porto/i.test(rec.raw) || ass.category.toLowerCase() === 'porto') {
- bg = COLORS.yellow;
- }
- // 3) Invoice-ID present in recipient or assignment AND amount matches
- else if ((rec.invNum || ass.invNum) && (rec.recAmount !== null || ass.lastAmt !== null)) {
- // prefer recAmount if present
- const candidateAmt = rec.recAmount !== null ? rec.recAmount : ass.lastAmt;
- if (candidateAmt !== null && mainAmt !== null && Math.abs(mainAmt - candidateAmt) < 0.001) {
- bg = COLORS.green;
- }
- }
- // 4) Stripe Servicegebühr (flexible patterns)
- else if (/STRP[\s\-_\u2011\u2013\u2014]*SG/i.test(assText) || /Servicegebühr/i.test(assText)) {
- bg = COLORS.blue;
- }
- // 5) Stripe Bestellgebühr
- else if (/STRP[\s\-_\u2011\u2013\u2014]*BG/i.test(assText) || /Bestellgebühr/i.test(assText)) {
- bg = COLORS.orange;
- }
- // 6) Stripe Auszahlungen
- else if (/STRP[\s\-_\u2011\u2013\u2014]*AUSZ/i.test(assText) || /Auszahlung/i.test(assText) || /AUSZAHL/i.test(assText)) {
- bg = COLORS.teal;
- }
- // Fallback: falls assignment/recipient enthalten 'txn_' oder 'STRP' aber keine oben match
- else if (/txn_\w{6,}/i.test(rec.raw + ' ' + ass.raw) || /STRP/i.test(ass.raw)) {
- bg = COLORS.teal;
- } else {
- bg = COLORS.red;
- }
- // Wende Hintergrundfarbe an (nur wenn anders)
- try {
- row.style.backgroundColor = bg;
- } catch(e) { /* silent */ }
- });
- }
- // ----------------------------- 6) OBSERVER ----------------------------------
- let scheduled = false;
- const throttledUpdate = () => {
- if (scheduled) return;
- scheduled = true;
- requestAnimationFrame(() => {
- try { updateRows(); } finally { scheduled = false; }
- });
- };
- const observer = new MutationObserver(throttledUpdate);
- observer.observe(document.body, { childList: true, subtree: true });
- // initial run and after full load
- window.addEventListener('load', () => setTimeout(updateRows, 200));
- // also attempt a few more times in case lexoffice renders later
- setTimeout(updateRows, 700);
- setTimeout(updateRows, 2000);
- })();
Advertisement
Add Comment
Please, Sign In to add comment