Update cache timestamps in config; add README for custom actions implementation

This commit is contained in:
2026-01-24 11:59:46 +01:00
parent ec612318c3
commit 6e0c104c6f
2 changed files with 976 additions and 2 deletions

974
custom/README.md Normal file
View File

@@ -0,0 +1,974 @@
# 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](#überblick)
2. [Architektur-Prinzipien](#architektur-prinzipien)
3. [Implementierungs-Schritte](#implementierungs-schritte)
4. [Code-Template](#code-template)
5. [Best Practices](#best-practices)
6. [Sicherheit](#sicherheit)
7. [Testing & Debugging](#testing--debugging)
8. [Beispiel-Implementierung](#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
<?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
<?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`
```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`
```javascript
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`
```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`
```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
```bash
# 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
```php
// ✅ 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
```php
// ✅ 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
```php
// ✅ 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
```php
// ✅ 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:
```php
// 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:
```php
// 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
```php
// ✅ 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:**
```javascript
// ✅ 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
```bash
# 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
```bash
# 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):**
```javascript
// 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](custom/Espo/Custom/Controllers/CMietobjekt.php)):
```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](custom/Espo/Custom/Services/CMietobjekt.php)):
```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](custom/Espo/Custom/Resources/metadata/clientDefs/CMietobjekt.json)):
```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](client/custom/src/handlers/mietobjekt/eviction-action.js)):
```javascript
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](custom/Espo/Custom/Resources/i18n/de_DE/CMietobjekt.json)):
```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/catch``commit()/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
### Nützliche Links
- [EspoCRM Dokumentation](https://docs.espocrm.com)
- [GitHub Repository](https://github.com/espocrm/espocrm)
- [Community Forum](https://forum.espocrm.com)
- [API Dokumentation](https://docs.espocrm.com/development/api/)
---
## 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 ✅

View File

@@ -359,8 +359,8 @@ return [
0 => 'youtube.com',
1 => 'google.com'
],
'cacheTimestamp' => 1769249126,
'microtime' => 1769249126.108164,
'cacheTimestamp' => 1769252191,
'microtime' => 1769252191.810945,
'siteUrl' => 'https://crm.bitbylaw.com',
'fullTextSearchMinLength' => 4,
'appTimestamp' => 1768843902,