diff --git a/custom/README.md b/custom/README.md new file mode 100644 index 00000000..9fdbd303 --- /dev/null +++ b/custom/README.md @@ -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 +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 +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": "", + "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('
' + userInput + '
'); +``` + +### 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 +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 +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": "", + "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 ✅ diff --git a/data/config.php b/data/config.php index e7a1339f..7f8ddeca 100644 --- a/data/config.php +++ b/data/config.php @@ -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,