# Entwicklungsplan: Entwicklungen-System (Posteingang mit KI-Analyse) **Version:** 2.0 **Datum:** 11. Februar 2026 **Status:** Erweiterte Spezifikation mit First-Read-Closes & Advanced Features - 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 5. **First-Read-Closes** - Sobald ein Team eine Entwicklung abschließt, wird der Block finalisiert. Alle nachfolgenden Dokumente bilden automatisch einen neuen Block. Dadurch sehen alle Teams identische Dokumenten-Gruppierungen und es gibt keine asynchronen Inkonsistenzen. ### 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 }, "finalisiert": { "type": "bool", "default": false, "readOnly": true, "isCustom": true, "tooltip": true }, "finalisierungsGrund": { "type": "enum", "options": [ "Erstes Team", "Manuell", "Automatisch" ], "readOnly": true, "isCustom": true, "tooltip": true }, "finalisiertAm": { "type": "datetime", "readOnly": true, "isCustom": true }, "finalisiertVon": { "type": "link", "entity": "User", "readOnly": true, "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" }, "finalisiertVon": { "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"] }, "finalisiert": { "columns": ["finalisiert"] }, "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)", "finalisiert": "Finalisiert", "finalisierungsGrund": "Finalisierungsgrund", "finalisiertAm": "Finalisiert am", "finalisiertVon": "Finalisiert von", "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", "finalisiert": "Block wurde geschlossen - neue Dokumente erzeugen automatisch einen neuen Block (First-Read-Closes Prinzip)", "finalisierungsGrund": "Grund der Finalisierung: Erstes Team = Team hat abgeschlossen | Manuell = Admin-Aktion | Automatisch = System-Regel" }, "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" }, "finalisierungsGrund": { "Erstes Team": "Erstes Team", "Manuell": "Manuell", "Automatisch": "Automatisch" } } } ``` **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)", "finalisiert": "Finalized", "finalisierungsGrund": "Finalization Reason", "finalisiertAm": "Finalized At", "finalisiertVon": "Finalized By", "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", "finalisiert": "Block has been closed - new documents will automatically create a new block (First-Read-Closes principle)", "finalisierungsGrund": "Reason for finalization: First Team = Team completed | Manual = Admin action | Automatic = System rule" }, "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" }, "finalisierungsGrund": { "Erstes Team": "First Team", "Manuell": "Manual", "Automatisch": "Automatic" } } } ``` **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}\n\n// Verhindere Änderungen an finalisierter Entwicklung\nif (\n finalisiert == true\n && entity\\isAttributeChanged('finalisiert') == false\n && (entity\\isAttributeChanged('status') || entity\\isAttributeChanged('syncStatus'))\n) {\n recordService\\throwBadRequest('Entwicklung ist finalisiert. Neue Dokumente erzeugen automatisch einen neuen Block.');\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": "finalisiert"}, {"name": "finalisierungsGrund"} ], [ {"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); // 6.5. FIRST-READ-CLOSES: Finalisiere Block bei erstem Abschluss if (!$entwicklung->get('finalisiert')) { $entwicklung->set([ 'finalisiert' => true, 'finalisierungsGrund' => 'Erstes Team', 'finalisiertAm' => date('Y-m-d H:i:s'), 'finalisiertVonId' => $this->user->getId() ]); $this->log->info("Block finalisiert durch erstes Team (Team {$teamId}, User {$this->user->getId()})"); } // 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'), 'finalisiert' => $entwicklung->get('finalisiert'), 'offeneTeams' => $offeneTeams ]); } } ``` **Route registrieren:** ```json { "routes": [ { "route": "/CEntwicklung/:id/abschliessen-fuer-team", "method": "post", "actionClassName": "Espo\\Custom\\Api\\CEntwicklung\\AbschliessenFuerTeam" } ] } ``` --- ### 4.3 API: Team zu Entwicklung hinzufügen **Datei:** `custom/Espo/Custom/Api/CEntwicklung/AddTeam.php` ```php getRouteParam('id'); $data = $request->getParsedBody(); $teamId = $data->teamId ?? null; $prioritaet = $data->prioritaet ?? 'Normal'; if (!$entwicklungId || !$teamId) { throw new BadRequest('entwicklungId oder teamId fehlt'); } $entwicklung = $this->entityManager->getEntity('CEntwicklung', $entwicklungId); if (!$entwicklung) { throw new NotFound('Entwicklung nicht gefunden'); } // Prüfe ob Team existiert $team = $this->entityManager->getEntity('Team', $teamId); if (!$team) { throw new NotFound('Team nicht gefunden'); } // Prüfe ob bereits existiert $existing = $this->entityManager ->getRDBRepository('CEntwicklungTeamZuordnung') ->where([ 'entwicklungId' => $entwicklungId, 'teamId' => $teamId ]) ->findOne(); if ($existing) { // Reaktiviere falls inaktiv if (!$existing->get('aktiv')) { $existing->set([ 'aktiv' => true, 'abgeschlossen' => false, 'prioritaet' => $prioritaet ]); $this->entityManager->saveEntity($existing); $this->log->info("Team {$teamId} reaktiviert für Entwicklung {$entwicklungId}"); return Response::json([ 'success' => true, 'message' => 'Team reaktiviert', 'zuordnungId' => $existing->getId() ]); } return Response::json([ 'success' => true, 'message' => 'Team bereits aktiv', 'zuordnungId' => $existing->getId() ]); } // Erstelle neue Zuordnung $zuordnung = $this->entityManager->createEntity('CEntwicklungTeamZuordnung', [ 'entwicklungId' => $entwicklungId, 'teamId' => $teamId, 'aktiv' => true, 'abgeschlossen' => false, 'prioritaet' => $prioritaet ]); $this->log->info("Team {$teamId} hinzugefügt zu Entwicklung {$entwicklungId} durch User {$this->user->getId()}"); return Response::json([ 'success' => true, 'message' => 'Team hinzugefügt', 'zuordnungId' => $zuordnung->getId() ]); } } ``` --- ### 4.4 API: Team von Entwicklung entfernen **Datei:** `custom/Espo/Custom/Api/CEntwicklung/RemoveTeam.php` ```php getRouteParam('id'); $data = $request->getParsedBody(); $teamId = $data->teamId ?? null; if (!$entwicklungId || !$teamId) { throw new BadRequest('entwicklungId oder teamId fehlt'); } $entwicklung = $this->entityManager->getEntity('CEntwicklung', $entwicklungId); if (!$entwicklung) { throw new NotFound('Entwicklung nicht gefunden'); } // Finde Zuordnung $zuordnung = $this->entityManager ->getRDBRepository('CEntwicklungTeamZuordnung') ->where([ 'entwicklungId' => $entwicklungId, 'teamId' => $teamId ]) ->findOne(); if (!$zuordnung) { throw new NotFound('Team-Zuordnung nicht gefunden'); } // Deaktiviere (soft delete) $zuordnung->set('aktiv', false); $this->entityManager->saveEntity($zuordnung); $this->log->info("Team {$teamId} entfernt von Entwicklung {$entwicklungId} durch User {$this->user->getId()}"); return Response::json([ 'success' => true, 'message' => 'Team entfernt' ]); } } ``` **Route registrieren in:** `custom/Espo/Custom/Resources/metadata/app/api.json` ```json { "routes": [ { "route": "/CEntwicklung/:id/aktiviere-teams", "method": "put", "actionClassName": "Espo\\Custom\\Api\\CEntwicklung\\AktiviereTeams" }, { "route": "/CEntwicklung/:id/abschliessen-fuer-team", "method": "post", "actionClassName": "Espo\\Custom\\Api\\CEntwicklung\\AbschliessenFuerTeam" }, { "route": "/CEntwicklung/:id/add-team", "method": "post", "actionClassName": "Espo\\Custom\\Api\\CEntwicklung\\AddTeam" }, { "route": "/CEntwicklung/:id/remove-team", "method": "post", "actionClassName": "Espo\\Custom\\Api\\CEntwicklung\\RemoveTeam" } ] } ``` --- ## 🔍 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][finalisiert]=false JA (und finalisiert=false) → - Dokument verknüpfen: PUT /api/v1/CDokumente/{id} {"entwicklungId": X} - Entwicklung-Status: PUT /api/v1/CEntwicklung/{id} {"status": "In Verarbeitung", "syncStatus": "unclean"} NEIN (oder finalisiert=true) → - Neue Entwicklung erstellen: POST /api/v1/CEntwicklung { "name": "Entwicklung #N - [Datum]", "parentType": "...", "parentId": "...", "status": "Neu", "syncStatus": "unclean", "finalisiert": false } - 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: Erweiterte Features ### 7.1 Feature: Team-basierte vs. Persönliche Filter **Ziel:** User sollen zwischen "Teams-Posteingang" (alle Entwicklungen des Teams) und "Nur meine" (nur zugewiesene) wechseln können. **Implementation:** **Datei:** `custom/Espo/Custom/Classes/Select/CEntwicklung/PrimaryFilters/NurMeine.php` ```php user->getId(); // Nur Entwicklungen, bei denen User direkt zugewiesen ist $queryBuilder->where([ 'assignedUserId' => $userId ]); } } ``` **Registrierung in selectDefs:** ```json { "primaryFilterClassNameMap": { "meineOffenen": "Espo\\Custom\\Classes\\Select\\CEntwicklung\\PrimaryFilters\\MeineOffenen", "nurMeine": "Espo\\Custom\\Classes\\Select\\CEntwicklung\\PrimaryFilters\\NurMeine" } } ``` **i18n:** ```json { "presetFilters": { "meineOffenen": "Teams-Posteingang", "nurMeine": "Nur meine Entwicklungen" } } ``` **Logik:** - "Teams-Posteingang" (meineOffenen) → Filtert nach Team-Zuordnungen (wie bisher) - "Nur meine" (nurMeine) → Filtert nach assignedUserId = currentUser - Middleware kann bei Zuweisung eines Teams optional einen User als assignedUser setzen --- ### 7.2 Feature: Task/Call Integration mit Status-Flow **Ziel:** KI schlägt Tasks/Calls vor (Status: "vorgeschlagen"), die automatisch genehmigt werden, wenn die Entwicklung als gelesen markiert wird. **Task-Entity erweitern:** **Datei:** `custom/Espo/Custom/Resources/metadata/entityDefs/Task.json` ```json { "fields": { "entwicklung": { "type": "link", "entity": "CEntwicklung", "isCustom": true }, "genehmigungsstatus": { "type": "enum", "options": [ "Vorgeschlagen", "Genehmigt", "Abgelehnt" ], "default": "Vorgeschlagen", "isCustom": true, "style": { "Vorgeschlagen": "warning", "Genehmigt": "success", "Abgelehnt": "danger" } } }, "links": { "entwicklung": { "type": "belongsTo", "entity": "CEntwicklung", "isCustom": true } } } ``` **Hook: Auto-Approve bei Entwicklung abgeschlossen** **Datei:** `custom/Espo/Custom/Hooks/CEntwicklung/AutoApproveTasksOnComplete.php` ```php isAttributeChanged('status')) { return; } $status = $entity->get('status'); if (!in_array($status, ['Bereit', 'In Review'])) { return; } // Finde alle vorgeschlagenen Tasks $tasks = $this->entityManager ->getRDBRepository('Task') ->where([ 'entwicklungId' => $entity->getId(), 'genehmigungsstatus' => 'Vorgeschlagen' ]) ->find(); foreach ($tasks as $task) { $task->set('genehmigungsstatus', 'Genehmigt'); $this->entityManager->saveEntity($task); } if (count($tasks) > 0) { $this->log->info("Auto-approved " . count($tasks) . " tasks for Entwicklung " . $entity->getId()); } } } ``` **Analog für Call-Entity implementieren** --- **Problem: Parent-Hierarchie** Tasks/Calls sind mit CEntwicklung verknüpft (belongsToParent), nicht direkt mit dem übergeordneten Vorgang (CVmhRumungsklage etc.). Das bedeutet: - Task.parent = CEntwicklung - CEntwicklung.parent = CVmhRumungsklage - **Aber:** Task wird NICHT automatisch in CVmhRumungsklage angezeigt **Lösung: Report Panels** Report Panels können über Subqueries arbeiten und Tasks/Calls aus allen zugehörigen Entwicklungen anzeigen. **Datei:** `custom/Espo/Custom/Resources/metadata/entityDefs/CVmhRumungsklage.json` ```json { "fields": { "entwicklungenTasks": { "type": "linkMultiple", "notStorable": true, "readOnly": true, "layoutDetailDisabled": true, "layoutListDisabled": true, "layoutMassUpdateDisabled": true }, "entwicklungenCalls": { "type": "linkMultiple", "notStorable": true, "readOnly": true, "layoutDetailDisabled": true, "layoutListDisabled": true, "layoutMassUpdateDisabled": true } } } ``` **Report Panel Definition:** **Datei:** `custom/Espo/Custom/Resources/metadata/clientDefs/CVmhRumungsklage.json` ```json { "bottomPanels": { "entwicklungenTasks": { "name": "entwicklungenTasks", "label": "Tasks aus Entwicklungen", "view": "views/record/panels/relationship", "recordListView": "views/record/list", "select": false, "create": false, "rowActionsView": "views/record/row-actions/relationship", "filterList": ["open", "completed"], "orderBy": "dateStart", "order": "desc" }, "entwicklungenCalls": { "name": "entwicklungenCalls", "label": "Anrufe aus Entwicklungen", "view": "views/record/panels/relationship", "recordListView": "views/record/list", "select": false, "create": false, "rowActionsView": "views/record/row-actions/relationship", "filterList": ["planned", "held"], "orderBy": "dateStart", "order": "desc" } } } ``` **Custom Select Manager für Tasks-Subquery:** **Datei:** `custom/Espo/Custom/Classes/Select/CVmhRumungsklage/AdditionalAppliers/EntwicklungenTasks.php` ```php where([ 'id=s' => [ 'from' => 'Task', 'select' => ['id'], 'whereClause' => [ 'parentType' => 'CEntwicklung', 'parentId=s' => [ 'from' => 'CEntwicklung', 'select' => ['id'], 'whereClause' => [ 'parentType' => 'CVmhRumungsklage', 'parentId' => '{alias}.id' ] ] ] ] ]); } } ``` **Custom Select Manager für Calls-Subquery:** **Datei:** `custom/Espo/Custom/Classes/Select/CVmhRumungsklage/AdditionalAppliers/EntwicklungenCalls.php` ```php where([ 'id=s' => [ 'from' => 'Call', 'select' => ['id'], 'whereClause' => [ 'parentType' => 'CEntwicklung', 'parentId=s' => [ 'from' => 'CEntwicklung', 'select' => ['id'], 'whereClause' => [ 'parentType' => 'CVmhRumungsklage', 'parentId' => '{alias}.id' ] ] ] ] ]); } } ``` **SelectDefs Registrierung:** **Datei:** `custom/Espo/Custom/Resources/metadata/selectDefs/CVmhRumungsklage.json` ```json { "additionalAppliers": { "entwicklungenTasks": "Espo\\Custom\\Classes\\Select\\CVmhRumungsklage\\AdditionalAppliers\\EntwicklungenTasks", "entwicklungenCalls": "Espo\\Custom\\Classes\\Select\\CVmhRumungsklage\\AdditionalAppliers\\EntwicklungenCalls" } } ``` **i18n:** **Datei:** `custom/Espo/Custom/Resources/i18n/de_DE/CVmhRumungsklage.json` ```json { "links": { "entwicklungenTasks": "Tasks aus Entwicklungen", "entwicklungenCalls": "Anrufe aus Entwicklungen" } } ``` **Analog implementieren für:** - `CMietinkasso` (gleiche Struktur, nur Entity-Name anpassen) - `CKuendigung` (gleiche Struktur, nur Entity-Name anpassen) **Vorteile:** - User sehen alle Tasks/Calls aus Entwicklungen im übergeordneten Vorgang - Filterung möglich (offen/abgeschlossen) - Keine Duplikate durch eindeutige Subquery - Read-Only Panel (keine versehentlichen Änderungen) **Hinweis:** Tasks/Calls werden zweimal angezeigt: 1. Im Standard-Panel (direkt mit Vorgang verknüpft) 2. Im Report-Panel (über Entwicklungen verknüpft) Dies ist beabsichtigt, da beide Verknüpfungsarten parallel existieren können. --- ### 7.3 Feature: KI-User Action Catalog **Ziel:** User können dem KI-User Aufgaben aus einem vordefinierten Katalog zuweisen. Middleware führt diese automatisiert aus. **Entity: CKiAktion** **Datei:** `custom/Espo/Custom/Resources/metadata/entityDefs/CKiAktion.json` ```json { "fields": { "name": { "type": "varchar", "required": true, "isCustom": true }, "entwicklung": { "type": "link", "entity": "CEntwicklung", "required": true, "isCustom": true }, "aktionstyp": { "type": "enum", "options": [ "Dokumente zusammenfassen", "Frist prüfen", "E-Mail-Entwurf erstellen", "Aktenbezug suchen", "Zahlungseingang prüfen", "Mahnung vorbereiten" ], "required": true, "isCustom": true }, "status": { "type": "enum", "options": [ "Wartend", "In Bearbeitung", "Abgeschlossen", "Fehler" ], "default": "Wartend", "isCustom": true, "style": { "Wartend": "default", "In Bearbeitung": "primary", "Abgeschlossen": "success", "Fehler": "danger" } }, "ergebnis": { "type": "text", "isCustom": true }, "fehlerMeldung": { "type": "text", "isCustom": true }, "assignedUser": { "type": "link", "entity": "User", "isCustom": true } }, "links": { "entwicklung": { "type": "belongsTo", "entity": "CEntwicklung" }, "assignedUser": { "type": "belongsTo", "entity": "User" } }, "collection": { "orderBy": "createdAt", "order": "desc" } } ``` **Custom View: Dropdown im Detail-View** **Datei:** `client/custom/src/views/c-entwicklung/record/detail.js` ```javascript define('custom:views/c-entwicklung/record/detail', ['views/record/detail'], function (Dep) { return Dep.extend({ setup: function () { Dep.prototype.setup.call(this); // Prüfe ob KI-User existiert und aktiv this.wait( this.getModelFactory().create('User').then(user => { user.fetch().then(() => { const kiUserId = this.getConfig().get('kiUserId'); if (kiUserId) { this.addKiActionButton(); } }); }) ); }, addKiActionButton: function () { this.addButton({ name: 'kiAktionErstellen', label: 'KI-Aktion zuweisen', style: 'warning', onClick: () => this.showKiActionModal() }); }, showKiActionModal: function () { this.createView('dialog', 'custom:views/modals/ki-aktion-auswahl', { entwicklungId: this.model.id, entwicklungName: this.model.get('name') }, view => { view.render(); }); } }); }); ``` **Modal für Aktions-Auswahl:** **Datei:** `client/custom/src/views/modals/ki-aktion-auswahl.js` ```javascript define('custom:views/modals/ki-aktion-auswahl', ['views/modal', 'model'], function (Dep, Model) { return Dep.extend({ template: 'custom:modals/ki-aktion-auswahl', data: function () { return { entwicklungName: this.options.entwicklungName, aktionstypen: this.getMetadata().get(['entityDefs', 'CKiAktion', 'fields', 'aktionstyp', 'options']) }; }, setup: function () { this.headerText = 'KI-Aktion zuweisen'; this.buttonList = [ { name: 'create', label: 'Erstellen', style: 'primary', onClick: () => this.create() }, { name: 'cancel', label: 'Abbrechen' } ]; }, create: function () { const aktionstyp = this.$el.find('[name="aktionstyp"]').val(); if (!aktionstyp) { Espo.Ui.error('Bitte wählen Sie einen Aktionstyp'); return; } const kiUserId = this.getConfig().get('kiUserId'); this.ajaxPostRequest('CKiAktion', { name: `${aktionstyp} - ${this.options.entwicklungName}`, entwicklungId: this.options.entwicklungId, aktionstyp: aktionstyp, assignedUserId: kiUserId, status: 'Wartend' }).then(() => { Espo.Ui.success('KI-Aktion wurde erstellt'); this.trigger('created'); this.close(); }); } }); }); ``` **Middleware: Task-Executor** ```python # middleware/ki_task_executor.py import time import requests from typing import Dict, Any class KiTaskExecutor: def __init__(self, espocrm_api: EspoCrmApi): self.api = espocrm_api def poll_pending_actions(self): """Pollt alle 30 Sekunden nach wartenden KI-Aktionen""" while True: try: actions = self.api.get('CKiAktion', params={ 'where': [{'type': 'equals', 'attribute': 'status', 'value': 'Wartend'}], 'maxSize': 10 }) for action in actions.get('list', []): self.execute_action(action) except Exception as e: logger.error(f"Error polling KI actions: {e}") time.sleep(30) def execute_action(self, action: Dict[str, Any]): """Führt eine KI-Aktion aus""" action_id = action['id'] aktionstyp = action['aktionstyp'] entwicklung_id = action['entwicklungId'] # Update Status self.api.put(f'CKiAktion/{action_id}', {'status': 'In Bearbeitung'}) try: # Route basierend auf Aktionstyp if aktionstyp == 'Dokumente zusammenfassen': result = self.summarize_documents(entwicklung_id) elif aktionstyp == 'Frist prüfen': result = self.check_deadlines(entwicklung_id) elif aktionstyp == 'E-Mail-Entwurf erstellen': result = self.create_email_draft(entwicklung_id) # ... weitere Aktionstypen else: result = f"Aktionstyp {aktionstyp} noch nicht implementiert" # Update mit Ergebnis self.api.put(f'CKiAktion/{action_id}', { 'status': 'Abgeschlossen', 'ergebnis': result }) except Exception as e: self.api.put(f'CKiAktion/{action_id}', { 'status': 'Fehler', 'fehlerMeldung': str(e) }) ``` --- ### 7.4 UI-Anpassungen für Features **Button: Team hinzufügen/entfernen** ```javascript // client/custom/src/views/c-entwicklung/record/detail.js (erweitern) addTeamManagementButtons: function () { this.addButton({ name: 'addTeam', label: 'Team hinzufügen', style: 'default', onClick: () => this.showAddTeamModal() }); }, showAddTeamModal: function () { // Team-Auswahl Modal this.createView('dialog', 'views/modals/select-records', { scope: 'Team', multiple: false }, view => { view.render(); this.listenToOnce(view, 'select', team => { this.ajaxPostRequest(`CEntwicklung/${this.model.id}/add-team`, { teamId: team.id, prioritaet: 'Normal' }).then(() => { Espo.Ui.success('Team hinzugefügt'); this.model.fetch(); }); }); }); } ``` --- ## 📊 Phase 8: 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: First-Read-Closes Prinzip** 1. Entwicklung mit 2 Teams (Mandatsbetreuung + Anwalt) 2. Mandatsbetreuung schließt ab → finalisiert=true, finalisierungsGrund="Erstes Team" 3. Neues Dokument wird uploaded 4. Middleware erkennt: Entwicklung finalisiert=true 5. Middleware erstellt NEUE Entwicklung für das neue Dokument 6. Validierung: Alte Entwicklung bleibt geschlossen, neue Entwicklung existiert **Szenario 4: Neues Dokument während Review (unfinalisiert)** 1. Entwicklung im Status "Bereit", finalisiert=false 2. Neues Dokument uploaded 3. Middleware setzt syncStatus = "unclean" 4. Abschluss-Button disabled (Formula Script) 5. Middleware re-analysiert 6. syncStatus = "clean", Abschluss wieder möglich **Szenario 5: 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 **Szenario 6: Team-Management durch User** 1. User öffnet Entwicklung 2. Klickt "Team hinzufügen" 3. Wählt Team "Zwangsvollstreckung" 4. Validierung: TeamZuordnung erstellt, aktiv=true 5. Klickt "Team entfernen" für dieses Team 6. Validierung: TeamZuordnung.aktiv=false **Szenario 7: Task-Genehmigung** 1. Middleware erstellt Task mit genehmigungsstatus="Vorgeschlagen" 2. User sieht Task in Entwicklung (gelb markiert) 3. User öffnet Entwicklung → Status wird "In Review" 4. Hook: Alle Tasks mit genehmigungsstatus="Vorgeschlagen" → "Genehmigt" 5. Validierung: Tasks sind grün markiert **Szenario 8: KI-Aktion Katalog** 1. User öffnet Entwicklung 2. Klickt "KI-Aktion zuweisen" 3. Wählt "Dokumente zusammenfassen" 4. CKiAktion erstellt mit status="Wartend" 5. Middleware pollt, findet Aktion 6. Middleware führt aus → status="Abgeschlossen", ergebnis gefüllt 7. User sieht Ergebnis in Stream/Panel **Szenario 9: Report Panel - Tasks aus Entwicklungen** 1. Entwicklung mit parentType="CVmhRumungsklage", parentId=X erstellt 2. Task mit parentType="CEntwicklung", parentId=Y erstellt (vorgeschlagen) 3. User öffnet CVmhRumungsklage (ID=X) 4. Sieht Bottom-Panel "Tasks aus Entwicklungen" 5. Task wird angezeigt trotz parent=CEntwicklung 6. Filter "Offen" zeigt nur nicht-abgeschlossene Tasks 7. Validierung: Subquery funktioniert, Task ist sichtbar --- ### 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) - [ ] KI-User erstellen und ID in Config eintragen - [ ] Test-Entwicklung manuell erstellen - [ ] Filter "Meine offenen" und "Nur meine" testen - [ ] API-Endpoints mit curl/Postman testen (inkl. add-team, remove-team) - [ ] Task mit genehmigungsstatus="Vorgeschlagen" erstellen und Auto-Approve testen - [ ] Report Panels testen: Task zu Entwicklung erstellen → in übergeordnetem Vorgang sichtbar - [ ] Report Panels für alle 3 Parent-Types testen (CVmhRumungsklage, CMietinkasso, CKuendigung) - [ ] KI-Aktion aus Katalog zuweisen und Middleware-Ausführung testen - [ ] First-Read-Closes Prinzip validieren (Block finalisieren, neues Dokument → neuer Block) - [ ] Middleware konfigurieren & starten (inkl. KI-Task-Executor) - [ ] 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 - ✅ First-Read-Closes: Block wird beim ersten Abschluss finalisiert - ✅ Neue Dokumente nach Finalisierung → neuer Block - ✅ KI-Analyse wird angezeigt - ✅ Teams sehen nur relevante Entwicklungen - ✅ Filter "Teams-Posteingang" vs "Nur meine" funktionieren - ✅ Team-Management: Add/Remove durch User möglich - ✅ Task-Genehmigung: Automatisch bei Entwicklung-Review - ✅ Report Panels: Tasks/Calls aus Entwicklungen in übergeordneten Vorgängen sichtbar - ✅ KI-Aktionen: Katalog verfügbar, Middleware führt aus - ✅ 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: UI/UX Verbesserungen - [ ] Kommentare zu Entwicklungen (Stream) - IMPLEMENTIERT - [ ] E-Mail-Benachrichtigungen bei neuen Entwicklungen - [ ] Dashboard-Widget: "Meine offenen Entwicklungen" - [ ] Bulk-Actions: Mehrere Entwicklungen gleichzeitig abschließen - [ ] Drag & Drop für Team-Prioritäten - [ ] Inline-Editing für Team-Zuordnungen ### 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 (inkl. erweiterte Features) **Nächster Schritt:** Phase 1 Implementierung starten **Geschätzte Dauer:** 3-4 Wochen (alle Phasen inkl. erweiterter Features) **Implementierungs-Reihenfolge:** 1. **Woche 1-2:** Basis-System (Phase 1-6) mit First-Read-Closes 2. **Woche 3:** Erweiterte Features (Phase 7.1-7.2: Filter + Team-Management) 3. **Woche 4:** Advanced Features (Phase 7.3-7.4: Task-Flow + KI-Katalog) **Komplexität der neuen Features:** - ⭐ Team-basierte Filter: 1 Tag (EINFACH) - ⭐⭐ Team Add/Remove: 2-3 Tage (MITTEL) - ⭐⭐⭐ Task/Call Integration mit Report Panels: 4-5 Tage (MITTEL-HOCH) - Task-Entity erweitern: 1 Tag - Auto-Approve Hook: 0.5 Tage - Report Panels für 3 Parent-Types: 2-2.5 Tage - Testing: 0.5 Tage - ⭐⭐⭐⭐ KI-User Action Catalog: 5-7 Tage (HOCH) --- *Ende des Entwicklungsplans*