# 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 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 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 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 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*