Guest User

Untitled

a guest
Dec 20th, 2025
10
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1.  
  2.  
  3. // ==UserScript==
  4. // @name         LexOffice Umsatz-Highlighter + Schwarzer Text (Robust, neue Identifier & DOM-Struktur)
  5. // @namespace    http://tampermonkey.net/
  6. // @version      2.1
  7. // @description  Zeilen einfärben + schwarzen Text erzwingen. Unterstützt verschiedene Layouts (mehrzeilig/einzeilig), OI-43232 oder reine Nummern, Betragsabgleich und Stripe-/Porto-Regeln.
  8. // @match        https://app.lexware.de/*
  9. // @grant        none
  10. // ==/UserScript==
  11.  
  12. (function() {
  13.     'use strict';
  14.  
  15.     // --------- 0) Kleines Utility: sichere .innerText-Lese-Funktion ----------
  16.     function textOf(el) {
  17.         try { return el ? el.innerText.trim() : ''; } catch(e){ return ''; }
  18.     }
  19.  
  20.     // ----------------------------- 1) GLOBAL CSS --------------------------------
  21.     const css = `
  22.         /* alle DataGrid-Zellen und Typography-Boxen auf schwarz zwingen */
  23.         .MuiDataGrid-root .MuiDataGrid-cell,
  24.         .MuiDataGrid-root .MuiTypography-root,
  25.         .MuiDataGrid-root .MuiBox-root {
  26.             color: #000000 !important;
  27.         }
  28.         /* kleine optische Verbesserung für Legende */
  29.         #tm-legend p, #tm-legend li { margin: 4px 0; }
  30.     `;
  31.     const style = document.createElement('style');
  32.     style.textContent = css;
  33.     document.head.appendChild(style);
  34.  
  35.     // ----------------------------- 2) FARBEN -------------------------------------
  36.     const COLORS = {
  37.         red:    '#edaaaa',
  38.         yellow: '#fffc97',
  39.         purple: '#ffa9f8',
  40.         green:  '#92ff99',
  41.         blue:   '#a7c9ff',
  42.         orange: '#ffd59e',
  43.         teal:   '#80e5ff'
  44.     };
  45.  
  46.     // ----------------------------- 3) ZAHLPARSER --------------------------------
  47.     function parseAmount(str) {
  48.         if (!str) return null;
  49.         const cleaned = String(str)
  50.             .replace(/\u00A0/g, '')      // NBSP entfernen
  51.             .replace(/[^\d,.\-€]/g, '')  // nur relevante Zeichen behalten
  52.             .replace(/\.(?=\d{3}(\D|$))/g, '') // Tausenderpunkte entfernen
  53.             .replace(',', '.')
  54.             .replace('€','').trim();
  55.         const n = parseFloat(cleaned);
  56.         return Number.isFinite(n) ? n : null;
  57.     }
  58.  
  59.     // ----------------------------- 4) LEGENDE -----------------------------------
  60.     function injectLegend() {
  61.         if (document.getElementById('tm-legend')) return;
  62.  
  63.         const legend = document.createElement('div');
  64.         legend.id = 'tm-legend';
  65.         Object.assign(legend.style, {
  66.             position: 'fixed',
  67.             top: '100px',
  68.             right: '20px',
  69.             background: 'white',
  70.             border: '1px solid #ccc',
  71.             padding: '10px 12px',
  72.             borderRadius: '6px',
  73.             boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
  74.             fontFamily: 'Arial, sans-serif',
  75.             fontSize: '12px',
  76.             color: '#000000',
  77.             zIndex: 99999,
  78.             cursor: 'move',
  79.             userSelect: 'none'
  80.         });
  81.  
  82.         legend.innerHTML = `
  83.             <div id="tm-legend-header" style="display:flex; justify-content:space-between; align-items:center; font-weight:600;">
  84.                 <span>Legende</span>
  85.                 <button id="tm-legend-toggle" style="margin-left:10px;background:#eee;border:none;padding:0 6px;cursor:pointer;border-radius:4px;"></button>
  86.             </div>
  87.             <ul id="tm-legend-body" style="list-style:none; padding:6px 0 0 0; margin:0;">
  88.                 <li><span style="display:inline-block;width:12px;height:12px;background:${COLORS.purple};margin-right:8px;"></span>Gebühren / Zinsen</li>
  89.                 <li><span style="display:inline-block;width:12px;height:12px;background:${COLORS.yellow};margin-right:8px;"></span>Deutsche Post / DHL / Porto</li>
  90.                 <li><span style="display:inline-block;width:12px;height:12px;background:${COLORS.green};margin-right:8px;"></span>Invoice-ID + Betrag match</li>
  91.                 <li><span style="display:inline-block;width:12px;height:12px;background:${COLORS.blue};margin-right:8px;"></span>Stripe Servicegebühr</li>
  92.                 <li><span style="display:inline-block;width:12px;height:12px;background:${COLORS.orange};margin-right:8px;"></span>Stripe Bestellgebühr</li>
  93.                 <li><span style="display:inline-block;width:12px;height:12px;background:${COLORS.teal};margin-right:8px;"></span>Stripe Auszahlung</li>
  94.                 <li><span style="display:inline-block;width:12px;height:12px;background:${COLORS.red};margin-right:8px;"></span>Sonstiges / Manual</li>
  95.             </ul>
  96.         `;
  97.         document.body.appendChild(legend);
  98.  
  99.         // Drag & Drop
  100.         const header = legend.querySelector('#tm-legend-header');
  101.         let dragging = false, offX = 0, offY = 0;
  102.         header.addEventListener('mousedown', e => {
  103.             dragging = true;
  104.             offX = e.clientX - legend.getBoundingClientRect().left;
  105.             offY = e.clientY - legend.getBoundingClientRect().top;
  106.             document.body.style.userSelect = 'none';
  107.         });
  108.         document.addEventListener('mousemove', e => {
  109.             if (!dragging) return;
  110.             legend.style.left = `${e.clientX - offX}px`;
  111.             legend.style.top = `${e.clientY - offY}px`;
  112.             legend.style.right = 'auto';
  113.         });
  114.         document.addEventListener('mouseup', () => {
  115.             dragging = false;
  116.             document.body.style.userSelect = '';
  117.         });
  118.  
  119.         // Toggle body
  120.         const toggle = legend.querySelector('#tm-legend-toggle');
  121.         const body = legend.querySelector('#tm-legend-body');
  122.         toggle.addEventListener('click', () => {
  123.             if (body.style.display === 'none') {
  124.                 body.style.display = 'block'; toggle.textContent = '✕';
  125.             } else {
  126.                 body.style.display = 'none'; toggle.textContent = '☰';
  127.             }
  128.         });
  129.     }
  130.  
  131.     // ----------------------------- 5) ROW PARSER --------------------------------
  132.     function extractFromRecipientCell(recCell) {
  133.         // robust: sammle Zeilen / Tokens aus DOM (unterstützt sowohl einzeilige als auch mehrzeilige Darstellung)
  134.         const raw = textOf(recCell);
  135.         // lines: split by newlines (falls vorhanden) oder multiple spaces
  136.         const lines = raw.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
  137.         // Fallback: wenn keine neuen Zeilen existieren, splitte nach doppelten spaces
  138.         if (lines.length === 1) {
  139.             const maybe = lines[0].split(/\s{2,}/).map(s => s.trim()).filter(Boolean);
  140.             if (maybe.length > 1) lines.splice(0,1, ...maybe);
  141.         }
  142.  
  143.         // Versuche: Name = alles vor Datum (dd.mm.yyyy). Datum finden und splitten
  144.         const joined = lines.join(' ');
  145.         const dateMatch = joined.match(/\b\d{2}\.\d{2}\.\d{4}\b/);
  146.         let name = '';
  147.         let afterDate = '';
  148.         if (dateMatch) {
  149.             const idx = joined.indexOf(dateMatch[0]);
  150.             name = joined.substring(0, idx).trim();
  151.             afterDate = joined.substring(idx + dateMatch[0].length).trim();
  152.         } else {
  153.             // Keine Datumserkennung -> erste Token als Name (Fallback)
  154.             name = lines[0] || '';
  155.             afterDate = lines.slice(1).join(' ');
  156.         }
  157.  
  158.         // Invoice-ID: OI-12345 oder reine Nummer (mind. 3 Ziffern)
  159.         const oiMatch = afterDate.match(/\bOI[-\s]?(\d{3,})\b/i);
  160.         const bareMatch = afterDate.match(/\b(\d{3,})\b/);
  161.         let invNum = null;
  162.         if (oiMatch) invNum = oiMatch[1];
  163.         else if (bareMatch) invNum = bareMatch[1];
  164.  
  165.         // Versuch Betrag in recipient cell zu finden (falls vorhanden)
  166.         const amtMatch = afterDate.match(/-?[\d\.,]+\s*€/);
  167.         const recAmount = amtMatch ? parseAmount(amtMatch[0]) : null;
  168.  
  169.         return { raw, lines, name, invNum, recAmount };
  170.     }
  171.  
  172.     function extractFromAssignmentCell(assCell) {
  173.         const raw = textOf(assCell);
  174.         // Suche nach Invoice-ID / Nummer auch hier (falls dort)
  175.         const oiMatch = raw.match(/\bOI[-\s]?(\d{3,})\b/i);
  176.         const bareMatch = raw.match(/\b(\d{3,})\b/);
  177.         const invNum = oiMatch ? oiMatch[1] : (bareMatch ? bareMatch[1] : null);
  178.  
  179.         // Suche nach Beträgen
  180.         const amtMatches = raw.match(/-?[\d\.,]+\s*€/g) || [];
  181.         const lastAmt = amtMatches.length ? parseAmount(amtMatches[amtMatches.length-1]) : null;
  182.  
  183.         // kurze Kategorie-Extraktion (falls vorhanden in subtitle)
  184.         const subtitleEl = assCell.querySelector('.MuiTypography-lxSubtitle2');
  185.         const category = textOf(subtitleEl) || '';
  186.  
  187.         return { raw, invNum, lastAmt, category };
  188.     }
  189.  
  190.     function updateRows() {
  191.         injectLegend();
  192.  
  193.         document.querySelectorAll('.MuiDataGrid-row').forEach(row => {
  194.             // prevent rows that are not fully loaded yet
  195.             const recCell  = row.querySelector('[data-field="recipientOrSenderName"]');
  196.             const assCell  = row.querySelector('[data-field="assignment"]');
  197.             const amtCell  = row.querySelector('[data-field="amount"]');
  198.  
  199.             if (!recCell || !assCell || !amtCell) return;
  200.  
  201.             const amtText = textOf(amtCell);
  202.             const mainAmt = parseAmount(amtText);
  203.  
  204.             const rec = extractFromRecipientCell(recCell);
  205.             const ass = extractFromAssignmentCell(assCell);
  206.             const assText = ass.raw || '';
  207.  
  208.             // Bestimme Farbe
  209.             let bg = COLORS.red; // default
  210.  
  211.             // 1) Gebühren / Zinsen
  212.             if (/Zinsen|Gebühren|Gebühr/i.test(ass.raw) || ass.category === 'Zinsen/Gebühren') {
  213.                 bg = COLORS.purple;
  214.             }
  215.             // 2) Post / DHL / Porto (Name-Check + Kategorie)
  216.             else if (/(Deutsche Post|DHL)/i.test(rec.raw) || /Porto/i.test(ass.raw) || /Porto/i.test(rec.raw) || ass.category.toLowerCase() === 'porto') {
  217.                 bg = COLORS.yellow;
  218.             }
  219.             // 3) Invoice-ID present in recipient or assignment AND amount matches
  220.             else if ((rec.invNum || ass.invNum) && (rec.recAmount !== null || ass.lastAmt !== null)) {
  221.                 // prefer recAmount if present
  222.                 const candidateAmt = rec.recAmount !== null ? rec.recAmount : ass.lastAmt;
  223.                 if (candidateAmt !== null && mainAmt !== null && Math.abs(mainAmt - candidateAmt) < 0.001) {
  224.                     bg = COLORS.green;
  225.                 }
  226.             }
  227.             // 4) Stripe Servicegebühr (flexible patterns)
  228.             else if (/STRP[\s\-_\u2011\u2013\u2014]*SG/i.test(assText) || /Servicegebühr/i.test(assText)) {
  229.                 bg = COLORS.blue;
  230.             }
  231.             // 5) Stripe Bestellgebühr
  232.             else if (/STRP[\s\-_\u2011\u2013\u2014]*BG/i.test(assText) || /Bestellgebühr/i.test(assText)) {
  233.                 bg = COLORS.orange;
  234.             }
  235.             // 6) Stripe Auszahlungen
  236.             else if (/STRP[\s\-_\u2011\u2013\u2014]*AUSZ/i.test(assText) || /Auszahlung/i.test(assText) || /AUSZAHL/i.test(assText)) {
  237.                 bg = COLORS.teal;
  238.             }
  239.             // Fallback: falls assignment/recipient enthalten 'txn_' oder 'STRP' aber keine oben match
  240.             else if (/txn_\w{6,}/i.test(rec.raw + ' ' + ass.raw) || /STRP/i.test(ass.raw)) {
  241.                 bg = COLORS.teal;
  242.             } else {
  243.                 bg = COLORS.red;
  244.             }
  245.  
  246.             // Wende Hintergrundfarbe an (nur wenn anders)
  247.             try {
  248.                 row.style.backgroundColor = bg;
  249.             } catch(e) { /* silent */ }
  250.         });
  251.     }
  252.  
  253.     // ----------------------------- 6) OBSERVER ----------------------------------
  254.     let scheduled = false;
  255.     const throttledUpdate = () => {
  256.         if (scheduled) return;
  257.         scheduled = true;
  258.         requestAnimationFrame(() => {
  259.             try { updateRows(); } finally { scheduled = false; }
  260.         });
  261.     };
  262.  
  263.     const observer = new MutationObserver(throttledUpdate);
  264.     observer.observe(document.body, { childList: true, subtree: true });
  265.  
  266.     // initial run and after full load
  267.     window.addEventListener('load', () => setTimeout(updateRows, 200));
  268.     // also attempt a few more times in case lexoffice renders later
  269.     setTimeout(updateRows, 700);
  270.     setTimeout(updateRows, 2000);
  271.  
  272. })();
Advertisement
Add Comment
Please, Sign In to add comment