Files
espocrm/README.md

65 KiB
Raw Blame History

KI-basierte Bearbeitung von EspoCRM: Struktur und Funktionsweise

Inhaltsverzeichnis

  1. Überblick
  2. Custom Directory Struktur
  3. Rebuild-Prozess
  4. Häufige Fehler vermeiden
  5. Dateiformate und JSON-Strukturen
  6. Workflow-Verwaltung
  7. Internationalisierung (i18n) und Tooltips
  8. Formula-Scripts und Custom PHP-Erweiterungen
  9. Panel-Labels und Übersetzungen
  10. Custom JavaScript & CSS Integration
  11. Reports und Report-Panels
  12. Portal-Freigabe-System
  13. Troubleshooting

Überblick

EspoCRM ist ein modular aufgebautes CRM-System, das auf PHP (Backend) und Backbone.js (Frontend) basiert. Konfigurationen für Entitäten, Felder, Beziehungen, Views und Layouts werden in JSON-basierten Metadata-Dateien gespeichert.

Die Anpassung erfolgt über das custom/-Verzeichnis, um Core-Dateien nicht zu überschreiben und Upgrades zu erleichtern. EspoCRM verwendet eine rekursive Merging-Mechanik: Custom-Dateien überschreiben oder erweitern Core-Definitionen.

Anpassungsprozess:

  1. JSON-Dateien im custom/-Verzeichnis erstellen/bearbeiten
  2. Rebuild-Script ausführen (validiert, merged, aktualisiert DB)
  3. Änderungen sind sofort wirksam

Keine integrierte KI-Schnittstelle existiert, aber mit Dateizugriff können automatisierte Anpassungen vorgenommen werden: Felder hinzufügen, Views anpassen, Beziehungen definieren, Workflows erstellen.

Custom Directory Struktur

Vollständige Übersicht: Siehe /custom/CUSTOM_DIRECTORY.md für detaillierte Dokumentation aller Custom-Verzeichnisse.

Wichtigste Bereiche:

  • custom/Espo/Custom/Resources/metadata/ - Backend-Definitionen (entityDefs, clientDefs, etc.)
  • custom/Espo/Custom/Classes/ - Custom PHP-Klassen (Formula-Funktionen, Services)
  • client/custom/src/ - Frontend JavaScript (Views, Module)
  • client/custom/css/ - Custom Stylesheets
  • custom/scripts/ - Wartungs-Scripts (Rebuild, Workflow-Manager)
  • custom/workflows/ - Versionierte Workflow-Definitionen

Rebuild-Prozess

WICHTIG: Nach jeder Änderung an Custom-Dateien muss ein Rebuild durchgeführt werden!

Validate & Rebuild Script (Empfohlen)

Zentrales Tool: custom/scripts/validate_and_rebuild.py

NEU ab Januar 2026: Erweitertes Python-basiertes Validierungs-Tool mit automatischen Checks!

Dieses Script sollte IMMER verwendet werden (nicht manueller Rebuild). Es führt automatisch aus:

Validierungen:

  • JSON-Syntax-Prüfung aller .json Dateien im custom/ Verzeichnis
  • Relationship-Konsistenz-Prüfung (bidirektionale Links, foreign-Definitionen)
  • Formula-Script Platzierung (korrekt in /formula/ statt /entityDefs/)
  • i18n-Vollständigkeit (fehlende Übersetzungen für Links)
  • Layout-Struktur-Prüfung
  • Dateirechte-Prüfung (www-data:www-data Owner)

Automatische Korrekturen:

  • Setzt fehlerhafte Dateirechte auf www-data:www-data
  • Korrigiert Verzeichnis-Permissions (775) und Datei-Permissions (664)

Rebuild:

  • Nur wenn keine kritischen Fehler gefunden werden!
  • Merged alle Custom-Metadata mit Core-Definitionen
  • Aktualisiert Datenbank-Schema (neue Felder, Tabellen, Indizes)
  • Leert Cache-Verzeichnis
  • Regeneriert Frontend-Assets

Verwendung:

# Im EspoCRM-Root-Verzeichnis ausführen
python3 custom/scripts/validate_and_rebuild.py

# Nur Validierung ohne Rebuild
python3 custom/scripts/validate_and_rebuild.py --dry-run

Ausgabe:

  • ✓ Grün: Alles in Ordnung, Rebuild erfolgreich
  • ⚠ Gelb: Warnungen (z.B. fehlende i18n), Rebuild wird trotzdem ausgeführt
  • ✗ Rot: Kritische Fehler (z.B. ungültiges JSON, fehlende Relationships), Rebuild wird NICHT ausgeführt

Bei Fehlern:

  • JSON-Syntax-Fehler werden mit Datei und Zeilennummer angezeigt
  • Relationship-Fehler zeigen fehlende Links zwischen Entities
  • Formula-Platzierungsfehler werden erkannt und gemeldet
  • i18n-Probleme werden als Warnungen angezeigt (kein Abbruch)
  • Dateirechte-Probleme werden automatisch korrigiert

Detaillierte Dokumentation: Siehe custom/scripts/VALIDATOR_README.md

Wann Rebuild erforderlich?

Backend-Änderungen:

  • entityDefs, clientDefs, layouts, scopes bearbeitet
  • Formula-Scripts erstellt/geändert
  • i18n-Dateien aktualisiert
  • Custom PHP-Klassen hinzugefügt
  • CSS in app/client.json registriert

Frontend-Änderungen:

  • JavaScript Views erstellt/geändert
  • CSS-Dateien hinzugefügt
  • ⚠️ Zusätzlich Browser Hard Refresh (Ctrl+Shift+R) erforderlich!

Workflows:

  • Kein Rebuild nötig (Import über workflow_manager.php)

Was der Rebuild bewirkt

  1. Metadata-Merging: Kombiniert Custom-Definitionen mit Core-Definitionen
  2. Datenbank-Schema-Update: Erstellt neue Tabellen/Spalten/Indizes basierend auf entityDefs
  3. Cache-Bereinigung: Löscht gecachte Metadata, Views, Templates
  4. Frontend-Build: Regeneriert JavaScript/CSS-Bundles
  5. ORM-Update: Aktualisiert Entity-Klassen und Repositories

Manuelle Dateirechte-Korrektur

Falls check_and_rebuild.sh Rechte-Probleme nicht beheben kann:

sudo chown -R www-data:www-data custom/ client/custom/ data/
sudo find custom/ -type f -name "*.json" -exec chmod 664 {} \;
sudo find custom/ -type d -exec chmod 775 {} \;
sudo find client/custom/ -type f -exec chmod 664 {} \;
sudo find client/custom/ -type d -exec chmod 775 {} \;

Häufige Fehler vermeiden

Basierend auf der Analyse von Git-Commits und praktischen Erfahrungen treten bestimmte Fehler besonders häufig auf. Diese Sektion hilft, die 5 häufigsten Fehler zu vermeiden.

⚠️ Top 5 Fehlerquellen

1. Formula-Scripts falsch platziert

Problem: Formula-Scripts werden in entityDefs/{Entity}.json statt in separater formula/{Entity}.json abgelegt.

Symptome:

  • beforeSaveApiScript wird nicht ausgeführt
  • Validierungen greifen nicht
  • Keine Fehler in Logs, Script wird einfach ignoriert

FALSCH:

// custom/Espo/Custom/Resources/metadata/entityDefs/CBankverbindungen.json
{
  "fields": {
    "iban": {"type": "varchar"}
  },
  "formula": {
    "beforeSaveApiScript": "..."  // ← Funktioniert NICHT!
  }
}

RICHTIG:

// custom/Espo/Custom/Resources/metadata/formula/CBankverbindungen.json
{
  "beforeSaveApiScript": "if (iban != null && iban != '') { ... }"
}

Zusätzliche Fallstricke:

  • string\isEmpty() verwenden (existiert nicht!)
  • Stattdessen: field != null && field != ''

Nach Korrektur: ./custom/scripts/check_and_rebuild.sh ausführen


2. Layout-Strukturfehler

Problem: Falsche Platzhalter und Strukturen in Layouts führen zu UI-Problemen.

⚠️ KRITISCH für EspoCRM 7.x: In EspoCRM 7.x ist false als Platzhalter NICHT MEHR ERLAUBT!

Symptome:

  • Layout wird nicht geladen, Fehlermeldung in UI
  • Felder werden nicht korrekt angeordnet
  • Leere Bereiche in Detail-Views
  • bottomPanelsDetail funktioniert nicht

FALSCH (EspoCRM 7.x):

// layouts/Entity/detail.json
{
  "rows": [
    [
      {"name": "field1"},
      {"name": "field2"},
      false,  // ← DEPRECATED! Funktioniert in 7.x NICHT!
      false   // ← Verursacht Layout-Fehler!
    ]
  ]
}

RICHTIG (EspoCRM 7.x):

{
  "rows": [
    [
      {"name": "field1"},
      {"name": "field2"},
      {},  // ← Leeres Objekt verwenden!
      {}   // ← Funktioniert in 7.x korrekt
    ]
  ]
}

FALSCH - bottomPanelsDetail.json als Array:

// layouts/Entity/bottomPanelsDetail.json
[
  {"name": "contacts"},
  {"name": "documents"}
]

RICHTIG - bottomPanelsDetail.json als Objekt:

{
  "contacts": {
    "index": 0,
    "sticked": true,
    "style": "warning"
  },
  "_tabBreak_0": {
    "index": 1,
    "columnBreak": true
  },
  "documents": {
    "index": 2
  },
  "activities": {
    "disabled": true
  },
  "history": {
    "disabled": true
  }
}

Häufige Layout-Fehler:

  • false als Platzhalter (seit EspoCRM 7.x nicht mehr unterstützt!)
  • Width-Attribute in Detail-Layouts (nur für List-Layouts!)
  • bottomPanelsDetail.json als Array statt Objekt
  • Index nicht angepasst nach Entfernung von Panels

Best Practice:

  • Immer {} statt false für leere Zellen in detail.json verwenden
  • Width nur in list.json und listSmall.json verwenden
  • bottomPanelsDetail.json MUSS Objekt-Format haben
  • Rows immer mit 4 Spalten füllen (nutze {} zum Auffüllen)
  • Nach Panel-Entfernung Indices neu nummerieren
  • _tabBreak_{index} verwenden um Panels auf verschiedene Tabs zu verteilen

3. Unvollständige Relationship-Definitionen

Problem: Bei hasMany-Relationships wird nur eine Seite definiert, die Gegenseite fehlt.

Symptome:

  • HTTP 404 "Link does not exist"-Fehler in Logs
  • Relationship-Panel zeigt keine Daten
  • Verknüpfungen können nicht erstellt werden

FALSCH - Nur eine Seite definiert:

// entityDefs/CMietobjekt.json
{
  "links": {
    "kontakte": {
      "type": "hasMany",
      "entity": "Contact",
      "foreign": "mietobjekte"  // ← Existiert nicht in Contact!
    }
  }
}

// entityDefs/Contact.json - FEHLT!

RICHTIG - Beide Seiten definiert:

// entityDefs/CMietobjekt.json
{
  "links": {
    "contactsMietobjekt": {
      "type": "hasMany",
      "relationName": "cMietobjektContactPortal",
      "foreign": "cMietobjektContactPortal",
      "entity": "Contact"
    }
  }
}

// entityDefs/Contact.json
{
  "links": {
    "cMietobjektContactPortal": {
      "type": "hasMany",
      "relationName": "cMietobjektContactPortal",  // ← IDENTISCH!
      "foreign": "contactsMietobjekt",  // ← Zeigt auf Gegenseite
      "entity": "CMietobjekt"
    }
  }
}

Kritische Punkte:

  • relationName muss auf beiden Seiten identisch sein
  • foreign zeigt auf den Link-Namen der Gegenseite
  • Beide entityDefs-Dateien müssen erstellt werden

Siehe auch: Abschnitt "Troubleshooting → Link does not exist"


4. i18n/Localization-Fehler

Problem: Labels nicht vollständig oder nur in einer Sprache definiert.

Symptome:

  • Relationship-Panels zeigen technische Namen statt Labels
  • Tooltips zeigen nur Feldnamen
  • In manchen Sprachen fehlen Beschriftungen

FALSCH - Unvollständig:

// i18n/de_DE/Entity.json
{
  "fields": {
    "mietobjekte": "Mietobjekte"
  },
  "links": {}  // ← Label fehlt!
}

// i18n/en_US/Entity.json - FEHLT KOMPLETT!

RICHTIG - Vollständig in beiden Sprachen:

// i18n/de_DE/Entity.json
{
  "fields": {
    "mietobjekte": "Mietobjekte"
  },
  "links": {
    "mietobjekte": "Mietobjekte"  // ← BEIDE Sektionen!
  },
  "tooltips": {
    "mietobjekte": "Verknüpfte Mietobjekte"
  }
}

// i18n/en_US/Entity.json  ← IMMER erstellen!
{
  "fields": {
    "mietobjekte": "Rental Properties"
  },
  "links": {
    "mietobjekte": "Rental Properties"
  },
  "tooltips": {
    "mietobjekte": "Linked rental properties"
  }
}

Kritische Regeln:

  • Labels IMMER in fields UND links definieren
  • en_US ist Fallback-Sprache → MUSS vollständig sein
  • Tooltips in ALLEN Sprachen konsistent definieren
  • Bei neuen Relationships beide Sprachen aktualisieren

Warum en_US kritisch ist: EspoCRM nutzt en_US als Fallback. Fehlt eine Definition dort, überschreibt sie möglicherweise die deutschen Labels!

Siehe auch: Abschnitt "Internationalisierung (i18n) und Tooltips"


5. Dateirechte-Probleme

Problem: Custom-Dateien gehören root statt www-data, EspoCRM kann nicht darauf zugreifen.

Symptome:

  • "Permission denied"-Fehler in Logs
  • Layouts können nicht über Admin-UI bearbeitet werden
  • HTTP 500-Fehler beim Speichern

Ursache: Dateien werden als Root-User erstellt (z.B. via sudo, Git-Checkout) und EspoCRM läuft als www-data.

Lösung: Das check_and_rebuild.sh Script prüft und korrigiert automatisch:

./custom/scripts/check_and_rebuild.sh

Manuelle Korrektur (falls nötig):

sudo chown -R www-data:www-data custom/ client/custom/ data/
sudo find custom/ -type f -name "*.json" -exec chmod 664 {} \;
sudo find custom/ -type d -exec chmod 775 {} \;

Prävention:

  • Änderungen direkt im Docker-Container vornehmen
  • Nach Git-Pull immer check_and_rebuild.sh ausführen
  • VSCode mit Remote-Development nutzt automatisch korrekte User

🛡️ Fehler-Prävention: Checkliste

Bei neuer Entity-Erstellung:

  • entityDefs in beiden Entities (bei Relationships)
  • relationName identisch auf beiden Seiten
  • foreign zeigt auf korrekten Link-Namen
  • i18n in de_DE UND en_US vollständig
  • Labels in fields UND links
  • Formula in formula/, NICHT in entityDefs/
  • Nach Erstellung: ./custom/scripts/check_and_rebuild.sh

Bei Hinzufügen neuer Felder:

  • Felder in entityDefs definieren mit korrektem Typ und Optionen
  • Felder zu relevanten Layouts hinzufügen (detail.json, list.json, etc.)
  • Felder sinnvoll gruppieren (eigenes Panel oder bestehendes Panel)
  • Labels in i18n/de_DE vollständig (fields UND links)
  • Labels in i18n/en_US vollständig (Fallback-Sprache!)
  • Tooltips in i18n hinzufügen falls erforderlich
  • Nach Änderung: ./custom/scripts/check_and_rebuild.sh
  • Browser Hard Refresh (Ctrl+Shift+R) durchführen

Bei Layout-Änderungen:

  • Keine false oder leere Objekte in Rows
  • Width nur in List-Layouts verwenden
  • Indices nach Panel-Entfernung neu nummerieren
  • Nach Änderung: ./custom/scripts/check_and_rebuild.sh

Bei Formula-Scripts:

  • Separate Datei formula/{Entity}.json erstellen
  • string\isEmpty() NICHT verwenden → != null && != ''
  • Null-Checks vor String-Operationen
  • Nach Erstellung: ./custom/scripts/check_and_rebuild.sh

Allgemein:

  • Immer ./custom/scripts/check_and_rebuild.sh nach Änderungen
  • Browser Hard Refresh (Ctrl+Shift+R) bei Frontend-Änderungen
  • Logs prüfen: tail -n 100 data/logs/espo-$(date +%Y-%m-%d).log

📊 Warum diese Fehler so häufig sind

Root Cause:

  1. EspoCRM Admin-UI generiert teilweise unvollständige JSON-Definitionen
  2. Bidirektionale Relationships sind komplex und nicht intuitiv
  3. en_US als Fallback ist nicht offensichtlich dokumentiert
  4. Formula-Platzierung wird im Admin-UI falsch vorgeschlagen
  5. Layout-Generierung erzeugt manchmal ungültige Array-Strukturen

Lösung:

  • Diese Dokumentation VOR der Entwicklung lesen
  • check_and_rebuild.sh Script konsequent nutzen
  • Bei Unsicherheit: Troubleshooting-Abschnitt konsultieren

Siehe auch:

Dateiformate und JSON-Strukturen

Alle Metadata-Dateien sind im JSON-Format. Die Strukturen sind hierarchisch: Objekte für Felder/Links, Arrays für Optionen/Listen.

Detaillierte Verzeichnisstruktur: Siehe /custom/CUSTOM_DIRECTORY.md

entityDefs/{EntityType}.json

Format-Beispiel:

{
  "fields": {
    "name": {
      "type": "varchar",
      "required": true,
      "len": 255
    },
    "status": {
      "type": "enum",
      "options": ["Active", "Inactive"],
      "default": "Active"
    },
    "employeeCount": {
      "type": "int"
    }
  },
  "links": {
    "account": {
      "type": "belongsTo",
      "entity": "Account",
      "foreign": "projects"
    },
    "teams": {
      "type": "hasMany",
      "entity": "Team",
      "relationName": "EntityTeam"
    }
  },
  "collection": {
    "sortBy": "createdAt",
    "asc": false,
    "boolFilters": ["onlyMy"]
  },
  "indexes": {
    "name": {
      "columns": ["name"]
    }
  }
}

Wichtige Eigenschaften:

  • fields - Feldtypen (varchar, enum, link, etc.), Validierungen, Optionen
  • links - Beziehungen zwischen Entitäten (belongsTo, hasMany, hasOne)
  • collection - Listen-View-Einstellungen (Sortierung, Filter)
  • indexes - Datenbank-Performance-Optimierung

KRITISCH - Bidirektionale Relationships:

Bei hasMany-Relationships müssen BEIDE Seiten definiert werden:

Beispiel: Contact ↔ Mietverhältnis

// custom/Espo/Custom/Resources/metadata/entityDefs/CVmhMietverhltnis.json
{
  "links": {
    "contactsMietverhltnis": {
      "type": "hasMany",
      "relationName": "cVmhMietverhltnisContact",
      "foreign": "cVmhMietverhltnisContact",
      "entity": "Contact"
    }
  }
}

// custom/Espo/Custom/Resources/metadata/entityDefs/Contact.json
{
  "links": {
    "cVmhMietverhltnisContact": {
      "type": "hasMany",
      "relationName": "cVmhMietverhltnisContact",
      "foreign": "contactsMietverhltnis",
      "entity": "CVmhMietverhltnis"
    }
  }
}

Wichtig:

  • relationName muss auf beiden Seiten identisch sein
  • foreign zeigt auf den Link-Namen der Gegenseite
  • Fehlt eine Seite → "404 Link does not exist"-Fehler

Vollständiger Prozess beim Hinzufügen neuer Felder:

Neue Felder in einer Entity erfordern Änderungen in drei Bereichen:

  1. entityDefs/{Entity}.json - Feld-Definition
  2. layouts/{Entity}/*.json - Sichtbarkeit in UI
  3. i18n/{Sprache}/{Entity}.json - Beschriftungen

Schritt-für-Schritt-Anleitung:

1. Feld in entityDefs definieren:

// custom/Espo/Custom/Resources/metadata/entityDefs/CVmhMietverhltnis.json
{
  "fields": {
    "kaltmiete": {
      "type": "currency",
      "required": true,
      "onlyDefaultCurrency": true,
      "min": 1,
      "decimal": true,
      "tooltip": true,
      "isCustom": true
    }
  }
}

2. Feld zu Layouts hinzufügen:

// custom/Espo/Custom/Resources/layouts/CVmhMietverhltnis/detail.json
[
  {
    "rows": [
      [
        {"name": "kaltmiete"},
        {"name": "warmmiete"}
      ]
    ],
    "customLabel": "Miethöhe",
    "noteText": "Erfassen Sie die Miethöhe in ihren einzelnen Bestandteilen",
    "noteStyle": "info"
  }
]

3. Labels in ALLEN Sprachen definieren:

// custom/Espo/Custom/Resources/i18n/de_DE/CVmhMietverhltnis.json
{
  "fields": {
    "kaltmiete": "Kaltmiete"
  },
  "tooltips": {
    "kaltmiete": "Monatliche Kaltmiete ohne Betriebskosten"
  }
}

// custom/Espo/Custom/Resources/i18n/en_US/CVmhMietverhltnis.json
{
  "fields": {
    "kaltmiete": "Base Rent"
  }
}

Wichtig:

  • IMMER de_DE UND en_US pflegen (en_US ist Fallback!)
  • Tooltips nur wenn "tooltip": true im entityDef gesetzt
  • Bei Link-Feldern Labels in fields UND links definieren
  • Nach Änderungen: ./custom/scripts/check_and_rebuild.sh

Typische Layout-Typen:

  • detail.json - Detail-Ansicht (Panels mit Rows)
  • list.json - Listen-Ansicht (Spalten mit Width)
  • listSmall.json - Kompakte Listen-Ansicht
  • detailSmall.json - Seitenleisten-Ansicht
  • filters.json - Filter-Felder

clientDefs/{EntityType}.json

Format-Beispiel:


{
  "controller": "controllers/record",
  "collection": "collection",
  "model": "model",
  "views": {
    "list": "views/record/list",
    "detail": "views/record/detail",
    "edit": "views/record/edit"
  },
  "recordViews": {
    "list": "views/record/list",
    "kanban": "custom:views/record/kanban"
  },
  "viewSetupHandlers": {
    "record/detail": ["custom:handlers/my-detail-handler"]
  }
}

Wichtige Eigenschaften:

  • views/recordViews - Pfade zu JavaScript Views (Custom mit Präfix custom:)
  • viewSetupHandlers - Dynamische View-Anpassungen
  • filterList - Report-Filter-Integration (mit __APPEND__)
  • sidePanels - Report-Panels für Side-Panel

layouts/{EntityType}/{LayoutType}.json

Format-Beispiel für Detail-View:


[
  {
    "label": "Overview",
    "rows": [
      [
        {"name": "name"},
        {"name": "assignedUser"}
      ],
      [
        {"name": "description"}
      ]
    ]
  },
  {
    "label": "Details",
    "rows": [
      [{"name": "createdAt"}]
    ]
  }
]

Struktur:

  • Arrays von Panels (Objekte mit label und rows)
  • rows sind Arrays von Zellen (Objekte mit name für Felder)
  • Unterstützt Parameter: width, notSortable, customLabel

Spezielle Features:

  • __APPEND__ - Als erstes Array-Element einfügen, um bestehende Werte zu erweitern
  • layoutAvailabilityList - Array für Feld-Sichtbarkeit in Layouts
  • layoutIgnoreList - Zu ignorierende Layouts

Workflow-Verwaltung

EspoCRM bietet zwei Workflow-Typen für Prozessautomatisierung:

1. Simple Workflows (Regel-basiert)

Trigger-basierte Workflows für einfache Automationen.

Trigger-Typen:

  • afterRecordSaved - Nach Erstellen oder Aktualisieren
  • afterRecordCreated - Nur nach Erstellen
  • afterRecordUpdated - Nur nach Aktualisieren
  • manual - Manuell ausgeführt
  • scheduled - Zeitgesteuert

Bedingungen:

  • Vergleiche: equals, notEquals, greaterThan, lessThan, contains, isEmpty
  • Änderungen: changed, notChanged, wasEqual, wasNotEqual

Aktionen:

  • sendEmail - E-Mail versenden
  • createEntity - Record erstellen
  • updateEntity - Record aktualisieren
  • relateTo / unrelateFrom - Verknüpfungen
  • createNotification - Benachrichtigung

2. BPM Flowcharts (Komplex)

Visuelle Workflows mit BPMN 2.0-Standard für komplexe, mehrstufige Geschäftsprozesse.

Komponenten:

  • Start-Events: Signal, Conditional, Timer
  • Gateways: Exclusive, Inclusive, Parallel
  • Tasks, End-Events

Verwendung: Über visuellen Designer im Admin-Interface

Workflow-Dateien

Speicherort: custom/workflows/*.json

Workflow-Definitionen werden als JSON versioniert und über Git verwaltet.

Format-Dokumentation: Siehe custom/workflows/README.md

Workflow Manager Script

Tool: custom/scripts/workflow_manager.php

Kommandozeilen-Tool für Workflow-Verwaltung (Simple und BPM).

Verfügbare Aktionen

1. Alle Workflows auflisten

docker exec espocrm php /var/www/html/custom/scripts/workflow_manager.php list

Zeigt beide Workflow-Typen (BPM Flowcharts und Simple Workflows) mit Status, ID, Name und Entity.

2. Workflow-Details anzeigen

docker exec espocrm php /var/www/html/custom/scripts/workflow_manager.php read <workflow-id>

Gibt alle Details eines Workflows als JSON aus.

3. Workflow importieren

docker exec espocrm php /var/www/html/custom/scripts/workflow_manager.php import \
  /var/www/html/custom/workflows/workflow.json

Importiert einen Workflow aus JSON-Datei. Unterstützt beide Workflow-Typen. Erstellt automatisch neue ID.

4. Workflow exportieren

docker exec espocrm php /var/www/html/custom/scripts/workflow_manager.php export \
  <workflow-id> /var/www/html/custom/workflows/exported.json

Exportiert einen Workflow in JSON-Datei für Backup oder Migration.

5. Workflow löschen

docker exec espocrm php /var/www/html/custom/scripts/workflow_manager.php delete <workflow-id>

Löscht einen Workflow (mit Bestätigung). Funktioniert für beide Workflow-Typen.

JSON-Formate

Simple Workflow Format

{
    "type": "simple",
    "name": "workflow-name",
    "entity_type": "EntityName",
    "trigger_type": "afterRecordSaved",
    "is_active": true,
    "description": "Beschreibung der Funktion",
    "category": "Kategorie-Name",
    "conditions_all": [
        {
            "comparison": "equals",
            "fieldToCompare": "fieldName",
            "value": "expectedValue",
            "subjectType": "value"
        }
    ],
    "conditions_any": [],
    "conditions_formula": null,
    "actions": [
        {
            "type": "sendEmail",
            "from": "specifiedEmailAddress",
            "fromEmailAddress": "sender@example.com",
            "to": "targetEntity",
            "emailTemplateId": null,
            "doNotStore": false
        }
    ]
}

Wichtige Felder:

  • category - Workflow-Kategorie für bessere Organisation
  • comparison - Vergleichsoperator
  • fieldToCompare - Feldname für Bedingung
  • subjectType - Typ des Vergleichswerts (value, field, etc.)
  • from / to - E-Mail-Empfänger (targetEntity, specifiedEmailAddress, system)

BPM Flowchart Format

{
    "type": "bpm",
    "name": "flowchart-name",
    "target_type": "EntityName",
    "is_active": true,
    "description": "Beschreibung",
    "data": {
        "list": [
            {
                "type": "eventStartSignal",
                "id": "start1",
                "signalName": "@signalName"
            }
        ]
    },
    "elements_data_hash": {},
    "event_start_all_id_list": []
}

Workflow-Entwicklung Best Practices

  1. Versionierung: Workflows als JSON im custom/workflows/ Verzeichnis versionieren
  2. Naming Convention: Beschreibende Namen mit Präfix (z.B. vmh-erstberatung-abschliessen.json)
  3. Entwicklungsprozess:
    • Workflow-JSON in custom/workflows/ erstellen
    • Mit workflow_manager.php import einspielen
    • Im Admin-Interface testen und bei Bedarf anpassen
    • Mit export aktualisierten Workflow sichern
    • JSON-Datei im Repository committen
  4. Backup: Regelmäßig Export wichtiger Workflows durchführen
  5. Dokumentation: Description-Feld aussagekräftig füllen

Beispiel-Workflow

Szenario: E-Mail bei Status-Wechsel zu "Warte auf Mandatierung"

{
    "type": "simple",
    "name": "vmh-erstberatung-abschliessen",
    "entity_type": "CVmhErstgespraech",
    "trigger_type": "afterRecordSaved",
    "is_active": true,
    "conditions_all": [
        {
            "comparison": "equals",
            "fieldToCompare": "status",
            "value": "Warte auf Mandatierung"
        },
        {
            "comparison": "changed",
            "fieldToCompare": "status"
        }
    ],
    "actions": [
        {
            "type": "sendEmail",
            "to": "targetEntity",
            "emailTemplateId": "template-id-here"
        }
    ]
}

Internationalisierung (i18n) und Tooltips

EspoCRM verwendet ein hierarchisches Mehrsprachen-System mit en_US als Basis-Fallback:

  1. Sprachpriorität: en_US → aktuelle Sprache (z.B. de_DE)
  2. Problem: Tooltips in en_US überschreiben Tooltips in anderen Sprachen
  3. Lösung: Tooltips MÜSSEN in ALLEN Sprachen definiert werden

Beispiel für korrektes Tooltip-Setup:

entityDefs/{Entity}.json:

{
  "fields": {
    "iban": {
      "type": "varchar",
      "tooltip": true  // Aktiviert Tooltip-Anzeige
    }
  }
}

i18n/de_DE/{Entity}.json:

{
  "fields": {
    "iban": "IBAN"
  },
  "tooltips": {
    "iban": "Internationale Bankkontonummer im Format DE89..."
  }
}

i18n/en_US/{Entity}.json:

{
  "fields": {
    "iban": "IBAN"
  },
  "tooltips": {
    "iban": "International Bank Account Number in format DE89..."
  }
}

Häufige Fehler:

FALSCH - Unvollständige en_US-Datei:

{
  "fields": [],
  "tooltips": {
    "iban": "iban2"  // Überschreibt deutschen Tooltip!
  }
}

RICHTIG - Vollständige Definitionen in beiden Sprachen:

  • Alle Felder in fields definieren
  • Alle Tooltips in tooltips definieren
  • Konsistente Struktur über alle Sprachen

Debugging von Tooltip-Problemen:

  1. Symptom: Tooltip zeigt nur Feldnamen (z.B. "iban" statt vollständiger Beschreibung)
  2. Ursache: Fehlerhafte oder fehlende Definition in i18n/en_US/{Entity}.json
  3. Prüfung:
    • Existiert i18n/en_US/{Entity}.json?
    • Enthält es fehlerhafte Tooltip-Definitionen?
    • Sind alle Tooltips konsistent über alle Sprachen?
  4. Lösung: Vervollständige en_US-Datei mit korrekten englischen Übersetzungen

Best Practices:

  • Erstelle immer sowohl de_DE als auch en_US Übersetzungen
  • Verwende beschreibende Tooltips mit Beispielen und Format-Hinweisen
  • Teste Tooltips nach jedem Rebuild in beiden Sprachen
  • Bei neuen Feldern: erst i18n-Dateien vollständig ausfüllen, dann Rebuild

Formula-Scripts und Custom PHP-Erweiterungen

EspoCRM bietet mächtige Erweiterungsmöglichkeiten durch Formula-Scripts und Custom PHP-Funktionen. Diese ermöglichen Validierungen, Berechnungen und Business-Logik direkt beim Speichern von Datensätzen.

Formula-Scripts: Grundlagen

WICHTIG: Dateistruktur

FALSCH - Formula in entityDefs:

// custom/Espo/Custom/Resources/metadata/entityDefs/Entity.json
{
  "fields": {...},
  "formula": {
    "beforeSaveApiScript": "..."  // FUNKTIONIERT NICHT!
  }
}

RICHTIG - Separate Formula-Datei:

// custom/Espo/Custom/Resources/metadata/formula/Entity.json
{
  "beforeSaveApiScript": "if (field != null) { ... }"
}

Verfügbare Formula-Script-Typen

  1. beforeSaveApiScript - Wird vor dem Speichern ausgeführt (UI + API, ab v7.5+)
  2. beforeSaveCustomScript - Nur bei internen Saves (ohne API)
  3. afterSaveScript - Nach dem Speichern

Verwendung: Validierungen, Berechnungen, Daten-Transformation vor dem Speichern

Verfügbare Formula-Funktionen

String-Funktionen:

  • string\concatenate(str1, str2) - Strings verbinden
  • string\replace(text, search, replace) - Ersetzen
  • string\substring(text, start, length) - Teilstring
  • string\length(text) - Länge
  • string\test(text, pattern) - Regex-Test
  • ⚠️ NICHT verfügbar: string\isEmpty() → Verwende field != null && field != ''

Logik:

  • if (condition) { ... }
  • &&, ||, ! (AND, OR, NOT)
  • ==, !=, >, <, >=, <=

Fehlerbehandlung:

  • recordService\throwBadRequest('Fehlermeldung') - Speichern abbrechen mit Fehlermeldung

Custom Formula-Funktionen erstellen

Beispiel: IBAN-Validierung mit Modulo-97-Algorithmus

1. PHP-Klasse erstellen:

Pfad: custom/Espo/Custom/Classes/FormulaFunctions/IbanGroup/ValidateType.php

<?php
namespace Espo\Custom\Classes\FormulaFunctions\IbanGroup;

use Espo\Core\Formula\Functions\BaseFunction;
use Espo\Core\Formula\ArgumentList;

class ValidateType extends BaseFunction
{
    public function process(ArgumentList $args)
    {
        if (count($args) < 1) {
            return false;
        }

        $iban = $this->evaluate($args[0]);
        
        if (!$iban || !is_string($iban)) {
            return false;
        }

        // IBAN-Validierungs-Logik hier
        // ... (siehe ValidateType.php für vollständige Implementierung)
        
        return $remainder === 1; // Modulo-97-Check
    }
}

2. Funktion registrieren:

Pfad: custom/Espo/Custom/Resources/metadata/app/formula.json

{
    "functionList": [
        "__APPEND__",
        {
            "name": "iban\\validate",
            "insertText": "iban\\validate(IBAN)"
        }
    ],
    "functionClassNameMap": {
        "iban\\validate": "Espo\\Custom\\Classes\\FormulaFunctions\\IbanGroup\\ValidateType"
    }
}

3. Funktion verwenden:

Pfad: custom/Espo/Custom/Resources/metadata/formula/CBankverbindungen.json

{
    "beforeSaveApiScript": "if (iban != null && iban != '') {\n    $ibanClean = string\\replace(iban, ' ', '');\n    if (!iban\\validate($ibanClean)) {\n        recordService\\throwBadRequest('Ungültige IBAN!');\n    }\n}"
}

Wichtige Hinweise zur Formula-Entwicklung

Namespace-Struktur:

  • Verzeichnis: custom/Espo/Custom/Classes/FormulaFunctions/{GroupName}/
  • Namespace: Espo\Custom\Classes\FormulaFunctions\{GroupName}
  • Klassenname: {FunctionName}Type (z.B. ValidateType)
  • Muss BaseFunction erweitern

Funktionsnamen:

  • Format: group\functionName (z.B. iban\validate, string\replace)
  • Backslash \ wird verwendet (nicht :: oder /)

Häufige Fehler:

Fehler Symptom Lösung
Formula in entityDefs statt formula/ Script wird nicht ausgeführt Separate formula/{Entity}.json erstellen
string\isEmpty() verwendet Error: "Unknown function" Verwende field != null && field != ''
Falsche Namespace-Struktur Funktion nicht gefunden Prüfe Namespace, Klassenname, Pfad
Funktion nicht registriert "Unknown function" Eintrag in app/formula.json erstellen
Keine __APPEND__ in functionList Überschreibt Core-Funktionen Immer "__APPEND__" als erstes Element

Debugging von Formula-Scripts

Logs prüfen:

tail -n 100 /var/www/html/data/logs/espo-*.log | grep -i "formula\|error"

Häufige Fehlermeldungen:

  • Unknown function: xxx → Funktion existiert nicht oder nicht registriert
  • Error 500 → Syntax-Fehler im Formula-Script oder PHP-Fehler
  • validationFailurethrowBadRequest() wurde aufgerufen

Test-Workflow:

  1. Formula-Script schreiben
  2. Rebuild ausführen: bash custom/scripts/check_and_rebuild.sh
  3. Cache leeren (falls nötig): rm -rf data/cache/*
  4. Testdaten speichern und Logs prüfen

Best Practices

Empfohlen:

  • Separate Formula-Dateien pro Entity in metadata/formula/
  • Wiederverwendbare Logik in Custom PHP-Funktionen auslagern
  • Aussagekräftige Fehlermeldungen mit throwBadRequest()
  • Null-Checks vor Operationen: field != null && field != ''
  • Code kommentieren für spätere Wartung

Vermeiden:

  • Formula-Scripts in entityDefs ablegen
  • Nicht-existente String-Funktionen wie isEmpty()
  • Komplexe Logik direkt in Formula (besser: PHP-Funktion)
  • Fehlende Registrierung in app/formula.json

Beispiel-Anwendungsfälle

  1. Validierung: IBAN-Check, Email-Format, Telefonnummern

  2. Berechnung: Gesamtpreise, Datumsberechnungen, Provisionen

  3. Daten-Transformation: Großbuchstaben, Formatierungen, Normalisierungen

  4. Business Rules: Status-Überprüfungen, Pflichtfeld-Logik, Abhängigkeiten

  5. Auslösen von Änderungen und Rebuild-Prozess

    Was Änderungen auslösen: Datei-Änderungen: Werden bei Merging berücksichtigt rekursiv, also überschreiben Customs Core. Datenbank-Effekte: Neue Felder/Links in entityDefs erzeugen Tabellen/Spalten (bei Rebuild). Frontend-Effekte: clientDefs/Layouts ändern UI sofort nach Rebuild (z. B. neue Panels, Views). Fehlerquellen: Ungültiges JSON oder falsche Typen können zu Fehlern führen (z. B. fehlende required-Felder).

Workflow-Verwaltung

EspoCRM bietet zwei Arten von Workflows für Automatisierung:

Simple Workflows (Regel-basiert)

  • Trigger-basierte Workflows für einfache Automationen
  • Trigger-Typen:
    • afterRecordSaved - Nach Erstellen oder Aktualisieren
    • afterRecordCreated - Nur nach Erstellen
    • afterRecordUpdated - Nur nach Aktualisieren
    • manual - Manuell ausgeführt
    • scheduled - Zeitgesteuert
  • Bedingungen:
    • Vergleiche: equals, notEquals, greaterThan, lessThan, contains, isEmpty
    • Änderungen: changed, notChanged, wasEqual
  • Aktionen:
    • sendEmail - E-Mail versenden
    • createEntity - Record erstellen
    • updateEntity - Record aktualisieren
    • relateTo / unrelateFrom - Verknüpfungen
    • createNotification - Benachrichtigung

BPM Flowcharts (Komplex)

  • Visuelle Workflows mit BPMN 2.0-Standard
  • Start-Events: Signal, Conditional, Timer
  • Gateways (Exclusive, Inclusive, Parallel), Tasks, End-Events
  • Für komplexe, mehrstufige Geschäftsprozesse

Workflow-Dateien

Workflow-Definitionen werden im Ordner custom/workflows/ als JSON abgelegt:

  • custom/workflows/*.json - Workflow-Definitionen (Simple oder BPM)
  • custom/workflows/README.md - Dokumentation zu Formaten und Verwendung

Workflow Manager Script

Zentrale Schnittstelle: custom/scripts/workflow_manager.php

Dieses Script ermöglicht die Verwaltung aller Workflows (Simple und BPM) über die Kommandozeile.

Unterstützte Funktionen:

  • ✓ Kategorisierung von Workflows
  • ✓ Import/Export mit Kategorie-Namen
  • ✓ Übersichtliche Darstellung nach Kategorien
  • ✓ Unterstützung für beide Workflow-Typen (Simple & BPM)

Verfügbare Aktionen

1. Alle Workflows auflisten

docker exec espocrm php /var/www/html/custom/scripts/workflow_manager.php list

Zeigt beide Workflow-Typen (BPM Flowcharts und Simple Workflows) mit Status, ID, Name und Entity.

2. Workflow-Details anzeigen

docker exec espocrm php /var/www/html/custom/scripts/workflow_manager.php read <workflow-id>

Gibt alle Details eines Workflows als JSON aus.

3. Workflow importieren

docker exec espocrm php /var/www/html/custom/scripts/workflow_manager.php import /var/www/html/custom/workflows/workflow.json

Importiert einen Workflow aus einer JSON-Datei. Unterstützt sowohl Simple Workflows als auch BPM Flowcharts. Erstellt automatisch eine neue ID.

4. Workflow exportieren

docker exec espocrm php /var/www/html/custom/scripts/workflow_manager.php export <workflow-id> /var/www/html/custom/workflows/exported.json

Exportiert einen Workflow in eine JSON-Datei für Backup oder Migration.

5. Workflow löschen

docker exec espocrm php /var/www/html/custom/scripts/workflow_manager.php delete <workflow-id>

Löscht einen Workflow (mit Bestätigung). Funktioniert für beide Workflow-Typen.

JSON-Formate

Simple Workflow Format

{
    "type": "simple",
    "name": "workflow-name",
    "entity_type": "EntityName",
    "trigger_type": "afterRecordSaved",
    "is_active": true,
    "description": "Beschreibung der Funktion",
    "category": "Kategorie-Name",
    "conditions_all": [
        {
            "comparison": "equals",
            "fieldToCompare": "fieldName",
            "value": "expectedValue",
            "subjectType": "value"
        }
    ],
    "conditions_any": [],
    "conditions_formula": null,
    "actions": [
        {
            "type": "sendEmail",
            "from": "specifiedEmailAddress",
            "fromEmailAddress": "sender@example.com",
            "to": "targetEntity",
            "emailTemplateId": null,
            "doNotStore": false
        }
    ]
}

Wichtige Felder:

  • category - NEU: Name der Workflow-Kategorie (optional, für bessere Organisation)
  • comparison - Vergleichsoperator (siehe Bedingungen oben)
  • fieldToCompare - Feldname für Bedingung
  • subjectType - Typ des Vergleichswerts (value, field, etc.)
  • from / to - E-Mail-Empfänger (targetEntity, specifiedEmailAddress, system)

BPM Flowchart Format

{
    "type": "bpm",
    "name": "flowchart-name",
    "target_type": "EntityName",
    "is_active": true,
    "description": "Beschreibung",
    "data": {
        "list": [
            {
                "type": "eventStartSignal",
                "id": "start1",
                "signalName": "@signalName"
            }
        ]
    },
    "elements_data_hash": {},
    "event_start_all_id_list": []
}

Best Practices

  1. Versionierung: Workflows als JSON-Dateien im custom/workflows/ Verzeichnis versionieren
  2. Naming Convention: Beschreibende Namen mit Präfix (z.B. vmh-erstberatung-abschliessen.json)
  3. Testen: Nach Import immer über Admin-Interface testen
  4. Backup: Regelmäßig Export für wichtige Workflows durchführen
  5. Dokumentation: Description-Feld aussagekräftig füllen

Beispiel-Workflows

custom/workflows/vmh-erstberatung-abschliessen.json

  • Sendet E-Mail bei Status-Wechsel zu "Warte auf Mandatierung"
  • Trigger: afterRecordSaved
  • Bedingungen: Status = "Warte auf Mandatierung" UND Status hat sich geändert
  • Aktion: E-Mail an targetEntity senden

Anwendungsbeispiel:

# Alle Workflows exportieren (Backup)
docker exec espocrm php /var/www/html/custom/scripts/workflow_manager.php list | grep ID | \
  awk '{print $3}' | while read id; do
    docker exec espocrm php /var/www/html/custom/scripts/workflow_manager.php export "${id%,}" \
      "/var/www/html/custom/workflows/backup-${id%,}.json"
done

# Workflow aus Datei (re-)importieren
docker exec espocrm php /var/www/html/custom/scripts/workflow_manager.php import \
  /var/www/html/custom/workflows/vmh-erstberatung-abschliessen.json

Workflow-Entwicklung mit KI

Für KI-gestützte Workflow-Erstellung:

  1. Workflow-Definition im custom/workflows/ Verzeichnis als JSON ablegen
  2. Mit import Befehl in EspoCRM einspielen
  3. Im Admin-Interface testen und bei Bedarf anpassen
  4. Mit export Befehl aktualisierten Workflow sichern
  5. JSON-Datei im Repository committen

Projektziele und Zukunftsvision: "Vermieterhelden"

Das Projekt "Vermieterhelden" ist ein maßgeschneidertes Backend-System auf Basis von EspoCRM für eine Anwaltskanzlei, spezialisiert auf die Durchführung und Verwaltung von immobilienrechtlichen Klagen (z. B. Räumungsklagen, Mietinkasso). Der aktuelle Fokus liegt auf der strukturierten Verwaltung von Stammdaten (Entitäten wie Mietverhältnisse, Mietobjekte, Beteiligte, Dokumente und Klagen) und der Abbildung von rechtlichen Workflows (z. B. automatisierte Tasks bei Statusänderungen, Fristen-Überwachung).

Zukünftige Ziele:

  • Customer Portal: Integration eines Mandanten-Portals, damit Klienten (Mieter/Vermieter) selbst auf relevante Daten zugreifen können (z. B. Status von Klagen, Dokumente hochladen). Dies nutzt EspoCRMs eingebaute Portal-Funktionalität für Self-Service.
  • KI-Integration über Middleware: Automatisierung von Prozessen via Webhooks und externer Middleware. Beispiele:
    • Automatische Analyse von Dokumenten (z. B. Verträge scannen und Felder extrahieren).
    • Intelligente Fristen-Erinnerungen basierend auf rechtlichen Regeln (z. B. Kündigungsfristen berechnen).
    • Workflow-Optimierung (z. B. Vorschläge für nächste Schritte in Klage-Prozessen).
    • Die KI soll über APIs/Webhooks angebunden werden, ohne EspoCRMs Core zu modifizieren, um Stabilität zu wahren.
  • Erweiterte Features: Mehrsprachigkeit, Mandanten-Isolation für mehrere Kanzlei-Teams, Integration mit externen Systemen (z. B. Gerichts-APIs, Buchhaltung).

Die KI kann diese Ziele unterstützen, indem sie JSON-Strukturen analysiert, Änderungen vorschlägt (z. B. neue Felder für Compliance) und Workflows modelliert. Das System soll skalierbar und benutzerfreundlich sein, um die Effizienz in der Rechtsbranche zu steigern.

  1. Bearbeitung von Entitäten und Layouts

Um EspoCRM anzupassen, bearbeite JSON-Dateien im custom/-Verzeichnis. Änderungen bleiben bei Updates erhalten, da sie Core-Dateien nicht überschreiben.

Entitäten bearbeiten:
    Pfad: custom/Espo/Custom/Resources/metadata/entityDefs/{EntityType}.json (z. B. CVmhErstgespraech.json).
    Struktur: JSON-Objekt mit "fields" (Felder definieren), "links" (Beziehungen), "collection" (Sortierung/Filter), "indexes" (Performance).
    Beispiel: Feld hinzufügen  Füge in "fields" ein neues Objekt ein, z. B. {"type": "varchar", "required": true}.
    Beispiel: Feld entfernen  Lösche den entsprechenden Schlüssel aus "fields".
    Hinweis: Änderungen wirken sich auf die Datenbank aus (z. B. neue Spalten bei Rebuild).

Layouts bearbeiten:
    Pfad: custom/Espo/Custom/Resources/layouts/{EntityType}/{LayoutType}.json (z. B. detail.json für Detail-View).
    Struktur: Array von Panels, jedes mit "label" und "rows" (Arrays von Zellen mit {"name": "feldname"}).
    Beispiel: Feld hinzufügen  Füge {"name": "neuesFeld"} in eine "rows"-Zeile ein.
    Beispiel: Feld entfernen  Lösche die entsprechende Zelle aus "rows".
    LayoutTypes: detail, list, edit, etc.  Passe Views an, um UI zu optimieren.

Rebuild durchführen:
    Nach Änderungen muss ein Rebuild durchgeführt werden.
    Verwende: `./custom/scripts/check_and_rebuild.sh` (validiert JSON, prüft Rechte, führt Rebuild durch)
    Siehe Abschnitt "Rebuild-Prozess" für Details.

Panel-Labels und Übersetzungen

Um Relationship-Panels und Links korrekt zu beschriften, müssen Labels in den i18n-Sprachdateien definiert werden.

Vollständige i18n-Dokumentation: Siehe Abschnitt "Internationalisierung (i18n) und Tooltips"

Kurzübersicht:

  • Labels in allen Sprachen definieren (de_DE UND en_US)
  • Labels in zwei Sektionen: fields UND links
  • Nach Änderungen: ./custom/scripts/check_and_rebuild.sh

Beispiel:

// custom/Espo/Custom/Resources/i18n/de_DE/CBeteiligte.json
{
  "fields": {
    "vmhvermieterbeteiligte": "Vermieter",
    "vmhmieterbeteiligte": "Mieter"
  },
  "links": {
    "vmhvermieterbeteiligte": "Vermieter",
    "vmhmieterbeteiligte": "Mieter"
  }
}

Tooltips:

  • Aktivierung: "tooltip": true in entityDef setzen
  • Definition in tooltips-Sektion der i18n-Dateien
  • Vollständige Dokumentation: Siehe Abschnitt "Internationalisierung (i18n) und Tooltips"

Custom JavaScript & CSS Integration

  • Symptom: HTTP 404-Fehler in Logs: "Link does not exist" beim Versuch, eine Relationship anzuzeigen oder zu verknüpfen.
  • Ursache: Bei hasMany-Relationships fehlt die Definition auf einer Seite der Beziehung. EspoCRM benötigt bidirektionale Link-Definitionen.
  • Lösung:
    • Prüfe beide entityDefs-Dateien (z.B. CBeteiligte.json UND Contact.json).
    • Stelle sicher, dass beide Seiten den Link mit derselben relationName definieren.
    • Das foreign-Attribut muss jeweils auf den Link-Namen der Gegenseite zeigen.
    • Beispiel:
      // In CBeteiligte.json:
      "contactsBeteiligte": {
        "type": "hasMany",
        "relationName": "cBeteiligteContact",
        "foreign": "cBeteiligteContact",
        "entity": "Contact"
      }
      
      // In Contact.json:
      "cBeteiligteContact": {
        "type": "hasMany",
        "relationName": "cBeteiligteContact",
        "foreign": "contactsBeteiligte",
        "entity": "CBeteiligte"
      }
      
    • Nach Korrektur: ./custom/scripts/check_and_rebuild.sh ausführen.

500-Fehler bei Layout-Änderungen

  • Symptom: HTTP 500-Fehler beim Versuch, Layouts in der EspoCRM-UI zu bearbeiten (z.B. "Permission denied for custom/Espo/Custom/Resources/layouts/...").
  • Ursache: Das custom/-Verzeichnis gehört root:root, aber der EspoCRM-Container läuft als www-data-User, der keine Schreibrechte hat.
  • Lösung:
    • Führe auf dem Host aus: chown -R www-data:www-data /var/lib/docker/volumes/vmh-espocrm_espocrm/_data/custom
    • Dies gibt www-data Schreibrechte für Custom-Dateien.
  • Prävention: Stelle sicher, dass neue Custom-Dateien mit korrekten Berechtigungen erstellt werden (z.B. via Docker-Container als www-data).

Allgemeine Tipps

  • WICHTIG: Nach jeder Änderung an Custom-Dateien das Check & Rebuild Script ausführen:

    ./custom/scripts/check_and_rebuild.sh
    

    Das Script prüft automatisch auf häufige Fehler (JSON-Syntax, Dateirechte) und führt bei Fehlerfreiheit den Rebuild durch. Siehe Abschnitt "Rebuild-Prozess" für Details.

  • Logs prüfen: tail -n 100 /var/lib/docker/volumes/vmh-espocrm_espocrm/_data/data/logs/espo-YYYY-MM-DD.log

  • Bei Relationship-Problemen: Logs nach "404" und "Link does not exist" durchsuchen: tail -n 500 /var/lib/docker/volumes/vmh-espocrm_espocrm/_data/data/logs/espo-$(date +%Y-%m-%d).log | grep -A 3 "404\|Link does not exist"

  • Bei DB-Problemen: Custom-Scripts wie workflow_manager.php verwenden.

Check & Rebuild Script

Das Script custom/scripts/check_and_rebuild.sh automatisiert die Qualitätssicherung und führt folgende Prüfungen durch:

  1. JSON-Syntax-Validierung: Prüft alle .json Dateien im custom/ Verzeichnis auf gültiges JSON
  2. Dateirechte-Prüfung: Stellt sicher, dass alle Dateien www-data:www-data als Owner haben
  3. System-Checks: Validiert Existenz von Cache- und Logs-Verzeichnissen
  4. Automatischer Rebuild: Bei Fehlerfreiheit wird der Rebuild durchgeführt

Verwendung:

# Im EspoCRM-Root-Verzeichnis ausführen
./custom/scripts/check_and_rebuild.sh

Ausgabe:

  • ✓ Grün: Alles in Ordnung
  • ⚠ Gelb: Warnungen (Rebuild wird trotzdem ausgeführt)
  • ✗ Rot: Fehler (Rebuild wird NICHT ausgeführt)

Bei Berechtigungsfehlern:

sudo chown -R www-data:www-data custom/
sudo find custom/ -type f -name "*.json" -exec chmod 664 {} \;
sudo find custom/ -type d -exec chmod 775 {} \;

9. Reports und Report-Panels

EspoCRM bietet über das Advanced Pack zwei Arten von Report-Integrationen: Report-Filter und Report-Panels. Diese ermöglichen die dynamische Anzeige von gefilterten Listen in Entity-Views.

Report-Filter

Report-Filter ermöglichen es, vordefinierte Filter auf List-Views anzuwenden, die in Datenbanktabellen gespeichert sind.

Struktur und Dateien:

  1. entityDefs/{EntityType}.json - Filter-Definition
{
  "collection": {
    "filters": {
      "reportFilterXXXXXXXXXX": {
        "isReportFilter": true,
        "id": "reportFilterIdHere"
      }
    }
  }
}
  1. selectDefs/{EntityType}.json - Filter-Klasse
{
  "primaryFilterClassNameMap": {
    "reportFilterXXXXXXXXXX": "Espo\\Modules\\Advanced\\Classes\\Select\\Common\\PrimaryFilters\\ReportFilter"
  }
}
  1. clientDefs/{EntityType}.json - Frontend-Integration
{
  "filterList": [
    "__APPEND__",
    {
      "isReportFilter": true,
      "name": "reportFilterXXXXXXXXXX",
      "accessDataList": [
        {
          "teamIdList": ["team-id-here"]
        }
      ]
    }
  ]
}
  1. i18n/{Language}/{EntityType}.json - Übersetzungen
{
  "presetFilters": {
    "reportFilterXXXXXXXXXX": "Filter-Name"
  }
}

Report-Panels

Report-Panels zeigen Listen von Entitäten in Side-Panels der Detail-View an. Sie können Team-basierte Zugriffskontrolle haben.

Struktur:

clientDefs/{EntityType}.json - Panel-Definition

{
  "sidePanels": {
    "detail": [
      "__APPEND__",
      {
        "isReportPanel": true,
        "name": "reportPanelXXXXXXXXXX",
        "label": "Panel-Titel",
        "view": "advanced:views/report-panel/record/panels/report-panel-side",
        "reportPanelId": "reportPanelIdHere",
        "reportType": "List",
        "reportEntityType": "EntityType",
        "displayType": "List",
        "displayTotal": false,
        "displayOnlyTotal": false,
        "useSiMultiplier": true,
        "accessDataList": [
          {
            "scope": "EntityType"
          },
          {
            "teamIdList": ["team-id-here"]
          }
        ]
      }
    ]
  }
}

Wichtige Eigenschaften:

  • isReportFilter/isReportPanel: Markiert den Eintrag als Report-Element
  • accessDataList: Array von Zugriffsbedingungen (Team-IDs, Scopes)
  • reportType: "List" für Listen-Reports
  • displayType: Anzeige-Typ ("List", "Chart", etc.)
  • view: Spezielle Report-Panel-View aus dem Advanced Pack
  • __APPEND__: Erweitert bestehende Arrays statt sie zu überschreiben

Best Practices:

  1. Naming Convention:

    • Filter: reportFilter{uniqueId} (z.B. reportFilter6972174b6540731c1)
    • Panels: reportPanel{uniqueId} (z.B. reportPanel697216784307d43ad)
  2. Team-basierte Zugriffskontrolle:

    • Definiere teamIdList in accessDataList für eingeschränkten Zugriff
    • Mehrere Teams können kombiniert werden
  3. Mehrsprachigkeit:

    • Labels in allen Sprachen definieren (de_DE, en_US)
    • Fehlerhafte Labels können zu UI-Problemen führen
  4. Datei-Abhängigkeiten:

    • Report-Filter benötigen 4 Dateien: entityDefs, selectDefs, clientDefs, i18n
    • Report-Panels benötigen 1 Datei: clientDefs
    • Fehlende Dateien führen zu nicht-funktionalen Filtern
  5. Placeholder-Dateien:

    • logicDefs/{EntityType}.json kann als leeres Objekt {} angelegt werden
    • Ermöglicht zukünftige Erweiterungen ohne Struktur-Änderungen

Beispiel-Implementation:

Szenario: UserTask-Filter für Team "vermieterhelden"

# entityDefs/BpmnUserTask.json
{
  "collection": {
    "filters": {
      "reportFilter6972174b6540731c1": {
        "isReportFilter": true,
        "id": "6972174b6540731c1"
      }
    }
  }
}

# selectDefs/BpmnUserTask.json
{
  "primaryFilterClassNameMap": {
    "reportFilter6972174b6540731c1": "Espo\\Modules\\Advanced\\Classes\\Select\\Common\\PrimaryFilters\\ReportFilter"
  }
}

# clientDefs/BpmnUserTask.json
{
  "filterList": [
    "__APPEND__",
    {
      "isReportFilter": true,
      "name": "reportFilter6972174b6540731c1",
      "accessDataList": [
        {
          "teamIdList": ["68da9bdd622c9958a"]
        }
      ]
    }
  ]
}

# i18n/en_US/BpmnUserTask.json
{
  "presetFilters": {
    "reportFilter6972174b6540731c1": "UserTask"
  }
}

Troubleshooting:

  • Filter erscheint nicht: Prüfe ob alle 4 Dateien existieren und Rebuild durchgeführt wurde
  • Zugriffsfehler: Überprüfe teamIdList und User-Team-Zuordnung
  • Leere Liste: Report-Definition in DB prüfen (Tabelle: report)
  • Falsches Label: i18n-Dateien in allen Sprachen prüfen

Nach Änderungen:

# Rebuild durchführen (validiert JSON, prüft Rechte)
./custom/scripts/check_and_rebuild.sh

Portal-Freigabe-System

Um Entitäten für Portalnutzer (Contact-Entität) freizugeben, wurde ein konsistentes Freigabe-System implementiert:

Implementierte Portal-Relationships:

  • CVmhMietverhltniscontactsMietverhltnis (relationName: cVmhMietverhltnisContact)
  • CBeteiligtecontactsBeteiligte (relationName: cBeteiligteContact)
  • CMietobjektcontactsMietobjekt (relationName: cMietobjektContactPortal)
  • CAdressencontactsAdressen (relationName: cAdressenContact)
  • CVmhRumungsklagecontactsRumungsklage (relationName: cVmhRumungsklageContact)

Pattern für neue Portal-Relationships:

  1. entityDefs der Hauptentität (z.B. CBeteiligte.json):
"contactsBeteiligte": {
  "type": "hasMany",
  "relationName": "cBeteiligteContact",
  "foreign": "cBeteiligteContact",
  "entity": "Contact",
  "audited": false,
  "isCustom": true
}
  1. entityDefs von Contact (Contact.json):
"cBeteiligteContact": {
  "type": "hasMany",
  "relationName": "cBeteiligteContact",
  "foreign": "contactsBeteiligte",
  "entity": "CBeteiligte",
  "audited": false,
  "isCustom": true
}
  1. clientDefs der Hauptentität (CBeteiligte.json):
"relationshipPanels": {
  "contactsBeteiligte": {
    "layout": null,
    "selectPrimaryFilterName": "portalUsers"
  }
}
  1. bottomPanelsDetail Layout (Tab-Ansicht):
{
  "_tabBreak_0": {
    "index": 0,
    "tabBreak": true,
    "tabLabel": "Freigabe für"
  },
  "contactsBeteiligte": {
    "dynamicLogicVisible": null,
    "style": "warning",
    "dynamicLogicStyled": null,
    "sticked": true,
    "index": 1
  }
}

Wichtige Hinweise:

  • selectPrimaryFilterName: "portalUsers" filtert automatisch auf Portal-User
  • Tab "Freigabe für" sollte immer der erste Tab im Bottom-Panel sein (index: 0)
  • Style "warning" hebt das Panel visuell hervor
  • Nach Änderungen: ./custom/scripts/check_and_rebuild.sh
  • Beide Seiten der Relationship müssen in entityDefs definiert sein

Troubleshooting

Symptom: HTTP 404-Fehler in Logs beim Versuch, eine Relationship anzuzeigen oder zu verknüpfen.

Ursache: Bei hasMany-Relationships fehlt die Definition auf einer Seite der Beziehung.

Lösung:

  1. Prüfe beide entityDefs-Dateien (z.B. CBeteiligte.json UND Contact.json)
  2. Stelle sicher, dass beide Seiten den Link mit derselben relationName definieren
  3. Das foreign-Attribut muss jeweils auf den Link-Namen der Gegenseite zeigen

Beispiel:

// In CBeteiligte.json:
"contactsBeteiligte": {
  "type": "hasMany",
  "relationName": "cBeteiligteContact",
  "foreign": "cBeteiligteContact",
  "entity": "Contact"
}

// In Contact.json:
"cBeteiligteContact": {
  "type": "hasMany",
  "relationName": "cBeteiligteContact",
  "foreign": "contactsBeteiligte",
  "entity": "CBeteiligte"
}

Nach Korrektur: ./custom/scripts/check_and_rebuild.sh

Tooltip zeigt nur Feldnamen

Symptom: Tooltip zeigt nur "iban" statt vollständiger Beschreibung.

Ursache: Fehlerhafte oder fehlende en_US i18n-Datei (en_US ist Fallback-Sprache).

Lösung:

  1. Existiert custom/Espo/Custom/Resources/i18n/en_US/{Entity}.json?
  2. Enthält es fehlerhafte Tooltip-Definitionen?
  3. Sind alle Tooltips konsistent über alle Sprachen?
  4. Vervollständige en_US-Datei mit korrekten englischen Übersetzungen

Korrekt:

// de_DE/{Entity}.json
{
  "fields": {"iban": "IBAN"},
  "tooltips": {"iban": "Internationale Bankkontonummer im Format DE89..."}
}

// en_US/{Entity}.json  
{
  "fields": {"iban": "IBAN"},
  "tooltips": {"iban": "International Bank Account Number in format DE89..."}
}

Nach Korrektur: ./custom/scripts/check_and_rebuild.sh

Formula-Script wird nicht ausgeführt

Symptom: beforeSaveApiScript triggert nicht, keine Validierung.

Ursache: Script in entityDefs statt formula/ abgelegt.

Lösung:

  • Erstelle separate Datei: custom/Espo/Custom/Resources/metadata/formula/{Entity}.json
  • NICHT in entityDefs/{Entity}.json einfügen!
// custom/Espo/Custom/Resources/metadata/formula/Entity.json
{
  "beforeSaveApiScript": "if (field != null && field != '') { ... }"
}

Nach Korrektur: ./custom/scripts/check_and_rebuild.sh

CSS-Änderungen nicht sichtbar

Symptom: CSS-Änderungen werden nicht im Browser angezeigt.

Ursache: Browser-Cache oder fehlende Registrierung.

Lösung:

  1. CSS in custom/Espo/Custom/Resources/metadata/app/client.json registrieren:
    {
      "cssList": ["__APPEND__", "client/custom/css/my-styles.css"]
    }
    
  2. ./custom/scripts/check_and_rebuild.sh ausführen
  3. Browser Hard Refresh (Ctrl+Shift+R / Cmd+Shift+R)

"Permission denied" bei Layout-Bearbeitung

Symptom: HTTP 500-Fehler beim Versuch, Layouts über die UI zu bearbeiten.

Ursache: Falsche Dateirechte - custom/-Verzeichnis gehört root statt www-data.

Lösung: Das check_and_rebuild.sh Script prüft und korrigiert Dateirechte automatisch. Falls manuelle Korrektur nötig:

sudo chown -R www-data:www-data custom/ client/custom/
sudo find custom/ -type f -name "*.json" -exec chmod 664 {} \;
sudo find custom/ -type d -exec chmod 775 {} \;

JSON-Syntax-Fehler

Symptom: Rebuild schlägt fehl, Script zeigt "Invalid JSON" Fehler.

Ursache: Ungültiges JSON (fehlende Kommas, Anführungszeichen, etc.).

Lösung:

  1. ./custom/scripts/check_and_rebuild.sh zeigt Datei und Zeilennummer an
  2. JSON-Validator verwenden (z.B. jsonlint.com)
  3. Häufige Fehler:
    • Komma nach letztem Array/Object-Element
    • Einfache statt doppelte Anführungszeichen
    • Fehlende schließende Klammern

"Unknown function" in Formula

Symptom: Error "Unknown function: xxx" beim Speichern.

Ursache: Funktion existiert nicht oder ist nicht registriert.

Lösung:

  1. Für Custom-Funktionen: Registrierung in app/formula.json prüfen
  2. string\isEmpty() existiert nicht → verwende field != null && field != ''
  3. Nach Registrierung: ./custom/scripts/check_and_rebuild.sh

Logs prüfen

Wichtige Log-Dateien:

# Aktuelles Log (heutiges Datum)
tail -n 100 data/logs/espo-$(date +%Y-%m-%d).log

# Nach bestimmten Fehlern suchen
tail -n 500 data/logs/espo-*.log | grep -i "404\|500\|error\|exception"

# Formula-Script-Fehler
tail -n 200 data/logs/espo-*.log | grep -i "formula\|script"

# Relationship-Fehler
tail -n 500 data/logs/espo-*.log | grep -i "link does not exist"

Allgemeine Troubleshooting-Schritte

  1. Nach jeder Änderung:

    ./custom/scripts/check_and_rebuild.sh
    
  2. Bei Frontend-Problemen:

    • Browser Hard Refresh (Ctrl+Shift+R)
    • Browser-Cache komplett leeren
    • Inkognito-Modus testen
  3. Bei Backend-Problemen:

    • Logs prüfen (siehe oben)
    • Dateirechte prüfen (www-data:www-data)
    • JSON-Syntax validieren
  4. Bei Relationship-Problemen:

    • Beide Seiten der Relationship prüfen
    • relationName identisch?
    • foreign zeigt auf korrekten Link-Namen?
    • Nach Rebuild: Cache manuell leeren falls nötig

Vollständige Dokumentation zur Custom Directory Struktur: Siehe /custom/CUSTOM_DIRECTORY.md

JavaScript-Module: client/custom/src/modules/ - AMD-Module für wiederverwendbare Logik

Custom Views: client/custom/src/views/ - Backbone.js Views für Entity-spezifisches UI

CSS-Stylesheets: client/custom/css/ - Custom CSS (Registrierung in app/client.json erforderlich)

JavaScript-Module einbinden

EspoCRM verwendet AMD/RequireJS für JavaScript-Module. Custom JavaScript-Dateien werden in client/custom/src/ abgelegt.

Beispiel: RVG-Gebührenrechner für CVmhErstgespraech

1. Modul erstellen (client/custom/src/modules/rvg-calculator.js):

define('custom:modules/rvg-calculator', [], function () {
    return {
        kalkuliereKosten: function(streitwert, anzahlKlaeger, anzahlBeklagte, ustProzent) {
            // Berechnungslogik
            return { /* Ergebnisobjekt */ };
        }
    };
});

2. Custom Field View erstellen (client/custom/src/views/{entity}/fields/{fieldname}.js):

define('custom:views/c-vmh-erstgespraech/fields/rvg-calculated', [
    'views/fields/currency',
    'custom:modules/rvg-calculator'
], function (Dep, RvgCalculator) {
    return Dep.extend({
        setup: function () {
            Dep.prototype.setup.call(this);
            this.listenTo(this.model, 'change:streitwert change:anzahlVermieter', this.calculate);
            this.listenTo(this.model, 'sync', this.calculate); // Initial load
        },
        calculate: function () {
            var result = RvgCalculator.kalkuliereKosten(/*...*/);
            this.model.set('kostenRaeumungsantrag', result.kostenRaeumungsantrag);
        }
    });
});

3. In entityDefs registrieren:

{
    "fields": {
        "vergleich1InstanzGk": {
            "type": "currency",
            "readOnly": true,
            "view": "custom:views/c-vmh-erstgespraech/fields/rvg-calculated"
        }
    }
}

Wichtige Patterns:

  • listenTo(model, 'sync', callback) - Für initiale Berechnung beim Laden
  • listenTo(model, 'change:field1 change:field2', callback) - Für Reaktivität
  • calculating Flag verhindert Rekursion bei model.set()
  • Browser-Cache: Hard Refresh (Ctrl+Shift+R) nach JS-Änderungen erforderlich

CSS-Manipulation & Feld-Hervorhebung

EspoCRM erlaubt Custom CSS über Metadata-Registrierung.

1. CSS-Datei erstellen (client/custom/css/erstgespraech-highlight.css):

/* Feld-Selektor über data-name Attribut */
.detail .cell[data-name="vorzusch1Instanz"] {
    background-color: #d4edda;
    padding: 10px;
    border-bottom: 4px solid #28a745;
    border-radius: 4px;
}

.detail .cell[data-name="vorzusch1Instanz"] .numeric-text {
    font-weight: bold;
    color: #155724;
    font-size: 1.1em;
}

2. CSS in Metadata registrieren (custom/Espo/Custom/Resources/metadata/app/client.json):

{
    "cssList": [
        "__APPEND__",
        "client/custom/css/erstgespraech-highlight.css"
    ]
}

Nach CSS-Änderungen:

  1. ./custom/scripts/check_and_rebuild.sh ausführen
  2. Browser Hard Refresh (Ctrl+Shift+R)

CSS-Targeting-Strategien:

  • Feld-spezifisch: .cell[data-name="fieldName"]
  • Entity-spezifisch: body[data-controller="CVmhErstgespraech"]
  • View-spezifisch: .detail (Detail-View), .edit (Edit-View), .list (List-View)
  • Label vs. Value:
    • .label-text - Feldlabel
    • .numeric-text / .text-default - Feldwert
    • .field[data-name="..."] - Field-Container

HTML-Struktur (Referenz):

<div class="cell col-sm-4 form-group" data-name="vorzusch1Instanz">
    <label class="control-label">
        <span class="label-text">Vorauszuschießende Kosten I. Inst.</span>
    </label>
    <div class="field" data-name="vorzusch1Instanz">
        <span class="numeric-text">3.067,63</span></div>
</div>

Best Practices:

  • CSS-Dateien in client/custom/css/ ablegen
  • __APPEND__ verwenden um Core-CSS zu erweitern, nicht zu überschreiben
  • Spezifische Selektoren verwenden um Kollisionen zu vermeiden
  • Nach CSS-Änderungen: ./custom/scripts/check_and_rebuild.sh + Browser Hard Refresh

RVG-Gebührenrechner (CVmhErstgespraech)

Implementierung: Automatische Berechnung von Anwalts- und Gerichtskosten nach RVG 2025 / GKG

Komponenten:

  1. Calculator-Modul (client/custom/src/modules/rvg-calculator.js):

    • getWertgebuehr(): RVG 2025 Tabelle (65 Stufen, €500-€2M)
    • getGerichtsgebuehr(): GKG progressive Berechnung
    • getZuschlag(): §7 RVG Personenzuschlag (+0.3 pro Person, max +2.0)
    • kalkuliereKosten(): Hauptfunktion für alle Szenarien
  2. Custom Field Views:

    • rvg-calculated.js: Trigger für alle Berechnungen
    • beruecksichtigte-personen.js: Live-Text-Anzeige "X Vermieter, Y Mieter, Z Dritte"
    • warmmiete.js: Kaltmiete + BK-Vorauszahlung + BK-Pauschale
    • streitwert.js: (Kaltmiete + BK-Pauschale) × 12
  3. Berechnete Felder (readOnly currency fields):

    • Außergerichtliche Gebühren: 1.3 + Zuschlag + Pauschale 20% (max 20€)
    • Kosten Räumungsantrag: 0.3 + 0.3/Person + Pauschale
      1. Instanz: 3.0 GK + RA-Kosten (1.3 Verf + 1.2 Term + Pauschale)
    • Säumnisszenario: 3.0 GK + reduzierte RA (0.5 Term statt 1.2)
    • Vergleichsszenario: 1.0 GK + RA (1.3 Verf + 1.2 Term + 1.0 Vergl)
  4. USt-Satz Handling:

    • Enum Field: "0" / "19" (String, nicht Integer!)
    • Konvertierung: parseInt(ustSatz) → dann /100 im Calculator
    • Wichtig: Expliziter Null-Check nötig, 0 ist falsy in || 19
  5. Reaktivität:

    • Listener auf: streitwert, anzahlVermieter, anzahlMieter, anzahlSonstigeVolljhrigeBewohner, ustSatz
    • Initial berechnen mit 'sync' Event
    • calculating Flag verhindert Rekursion

Layout-Panels:

  • Gebührenberechnung (primary, info-note): Standard 1. Instanz Kosten
  • Säumnisszenario I. Inst. (primary, success-note): Beklagte erscheint nicht
  • Vergleichsszenario I. Inst. (primary, success-note): Einigung vor Urteil

Hervorhebung: "Vorauszuschießende Kosten I. Inst." wird via CSS hervorgehoben (grüner Hintergrund, fetter Wert)