Files
espocrm/custom/ENTWICKLUNGSPLAN_ENTWICKLUNGEN.md
bsiggel 6a8a4a2882 Add development plan for intelligent inbox system with AI analysis
- Introduced comprehensive specifications for the "Entwicklungen-System" aimed at automating document processing for eviction lawsuits, rent collection, and terminations.
- Defined core principles, architecture overview, and detailed entity models including CEntwicklung and CEntwicklungTeamZuordnung.
- Implemented validation rules, custom API endpoints, and middleware integration for document processing and absence management.
- Established testing and quality assurance strategies, including unit tests and integration scenarios.
- Outlined deployment checklist and rollback plan to ensure smooth implementation.
- Included future roadmap for potential enhancements and defined roles and responsibilities for team members.
2026-01-25 22:13:44 +01:00

1511 lines
38 KiB
Markdown

# Entwicklungsplan: Entwicklungen-System (Posteingang mit KI-Analyse)
**Version:** 1.0
**Datum:** 25. Januar 2026
**Status:** Spezifikation finalisiert, bereit für Implementierung
---
## 📋 Executive Summary
### Ziel
Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorgängen (Räumungsklagen, Mietinkasso, Kündigungen). Dokumente werden automatisch zu "Entwicklungen" gruppiert, durch KI analysiert und relevanten Teams zur Review vorgelegt.
### Kernprinzipien
1. **EspoCRM = Data Layer** - Speichert Entities, stellt UI bereit, validiert Daten
2. **Middleware = Business Logic** - KI-Analyse, Team-Zuweisung, Abwesenheitsvertretung
3. **Clean Separation** - Keine komplexen Hooks/Workflows in EspoCRM
4. **Team-basiert** - Dynamische Zuordnung zu Teams statt fixer Workflows
### Architektur-Übersicht
```
┌─────────────────────────────────────────────────────────────────┐
│ MIDDLEWARE │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Dokument- │ │ KI-Analyse │ │ Abwesenheits-│ │
│ │ Polling │→ │ & Team- │→ │ Management │ │
│ │ │ │ Entscheidung │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ ↕ API ↕ API ↕ API │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ ESPOCRM │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ CEntwicklung │←→│ CEntwicklung │←→│ CDokumente │ │
│ │ │ │ TeamZuordnung│ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ ↑ ↑ ↑ │
│ └──────────────────┴──────────────────┘ │
│ Parent: Räumungsklage / Mietinkasso │
└─────────────────────────────────────────────────────────────────┘
```
---
## 🎯 Phase 1: Entities & Datenmodell (MVP)
### 1.1 Entity: CEntwicklung
**Zweck:** Gruppierung von Dokumenten mit KI-Analyse und Status-Tracking
**Datei:** `custom/Espo/Custom/Resources/metadata/entityDefs/CEntwicklung.json`
```json
{
"fields": {
"name": {
"type": "varchar",
"required": true,
"maxLength": 255,
"trim": true,
"isCustom": true
},
"status": {
"type": "enum",
"options": [
"Neu",
"In Verarbeitung",
"Bereit",
"In Review",
"Teilweise abgeschlossen",
"Abgeschlossen"
],
"default": "Neu",
"required": true,
"isCustom": true,
"style": {
"Neu": "default",
"In Verarbeitung": "primary",
"Bereit": "success",
"In Review": "warning",
"Teilweise abgeschlossen": "info",
"Abgeschlossen": "success"
}
},
"syncStatus": {
"type": "enum",
"options": ["clean", "unclean"],
"default": "unclean",
"required": true,
"isCustom": true,
"tooltip": true
},
"kiAnalyse": {
"type": "text",
"isCustom": true,
"tooltip": true
},
"zusammenfassung": {
"type": "varchar",
"maxLength": 500,
"isCustom": true,
"tooltip": true
},
"anzahlDokumente": {
"type": "int",
"readOnly": true,
"notStorable": false,
"isCustom": true
},
"anzahlTeamsAktiv": {
"type": "int",
"readOnly": true,
"notStorable": false,
"isCustom": true
},
"anzahlTeamsAbgeschlossen": {
"type": "int",
"readOnly": true,
"notStorable": false,
"isCustom": true
},
"createdAt": {
"type": "datetime",
"readOnly": true
},
"modifiedAt": {
"type": "datetime",
"readOnly": true
},
"createdBy": {
"type": "link",
"entity": "User",
"readOnly": true
},
"modifiedBy": {
"type": "link",
"entity": "User",
"readOnly": true
},
"assignedUser": {
"type": "link",
"entity": "User",
"isCustom": true
},
"teams": {
"type": "linkMultiple",
"isCustom": true
}
},
"links": {
"parent": {
"type": "belongsToParent",
"entityList": [
"CVmhRumungsklage",
"CMietinkasso",
"CKuendigung"
]
},
"dokumente": {
"type": "hasMany",
"entity": "CDokumente",
"foreign": "entwicklung"
},
"teamZuordnungen": {
"type": "hasMany",
"entity": "CEntwicklungTeamZuordnung",
"foreign": "entwicklung"
},
"createdBy": {
"type": "belongsTo",
"entity": "User"
},
"modifiedBy": {
"type": "belongsTo",
"entity": "User"
},
"assignedUser": {
"type": "belongsTo",
"entity": "User"
},
"teams": {
"type": "hasMany",
"entity": "Team",
"relationName": "EntityTeam",
"layoutRelationshipsDisabled": true
}
},
"collection": {
"orderBy": "createdAt",
"order": "desc",
"textFilterFields": ["name", "zusammenfassung"]
},
"indexes": {
"parent": {
"columns": ["parentType", "parentId"]
},
"status": {
"columns": ["status"]
},
"syncStatus": {
"columns": ["syncStatus"]
},
"createdAt": {
"columns": ["createdAt"]
}
}
}
```
---
### 1.2 Entity: CEntwicklungTeamZuordnung
**Zweck:** Junction Table für dynamische Team-Zuordnung mit Abschluss-Tracking
**Datei:** `custom/Espo/Custom/Resources/metadata/entityDefs/CEntwicklungTeamZuordnung.json`
```json
{
"fields": {
"name": {
"type": "varchar",
"notStorable": true,
"select": {
"select": "CONCAT:(team.name, ' - ', entwicklung.name)"
},
"orderBy": {
"order": [
["team.name", "{direction}"]
]
}
},
"entwicklung": {
"type": "link",
"entity": "CEntwicklung",
"required": true,
"isCustom": true
},
"team": {
"type": "link",
"entity": "Team",
"required": true,
"isCustom": true
},
"aktiv": {
"type": "bool",
"default": true,
"isCustom": true,
"tooltip": true
},
"abgeschlossen": {
"type": "bool",
"default": false,
"isCustom": true
},
"abgeschlossenAm": {
"type": "datetime",
"readOnly": true,
"isCustom": true
},
"abgeschlossenVon": {
"type": "link",
"entity": "User",
"readOnly": true,
"isCustom": true
},
"prioritaet": {
"type": "enum",
"options": ["Niedrig", "Normal", "Hoch"],
"default": "Normal",
"isCustom": true,
"style": {
"Niedrig": "default",
"Normal": "primary",
"Hoch": "danger"
}
},
"createdAt": {
"type": "datetime",
"readOnly": true
},
"modifiedAt": {
"type": "datetime",
"readOnly": true
}
},
"links": {
"entwicklung": {
"type": "belongsTo",
"entity": "CEntwicklung",
"foreign": "teamZuordnungen"
},
"team": {
"type": "belongsTo",
"entity": "Team"
},
"abgeschlossenVon": {
"type": "belongsTo",
"entity": "User"
}
},
"collection": {
"orderBy": "createdAt",
"order": "desc"
},
"indexes": {
"entwicklungTeam": {
"columns": ["entwicklungId", "teamId"],
"unique": true
},
"aktiv": {
"columns": ["aktiv"]
},
"abgeschlossen": {
"columns": ["abgeschlossen"]
}
}
}
```
---
### 1.3 Team-Entity erweitern
**Zweck:** Kategorisierung für Filter-Logik (Anwalt vs. Team-Teams)
**Datei:** `custom/Espo/Custom/Resources/metadata/entityDefs/Team.json`
```json
{
"fields": {
"teamKategorie": {
"type": "enum",
"options": [
"Anwalt",
"Mandatsbetreuung",
"Zwangsvollstreckung",
"Sonstiges"
],
"default": "Sonstiges",
"isCustom": true,
"tooltip": true
}
}
}
```
**Post-Setup-Aufgabe:** Bestehende Teams kategorisieren
- Team "Anwalt" → teamKategorie = "Anwalt"
- Team "Mandatsbetreuung" → teamKategorie = "Mandatsbetreuung"
- Team "Zwangsvollstreckung" → teamKategorie = "Zwangsvollstreckung"
---
### 1.4 User-Entity erweitern
**Zweck:** Abwesenheits-Management für automatische Umverteilung
**Datei:** `custom/Espo/Custom/Resources/metadata/entityDefs/User.json`
```json
{
"fields": {
"abwesend": {
"type": "bool",
"default": false,
"isCustom": true,
"tooltip": true
},
"abwesendBis": {
"type": "date",
"isCustom": true,
"tooltip": true
},
"vertretung": {
"type": "link",
"entity": "User",
"isCustom": true,
"tooltip": true
}
},
"links": {
"vertretung": {
"type": "belongsTo",
"entity": "User",
"isCustom": true
}
}
}
```
---
### 1.5 CDokumente erweitern
**Zweck:** Verknüpfung zu Entwicklungen
**Datei:** `custom/Espo/Custom/Resources/metadata/entityDefs/CDokumente.json`
```json
{
"fields": {
"entwicklung": {
"type": "link",
"entity": "CEntwicklung",
"isCustom": true
}
},
"links": {
"entwicklung": {
"type": "belongsTo",
"entity": "CEntwicklung",
"foreign": "dokumente",
"isCustom": true
}
}
}
```
---
### 1.6 Scopes definieren
**Datei:** `custom/Espo/Custom/Resources/metadata/scopes/CEntwicklung.json`
```json
{
"entity": true,
"tab": true,
"acl": "recordAllTeamOwnNo",
"aclPortal": false,
"customizable": true,
"stream": true,
"disabled": false,
"type": "Base",
"module": "Custom",
"object": true,
"isCustom": true,
"importable": false,
"notifications": true,
"calendar": false
}
```
**Datei:** `custom/Espo/Custom/Resources/metadata/scopes/CEntwicklungTeamZuordnung.json`
```json
{
"entity": true,
"tab": false,
"acl": "recordAllTeamNo",
"aclPortal": false,
"customizable": true,
"stream": false,
"disabled": false,
"type": "Base",
"module": "Custom",
"object": true,
"isCustom": true
}
```
---
### 1.7 Internationalisierung (i18n)
**Datei:** `custom/Espo/Custom/Resources/i18n/de_DE/CEntwicklung.json`
```json
{
"labels": {
"Create CEntwicklung": "Entwicklung erstellen",
"CEntwicklung": "Entwicklung",
"cEntwicklungs": "Entwicklungen"
},
"fields": {
"name": "Bezeichnung",
"status": "Status",
"syncStatus": "Synchronisations-Status",
"kiAnalyse": "KI-Analyse",
"zusammenfassung": "Zusammenfassung",
"anzahlDokumente": "Anzahl Dokumente",
"anzahlTeamsAktiv": "Teams (aktiv)",
"anzahlTeamsAbgeschlossen": "Teams (abgeschlossen)",
"parent": "Vorgang",
"dokumente": "Dokumente",
"teamZuordnungen": "Team-Zuordnungen"
},
"links": {
"parent": "Vorgang",
"dokumente": "Dokumente",
"teamZuordnungen": "Team-Zuordnungen"
},
"tooltips": {
"syncStatus": "clean = KI-Analyse aktuell | unclean = Neue Dokumente, Analyse ausstehend",
"kiAnalyse": "Automatisch generierte Zusammenfassung durch KI-Middleware",
"zusammenfassung": "Kurze Zusammenfassung für Listen-Ansicht"
},
"options": {
"status": {
"Neu": "Neu",
"In Verarbeitung": "In Verarbeitung",
"Bereit": "Bereit",
"In Review": "In Review",
"Teilweise abgeschlossen": "Teilweise abgeschlossen",
"Abgeschlossen": "Abgeschlossen"
},
"syncStatus": {
"clean": "Aktuell",
"unclean": "Ausstehend"
}
}
}
```
**Datei:** `custom/Espo/Custom/Resources/i18n/en_US/CEntwicklung.json`
```json
{
"labels": {
"Create CEntwicklung": "Create Development",
"CEntwicklung": "Development",
"cEntwicklungs": "Developments"
},
"fields": {
"name": "Name",
"status": "Status",
"syncStatus": "Sync Status",
"kiAnalyse": "AI Analysis",
"zusammenfassung": "Summary",
"anzahlDokumente": "Number of Documents",
"anzahlTeamsAktiv": "Teams (active)",
"anzahlTeamsAbgeschlossen": "Teams (completed)",
"parent": "Parent Record",
"dokumente": "Documents",
"teamZuordnungen": "Team Assignments"
},
"links": {
"parent": "Parent Record",
"dokumente": "Documents",
"teamZuordnungen": "Team Assignments"
},
"tooltips": {
"syncStatus": "clean = AI analysis up-to-date | unclean = New documents, analysis pending",
"kiAnalyse": "Automatically generated summary by AI middleware",
"zusammenfassung": "Short summary for list views"
},
"options": {
"status": {
"Neu": "New",
"In Verarbeitung": "Processing",
"Bereit": "Ready",
"In Review": "In Review",
"Teilweise abgeschlossen": "Partially Completed",
"Abgeschlossen": "Completed"
},
"syncStatus": {
"clean": "Up-to-date",
"unclean": "Pending"
}
}
}
```
**Analog für CEntwicklungTeamZuordnung, Team.teamKategorie, User.abwesend**
---
## 🔧 Phase 2: Validierung & Business Rules
### 2.1 Formula-Script: Abschluss nur bei clean
**Datei:** `custom/Espo/Custom/Resources/metadata/formula/CEntwicklung.json`
```json
{
"beforeSaveApiScript": "// Verhindere Abschluss bei unclean Status\nif (\n (status == 'Abgeschlossen' || entity\\isAttributeChanged('status'))\n && syncStatus == 'unclean'\n) {\n recordService\\throwBadRequest('Entwicklung kann nicht abgeschlossen werden: Neue Dokumente vorhanden (Status: unclean). Bitte warten Sie auf die KI-Analyse.');\n}"
}
```
**Test-Szenario:**
1. Entwicklung mit syncStatus = "unclean"
2. User versucht manuell status = "Abgeschlossen" zu setzen
3. Erwartung: Error-Message, Speichern verhindert
---
### 2.2 Hook: Berechnete Felder aktualisieren
**Datei:** `custom/Espo/Custom/Hooks/CEntwicklung/UpdateTeamStats.php`
```php
<?php
namespace Espo\Custom\Hooks\CEntwicklung;
use Espo\ORM\Entity;
use Espo\Core\Hook\Hook\BeforeSave;
class UpdateTeamStats implements BeforeSave
{
public function __construct(
private \Espo\ORM\EntityManager $entityManager
) {}
public function beforeSave(Entity $entity, array $options): void
{
// Zähle Dokumente
if ($entity->isNew() || $entity->isAttributeChanged('id')) {
$dokumenteCount = $this->entityManager
->getRDBRepository('CDokumente')
->where(['entwicklungId' => $entity->getId()])
->count();
$entity->set('anzahlDokumente', $dokumenteCount);
}
// Zähle Team-Zuordnungen
$zuordnungen = $this->entityManager
->getRDBRepository('CEntwicklungTeamZuordnung')
->where(['entwicklungId' => $entity->getId()])
->find();
$aktiv = 0;
$abgeschlossen = 0;
foreach ($zuordnungen as $z) {
if ($z->get('aktiv')) {
$aktiv++;
if ($z->get('abgeschlossen')) {
$abgeschlossen++;
}
}
}
$entity->set('anzahlTeamsAktiv', $aktiv);
$entity->set('anzahlTeamsAbgeschlossen', $abgeschlossen);
}
}
```
---
## 🎨 Phase 3: Layouts & UI
### 3.1 Detail-Layout
**Datei:** `custom/Espo/Custom/Resources/layouts/CEntwicklung/detail.json`
```json
[
{
"label": "Übersicht",
"rows": [
[
{"name": "name"},
{"name": "status"}
],
[
{"name": "syncStatus"},
{"name": "parent"}
],
[
{"name": "anzahlDokumente"},
{"name": "anzahlTeamsAktiv"}
],
[
{"name": "zusammenfassung", "span": 2}
]
]
},
{
"label": "KI-Analyse",
"rows": [
[
{"name": "kiAnalyse", "span": 2}
]
]
},
{
"label": "System",
"rows": [
[
{"name": "createdAt"},
{"name": "modifiedAt"}
],
[
{"name": "createdBy"},
{"name": "modifiedBy"}
]
]
}
]
```
---
### 3.2 List-Layout
**Datei:** `custom/Espo/Custom/Resources/layouts/CEntwicklung/list.json`
```json
[
{"name": "name", "width": 30},
{"name": "status", "width": 15},
{"name": "syncStatus", "width": 10},
{"name": "parent", "width": 20},
{"name": "anzahlDokumente", "width": 10},
{"name": "createdAt", "width": 15}
]
```
---
### 3.3 Bottom-Panels
**Datei:** `custom/Espo/Custom/Resources/layouts/CEntwicklung/bottomPanelsDetail.json`
```json
{
"teamZuordnungen": {
"index": 0,
"sticked": true,
"style": "info",
"label": "Team-Zuordnungen"
},
"dokumente": {
"index": 1,
"sticked": false,
"label": "Dokumente"
},
"stream": {
"index": 2,
"sticked": false
}
}
```
---
### 3.4 ClientDefs
**Datei:** `custom/Espo/Custom/Resources/metadata/clientDefs/CEntwicklung.json`
```json
{
"controller": "controllers/record",
"iconClass": "fas fa-inbox",
"color": "#3498db",
"filterList": [
"meineOffenen",
{
"name": "bereit"
},
{
"name": "inReview"
}
],
"boolFilterList": [
"onlyMy"
],
"defaultFilterPreset": "meineOffenen"
}
```
---
## 🔌 Phase 4: Custom API Endpoints
### 4.1 API: Team-Aktivierung
**Datei:** `custom/Espo/Custom/Api/CEntwicklung/AktiviereTeams.php`
```php
<?php
namespace Espo\Custom\Api\CEntwicklung;
use Espo\Core\Api\Action;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\NotFound;
class AktiviereTeams implements Action
{
public function __construct(
private \Espo\ORM\EntityManager $entityManager,
private \Espo\Core\Utils\Log $log
) {}
public function process(Request $request): Response
{
$id = $request->getRouteParam('id');
if (!$id) {
throw new BadRequest('ID fehlt');
}
$entwicklung = $this->entityManager->getEntity('CEntwicklung', $id);
if (!$entwicklung) {
throw new NotFound('Entwicklung nicht gefunden');
}
$data = $request->getParsedBody();
// 1. Update Entwicklung
$entwicklung->set([
'kiAnalyse' => $data->kiAnalyse ?? null,
'zusammenfassung' => $data->zusammenfassung ?? null,
'status' => $data->status ?? 'Bereit',
'syncStatus' => $data->syncStatus ?? 'clean'
]);
$this->entityManager->saveEntity($entwicklung);
// 2. Lösche alte Zuordnungen (soft delete - setze inaktiv)
$this->entityManager
->getQueryBuilder()
->update()
->in('CEntwicklungTeamZuordnung')
->set(['aktiv' => false])
->where(['entwicklungId' => $id])
->execute();
// 3. Erstelle neue Zuordnungen
if (isset($data->teams) && is_array($data->teams)) {
foreach ($data->teams as $teamData) {
$teamId = $teamData->teamId ?? null;
if (!$teamId) {
$this->log->warning("Team-ID fehlt in teams-Array");
continue;
}
// Prüfe ob bereits existiert
$existing = $this->entityManager
->getRDBRepository('CEntwicklungTeamZuordnung')
->where([
'entwicklungId' => $id,
'teamId' => $teamId
])
->findOne();
if ($existing) {
// Reaktiviere
$existing->set([
'aktiv' => true,
'abgeschlossen' => false,
'prioritaet' => $teamData->prioritaet ?? 'Normal'
]);
$this->entityManager->saveEntity($existing);
} else {
// Erstelle neu
$zuordnung = $this->entityManager->createEntity('CEntwicklungTeamZuordnung', [
'entwicklungId' => $id,
'teamId' => $teamId,
'aktiv' => true,
'abgeschlossen' => false,
'prioritaet' => $teamData->prioritaet ?? 'Normal'
]);
}
}
}
$this->log->info("Teams aktiviert für Entwicklung {$id}");
return Response::json([
'success' => true,
'entwicklungId' => $id
]);
}
}
```
**Route registrieren:**
**Datei:** `custom/Espo/Custom/Resources/metadata/app/api.json`
```json
{
"routes": [
{
"route": "/CEntwicklung/:id/aktiviere-teams",
"method": "put",
"actionClassName": "Espo\\Custom\\Api\\CEntwicklung\\AktiviereTeams"
}
]
}
```
---
### 4.2 API: Abschluss für Team
**Datei:** `custom/Espo/Custom/Api/CEntwicklung/AbschliessenFuerTeam.php`
```php
<?php
namespace Espo\Custom\Api\CEntwicklung;
use Espo\Core\Api\Action;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
class AbschliessenFuerTeam implements Action
{
public function __construct(
private \Espo\ORM\EntityManager $entityManager,
private \Espo\Core\Acl\Table $acl,
private \Espo\Entities\User $user,
private \Espo\Core\Utils\Log $log
) {}
public function process(Request $request): Response
{
$entwicklungId = $request->getRouteParam('id');
$data = $request->getParsedBody();
$teamId = $data->teamId ?? null;
if (!$entwicklungId || !$teamId) {
throw new BadRequest('entwicklungId oder teamId fehlt');
}
// 1. Validierung: Ist User in diesem Team?
$userTeams = $this->user->getLinkMultipleIdList('teams');
if (!in_array($teamId, $userTeams)) {
throw new Forbidden('User nicht in angegebenem Team');
}
// 2. Lade Entwicklung
$entwicklung = $this->entityManager->getEntity('CEntwicklung', $entwicklungId);
if (!$entwicklung) {
throw new NotFound('Entwicklung nicht gefunden');
}
// 3. Validierung: syncStatus = clean?
if ($entwicklung->get('syncStatus') !== 'clean') {
throw new BadRequest('Entwicklung hat neue Dokumente (unclean) - bitte warten Sie auf die KI-Analyse');
}
// 4. Finde Zuordnung
$zuordnung = $this->entityManager
->getRDBRepository('CEntwicklungTeamZuordnung')
->where([
'entwicklungId' => $entwicklungId,
'teamId' => $teamId,
'aktiv' => true
])
->findOne();
if (!$zuordnung) {
throw new NotFound('Team-Zuordnung nicht gefunden oder nicht aktiv');
}
// 5. Bereits abgeschlossen?
if ($zuordnung->get('abgeschlossen')) {
return Response::json([
'success' => true,
'message' => 'Bereits abgeschlossen',
'alreadyCompleted' => true
]);
}
// 6. Abschluss setzen
$zuordnung->set([
'abgeschlossen' => true,
'abgeschlossenAm' => date('Y-m-d H:i:s'),
'abgeschlossenVonId' => $this->user->getId()
]);
$this->entityManager->saveEntity($zuordnung);
// 7. Prüfe: Alle Teams abgeschlossen?
$offeneTeams = $this->entityManager
->getRDBRepository('CEntwicklungTeamZuordnung')
->where([
'entwicklungId' => $entwicklungId,
'aktiv' => true,
'abgeschlossen' => false
])
->count();
// 8. Update Entwicklung-Status
if ($offeneTeams === 0) {
$entwicklung->set('status', 'Abgeschlossen');
} else {
$entwicklung->set('status', 'Teilweise abgeschlossen');
}
$this->entityManager->saveEntity($entwicklung);
$this->log->info("Team {$teamId} hat Entwicklung {$entwicklungId} abgeschlossen");
return Response::json([
'success' => true,
'status' => $entwicklung->get('status'),
'offeneTeams' => $offeneTeams
]);
}
}
```
**Route registrieren:**
```json
{
"routes": [
{
"route": "/CEntwicklung/:id/abschliessen-fuer-team",
"method": "post",
"actionClassName": "Espo\\Custom\\Api\\CEntwicklung\\AbschliessenFuerTeam"
}
]
}
```
---
## 🔍 Phase 5: Custom Primary Filter
### 5.1 Filter: Meine offenen Entwicklungen
**Datei:** `custom/Espo/Custom/Classes/Select/CEntwicklung/PrimaryFilters/MeineOffenen.php`
```php
<?php
namespace Espo\Custom\Classes\Select\CEntwicklung\PrimaryFilters;
use Espo\Core\Select\Primary\Filter;
use Espo\ORM\Query\SelectBuilder;
use Espo\Entities\User;
use Espo\ORM\EntityManager;
class MeineOffenen implements Filter
{
public function __construct(
private User $user,
private EntityManager $entityManager
) {}
public function apply(SelectBuilder $queryBuilder): void
{
$userId = $this->user->getId();
$userTeams = $this->user->getLinkMultipleIdList('teams');
if (empty($userTeams)) {
// User hat keine Teams -> zeige nichts
$queryBuilder->where(['id' => null]);
return;
}
// Prüfe ob User in "Anwalt"-Team ist
$anwaltTeams = $this->entityManager
->getRDBRepository('Team')
->where(['teamKategorie' => 'Anwalt'])
->select(['id'])
->find();
$anwaltTeamIds = [];
foreach ($anwaltTeams as $team) {
$anwaltTeamIds[] = $team->getId();
}
$isAnwalt = !empty(array_intersect($userTeams, $anwaltTeamIds));
// Join zu TeamZuordnungen
$queryBuilder->distinct();
$queryBuilder->leftJoin('teamZuordnungen', 'tz');
$conditions = [];
// Bedingung 1: Standard-Teams (Mandatsbetreuung, ZV)
$standardTeamIds = array_diff($userTeams, $anwaltTeamIds);
if (!empty($standardTeamIds)) {
$conditions[] = [
'tz.teamId' => $standardTeamIds,
'tz.aktiv' => true,
'tz.abgeschlossen' => false
];
}
// Bedingung 2: Anwalt-Teams (nur eigene Vorgänge)
if ($isAnwalt && !empty($anwaltTeamIds)) {
// Subquery für jede Parent-Entität
$parentConditions = [];
foreach (['CVmhRumungsklage', 'CMietinkasso', 'CKuendigung'] as $parentType) {
$alias = strtolower(str_replace('C', '', $parentType));
$queryBuilder->leftJoin(
'parent',
$alias,
[
"{$alias}.id:" => 'parentId',
'parentType' => $parentType
]
);
$parentConditions[] = [
'parentType' => $parentType,
"{$alias}.assignedUserId" => $userId
];
}
$conditions[] = [
'tz.teamId' => $anwaltTeamIds,
'tz.aktiv' => true,
'tz.abgeschlossen' => false,
'OR' => $parentConditions
];
}
if (!empty($conditions)) {
$queryBuilder->where([
'OR' => $conditions
]);
} else {
// Keine passenden Bedingungen -> zeige nichts
$queryBuilder->where(['id' => null]);
}
}
}
```
**Filter registrieren:**
**Datei:** `custom/Espo/Custom/Resources/metadata/selectDefs/CEntwicklung.json`
```json
{
"primaryFilterClassNameMap": {
"meineOffenen": "Espo\\Custom\\Classes\\Select\\CEntwicklung\\PrimaryFilters\\MeineOffenen"
},
"boolFilterDefs": {
"meineOffenen": {}
}
}
```
**i18n:**
```json
{
"presetFilters": {
"meineOffenen": "Meine offenen Entwicklungen"
}
}
```
---
## 🚀 Phase 6: Middleware-Integration (Spezifikation)
### 6.1 Polling-Endpoints
**Middleware nutzt Standard-EspoCRM-API:**
#### Neue Dokumente ohne Entwicklung finden:
```http
GET /api/v1/CDokumente?where[0][type]=isNull&where[0][attribute]=entwicklungId&maxSize=50&orderBy=createdAt&order=asc
```
#### Entwicklungen mit unclean Status:
```http
GET /api/v1/CEntwicklung?where[0][type]=equals&where[0][attribute]=syncStatus&where[0][value]=unclean&maxSize=10
```
#### Abwesende User:
```http
GET /api/v1/User?where[0][type]=equals&where[0][attribute]=abwesend&where[0][value]=true
```
---
### 6.2 Middleware-Workflow: Dokument-Verarbeitung
**Pseudocode:**
```
POLLING JOB (alle 60 Sekunden):
1. Query neue Dokumente ohne Entwicklung
2. Für jedes Dokument:
a) Hat es einen Parent?
NEIN → Skip (keine Zuordnung möglich)
b) Existiert offene Entwicklung für diesen Parent?
Query: /api/v1/CEntwicklung?where[0][parentId]={id}&where[1][syncStatus]=unclean
JA →
- Dokument verknüpfen: PUT /api/v1/CDokumente/{id} {"entwicklungId": X}
- Entwicklung-Status: PUT /api/v1/CEntwicklung/{id} {"status": "In Verarbeitung"}
NEIN →
- Neue Entwicklung erstellen:
POST /api/v1/CEntwicklung {
"name": "Entwicklung #N - [Datum]",
"parentType": "...",
"parentId": "...",
"status": "Neu",
"syncStatus": "unclean"
}
- Dokument verknüpfen
3. Queue für KI-Analyse füllen
ANALYSE JOB (async Worker):
1. Hole Entwicklung aus Queue
2. Download alle Dokumente:
GET /api/v1/CEntwicklung/{id}/dokumente
Für jedes: GET /api/v1/Attachment/{attachmentId}
3. KI-Verarbeitung:
- OCR falls nötig
- Inhaltsanalyse
- Team-Entscheidung:
* Regex/Keywords für Zwangsvollstreckung
* Sentiment-Analyse für Dringlichkeit
* Named-Entity-Recognition für Beteiligte
- Priorität ableiten
4. Update via Custom API:
PUT /api/v1/CEntwicklung/{id}/aktiviere-teams {
"kiAnalyse": "Lange Zusammenfassung...",
"zusammenfassung": "Kurz...",
"status": "Bereit",
"syncStatus": "clean",
"teams": [
{"teamId": "66ab...", "prioritaet": "Hoch"}
]
}
5. Optional: Benachrichtigungen triggern
```
---
### 6.3 Middleware-Workflow: Abwesenheitsvertretung
**Pseudocode:**
```
POLLING JOB (alle 5 Minuten):
1. Query abwesende User
2. Für jeden User:
a) Prüfe abwesendBis:
Wenn abwesendBis <= heute:
- PUT /api/v1/User/{id} {"abwesend": false}
- Skip (User ist zurück)
b) Ermittle Vertreter:
- Prio 1: User.vertretung (falls gesetzt)
- Prio 2: Team-Leader (Query: Team mit User als Member)
- Prio 3: User mit wenigsten offenen Entwicklungen
c) Query offene Entwicklungen für Anwalt-Teams:
GET /api/v1/CEntwicklung
?where[0][type]=in
&where[0][attribute]=parentType
&where[0][value][]=CVmhRumungsklage
&...
Filtere lokal nach: Parent.assignedUserId = abwesenderUser
d) Für jede Entwicklung:
- Update Parent-Vorgang:
PUT /api/v1/{ParentType}/{parentId} {
"assignedUserId": "vertreterUserId"
}
- Stream-Eintrag erstellen:
POST /api/v1/Note {
"parentType": "CEntwicklung",
"parentId": "{entwicklungId}",
"type": "Post",
"post": "Umverteilt von [Abwesender] zu [Vertreter] (Abwesenheit)"
}
e) Analog für Tasks, Workflows, etc.
```
---
## 📊 Phase 7: Testing & Qualitätssicherung
### 7.1 Unit-Tests (Custom PHP-Klassen)
**Datei:** `tests/unit/Espo/Custom/Api/CEntwicklung/AktiviereTeamsTest.php`
**Test-Cases:**
- ✅ Teams werden korrekt aktiviert
- ✅ Alte Zuordnungen werden deaktiviert
- ✅ Entwicklung-Status wird aktualisiert
- ✅ Fehlerbehandlung bei fehlender ID
- ✅ Fehlerbehandlung bei ungültigem Team
---
### 7.2 Integration-Tests
**Szenario 1: Dokument → Entwicklung → Analyse → Abschluss**
1. Upload Dokument via UI
2. Middleware erkennt Dokument (manuell triggern)
3. Middleware erstellt Entwicklung
4. Middleware analysiert & aktiviert Teams
5. User reviewed & schließt ab
6. Validierung: Status = "Abgeschlossen"
**Szenario 2: Mehrere Teams parallel**
1. Entwicklung mit 2 Teams (Mandatsbetreuung + Anwalt)
2. Mandatsbetreuung schließt ab → Status = "Teilweise abgeschlossen"
3. Anwalt schließt ab → Status = "Abgeschlossen"
**Szenario 3: Neues Dokument während Review**
1. Entwicklung im Status "Bereit"
2. Neues Dokument uploaded
3. Middleware setzt syncStatus = "unclean"
4. Abschluss-Button disabled
5. Middleware re-analysiert
6. syncStatus = "clean", Abschluss wieder möglich
**Szenario 4: Abwesenheitsvertretung**
1. User A setzt abwesend = true, vertretung = User B
2. Middleware pollt
3. Räumungsklagen von User A werden umverteilt
4. Entwicklungen erscheinen in User B's Liste
5. User A setzt abwesend = false
6. Keine weitere Umverteilung
---
### 7.3 Performance-Tests
**Metriken:**
- Query-Zeit für "Meine offenen" Filter < 500ms (bei 1000 Entwicklungen)
- Middleware Polling-Overhead < 1 CPU-Sekunde pro Cycle
- Abschluss-API < 200ms Response-Time
---
## 📋 Deployment-Checkliste
### Pre-Deployment
- [ ] Alle JSON-Dateien validiert (Syntax)
- [ ] Relationships bidirektional definiert
- [ ] i18n vollständig (de_DE + en_US)
- [ ] Custom API-Routes registriert
- [ ] PHP-Klassen Namespace korrekt
### Deployment
- [ ] Files via Git committen
- [ ] `python3 custom/scripts/validate_and_rebuild.py` ausführen
- [ ] Keine Errors in Validation
- [ ] Rebuild erfolgreich
- [ ] Browser Hard Refresh (Ctrl+Shift+R)
### Post-Deployment
- [ ] Teams kategorisieren (teamKategorie setzen)
- [ ] Test-Entwicklung manuell erstellen
- [ ] Filter "Meine offenen" testen
- [ ] API-Endpoints mit curl/Postman testen
- [ ] Middleware konfigurieren & starten
- [ ] End-to-End-Test durchführen
---
## 🔄 Rollback-Plan
**Bei Problemen:**
1. Git: `git revert HEAD` (letzten Commit rückgängig)
2. Rebuild: `python3 custom/scripts/validate_and_rebuild.py`
3. Cache leeren: `rm -rf data/cache/*`
4. Middleware stoppen
5. DB-Rollback falls nötig: `DROP TABLE c_entwicklung, c_entwicklung_team_zuordnung;`
---
## 📚 Dokumentation & Wissenstransfer
### Für Entwickler
- Dieser Plan (`ENTWICKLUNGSPLAN_ENTWICKLUNGEN.md`)
- Code-Kommentare in PHP-Klassen
- API-Dokumentation (Swagger/Postman Collection)
### Für User
- User-Guide: "Wie nutze ich Entwicklungen?"
- Video-Tutorial: Entwicklung reviewen & abschließen
- FAQ: Häufige Fragen
### Für Admins
- Team-Setup-Guide (teamKategorie konfigurieren)
- Middleware-Setup-Guide
- Troubleshooting-Guide
---
## 🎯 Success Metrics
**Funktional:**
- ✅ Dokumente werden automatisch zu Entwicklungen gruppiert
- ✅ KI-Analyse wird angezeigt
- ✅ Teams sehen nur relevante Entwicklungen
- ✅ Abschluss-Workflow funktioniert
- ✅ Abwesenheitsvertretung funktioniert
**Performance:**
- ✅ Filter < 500ms
- ✅ API-Calls < 200ms
- ✅ Middleware-Polling ohne Fehler
**Usability:**
- ✅ User finden Entwicklungen intuitiv
- ✅ Abschluss-Prozess klar
- ✅ Fehler-Messages verständlich
---
## 🔮 Zukünftige Erweiterungen (Roadmap)
### v2.0: Erweiterte Features
- [ ] Kommentare zu Entwicklungen (Stream)
- [ ] E-Mail-Benachrichtigungen bei neuen Entwicklungen
- [ ] Dashboard-Widget: "Meine offenen Entwicklungen"
- [ ] Bulk-Actions: Mehrere Entwicklungen gleichzeitig abschließen
### v2.5: Analytics
- [ ] Report: Durchschnittliche Bearbeitungszeit pro Team
- [ ] Report: Anzahl Entwicklungen pro Vorgang
- [ ] Dashboard: Entwicklungen-Pipeline (Kanban-View)
### v3.0: Advanced
- [ ] Workflow-Integration: Auto-Task bei neuer Entwicklung
- [ ] Custom Notification-Channels (Slack, Teams)
- [ ] Mobile-App-Integration
- [ ] KI-gestützte Prioritäts-Vorhersage
---
## 👥 Rollen & Verantwortlichkeiten
**Backend-Entwickler:**
- Entity-Definitionen
- Custom API-Endpoints
- Hooks & Formula-Scripts
- Performance-Optimierung
**Frontend-Entwickler:**
- Layouts (Detail, List, Panels)
- Custom Views (falls nötig)
- CSS-Anpassungen
- UI/UX-Testing
**Middleware-Entwickler:**
- Polling-Jobs
- KI-Integration
- Abwesenheits-Logik
- Error-Handling
**QA-Engineer:**
- Test-Cases erstellen
- Integration-Tests
- Performance-Tests
- Bug-Tracking
**Product Owner:**
- Requirements validieren
- User-Feedback einholen
- Prioritäten setzen
- Acceptance-Tests
---
## 📞 Support & Kontakt
**Bei Fragen zur Implementierung:**
- EspoCRM-Dokumentation: https://docs.espocrm.com
- Custom Development Guide: `README.md`
- KI-Overview-Script: `bash custom/scripts/ki-overview.sh`
**Bei Problemen:**
- Logs prüfen: `tail -f data/logs/espo-*.log`
- Validator: `python3 custom/scripts/validate_and_rebuild.py --dry-run`
- Git-History: `git log --oneline custom/Espo/Custom/`
---
**Status:** ✅ Spezifikation vollständig
**Nächster Schritt:** Phase 1 Implementierung starten
**Geschätzte Dauer:** 2-3 Wochen (alle Phasen)
---
*Ende des Entwicklungsplans*