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

38 KiB

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

{
  "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

{
  "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

{
  "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

{
  "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

{
  "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

{
  "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

{
  "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

{
  "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

{
  "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

{
  "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
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

[
  {
    "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

[
  {"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

{
  "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

{
  "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
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

{
  "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
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:

{
  "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
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

{
  "primaryFilterClassNameMap": {
    "meineOffenen": "Espo\\Custom\\Classes\\Select\\CEntwicklung\\PrimaryFilters\\MeineOffenen"
  },
  "boolFilterDefs": {
    "meineOffenen": {}
  }
}

i18n:

{
  "presetFilters": {
    "meineOffenen": "Meine offenen Entwicklungen"
  }
}

🚀 Phase 6: Middleware-Integration (Spezifikation)

6.1 Polling-Endpoints

Middleware nutzt Standard-EspoCRM-API:

Neue Dokumente ohne Entwicklung finden:

GET /api/v1/CDokumente?where[0][type]=isNull&where[0][attribute]=entwicklungId&maxSize=50&orderBy=createdAt&order=asc

Entwicklungen mit unclean Status:

GET /api/v1/CEntwicklung?where[0][type]=equals&where[0][attribute]=syncStatus&where[0][value]=unclean&maxSize=10

Abwesende User:

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