diff --git a/custom/ENTWICKLUNGSPLAN_ENTWICKLUNGEN.md b/custom/ENTWICKLUNGSPLAN_ENTWICKLUNGEN.md index 9f6e2d81..33b52160 100644 --- a/custom/ENTWICKLUNGSPLAN_ENTWICKLUNGEN.md +++ b/custom/ENTWICKLUNGSPLAN_ENTWICKLUNGEN.md @@ -1,8 +1,8 @@ # Entwicklungsplan: Entwicklungen-System (Posteingang mit KI-Analyse) -**Version:** 1.0 -**Datum:** 25. Januar 2026 -**Status:** Spezifikation finalisiert, bereit für Implementierung +**Version:** 2.0 +**Datum:** 11. Februar 2026 +**Status:** Erweiterte Spezifikation mit First-Read-Closes & Advanced Features - Bereit für Implementierung --- @@ -16,6 +16,7 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg 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 @@ -121,6 +122,35 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg "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 @@ -181,6 +211,10 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg "type": "belongsTo", "entity": "User" }, + "finalisiertVon": { + "type": "belongsTo", + "entity": "User" + }, "teams": { "type": "hasMany", "entity": "Team", @@ -205,6 +239,9 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg "syncStatus": { "columns": ["syncStatus"] }, + "finalisiert": { + "columns": ["finalisiert"] + }, "createdAt": { "columns": ["createdAt"] } @@ -492,6 +529,10 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg "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" @@ -504,7 +545,9 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg "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" + "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": { @@ -518,6 +561,11 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg "syncStatus": { "clean": "Aktuell", "unclean": "Ausstehend" + }, + "finalisierungsGrund": { + "Erstes Team": "Erstes Team", + "Manuell": "Manuell", + "Automatisch": "Automatisch" } } } @@ -541,6 +589,10 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg "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" @@ -553,7 +605,9 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg "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" + "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": { @@ -567,6 +621,11 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg "syncStatus": { "clean": "Up-to-date", "unclean": "Pending" + }, + "finalisierungsGrund": { + "Erstes Team": "First Team", + "Manuell": "Manual", + "Automatisch": "Automatic" } } } @@ -584,7 +643,7 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg ```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}" + "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}" } ``` @@ -673,6 +732,10 @@ class UpdateTeamStats implements BeforeSave {"name": "anzahlDokumente"}, {"name": "anzahlTeamsAktiv"} ], + [ + {"name": "finalisiert"}, + {"name": "finalisierungsGrund"} + ], [ {"name": "zusammenfassung", "span": 2} ] @@ -985,6 +1048,18 @@ class AbschliessenFuerTeam implements Action $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') @@ -1009,6 +1084,7 @@ class AbschliessenFuerTeam implements Action return Response::json([ 'success' => true, 'status' => $entwicklung->get('status'), + 'finalisiert' => $entwicklung->get('finalisiert'), 'offeneTeams' => $offeneTeams ]); } @@ -1031,6 +1107,205 @@ class AbschliessenFuerTeam implements Action --- +### 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 @@ -1201,20 +1476,21 @@ POLLING JOB (alle 60 Sekunden): 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 + Query: /api/v1/CEntwicklung?where[0][parentId]={id}&where[1][finalisiert]=false - JA → + JA (und finalisiert=false) → - Dokument verknüpfen: PUT /api/v1/CDokumente/{id} {"entwicklungId": X} - - Entwicklung-Status: PUT /api/v1/CEntwicklung/{id} {"status": "In Verarbeitung"} + - Entwicklung-Status: PUT /api/v1/CEntwicklung/{id} {"status": "In Verarbeitung", "syncStatus": "unclean"} - NEIN → + NEIN (oder finalisiert=true) → - Neue Entwicklung erstellen: POST /api/v1/CEntwicklung { "name": "Entwicklung #N - [Datum]", "parentType": "...", "parentId": "...", "status": "Neu", - "syncStatus": "unclean" + "syncStatus": "unclean", + "finalisiert": false } - Dokument verknüpfen @@ -1298,7 +1574,661 @@ POLLING JOB (alle 5 Minuten): --- -## 📊 Phase 7: Testing & Qualitätssicherung +## ⚡ 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) @@ -1328,15 +2258,23 @@ POLLING JOB (alle 5 Minuten): 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" +**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 +4. Abschluss-Button disabled (Formula Script) 5. Middleware re-analysiert 6. syncStatus = "clean", Abschluss wieder möglich -**Szenario 4: Abwesenheitsvertretung** +**Szenario 5: Abwesenheitsvertretung** 1. User A setzt abwesend = true, vertretung = User B 2. Middleware pollt 3. Räumungsklagen von User A werden umverteilt @@ -1344,6 +2282,39 @@ POLLING JOB (alle 5 Minuten): 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 @@ -1373,10 +2344,16 @@ POLLING JOB (alle 5 Minuten): ### Post-Deployment - [ ] Teams kategorisieren (teamKategorie setzen) +- [ ] KI-User erstellen und ID in Config eintragen - [ ] Test-Entwicklung manuell erstellen -- [ ] Filter "Meine offenen" testen -- [ ] API-Endpoints mit curl/Postman testen -- [ ] Middleware konfigurieren & starten +- [ ] 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 --- @@ -1415,8 +2392,15 @@ POLLING JOB (alle 5 Minuten): **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 @@ -1434,11 +2418,13 @@ POLLING JOB (alle 5 Minuten): ## 🔮 Zukünftige Erweiterungen (Roadmap) -### v2.0: Erweiterte Features -- [ ] Kommentare zu Entwicklungen (Stream) +### 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 @@ -1501,9 +2487,24 @@ POLLING JOB (alle 5 Minuten): --- -**Status:** ✅ Spezifikation vollständig +**Status:** ✅ Spezifikation vollständig (inkl. erweiterte Features) **Nächster Schritt:** Phase 1 Implementierung starten -**Geschätzte Dauer:** 2-3 Wochen (alle Phasen) +**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) --- diff --git a/custom/Espo/Custom/Controllers/CPuls.php b/custom/Espo/Custom/Controllers/CPuls.php new file mode 100644 index 00000000..c513f1a8 --- /dev/null +++ b/custom/Espo/Custom/Controllers/CPuls.php @@ -0,0 +1,7 @@ + false, 'isInstalled' => true, - 'microtimeInternal' => 1769341140.824028, + 'microtimeInternal' => 1770972795.018931, 'cryptKey' => '75886e68937f6ec6e34fabe5603c9f0c', 'hashSecretKey' => '0c7b8cf622d364a26cfe5d31145c8f38', 'defaultPermissions' => [ @@ -39,7 +39,7 @@ return [ 'group' => 'www-data' ], 'actualDatabaseType' => 'mariadb', - 'actualDatabaseVersion' => '12.1.2', + 'actualDatabaseVersion' => '12.2.2', 'instanceId' => '4437546e-79fc-40f6-ac04-448b526c1401', 'webSocketZeroMQSubmissionDsn' => 'tcp://espocrm-websocket:7777', 'webSocketZeroMQSubscriberDsn' => 'tcp://*:7777', diff --git a/data/config.php b/data/config.php index 082a4aa3..1f592181 100644 --- a/data/config.php +++ b/data/config.php @@ -127,7 +127,8 @@ return [ 32 => 'Import', 33 => 'GlobalStream', 34 => 'Report', - 35 => 'CCallQueues' + 35 => 'CCallQueues', + 36 => 'CPuls' ], 'quickCreateList' => [ 0 => 'Account', @@ -357,7 +358,7 @@ return [ 0 => 'youtube.com', 1 => 'google.com' ], - 'microtime' => 1770591071.892102, + 'microtime' => 1770972794.710849, 'siteUrl' => 'https://crm.bitbylaw.com', 'fullTextSearchMinLength' => 4, 'webSocketUrl' => 'ws://api.bitbylaw.com:5000/espocrm/ws', diff --git a/data/state.php b/data/state.php index fa807826..7a31742a 100644 --- a/data/state.php +++ b/data/state.php @@ -1,7 +1,7 @@ 1770591072, - 'microtimeState' => 1770591072.009735, + 'cacheTimestamp' => 1770972795, + 'microtimeState' => 1770972795.029599, 'currencyRates' => [ 'EUR' => 1.0 ],