Files
espocrm/custom
bsiggel 552540e214 feat: Add KI-Einstiegsscript for comprehensive project overview
- Introduced `ki_project_overview.py` for automated analysis of EspoCRM project structure, entities, relationships, custom PHP classes, workflows, frontend adjustments, and internationalization.
- Created `ki-overview.sh` wrapper script for executing the Python script with various output options.
- Updated `README.md` to include a quick start section for the new KI entry script.
- Added detailed documentation in `KI_OVERVIEW_README.md` explaining the script's purpose, usage, and output format.
- Summarized the new features and files in `KI_OVERVIEW_SUMMARY.md`.
- Enhanced `.vscode/settings.json` to approve new scripts for execution.
2026-01-25 12:34:46 +01:00
..

EspoCRM Custom Actions - Implementierungsprinzip

Version: 1.0
Datum: 24. Januar 2026
Blueprint für: Custom Button Actions mit Entity-Erstellung und Relationen


📋 Inhaltsverzeichnis

  1. Überblick
  2. Architektur-Prinzipien
  3. Implementierungs-Schritte
  4. Code-Template
  5. Best Practices
  6. Sicherheit
  7. Testing & Debugging
  8. Beispiel-Implementierung

Überblick

Dieses Dokument beschreibt das standardisierte Vorgehen zur Implementierung von Custom Button Actions in EspoCRM, die:

  • Von einer Entity-Detailansicht ausgelöst werden
  • Eine neue verwandte Entity erstellen
  • Automatisch Daten und Relationen übertragen
  • ACL-konform und transaktionssicher arbeiten

Basis-Beispiel: Button "Räumungsklage einleiten" in Mietobjekt → Erstellt CVmhRumungsklage mit allen Relationen


Architektur-Prinzipien

1. Drei-Schichten-Architektur

┌─────────────────────────────────────────────────────┐
│ FRONTEND (client/custom/)                           │
│ • JavaScript Handler (Action Trigger)               │
│ • Button Definition (clientDefs)                    │
│ • i18n Translations                                 │
└────────────────┬────────────────────────────────────┘
                 │ AJAX POST Request
┌────────────────▼────────────────────────────────────┐
│ CONTROLLER (custom/Espo/Custom/Controllers/)        │
│ • Request Validation                                │
│ • Service Call                                      │
│ • Response Formatting                               │
└────────────────┬────────────────────────────────────┘
                 │ Method Call
┌────────────────▼────────────────────────────────────┐
│ SERVICE (custom/Espo/Custom/Services/)              │
│ • ACL Security Checks                               │
│ • Business Logic                                    │
│ • Transaction Management                            │
│ • Entity Operations                                 │
└─────────────────────────────────────────────────────┘

2. Separation of Concerns

Schicht Verantwortlichkeit Nicht erlaubt
Frontend User Interaction, Confirmation, Navigation Business Logic, Direct DB Access
Controller HTTP Request/Response Handling Business Logic, Direct Entity Manipulation
Service Business Logic, ACL, Transactions, Entity Operations HTTP Handling, View Logic

Implementierungs-Schritte

Schritt 1: Backend Controller erstellen

Datei: custom/Espo/Custom/Controllers/[EntityName].php

<?php
namespace Espo\Custom\Controllers;

use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Api\Request;

class CYourEntity extends \Espo\Core\Templates\Controllers\Base
{
    /**
     * POST Action: Beschreibung der Aktion
     */
    public function postActionYourAction(Request $request): array
    {
        $data = $request->getParsedBody();
        
        $id = $data->id ?? null;
        if (!$id) {
            throw new BadRequest('No Entity ID provided');
        }
        
        $service = $this->getRecordService();
        $result = $service->yourAction($id);
        
        return $result;
    }
}

Wichtig:

  • Methode MUSS postAction[Name] heißen
  • Parameter ist Request $request
  • Return Type ist array
  • Extends \Espo\Core\Templates\Controllers\Base

Schritt 2: Backend Service erstellen

Datei: custom/Espo/Custom/Services/[EntityName].php

<?php
namespace Espo\Custom\Services;

use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;

class CYourEntity extends \Espo\Services\Record
{
    /**
     * Beschreibung der Business-Logik
     * 
     * @param string $sourceId
     * @return array
     * @throws NotFound
     * @throws Forbidden
     */
    public function yourAction(string $sourceId): array
    {
        // 1. LADEN: Source Entity laden
        $source = $this->entityManager->getEntity('CYourEntity', $sourceId);
        if (!$source) {
            throw new NotFound('Entity not found');
        }
        
        // 2. SECURITY: ACL-Checks
        if (!$this->acl->check($source, 'read')) {
            throw new Forbidden('No read access to source entity');
        }
        
        if (!$this->acl->checkScope('CTargetEntity', 'create')) {
            throw new Forbidden('No create access to target entity');
        }
        
        // 3. TRANSACTION: Start
        $this->entityManager->getTransactionManager()->start();
        
        try {
            // 4. ERSTELLEN: Target Entity
            $data = new \stdClass();
            $data->name = 'Generated from ' . $source->get('name');
            
            // Optional: User und Teams übernehmen
            if ($source->get('assignedUserId')) {
                $data->assignedUserId = $source->get('assignedUserId');
            }
            
            $teamsIds = $source->getLinkMultipleIdList('teams');
            if (!empty($teamsIds)) {
                $data->teamsIds = $teamsIds;
            }
            
            $target = $this->entityManager->createEntity('CTargetEntity', (array)$data);
            
            if (!$target) {
                throw new \RuntimeException('Failed to create target entity');
            }
            
            // 5. RELATIONEN: Verknüpfen
            $targetRepo = $this->entityManager->getRepository('CTargetEntity');
            
            // Source → Target
            $targetRepo
                ->getRelation($target, 'sources')
                ->relate($source);
            
            // Related Entities übertragen
            $relatedEntities = $this->entityManager
                ->getRepository('CYourEntity')
                ->getRelation($source, 'relatedThings')
                ->find();
            
            foreach ($relatedEntities as $related) {
                $targetRepo
                    ->getRelation($target, 'relatedThings')
                    ->relate($related);
            }
            
            // 6. COMMIT: Transaktion abschließen
            $this->entityManager->getTransactionManager()->commit();
            
            // 7. RETURN: ID und Name für Frontend
            return [
                'id' => $target->getId(),
                'name' => $target->get('name')
            ];
            
        } catch (\Exception $e) {
            // ROLLBACK: Bei jedem Fehler
            $this->entityManager->getTransactionManager()->rollback();
            throw $e;
        }
    }
}

Wichtig:

  • Extends \Espo\Services\Record
  • Immer Transactions verwenden
  • Alle Exceptions im catch-Block fangen
  • Rollback bei Fehlern

Schritt 3: Button im Frontend konfigurieren

Datei: custom/Espo/Custom/Resources/metadata/clientDefs/[EntityName].json

{
    "menu": {
        "detail": {
            "buttons": [
                {
                    "name": "initiateYourAction",
                    "label": "Your Action Button",
                    "handler": "custom:handlers/your-entity/your-action",
                    "iconHtml": "<span class=\"fas fa-bolt\"></span>",
                    "style": "danger",
                    "acl": "edit"
                }
            ]
        }
    }
}

Button-Optionen:

  • name: Eindeutiger Identifier
  • label: Translation-Key (siehe i18n)
  • handler: Pfad zum JavaScript Handler
  • iconHtml: FontAwesome Icon oder HTML
  • style: default, success, danger, warning, info
  • acl: read, edit, delete (benötigte Permission)

Schritt 4: JavaScript Handler implementieren

Datei: client/custom/src/handlers/your-entity/your-action.js

define('custom:handlers/your-entity/your-action', [], function () {
    
    class YourActionHandler {
        
        constructor(view) {
            this.view = view;
        }
        
        /**
         * Setup-Methode wird automatisch aufgerufen
         */
        initYourAction() {
            this.view.listenTo(
                this.view, 
                'after:render', 
                () => this.controlButtonVisibility()
            );
        }
        
        /**
         * Button-Sichtbarkeit steuern (optional)
         */
        controlButtonVisibility() {
            const model = this.view.model;
            
            // Beispiel: Button nur bei bestimmten Bedingungen zeigen
            if (!model.get('someField')) {
                this.view.hideHeaderActionItem('initiateYourAction');
            } else {
                this.view.showHeaderActionItem('initiateYourAction');
            }
        }
        
        /**
         * Action-Handler (wird bei Button-Click aufgerufen)
         */
        actionInitiateYourAction() {
            const model = this.view.model;
            
            // 1. CONFIRMATION: User bestätigen lassen
            this.view.confirm(
                this.view.translate('confirmationMessage', 'messages', 'CYourEntity'),
                () => {
                    this.initiateAction(model.id);
                }
            );
        }
        
        /**
         * AJAX Request zum Backend
         */
        initiateAction(entityId) {
            Espo.Ui.notify(this.view.translate('pleaseWait', 'messages'));
            
            Espo.Ajax.postRequest('CYourEntity/action/yourAction', {
                id: entityId
            })
            .then(response => {
                Espo.Ui.success(
                    this.view.translate('successMessage', 'messages', 'CYourEntity')
                );
                
                // Navigation zur erstellten Entity
                this.view.getRouter().navigate(
                    '#CTargetEntity/view/' + response.id, 
                    {trigger: true}
                );
            })
            .catch(xhr => {
                console.error('Action failed:', xhr);
                
                let errorMessage = this.view.translate('errorMessage', 'messages', 'CYourEntity');
                
                if (xhr.status === 403) {
                    errorMessage = this.view.translate('Access denied', 'messages');
                } else if (xhr.status === 404) {
                    errorMessage = this.view.translate('Not found', 'messages');
                }
                
                Espo.Ui.error(errorMessage);
            });
        }
    }
    
    return YourActionHandler;
});

Wichtig:

  • Class-Name muss mit Filename übereinstimmen (CamelCase)
  • Methode init[ButtonName]() für Setup
  • Methode action[ButtonName]() für Click-Handler
  • Immer Confirmation Dialog für kritische Actions
  • Error Handling mit spezifischen HTTP-Status-Codes

Schritt 5: Übersetzungen hinzufügen

Datei: custom/Espo/Custom/Resources/i18n/de_DE/[EntityName].json

{
    "labels": {
        "Your Action Button": "Ihre Aktion"
    },
    "messages": {
        "confirmationMessage": "Möchten Sie diese Aktion wirklich ausführen?",
        "successMessage": "Die Aktion wurde erfolgreich ausgeführt",
        "errorMessage": "Fehler beim Ausführen der Aktion"
    }
}

Datei: custom/Espo/Custom/Resources/i18n/en_US/[EntityName].json

{
    "labels": {
        "Your Action Button": "Your Action"
    },
    "messages": {
        "confirmationMessage": "Do you really want to execute this action?",
        "successMessage": "Action executed successfully",
        "errorMessage": "Error executing action"
    }
}

Schritt 6: Validierung & Rebuild

# Im Root-Verzeichnis von EspoCRM
python3 custom/scripts/validate_and_rebuild.py

Prüfungen:

  • JSON-Syntax aller Dateien
  • PHP-Syntax (Service, Controller)
  • JavaScript-Syntax (Handler)
  • Relationship-Konsistenz
  • Automatischer Cache-Clear & Rebuild

Best Practices

1. Naming Conventions

Element Pattern Beispiel
Controller Action postAction[Name] postActionInitiateEviction
Service Method camelCase initiateEviction
Button Name camelCase initiateEviction
JS Handler File kebab-case.js eviction-action.js
JS Class Name PascalCase EvictionActionHandler
JS Init Method init[ButtonName] initInitiateEviction
JS Action Method action[ButtonName] actionInitiateEviction

2. Transaction Management

Immer Transactions verwenden bei:

  • Entity-Erstellung
  • Mehreren Relationierungen
  • Datenänderungen über mehrere Entities
// ✅ RICHTIG
$this->entityManager->getTransactionManager()->start();
try {
    // ... operations
    $this->entityManager->getTransactionManager()->commit();
} catch (\Exception $e) {
    $this->entityManager->getTransactionManager()->rollback();
    throw $e;
}

// ❌ FALSCH - Keine Transaction
$entity = $this->entityManager->createEntity(...);
// Bei Fehler in nächstem Schritt: Inkonsistente Daten!
$this->entityManager->getRepository(...)->relate(...);

3. Repository Caching

// ✅ RICHTIG - Repository nur 1× holen
$targetRepo = $this->entityManager->getRepository('Target');
foreach ($items as $item) {
    $targetRepo->getRelation($target, 'items')->relate($item);
}

// ❌ FALSCH - Repository in Schleife holen
foreach ($items as $item) {
    $this->entityManager->getRepository('Target')
        ->getRelation($target, 'items')
        ->relate($item);
}

4. Error Messages

// ✅ RICHTIG - Spezifische Exceptions
if (!$source) {
    throw new NotFound('Mietobjekt not found');
}
if (!$this->acl->check($source, 'read')) {
    throw new Forbidden('No read access to Mietobjekt');
}

// ❌ FALSCH - Generische Fehler
if (!$source) {
    throw new \Exception('Error');
}

5. Null Checks

// ✅ RICHTIG
$entity = $this->entityManager->createEntity(...);
if (!$entity) {
    throw new \RuntimeException('Failed to create entity');
}

// ❌ FALSCH - Keine Prüfung
$entity = $this->entityManager->createEntity(...);
$entity->getId(); // Kann zu Fehler führen!

Sicherheit

1. ACL-Checks (Access Control Lists)

Minimum: Immer beide Checks durchführen:

// Check 1: Kann User Source-Entity lesen?
if (!$this->acl->check($sourceEntity, 'read')) {
    throw new Forbidden('No read access');
}

// Check 2: Kann User Target-Entity erstellen?
if (!$this->acl->checkScope('TargetEntity', 'create')) {
    throw new Forbidden('No create access');
}

Optional: Zusätzliche Checks je nach Business-Logik:

// Check 3: Kann User Source-Entity bearbeiten?
if (!$this->acl->check($sourceEntity, 'edit')) {
    throw new Forbidden('No edit access');
}

// Check 4: Ownership-Check
if ($sourceEntity->get('assignedUserId') !== $this->user->getId()) {
    throw new Forbidden('Not the owner');
}

2. Input Validation

// ✅ RICHTIG - ID validieren
$id = $data->id ?? null;
if (!$id || !is_string($id)) {
    throw new BadRequest('Invalid ID provided');
}

// Optional: Format prüfen
if (!preg_match('/^[a-f0-9]{17}$/', $id)) {
    throw new BadRequest('Invalid ID format');
}

3. SQL Injection Prevention

Automatisch geschützt durch:

  • EntityManager ORM (immer verwenden!)
  • Repository Pattern
  • Prepared Statements

Niemals:

  • Direct SQL Queries
  • String Concatenation in Queries
  • $pdo->query() direkt verwenden

4. XSS Prevention

Automatisch geschützt durch:

  • EspoCRM Template Engine
  • Automatic HTML Escaping
  • Content Security Policy Headers

Im Frontend:

// ✅ RICHTIG - translate() escaped automatisch
this.view.translate('message', 'messages', 'Entity');

// ❌ FALSCH - Rohe HTML-Injection
$('.container').html('<div>' + userInput + '</div>');

5. CSRF Protection

Automatisch geschützt durch:

  • EspoCRM API Token System
  • Espo.Ajax verwendet automatisch CSRF-Token
  • Session-basierte Validierung

Kein manueller Code nötig!


Testing & Debugging

1. Entwickler-Workflow

# 1. Code ändern
vim custom/Espo/Custom/Services/YourEntity.php

# 2. Validieren & Rebuild
python3 custom/scripts/validate_and_rebuild.py

# 3. Browser Hard-Refresh
# Ctrl + Shift + R (Chrome/Firefox)
# Cmd + Shift + R (Mac)

# 4. Test durchführen

# 5. Logs prüfen bei Fehlern
tail -f data/logs/espo-$(date +%Y-%m-%d).log

2. Häufige Fehler & Lösungen

Fehler Ursache Lösung
Error 500: Call to undefined method getAcl() Falsche ACL-Methode Nutze $this->acl statt $this->getAcl()
Error 500: Entity does not have a relation 'xyz' Relation existiert nicht Prüfe entityDefs, korrekter Relationname
Button erscheint nicht Falsche clientDefs-Struktur Nutze menu.detail.buttons nicht detailActionList
JavaScript-Fehler Handler nicht gefunden Prüfe Pfad und Class-Name, Cache leeren
Transaction-Fehler Verschachtelte Transactions Nur eine Transaction auf Service-Level

3. Debugging-Kommandos

# Logs live verfolgen
tail -f data/logs/espo-$(date +%Y-%m-%d).log

# Letzten Fehler finden
tail -n 200 data/logs/espo-$(date +%Y-%m-%d).log | grep -i "error"

# PHP-Syntax prüfen
find custom/Espo/Custom -name "*.php" -exec php -l {} \;

# JSON-Syntax prüfen
find custom/Espo/Custom/Resources/metadata -name "*.json" -exec python3 -m json.tool {} \; > /dev/null

# Cache manuell löschen
rm -rf data/cache/*

4. Browser DevTools

Console (F12):

// Current Model inspizieren
console.log(this.model.attributes);

// AJAX Request debuggen
Espo.Ajax.postRequest('Entity/action/test', {id: 'xyz'})
    .then(r => console.log('Success:', r))
    .catch(e => console.error('Error:', e));

Beispiel-Implementierung

Use Case: "Räumungsklage einleiten"

Requirement:

  • Button in Mietobjekt-Detailansicht
  • Erstellt CVmhRumungsklage
  • Verknüpft Mietverhältnisse, Vermieter (Kläger), Mieter (Beklagte)
  • Kopiert assignedUser und Teams

Dateien-Struktur

custom/Espo/Custom/
├── Controllers/
│   └── CMietobjekt.php              # POST Action Endpoint
├── Services/
│   └── CMietobjekt.php              # Business Logic
└── Resources/
    ├── metadata/clientDefs/
    │   └── CMietobjekt.json         # Button Definition
    └── i18n/
        ├── de_DE/
        │   └── CMietobjekt.json     # Deutsche Übersetzungen
        └── en_US/
            └── CMietobjekt.json     # Englische Übersetzungen

client/custom/src/handlers/
└── mietobjekt/
    └── eviction-action.js           # Frontend Handler

Vollständiger Code

1. Controller (CMietobjekt.php):

<?php
namespace Espo\Custom\Controllers;

use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Api\Request;

class CMietobjekt extends \Espo\Core\Templates\Controllers\Base
{
    public function postActionInitiateEviction(Request $request): array
    {
        $data = $request->getParsedBody();
        $id = $data->id ?? null;
        
        if (!$id) {
            throw new BadRequest('No Mietobjekt ID provided');
        }
        
        return $this->getRecordService()->initiateEviction($id);
    }
}

2. Service (CMietobjekt.php):

<?php
namespace Espo\Custom\Services;

use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;

class CMietobjekt extends \Espo\Services\Record
{
    public function initiateEviction(string $mietobjektId): array
    {
        // 1. Load & Validate
        $mietobjekt = $this->entityManager->getEntity('CMietobjekt', $mietobjektId);
        if (!$mietobjekt) {
            throw new NotFound('Mietobjekt not found');
        }
        
        // 2. ACL Checks
        if (!$this->acl->check($mietobjekt, 'read')) {
            throw new Forbidden('No read access to Mietobjekt');
        }
        if (!$this->acl->checkScope('CVmhRumungsklage', 'create')) {
            throw new Forbidden('No create access to Räumungsklage');
        }
        
        // 3. Start Transaction
        $this->entityManager->getTransactionManager()->start();
        
        try {
            // 4. Create Räumungsklage
            $data = new \stdClass();
            $data->name = 'Räumungsklage - ' . $mietobjekt->get('name');
            
            if ($mietobjekt->get('assignedUserId')) {
                $data->assignedUserId = $mietobjekt->get('assignedUserId');
            }
            
            $teamsIds = $mietobjekt->getLinkMultipleIdList('teams');
            if (!empty($teamsIds)) {
                $data->teamsIds = $teamsIds;
            }
            
            $raeumungsklage = $this->entityManager->createEntity(
                'CVmhRumungsklage', 
                (array)$data
            );
            
            if (!$raeumungsklage) {
                throw new \RuntimeException('Failed to create Räumungsklage');
            }
            
            $repo = $this->entityManager->getRepository('CVmhRumungsklage');
            
            // 5. Link Relations
            $repo->getRelation($raeumungsklage, 'mietobjekte')
                ->relate($mietobjekt);
            
            $mietverhaeltnisse = $this->entityManager
                ->getRepository('CMietobjekt')
                ->getRelation($mietobjekt, 'vmhMietverhltnises')
                ->find();
            
            foreach ($mietverhaeltnisse as $mv) {
                $repo->getRelation($raeumungsklage, 'vmhMietverhltnises')
                    ->relate($mv);
                
                // Link Vermieter as Kläger
                $vermieter = $this->entityManager
                    ->getRepository('CVmhMietverhltnis')
                    ->getRelation($mv, 'vmhbeteiligtevermieter')
                    ->find();
                foreach ($vermieter as $v) {
                    $repo->getRelation($raeumungsklage, 'klaeger')->relate($v);
                }
                
                // Link Mieter as Beklagte
                $mieter = $this->entityManager
                    ->getRepository('CVmhMietverhltnis')
                    ->getRelation($mv, 'vmhbeteiligtemieter')
                    ->find();
                foreach ($mieter as $m) {
                    $repo->getRelation($raeumungsklage, 'beklagte')->relate($m);
                }
            }
            
            // 6. Commit
            $this->entityManager->getTransactionManager()->commit();
            
            return [
                'id' => $raeumungsklage->getId(),
                'name' => $raeumungsklage->get('name')
            ];
            
        } catch (\Exception $e) {
            $this->entityManager->getTransactionManager()->rollback();
            throw $e;
        }
    }
}

3. Button Config (clientDefs/CMietobjekt.json):

{
    "menu": {
        "detail": {
            "buttons": [
                {
                    "name": "initiateEviction",
                    "label": "Initiate Eviction",
                    "handler": "custom:handlers/mietobjekt/eviction-action",
                    "iconHtml": "<span class=\"fas fa-gavel\"></span>",
                    "style": "danger",
                    "acl": "edit"
                }
            ]
        }
    }
}

4. JavaScript Handler (eviction-action.js):

define('custom:handlers/mietobjekt/eviction-action', [], function () {
    
    class EvictionActionHandler {
        
        constructor(view) {
            this.view = view;
        }
        
        initInitiateEviction() {
            // Optional: Button-Logik nach Render
        }
        
        actionInitiateEviction() {
            this.view.confirm(
                this.view.translate('confirmEviction', 'messages', 'CMietobjekt'),
                () => this.initiateEviction(this.view.model.id)
            );
        }
        
        initiateEviction(id) {
            Espo.Ui.notify(this.view.translate('pleaseWait', 'messages'));
            
            Espo.Ajax.postRequest('CMietobjekt/action/initiateEviction', { id })
                .then(response => {
                    Espo.Ui.success(
                        this.view.translate('evictionCreated', 'messages', 'CMietobjekt')
                    );
                    this.view.getRouter().navigate(
                        '#CVmhRumungsklage/view/' + response.id,
                        { trigger: true }
                    );
                })
                .catch(xhr => {
                    let msg = this.view.translate('evictionError', 'messages', 'CMietobjekt');
                    if (xhr.status === 403) {
                        msg = this.view.translate('Access denied', 'messages');
                    }
                    Espo.Ui.error(msg);
                });
        }
    }
    
    return EvictionActionHandler;
});

5. Translations (i18n/de_DE/CMietobjekt.json):

{
    "labels": {
        "Initiate Eviction": "Räumungsklage einleiten"
    },
    "messages": {
        "confirmEviction": "Möchten Sie wirklich eine Räumungsklage einleiten?",
        "evictionCreated": "Räumungsklage wurde erfolgreich erstellt",
        "evictionError": "Fehler beim Erstellen der Räumungsklage"
    }
}

Checkliste für neue Implementierungen

Vor dem Start

  • Requirements klar definiert (Welche Entity? Welche Relationen?)
  • Entity-Relationen in entityDefs geprüft
  • ACL-Requirements geklärt
  • Datenfluss dokumentiert

Implementierung

  • Controller erstellt mit postAction[Name] Methode
  • Service erstellt mit Business-Logik
  • ACL-Checks: check() für Source + checkScope() für Target
  • Transaction: start()try/catchcommit()/rollback()
  • Null-Check nach createEntity()
  • Repository-Caching bei Loops
  • Button in clientDefs konfiguriert (menu.detail.buttons)
  • JavaScript Handler erstellt
  • Init-Methode: init[ButtonName]()
  • Action-Methode: action[ButtonName]()
  • Confirmation Dialog implementiert
  • Error Handling mit HTTP-Status-Codes
  • Translations: Deutsch + Englisch
  • Labels + Messages vollständig

Testing

  • Validierung erfolgreich: validate_and_rebuild.py
  • Browser Hard-Refresh durchgeführt
  • Button erscheint in Detailansicht
  • Confirmation Dialog funktioniert
  • Entity wird korrekt erstellt
  • Relationen sind korrekt verknüpft
  • Navigation zur neuen Entity funktioniert
  • ACL wird respektiert (Test mit eingeschränktem User)
  • Error Cases getestet (keine Permission, Entity nicht gefunden)
  • Logs geprüft: Keine Errors

Dokumentation

  • Code-Kommentare hinzugefügt
  • PHPDoc vollständig
  • README aktualisiert (falls generische Änderungen)

Support & Weiterentwicklung

Erweiterungen des Templates

Optional implementierbar:

  • Conditional Button Visibility (nur unter bestimmten Bedingungen)
  • Batch Operations (mehrere Entities gleichzeitig)
  • Background Jobs (lange Operationen asynchron)
  • Email-Benachrichtigungen nach Erstellung
  • PDF-Generierung
  • Webhook-Trigger
  • Audit Logging

Performance-Optimierungen

Bei großen Datenmengen (> 100 Relations):

  • Batch-Insert mit saveEntity() statt createEntity()
  • Eager Loading mit with()
  • Query Optimization mit select() und where()
  • Background Jobs für lange Operationen

Versionierung

Version Datum Änderungen
1.0 24.01.2026 Initiale Version basierend auf Räumungsklage-Implementierung

Erstellt von: EspoCRM Development Team
Letzte Aktualisierung: 24. Januar 2026
Status: Production Ready