From e1a963ffab067bd9c86e4ae5606f11b03d455340 Mon Sep 17 00:00:00 2001 From: bsiggel Date: Fri, 13 Feb 2026 10:09:19 +0100 Subject: [PATCH] feat(CPuls): Enhance CPuls entity with new fields, tooltips, and options; add localization for German and English - Added new fields to CPuls entity including status, syncStatus, kiAnalyse, and others. - Implemented localization for CPuls in German (de_DE) and English (en_US). - Introduced new API actions for team activation and completion of CPuls. - Created hooks to update team statistics and manage document counts. - Added new entity definitions and metadata for CPulsTeamZuordnung and Team. - Implemented validation logic in formulas to prevent completion of unclean Puls. - Updated layouts for detail and list views of CPuls. - Enhanced user entity with absence tracking fields. - Added scopes for CPuls and CPulsTeamZuordnung. --- custom/ENTWICKLUNGSPLAN_ENTWICKLUNGEN.md | 504 +++++++++--------- .../Custom/Api/CPuls/AbschliessenFuerTeam.php | 121 +++++ .../Espo/Custom/Api/CPuls/AktiviereTeams.php | 99 ++++ .../Custom/Hooks/CPuls/UpdateTeamStats.php | 46 ++ .../Custom/Resources/i18n/de_DE/CPuls.json | 61 ++- .../i18n/de_DE/CPulsTeamZuordnung.json | 32 ++ .../Custom/Resources/i18n/en_US/CPuls.json | 70 ++- .../layouts/CPuls/bottomPanelsDetail.json | 17 + .../Resources/layouts/CPuls/detail.json | 47 ++ .../Custom/Resources/layouts/CPuls/list.json | 8 + .../Custom/Resources/metadata/app/api.json | 14 + .../Resources/metadata/clientDefs/CPuls.json | 51 +- .../metadata/entityDefs/CDokumente.json | 11 + .../Resources/metadata/entityDefs/CPuls.json | 186 +++++-- .../entityDefs/CPulsTeamZuordnung.json | 100 ++++ .../Resources/metadata/entityDefs/Team.json | 16 + .../Resources/metadata/entityDefs/User.json | 22 + .../Resources/metadata/formula/CPuls.json | 3 + .../Resources/metadata/scopes/CPuls.json | 20 +- .../metadata/scopes/CPulsTeamZuordnung.json | 13 + data/config.php | 2 +- data/state.php | 4 +- 22 files changed, 1073 insertions(+), 374 deletions(-) create mode 100644 custom/Espo/Custom/Api/CPuls/AbschliessenFuerTeam.php create mode 100644 custom/Espo/Custom/Api/CPuls/AktiviereTeams.php create mode 100644 custom/Espo/Custom/Hooks/CPuls/UpdateTeamStats.php create mode 100644 custom/Espo/Custom/Resources/i18n/de_DE/CPulsTeamZuordnung.json create mode 100644 custom/Espo/Custom/Resources/layouts/CPuls/bottomPanelsDetail.json create mode 100644 custom/Espo/Custom/Resources/layouts/CPuls/detail.json create mode 100644 custom/Espo/Custom/Resources/layouts/CPuls/list.json create mode 100644 custom/Espo/Custom/Resources/metadata/app/api.json create mode 100644 custom/Espo/Custom/Resources/metadata/entityDefs/CPulsTeamZuordnung.json create mode 100644 custom/Espo/Custom/Resources/metadata/entityDefs/Team.json create mode 100644 custom/Espo/Custom/Resources/metadata/formula/CPuls.json create mode 100644 custom/Espo/Custom/Resources/metadata/scopes/CPulsTeamZuordnung.json diff --git a/custom/ENTWICKLUNGSPLAN_ENTWICKLUNGEN.md b/custom/ENTWICKLUNGSPLAN_ENTWICKLUNGEN.md index 33b52160..6cb59cd6 100644 --- a/custom/ENTWICKLUNGSPLAN_ENTWICKLUNGEN.md +++ b/custom/ENTWICKLUNGSPLAN_ENTWICKLUNGEN.md @@ -1,4 +1,4 @@ -# Entwicklungsplan: Entwicklungen-System (Posteingang mit KI-Analyse) +# Pulssplan: Puls-System (Posteingang mit KI-Analyse) **Version:** 2.0 **Datum:** 11. Februar 2026 @@ -16,7 +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. +5. **First-Read-Closes** - Sobald ein Team eine Puls 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 @@ -34,7 +34,7 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg ┌─────────────────────────────────────────────────────────────────┐ │ ESPOCRM │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ CEntwicklung │←→│ CEntwicklung │←→│ CDokumente │ │ +│ │ CPuls │←→│ CPuls │←→│ CDokumente │ │ │ │ │ │ TeamZuordnung│ │ │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ ↑ ↑ ↑ │ @@ -47,11 +47,11 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg ## 🎯 Phase 1: Entities & Datenmodell (MVP) -### 1.1 Entity: CEntwicklung +### 1.1 Entity: CPuls **Zweck:** Gruppierung von Dokumenten mit KI-Analyse und Status-Tracking -**Datei:** `custom/Espo/Custom/Resources/metadata/entityDefs/CEntwicklung.json` +**Datei:** `custom/Espo/Custom/Resources/metadata/entityDefs/CPuls.json` ```json { @@ -192,12 +192,12 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg "dokumente": { "type": "hasMany", "entity": "CDokumente", - "foreign": "entwicklung" + "foreign": "puls" }, "teamZuordnungen": { "type": "hasMany", - "entity": "CEntwicklungTeamZuordnung", - "foreign": "entwicklung" + "entity": "CPulsTeamZuordnung", + "foreign": "puls" }, "createdBy": { "type": "belongsTo", @@ -251,11 +251,11 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg --- -### 1.2 Entity: CEntwicklungTeamZuordnung +### 1.2 Entity: CPulsTeamZuordnung **Zweck:** Junction Table für dynamische Team-Zuordnung mit Abschluss-Tracking -**Datei:** `custom/Espo/Custom/Resources/metadata/entityDefs/CEntwicklungTeamZuordnung.json` +**Datei:** `custom/Espo/Custom/Resources/metadata/entityDefs/CPulsTeamZuordnung.json` ```json { @@ -264,7 +264,7 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg "type": "varchar", "notStorable": true, "select": { - "select": "CONCAT:(team.name, ' - ', entwicklung.name)" + "select": "CONCAT:(team.name, ' - ', puls.name)" }, "orderBy": { "order": [ @@ -272,9 +272,9 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg ] } }, - "entwicklung": { + "puls": { "type": "link", - "entity": "CEntwicklung", + "entity": "CPuls", "required": true, "isCustom": true }, @@ -328,9 +328,9 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg }, "links": { - "entwicklung": { + "puls": { "type": "belongsTo", - "entity": "CEntwicklung", + "entity": "CPuls", "foreign": "teamZuordnungen" }, "team": { @@ -349,8 +349,8 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg }, "indexes": { - "entwicklungTeam": { - "columns": ["entwicklungId", "teamId"], + "pulsTeam": { + "columns": ["pulsId", "teamId"], "unique": true }, "aktiv": { @@ -439,24 +439,24 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg ### 1.5 CDokumente erweitern -**Zweck:** Verknüpfung zu Entwicklungen +**Zweck:** Verknüpfung zu Pulsen **Datei:** `custom/Espo/Custom/Resources/metadata/entityDefs/CDokumente.json` ```json { "fields": { - "entwicklung": { + "puls": { "type": "link", - "entity": "CEntwicklung", + "entity": "CPuls", "isCustom": true } }, "links": { - "entwicklung": { + "puls": { "type": "belongsTo", - "entity": "CEntwicklung", + "entity": "CPuls", "foreign": "dokumente", "isCustom": true } @@ -468,7 +468,7 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg ### 1.6 Scopes definieren -**Datei:** `custom/Espo/Custom/Resources/metadata/scopes/CEntwicklung.json` +**Datei:** `custom/Espo/Custom/Resources/metadata/scopes/CPuls.json` ```json { @@ -489,7 +489,7 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg } ``` -**Datei:** `custom/Espo/Custom/Resources/metadata/scopes/CEntwicklungTeamZuordnung.json` +**Datei:** `custom/Espo/Custom/Resources/metadata/scopes/CPulsTeamZuordnung.json` ```json { @@ -511,13 +511,13 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg ### 1.7 Internationalisierung (i18n) -**Datei:** `custom/Espo/Custom/Resources/i18n/de_DE/CEntwicklung.json` +**Datei:** `custom/Espo/Custom/Resources/i18n/de_DE/CPuls.json` ```json { "labels": { - "Create CEntwicklung": "Entwicklung erstellen", - "CEntwicklung": "Entwicklung", + "Create CPuls": "Puls erstellen", + "CPuls": "Entwicklung", "cEntwicklungs": "Entwicklungen" }, "fields": { @@ -571,13 +571,13 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg } ``` -**Datei:** `custom/Espo/Custom/Resources/i18n/en_US/CEntwicklung.json` +**Datei:** `custom/Espo/Custom/Resources/i18n/en_US/CPuls.json` ```json { "labels": { - "Create CEntwicklung": "Create Development", - "CEntwicklung": "Development", + "Create CPuls": "Create Development", + "CPuls": "Development", "cEntwicklungs": "Developments" }, "fields": { @@ -631,7 +631,7 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg } ``` -**Analog für CEntwicklungTeamZuordnung, Team.teamKategorie, User.abwesend** +**Analog für CPulsTeamZuordnung, Team.teamKategorie, User.abwesend** --- @@ -639,16 +639,16 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg ### 2.1 Formula-Script: Abschluss nur bei clean -**Datei:** `custom/Espo/Custom/Resources/metadata/formula/CEntwicklung.json` +**Datei:** `custom/Espo/Custom/Resources/metadata/formula/CPuls.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}" + "beforeSaveApiScript": "// Verhindere Abschluss bei unclean Status\nif (\n (status == 'Abgeschlossen' || entity\\isAttributeChanged('status'))\n && syncStatus == 'unclean'\n) {\n recordService\\throwBadRequest('Puls kann nicht abgeschlossen werden: Neue Dokumente vorhanden (Status: unclean). Bitte warten Sie auf die KI-Analyse.');\n}\n\n// Verhindere Änderungen an finalisierter Puls\nif (\n finalisiert == true\n && entity\\isAttributeChanged('finalisiert') == false\n && (entity\\isAttributeChanged('status') || entity\\isAttributeChanged('syncStatus'))\n) {\n recordService\\throwBadRequest('Puls ist finalisiert. Neue Dokumente erzeugen automatisch einen neuen Block.');\n}" } ``` **Test-Szenario:** -1. Entwicklung mit syncStatus = "unclean" +1. Puls mit syncStatus = "unclean" 2. User versucht manuell status = "Abgeschlossen" zu setzen 3. Erwartung: Error-Message, Speichern verhindert @@ -656,11 +656,11 @@ Implementierung eines intelligenten Posteingangs-Systems für Dokumente zu Vorg ### 2.2 Hook: Berechnete Felder aktualisieren -**Datei:** `custom/Espo/Custom/Hooks/CEntwicklung/UpdateTeamStats.php` +**Datei:** `custom/Espo/Custom/Hooks/CPuls/UpdateTeamStats.php` ```php isNew() || $entity->isAttributeChanged('id')) { $dokumenteCount = $this->entityManager ->getRDBRepository('CDokumente') - ->where(['entwicklungId' => $entity->getId()]) + ->where(['pulsId' => $entity->getId()]) ->count(); $entity->set('anzahlDokumente', $dokumenteCount); @@ -685,8 +685,8 @@ class UpdateTeamStats implements BeforeSave // Zähle Team-Zuordnungen $zuordnungen = $this->entityManager - ->getRDBRepository('CEntwicklungTeamZuordnung') - ->where(['entwicklungId' => $entity->getId()]) + ->getRDBRepository('CPulsTeamZuordnung') + ->where(['pulsId' => $entity->getId()]) ->find(); $aktiv = 0; @@ -713,7 +713,7 @@ class UpdateTeamStats implements BeforeSave ### 3.1 Detail-Layout -**Datei:** `custom/Espo/Custom/Resources/layouts/CEntwicklung/detail.json` +**Datei:** `custom/Espo/Custom/Resources/layouts/CPuls/detail.json` ```json [ @@ -769,7 +769,7 @@ class UpdateTeamStats implements BeforeSave ### 3.2 List-Layout -**Datei:** `custom/Espo/Custom/Resources/layouts/CEntwicklung/list.json` +**Datei:** `custom/Espo/Custom/Resources/layouts/CPuls/list.json` ```json [ @@ -786,7 +786,7 @@ class UpdateTeamStats implements BeforeSave ### 3.3 Bottom-Panels -**Datei:** `custom/Espo/Custom/Resources/layouts/CEntwicklung/bottomPanelsDetail.json` +**Datei:** `custom/Espo/Custom/Resources/layouts/CPuls/bottomPanelsDetail.json` ```json { @@ -812,7 +812,7 @@ class UpdateTeamStats implements BeforeSave ### 3.4 ClientDefs -**Datei:** `custom/Espo/Custom/Resources/metadata/clientDefs/CEntwicklung.json` +**Datei:** `custom/Espo/Custom/Resources/metadata/clientDefs/CPuls.json` ```json { @@ -841,11 +841,11 @@ class UpdateTeamStats implements BeforeSave ### 4.1 API: Team-Aktivierung -**Datei:** `custom/Espo/Custom/Api/CEntwicklung/AktiviereTeams.php` +**Datei:** `custom/Espo/Custom/Api/CPuls/AktiviereTeams.php` ```php entityManager->getEntity('CEntwicklung', $id); + $puls = $this->entityManager->getEntity('CPuls', $id); - if (!$entwicklung) { - throw new NotFound('Entwicklung nicht gefunden'); + if (!$puls) { + throw new NotFound('Puls nicht gefunden'); } $data = $request->getParsedBody(); - // 1. Update Entwicklung - $entwicklung->set([ + // 1. Update Puls + $puls->set([ 'kiAnalyse' => $data->kiAnalyse ?? null, 'zusammenfassung' => $data->zusammenfassung ?? null, 'status' => $data->status ?? 'Bereit', 'syncStatus' => $data->syncStatus ?? 'clean' ]); - $this->entityManager->saveEntity($entwicklung); + $this->entityManager->saveEntity($puls); // 2. Lösche alte Zuordnungen (soft delete - setze inaktiv) $this->entityManager ->getQueryBuilder() ->update() - ->in('CEntwicklungTeamZuordnung') + ->in('CPulsTeamZuordnung') ->set(['aktiv' => false]) - ->where(['entwicklungId' => $id]) + ->where(['pulsId' => $id]) ->execute(); // 3. Erstelle neue Zuordnungen @@ -907,9 +907,9 @@ class AktiviereTeams implements Action // Prüfe ob bereits existiert $existing = $this->entityManager - ->getRDBRepository('CEntwicklungTeamZuordnung') + ->getRDBRepository('CPulsTeamZuordnung') ->where([ - 'entwicklungId' => $id, + 'pulsId' => $id, 'teamId' => $teamId ]) ->findOne(); @@ -924,8 +924,8 @@ class AktiviereTeams implements Action $this->entityManager->saveEntity($existing); } else { // Erstelle neu - $zuordnung = $this->entityManager->createEntity('CEntwicklungTeamZuordnung', [ - 'entwicklungId' => $id, + $zuordnung = $this->entityManager->createEntity('CPulsTeamZuordnung', [ + 'pulsId' => $id, 'teamId' => $teamId, 'aktiv' => true, 'abgeschlossen' => false, @@ -935,11 +935,11 @@ class AktiviereTeams implements Action } } - $this->log->info("Teams aktiviert für Entwicklung {$id}"); + $this->log->info("Teams aktiviert für Puls {$id}"); return Response::json([ 'success' => true, - 'entwicklungId' => $id + 'pulsId' => $id ]); } } @@ -953,9 +953,9 @@ class AktiviereTeams implements Action { "routes": [ { - "route": "/CEntwicklung/:id/aktiviere-teams", + "route": "/CPuls/:id/aktiviere-teams", "method": "put", - "actionClassName": "Espo\\Custom\\Api\\CEntwicklung\\AktiviereTeams" + "actionClassName": "Espo\\Custom\\Api\\CPuls\\AktiviereTeams" } ] } @@ -965,11 +965,11 @@ class AktiviereTeams implements Action ### 4.2 API: Abschluss für Team -**Datei:** `custom/Espo/Custom/Api/CEntwicklung/AbschliessenFuerTeam.php` +**Datei:** `custom/Espo/Custom/Api/CPuls/AbschliessenFuerTeam.php` ```php getRouteParam('id'); + $pulsId = $request->getRouteParam('id'); $data = $request->getParsedBody(); $teamId = $data->teamId ?? null; - if (!$entwicklungId || !$teamId) { - throw new BadRequest('entwicklungId oder teamId fehlt'); + if (!$pulsId || !$teamId) { + throw new BadRequest('pulsId oder teamId fehlt'); } // 1. Validierung: Ist User in diesem Team? @@ -1004,23 +1004,23 @@ class AbschliessenFuerTeam implements Action throw new Forbidden('User nicht in angegebenem Team'); } - // 2. Lade Entwicklung - $entwicklung = $this->entityManager->getEntity('CEntwicklung', $entwicklungId); + // 2. Lade Puls + $puls = $this->entityManager->getEntity('CPuls', $pulsId); - if (!$entwicklung) { - throw new NotFound('Entwicklung nicht gefunden'); + if (!$puls) { + throw new NotFound('Puls 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'); + if ($puls->get('syncStatus') !== 'clean') { + throw new BadRequest('Puls hat neue Dokumente (unclean) - bitte warten Sie auf die KI-Analyse'); } // 4. Finde Zuordnung $zuordnung = $this->entityManager - ->getRDBRepository('CEntwicklungTeamZuordnung') + ->getRDBRepository('CPulsTeamZuordnung') ->where([ - 'entwicklungId' => $entwicklungId, + 'pulsId' => $pulsId, 'teamId' => $teamId, 'aktiv' => true ]) @@ -1049,8 +1049,8 @@ class AbschliessenFuerTeam implements Action $this->entityManager->saveEntity($zuordnung); // 6.5. FIRST-READ-CLOSES: Finalisiere Block bei erstem Abschluss - if (!$entwicklung->get('finalisiert')) { - $entwicklung->set([ + if (!$puls->get('finalisiert')) { + $puls->set([ 'finalisiert' => true, 'finalisierungsGrund' => 'Erstes Team', 'finalisiertAm' => date('Y-m-d H:i:s'), @@ -1062,29 +1062,29 @@ class AbschliessenFuerTeam implements Action // 7. Prüfe: Alle Teams abgeschlossen? $offeneTeams = $this->entityManager - ->getRDBRepository('CEntwicklungTeamZuordnung') + ->getRDBRepository('CPulsTeamZuordnung') ->where([ - 'entwicklungId' => $entwicklungId, + 'pulsId' => $pulsId, 'aktiv' => true, 'abgeschlossen' => false ]) ->count(); - // 8. Update Entwicklung-Status + // 8. Update Puls-Status if ($offeneTeams === 0) { - $entwicklung->set('status', 'Abgeschlossen'); + $puls->set('status', 'Abgeschlossen'); } else { - $entwicklung->set('status', 'Teilweise abgeschlossen'); + $puls->set('status', 'Teilweise abgeschlossen'); } - $this->entityManager->saveEntity($entwicklung); + $this->entityManager->saveEntity($puls); - $this->log->info("Team {$teamId} hat Entwicklung {$entwicklungId} abgeschlossen"); + $this->log->info("Team {$teamId} hat Puls {$pulsId} abgeschlossen"); return Response::json([ 'success' => true, - 'status' => $entwicklung->get('status'), - 'finalisiert' => $entwicklung->get('finalisiert'), + 'status' => $puls->get('status'), + 'finalisiert' => $puls->get('finalisiert'), 'offeneTeams' => $offeneTeams ]); } @@ -1097,9 +1097,9 @@ class AbschliessenFuerTeam implements Action { "routes": [ { - "route": "/CEntwicklung/:id/abschliessen-fuer-team", + "route": "/CPuls/:id/abschliessen-fuer-team", "method": "post", - "actionClassName": "Espo\\Custom\\Api\\CEntwicklung\\AbschliessenFuerTeam" + "actionClassName": "Espo\\Custom\\Api\\CPuls\\AbschliessenFuerTeam" } ] } @@ -1107,13 +1107,13 @@ class AbschliessenFuerTeam implements Action --- -### 4.3 API: Team zu Entwicklung hinzufügen +### 4.3 API: Team zu Puls hinzufügen -**Datei:** `custom/Espo/Custom/Api/CEntwicklung/AddTeam.php` +**Datei:** `custom/Espo/Custom/Api/CPuls/AddTeam.php` ```php getRouteParam('id'); + $pulsId = $request->getRouteParam('id'); $data = $request->getParsedBody(); $teamId = $data->teamId ?? null; $prioritaet = $data->prioritaet ?? 'Normal'; - if (!$entwicklungId || !$teamId) { - throw new BadRequest('entwicklungId oder teamId fehlt'); + if (!$pulsId || !$teamId) { + throw new BadRequest('pulsId oder teamId fehlt'); } - $entwicklung = $this->entityManager->getEntity('CEntwicklung', $entwicklungId); + $puls = $this->entityManager->getEntity('CPuls', $pulsId); - if (!$entwicklung) { - throw new NotFound('Entwicklung nicht gefunden'); + if (!$puls) { + throw new NotFound('Puls nicht gefunden'); } // Prüfe ob Team existiert @@ -1155,9 +1155,9 @@ class AddTeam implements Action // Prüfe ob bereits existiert $existing = $this->entityManager - ->getRDBRepository('CEntwicklungTeamZuordnung') + ->getRDBRepository('CPulsTeamZuordnung') ->where([ - 'entwicklungId' => $entwicklungId, + 'pulsId' => $pulsId, 'teamId' => $teamId ]) ->findOne(); @@ -1172,7 +1172,7 @@ class AddTeam implements Action ]); $this->entityManager->saveEntity($existing); - $this->log->info("Team {$teamId} reaktiviert für Entwicklung {$entwicklungId}"); + $this->log->info("Team {$teamId} reaktiviert für Puls {$pulsId}"); return Response::json([ 'success' => true, @@ -1189,15 +1189,15 @@ class AddTeam implements Action } // Erstelle neue Zuordnung - $zuordnung = $this->entityManager->createEntity('CEntwicklungTeamZuordnung', [ - 'entwicklungId' => $entwicklungId, + $zuordnung = $this->entityManager->createEntity('CPulsTeamZuordnung', [ + 'pulsId' => $pulsId, 'teamId' => $teamId, 'aktiv' => true, 'abgeschlossen' => false, 'prioritaet' => $prioritaet ]); - $this->log->info("Team {$teamId} hinzugefügt zu Entwicklung {$entwicklungId} durch User {$this->user->getId()}"); + $this->log->info("Team {$teamId} hinzugefügt zu Puls {$pulsId} durch User {$this->user->getId()}"); return Response::json([ 'success' => true, @@ -1210,13 +1210,13 @@ class AddTeam implements Action --- -### 4.4 API: Team von Entwicklung entfernen +### 4.4 API: Team von Puls entfernen -**Datei:** `custom/Espo/Custom/Api/CEntwicklung/RemoveTeam.php` +**Datei:** `custom/Espo/Custom/Api/CPuls/RemoveTeam.php` ```php getRouteParam('id'); + $pulsId = $request->getRouteParam('id'); $data = $request->getParsedBody(); $teamId = $data->teamId ?? null; - if (!$entwicklungId || !$teamId) { - throw new BadRequest('entwicklungId oder teamId fehlt'); + if (!$pulsId || !$teamId) { + throw new BadRequest('pulsId oder teamId fehlt'); } - $entwicklung = $this->entityManager->getEntity('CEntwicklung', $entwicklungId); + $puls = $this->entityManager->getEntity('CPuls', $pulsId); - if (!$entwicklung) { - throw new NotFound('Entwicklung nicht gefunden'); + if (!$puls) { + throw new NotFound('Puls nicht gefunden'); } // Finde Zuordnung $zuordnung = $this->entityManager - ->getRDBRepository('CEntwicklungTeamZuordnung') + ->getRDBRepository('CPulsTeamZuordnung') ->where([ - 'entwicklungId' => $entwicklungId, + 'pulsId' => $pulsId, 'teamId' => $teamId ]) ->findOne(); @@ -1265,7 +1265,7 @@ class RemoveTeam implements Action $zuordnung->set('aktiv', false); $this->entityManager->saveEntity($zuordnung); - $this->log->info("Team {$teamId} entfernt von Entwicklung {$entwicklungId} durch User {$this->user->getId()}"); + $this->log->info("Team {$teamId} entfernt von Puls {$pulsId} durch User {$this->user->getId()}"); return Response::json([ 'success' => true, @@ -1281,24 +1281,24 @@ class RemoveTeam implements Action { "routes": [ { - "route": "/CEntwicklung/:id/aktiviere-teams", + "route": "/CPuls/:id/aktiviere-teams", "method": "put", - "actionClassName": "Espo\\Custom\\Api\\CEntwicklung\\AktiviereTeams" + "actionClassName": "Espo\\Custom\\Api\\CPuls\\AktiviereTeams" }, { - "route": "/CEntwicklung/:id/abschliessen-fuer-team", + "route": "/CPuls/:id/abschliessen-fuer-team", "method": "post", - "actionClassName": "Espo\\Custom\\Api\\CEntwicklung\\AbschliessenFuerTeam" + "actionClassName": "Espo\\Custom\\Api\\CPuls\\AbschliessenFuerTeam" }, { - "route": "/CEntwicklung/:id/add-team", + "route": "/CPuls/:id/add-team", "method": "post", - "actionClassName": "Espo\\Custom\\Api\\CEntwicklung\\AddTeam" + "actionClassName": "Espo\\Custom\\Api\\CPuls\\AddTeam" }, { - "route": "/CEntwicklung/:id/remove-team", + "route": "/CPuls/:id/remove-team", "method": "post", - "actionClassName": "Espo\\Custom\\Api\\CEntwicklung\\RemoveTeam" + "actionClassName": "Espo\\Custom\\Api\\CPuls\\RemoveTeam" } ] } @@ -1308,13 +1308,13 @@ class RemoveTeam implements Action ## 🔍 Phase 5: Custom Primary Filter -### 5.1 Filter: Meine offenen Entwicklungen +### 5.1 Filter: Meine offenen Pulsen -**Datei:** `custom/Espo/Custom/Classes/Select/CEntwicklung/PrimaryFilters/MeineOffenen.php` +**Datei:** `custom/Espo/Custom/Classes/Select/CPuls/PrimaryFilters/MeineOffenen.php` ```php user->getId(); - // Nur Entwicklungen, bei denen User direkt zugewiesen ist + // Nur Pulsen, bei denen User direkt zugewiesen ist $queryBuilder->where([ 'assignedUserId' => $userId ]); @@ -1615,8 +1615,8 @@ class NurMeine implements Filter ```json { "primaryFilterClassNameMap": { - "meineOffenen": "Espo\\Custom\\Classes\\Select\\CEntwicklung\\PrimaryFilters\\MeineOffenen", - "nurMeine": "Espo\\Custom\\Classes\\Select\\CEntwicklung\\PrimaryFilters\\NurMeine" + "meineOffenen": "Espo\\Custom\\Classes\\Select\\CPuls\\PrimaryFilters\\MeineOffenen", + "nurMeine": "Espo\\Custom\\Classes\\Select\\CPuls\\PrimaryFilters\\NurMeine" } } ``` @@ -1627,7 +1627,7 @@ class NurMeine implements Filter { "presetFilters": { "meineOffenen": "Teams-Posteingang", - "nurMeine": "Nur meine Entwicklungen" + "nurMeine": "Nur meine Pulsen" } } ``` @@ -1641,7 +1641,7 @@ class NurMeine implements Filter ### 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. +**Ziel:** KI schlägt Tasks/Calls vor (Status: "vorgeschlagen"), die automatisch genehmigt werden, wenn die Puls als gelesen markiert wird. **Task-Entity erweitern:** @@ -1650,9 +1650,9 @@ class NurMeine implements Filter ```json { "fields": { - "entwicklung": { + "puls": { "type": "link", - "entity": "CEntwicklung", + "entity": "CPuls", "isCustom": true }, "genehmigungsstatus": { @@ -1672,22 +1672,22 @@ class NurMeine implements Filter } }, "links": { - "entwicklung": { + "puls": { "type": "belongsTo", - "entity": "CEntwicklung", + "entity": "CPuls", "isCustom": true } } } ``` -**Hook: Auto-Approve bei Entwicklung abgeschlossen** +**Hook: Auto-Approve bei Puls abgeschlossen** -**Datei:** `custom/Espo/Custom/Hooks/CEntwicklung/AutoApproveTasksOnComplete.php` +**Datei:** `custom/Espo/Custom/Hooks/CPuls/AutoApproveTasksOnComplete.php` ```php entityManager ->getRDBRepository('Task') ->where([ - 'entwicklungId' => $entity->getId(), + 'pulsId' => $entity->getId(), 'genehmigungsstatus' => 'Vorgeschlagen' ]) ->find(); @@ -1727,7 +1727,7 @@ class AutoApproveTasksOnComplete implements AfterSave } if (count($tasks) > 0) { - $this->log->info("Auto-approved " . count($tasks) . " tasks for Entwicklung " . $entity->getId()); + $this->log->info("Auto-approved " . count($tasks) . " tasks for Puls " . $entity->getId()); } } } @@ -1739,21 +1739,21 @@ class AutoApproveTasksOnComplete implements AfterSave **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 +Tasks/Calls sind mit CPuls verknüpft (belongsToParent), nicht direkt mit dem übergeordneten Vorgang (CVmhRumungsklage etc.). Das bedeutet: +- Task.parent = CPuls +- CPuls.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. +Report Panels können über Subqueries arbeiten und Tasks/Calls aus allen zugehörigen Pulsen anzeigen. **Datei:** `custom/Espo/Custom/Resources/metadata/entityDefs/CVmhRumungsklage.json` ```json { "fields": { - "entwicklungenTasks": { + "pulseTasks": { "type": "linkMultiple", "notStorable": true, "readOnly": true, @@ -1761,7 +1761,7 @@ Report Panels können über Subqueries arbeiten und Tasks/Calls aus allen zugeh "layoutListDisabled": true, "layoutMassUpdateDisabled": true }, - "entwicklungenCalls": { + "pulseCalls": { "type": "linkMultiple", "notStorable": true, "readOnly": true, @@ -1780,9 +1780,9 @@ Report Panels können über Subqueries arbeiten und Tasks/Calls aus allen zugeh ```json { "bottomPanels": { - "entwicklungenTasks": { - "name": "entwicklungenTasks", - "label": "Tasks aus Entwicklungen", + "pulseTasks": { + "name": "pulseTasks", + "label": "Tasks aus Pulsen", "view": "views/record/panels/relationship", "recordListView": "views/record/list", "select": false, @@ -1792,9 +1792,9 @@ Report Panels können über Subqueries arbeiten und Tasks/Calls aus allen zugeh "orderBy": "dateStart", "order": "desc" }, - "entwicklungenCalls": { - "name": "entwicklungenCalls", - "label": "Anrufe aus Entwicklungen", + "pulseCalls": { + "name": "pulseCalls", + "label": "Anrufe aus Pulsen", "view": "views/record/panels/relationship", "recordListView": "views/record/list", "select": false, @@ -1810,7 +1810,7 @@ Report Panels können über Subqueries arbeiten und Tasks/Calls aus allen zugeh **Custom Select Manager für Tasks-Subquery:** -**Datei:** `custom/Espo/Custom/Classes/Select/CVmhRumungsklage/AdditionalAppliers/EntwicklungenTasks.php` +**Datei:** `custom/Espo/Custom/Classes/Select/CVmhRumungsklage/AdditionalAppliers/PulseTasks.php` ```php where([ 'id=s' => [ 'from' => 'Task', 'select' => ['id'], 'whereClause' => [ - 'parentType' => 'CEntwicklung', + 'parentType' => 'CPuls', 'parentId=s' => [ - 'from' => 'CEntwicklung', + 'from' => 'CPuls', 'select' => ['id'], 'whereClause' => [ 'parentType' => 'CVmhRumungsklage', @@ -1851,7 +1851,7 @@ class EntwicklungenTasks implements AdditionalApplier **Custom Select Manager für Calls-Subquery:** -**Datei:** `custom/Espo/Custom/Classes/Select/CVmhRumungsklage/AdditionalAppliers/EntwicklungenCalls.php` +**Datei:** `custom/Espo/Custom/Classes/Select/CVmhRumungsklage/AdditionalAppliers/PulseCalls.php` ```php where([ 'id=s' => [ 'from' => 'Call', 'select' => ['id'], 'whereClause' => [ - 'parentType' => 'CEntwicklung', + 'parentType' => 'CPuls', 'parentId=s' => [ - 'from' => 'CEntwicklung', + 'from' => 'CPuls', 'select' => ['id'], 'whereClause' => [ 'parentType' => 'CVmhRumungsklage', @@ -1897,8 +1897,8 @@ class EntwicklungenCalls implements AdditionalApplier ```json { "additionalAppliers": { - "entwicklungenTasks": "Espo\\Custom\\Classes\\Select\\CVmhRumungsklage\\AdditionalAppliers\\EntwicklungenTasks", - "entwicklungenCalls": "Espo\\Custom\\Classes\\Select\\CVmhRumungsklage\\AdditionalAppliers\\EntwicklungenCalls" + "pulseTasks": "Espo\\Custom\\Classes\\Select\\CVmhRumungsklage\\AdditionalAppliers\\PulseTasks", + "pulseCalls": "Espo\\Custom\\Classes\\Select\\CVmhRumungsklage\\AdditionalAppliers\\PulseCalls" } } ``` @@ -1910,8 +1910,8 @@ class EntwicklungenCalls implements AdditionalApplier ```json { "links": { - "entwicklungenTasks": "Tasks aus Entwicklungen", - "entwicklungenCalls": "Anrufe aus Entwicklungen" + "pulseTasks": "Tasks aus Pulsen", + "pulseCalls": "Anrufe aus Pulsen" } } ``` @@ -1921,14 +1921,14 @@ class EntwicklungenCalls implements AdditionalApplier - `CKuendigung` (gleiche Struktur, nur Entity-Name anpassen) **Vorteile:** -- User sehen alle Tasks/Calls aus Entwicklungen im übergeordneten Vorgang +- User sehen alle Tasks/Calls aus Pulsen 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) +2. Im Report-Panel (über Pulsen verknüpft) Dies ist beabsichtigt, da beide Verknüpfungsarten parallel existieren können. @@ -1950,9 +1950,9 @@ Dies ist beabsichtigt, da beide Verknüpfungsarten parallel existieren können. "required": true, "isCustom": true }, - "entwicklung": { + "puls": { "type": "link", - "entity": "CEntwicklung", + "entity": "CPuls", "required": true, "isCustom": true }, @@ -2001,9 +2001,9 @@ Dies ist beabsichtigt, da beide Verknüpfungsarten parallel existieren können. } }, "links": { - "entwicklung": { + "puls": { "type": "belongsTo", - "entity": "CEntwicklung" + "entity": "CPuls" }, "assignedUser": { "type": "belongsTo", @@ -2019,10 +2019,10 @@ Dies ist beabsichtigt, da beide Verknüpfungsarten parallel existieren können. **Custom View: Dropdown im Detail-View** -**Datei:** `client/custom/src/views/c-entwicklung/record/detail.js` +**Datei:** `client/custom/src/views/c-puls/record/detail.js` ```javascript -define('custom:views/c-entwicklung/record/detail', ['views/record/detail'], function (Dep) { +define('custom:views/c-puls/record/detail', ['views/record/detail'], function (Dep) { return Dep.extend({ setup: function () { @@ -2053,8 +2053,8 @@ define('custom:views/c-entwicklung/record/detail', ['views/record/detail'], func showKiActionModal: function () { this.createView('dialog', 'custom:views/modals/ki-aktion-auswahl', { - entwicklungId: this.model.id, - entwicklungName: this.model.get('name') + pulsId: this.model.id, + pulsName: this.model.get('name') }, view => { view.render(); }); @@ -2075,7 +2075,7 @@ define('custom:views/modals/ki-aktion-auswahl', ['views/modal', 'model'], functi data: function () { return { - entwicklungName: this.options.entwicklungName, + pulsName: this.options.pulsName, aktionstypen: this.getMetadata().get(['entityDefs', 'CKiAktion', 'fields', 'aktionstyp', 'options']) }; }, @@ -2108,8 +2108,8 @@ define('custom:views/modals/ki-aktion-auswahl', ['views/modal', 'model'], functi const kiUserId = this.getConfig().get('kiUserId'); this.ajaxPostRequest('CKiAktion', { - name: `${aktionstyp} - ${this.options.entwicklungName}`, - entwicklungId: this.options.entwicklungId, + name: `${aktionstyp} - ${this.options.pulsName}`, + pulsId: this.options.pulsId, aktionstyp: aktionstyp, assignedUserId: kiUserId, status: 'Wartend' @@ -2157,7 +2157,7 @@ class KiTaskExecutor: """Führt eine KI-Aktion aus""" action_id = action['id'] aktionstyp = action['aktionstyp'] - entwicklung_id = action['entwicklungId'] + puls_id = action['pulsId'] # Update Status self.api.put(f'CKiAktion/{action_id}', {'status': 'In Bearbeitung'}) @@ -2165,11 +2165,11 @@ class KiTaskExecutor: try: # Route basierend auf Aktionstyp if aktionstyp == 'Dokumente zusammenfassen': - result = self.summarize_documents(entwicklung_id) + result = self.summarize_documents(puls_id) elif aktionstyp == 'Frist prüfen': - result = self.check_deadlines(entwicklung_id) + result = self.check_deadlines(puls_id) elif aktionstyp == 'E-Mail-Entwurf erstellen': - result = self.create_email_draft(entwicklung_id) + result = self.create_email_draft(puls_id) # ... weitere Aktionstypen else: result = f"Aktionstyp {aktionstyp} noch nicht implementiert" @@ -2194,7 +2194,7 @@ class KiTaskExecutor: **Button: Team hinzufügen/entfernen** ```javascript -// client/custom/src/views/c-entwicklung/record/detail.js (erweitern) +// client/custom/src/views/c-puls/record/detail.js (erweitern) addTeamManagementButtons: function () { this.addButton({ @@ -2214,7 +2214,7 @@ showAddTeamModal: function () { view.render(); this.listenToOnce(view, 'select', team => { - this.ajaxPostRequest(`CEntwicklung/${this.model.id}/add-team`, { + this.ajaxPostRequest(`CPuls/${this.model.id}/add-team`, { teamId: team.id, prioritaet: 'Normal' }).then(() => { @@ -2232,12 +2232,12 @@ showAddTeamModal: function () { ### 7.1 Unit-Tests (Custom PHP-Klassen) -**Datei:** `tests/unit/Espo/Custom/Api/CEntwicklung/AktiviereTeamsTest.php` +**Datei:** `tests/unit/Espo/Custom/Api/CPuls/AktiviereTeamsTest.php` **Test-Cases:** - ✅ Teams werden korrekt aktiviert - ✅ Alte Zuordnungen werden deaktiviert -- ✅ Entwicklung-Status wird aktualisiert +- ✅ Puls-Status wird aktualisiert - ✅ Fehlerbehandlung bei fehlender ID - ✅ Fehlerbehandlung bei ungültigem Team @@ -2245,29 +2245,29 @@ showAddTeamModal: function () { ### 7.2 Integration-Tests -**Szenario 1: Dokument → Entwicklung → Analyse → Abschluss** +**Szenario 1: Dokument → Puls → Analyse → Abschluss** 1. Upload Dokument via UI 2. Middleware erkennt Dokument (manuell triggern) -3. Middleware erstellt Entwicklung +3. Middleware erstellt Puls 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) +1. Puls 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) +1. Puls 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 +4. Middleware erkennt: Puls finalisiert=true +5. Middleware erstellt NEUE Puls für das neue Dokument +6. Validierung: Alte Puls bleibt geschlossen, neue Puls existiert **Szenario 4: Neues Dokument während Review (unfinalisiert)** -1. Entwicklung im Status "Bereit", finalisiert=false +1. Puls im Status "Bereit", finalisiert=false 2. Neues Dokument uploaded 3. Middleware setzt syncStatus = "unclean" 4. Abschluss-Button disabled (Formula Script) @@ -2278,12 +2278,12 @@ showAddTeamModal: function () { 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 +4. Pulsen 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 +1. User öffnet Puls 2. Klickt "Team hinzufügen" 3. Wählt Team "Zwangsvollstreckung" 4. Validierung: TeamZuordnung erstellt, aktiv=true @@ -2292,13 +2292,13 @@ showAddTeamModal: function () { **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" +2. User sieht Task in Puls (gelb markiert) +3. User öffnet Puls → 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 +1. User öffnet Puls 2. Klickt "KI-Aktion zuweisen" 3. Wählt "Dokumente zusammenfassen" 4. CKiAktion erstellt mit status="Wartend" @@ -2306,12 +2306,12 @@ showAddTeamModal: function () { 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) +**Szenario 9: Report Panel - Tasks aus Pulsen** +1. Puls mit parentType="CVmhRumungsklage", parentId=X erstellt +2. Task mit parentType="CPuls", parentId=Y erstellt (vorgeschlagen) 3. User öffnet CVmhRumungsklage (ID=X) -4. Sieht Bottom-Panel "Tasks aus Entwicklungen" -5. Task wird angezeigt trotz parent=CEntwicklung +4. Sieht Bottom-Panel "Tasks aus Pulsen" +5. Task wird angezeigt trotz parent=CPuls 6. Filter "Offen" zeigt nur nicht-abgeschlossene Tasks 7. Validierung: Subquery funktioniert, Task ist sichtbar @@ -2320,7 +2320,7 @@ showAddTeamModal: function () { ### 7.3 Performance-Tests **Metriken:** -- Query-Zeit für "Meine offenen" Filter < 500ms (bei 1000 Entwicklungen) +- Query-Zeit für "Meine offenen" Filter < 500ms (bei 1000 Pulsen) - Middleware Polling-Overhead < 1 CPU-Sekunde pro Cycle - Abschluss-API < 200ms Response-Time @@ -2345,11 +2345,11 @@ showAddTeamModal: function () { ### Post-Deployment - [ ] Teams kategorisieren (teamKategorie setzen) - [ ] KI-User erstellen und ID in Config eintragen -- [ ] Test-Entwicklung manuell erstellen +- [ ] Test-Puls 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 testen: Task zu Puls 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) @@ -2365,7 +2365,7 @@ showAddTeamModal: function () { 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;` +5. DB-Rollback falls nötig: `DROP TABLE c_puls, c_puls_team_zuordnung;` --- @@ -2377,8 +2377,8 @@ showAddTeamModal: function () { - API-Dokumentation (Swagger/Postman Collection) ### Für User -- User-Guide: "Wie nutze ich Entwicklungen?" -- Video-Tutorial: Entwicklung reviewen & abschließen +- User-Guide: "Wie nutze ich Pulsen?" +- Video-Tutorial: Puls reviewen & abschließen - FAQ: Häufige Fragen ### Für Admins @@ -2391,15 +2391,15 @@ showAddTeamModal: function () { ## 🎯 Success Metrics **Funktional:** -- ✅ Dokumente werden automatisch zu Entwicklungen gruppiert +- ✅ Dokumente werden automatisch zu Pulsen 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 +- ✅ Teams sehen nur relevante Pulsen - ✅ 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 +- ✅ Task-Genehmigung: Automatisch bei Puls-Review +- ✅ Report Panels: Tasks/Calls aus Pulsen in übergeordneten Vorgängen sichtbar - ✅ KI-Aktionen: Katalog verfügbar, Middleware führt aus - ✅ Abschluss-Workflow funktioniert - ✅ Abwesenheitsvertretung funktioniert @@ -2410,7 +2410,7 @@ showAddTeamModal: function () { - ✅ Middleware-Polling ohne Fehler **Usability:** -- ✅ User finden Entwicklungen intuitiv +- ✅ User finden Pulsen intuitiv - ✅ Abschluss-Prozess klar - ✅ Fehler-Messages verständlich @@ -2419,20 +2419,20 @@ showAddTeamModal: function () { ## 🔮 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 +- [ ] Kommentare zu Pulsen (Stream) - IMPLEMENTIERT +- [ ] E-Mail-Benachrichtigungen bei neuen Pulsen +- [ ] Dashboard-Widget: "Meine offenen Pulsen" +- [ ] Bulk-Actions: Mehrere Pulsen 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) +- [ ] Report: Anzahl Pulsen pro Vorgang +- [ ] Dashboard: Pulsen-Pipeline (Kanban-View) ### v3.0: Advanced -- [ ] Workflow-Integration: Auto-Task bei neuer Entwicklung +- [ ] Workflow-Integration: Auto-Task bei neuer Puls - [ ] Custom Notification-Channels (Slack, Teams) - [ ] Mobile-App-Integration - [ ] KI-gestützte Prioritäts-Vorhersage @@ -2508,4 +2508,4 @@ showAddTeamModal: function () { --- -*Ende des Entwicklungsplans* +*Ende des Pulssplans* diff --git a/custom/Espo/Custom/Api/CPuls/AbschliessenFuerTeam.php b/custom/Espo/Custom/Api/CPuls/AbschliessenFuerTeam.php new file mode 100644 index 00000000..b6b5666c --- /dev/null +++ b/custom/Espo/Custom/Api/CPuls/AbschliessenFuerTeam.php @@ -0,0 +1,121 @@ +getRouteParam('id'); + $data = $request->getParsedBody(); + $teamId = $data->teamId ?? null; + + if (!$pulsId || !$teamId) { + throw new BadRequest('pulsId 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 Puls + $puls = $this->entityManager->getEntity('CPuls', $pulsId); + + if (!$puls) { + throw new NotFound('Puls nicht gefunden'); + } + + // 3. Validierung: syncStatus = clean? + if ($puls->get('syncStatus') !== 'clean') { + throw new BadRequest('Puls hat neue Dokumente (unclean) - bitte warten Sie auf die KI-Analyse'); + } + + // 4. Finde Zuordnung + $zuordnung = $this->entityManager + ->getRDBRepository('CPulsTeamZuordnung') + ->where([ + 'pulsId' => $pulsId, + '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 (!$puls->get('finalisiert')) { + $puls->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('CPulsTeamZuordnung') + ->where([ + 'pulsId' => $pulsId, + 'aktiv' => true, + 'abgeschlossen' => false + ]) + ->count(); + + // 8. Update Puls-Status + if ($offeneTeams === 0) { + $puls->set('status', 'Abgeschlossen'); + } else { + $puls->set('status', 'Teilweise abgeschlossen'); + } + + $this->entityManager->saveEntity($puls); + + $this->log->info("Team {$teamId} hat Puls {$pulsId} abgeschlossen"); + + return Response::json([ + 'success' => true, + 'status' => $puls->get('status'), + 'finalisiert' => $puls->get('finalisiert'), + 'offeneTeams' => $offeneTeams + ]); + } +} diff --git a/custom/Espo/Custom/Api/CPuls/AktiviereTeams.php b/custom/Espo/Custom/Api/CPuls/AktiviereTeams.php new file mode 100644 index 00000000..fc0f3704 --- /dev/null +++ b/custom/Espo/Custom/Api/CPuls/AktiviereTeams.php @@ -0,0 +1,99 @@ +getRouteParam('id'); + + if (!$id) { + throw new BadRequest('ID fehlt'); + } + + $puls = $this->entityManager->getEntity('CPuls', $id); + + if (!$puls) { + throw new NotFound('Puls nicht gefunden'); + } + + $data = $request->getParsedBody(); + + // 1. Update Puls + $puls->set([ + 'kiAnalyse' => $data->kiAnalyse ?? null, + 'zusammenfassung' => $data->zusammenfassung ?? null, + 'status' => $data->status ?? 'Bereit', + 'syncStatus' => $data->syncStatus ?? 'clean' + ]); + + $this->entityManager->saveEntity($puls); + + // 2. Lösche alte Zuordnungen (soft delete - setze inaktiv) + $this->entityManager + ->getQueryBuilder() + ->update() + ->in('CPulsTeamZuordnung') + ->set(['aktiv' => false]) + ->where(['pulsId' => $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('CPulsTeamZuordnung') + ->where([ + 'pulsId' => $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('CPulsTeamZuordnung', [ + 'pulsId' => $id, + 'teamId' => $teamId, + 'aktiv' => true, + 'abgeschlossen' => false, + 'prioritaet' => $teamData->prioritaet ?? 'Normal' + ]); + } + } + } + + $this->log->info("Teams aktiviert für Puls {$id}"); + + return Response::json([ + 'success' => true, + 'pulsId' => $id + ]); + } +} diff --git a/custom/Espo/Custom/Hooks/CPuls/UpdateTeamStats.php b/custom/Espo/Custom/Hooks/CPuls/UpdateTeamStats.php new file mode 100644 index 00000000..cfdd1987 --- /dev/null +++ b/custom/Espo/Custom/Hooks/CPuls/UpdateTeamStats.php @@ -0,0 +1,46 @@ +isNew() || $entity->isAttributeChanged('id')) { + $dokumenteCount = $this->entityManager + ->getRDBRepository('CDokumente') + ->where(['pulsId' => $entity->getId()]) + ->count(); + + $entity->set('anzahlDokumente', $dokumenteCount); + } + + // Zähle Team-Zuordnungen + $zuordnungen = $this->entityManager + ->getRDBRepository('CPulsTeamZuordnung') + ->where(['pulsId' => $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); + } +} diff --git a/custom/Espo/Custom/Resources/i18n/de_DE/CPuls.json b/custom/Espo/Custom/Resources/i18n/de_DE/CPuls.json index b83bf577..973ead70 100644 --- a/custom/Espo/Custom/Resources/i18n/de_DE/CPuls.json +++ b/custom/Espo/Custom/Resources/i18n/de_DE/CPuls.json @@ -1,9 +1,58 @@ { - "links": { - "calls": "Anrufe", - "tasks": "Aufgaben" - }, "labels": { - "Create CPuls": "Puls erstellen" + "Create CPuls": "Puls erstellen", + "CPuls": "Puls", + "cPuls": "Pulse" + }, + "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" + } + }, + "presetFilters": { + "meineOffenen": "Meine offenen Pulse" } -} \ No newline at end of file +} diff --git a/custom/Espo/Custom/Resources/i18n/de_DE/CPulsTeamZuordnung.json b/custom/Espo/Custom/Resources/i18n/de_DE/CPulsTeamZuordnung.json new file mode 100644 index 00000000..5b1887ac --- /dev/null +++ b/custom/Espo/Custom/Resources/i18n/de_DE/CPulsTeamZuordnung.json @@ -0,0 +1,32 @@ +{ + "labels": { + "Create CPulsTeamZuordnung": "Team-Zuordnung erstellen", + "CPulsTeamZuordnung": "Puls Team-Zuordnung", + "cPulsTeamZuordnung": "Puls Team-Zuordnungen" + }, + "fields": { + "name": "Name", + "puls": "Puls", + "team": "Team", + "aktiv": "Aktiv", + "abgeschlossen": "Abgeschlossen", + "abgeschlossenAm": "Abgeschlossen am", + "abgeschlossenVon": "Abgeschlossen von", + "prioritaet": "Priorität" + }, + "links": { + "puls": "Puls", + "team": "Team", + "abgeschlossenVon": "Abgeschlossen von" + }, + "tooltips": { + "aktiv": "Ist diese Team-Zuordnung aktiv? Inaktive werden nicht angezeigt." + }, + "options": { + "prioritaet": { + "Niedrig": "Niedrig", + "Normal": "Normal", + "Hoch": "Hoch" + } + } +} diff --git a/custom/Espo/Custom/Resources/i18n/en_US/CPuls.json b/custom/Espo/Custom/Resources/i18n/en_US/CPuls.json index 7d261013..e991dd14 100644 --- a/custom/Espo/Custom/Resources/i18n/en_US/CPuls.json +++ b/custom/Espo/Custom/Resources/i18n/en_US/CPuls.json @@ -1,12 +1,58 @@ -{ - "fields": { - }, - "links": { - "meetings": "Meetings", - "calls": "Calls", - "tasks": "Tasks" - }, - "labels": { - "Create CPuls": "Create Puls" - } -} +{ + "labels": { + "Create CPuls": "Create Pulse", + "CPuls": "Pulse", + "cPuls": "Pulses" + }, + "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" + } + }, + "presetFilters": { + "meineOffenen": "My Open Pulses" + } +} diff --git a/custom/Espo/Custom/Resources/layouts/CPuls/bottomPanelsDetail.json b/custom/Espo/Custom/Resources/layouts/CPuls/bottomPanelsDetail.json new file mode 100644 index 00000000..e2d9c874 --- /dev/null +++ b/custom/Espo/Custom/Resources/layouts/CPuls/bottomPanelsDetail.json @@ -0,0 +1,17 @@ +{ + "teamZuordnungen": { + "index": 0, + "sticked": true, + "style": "info", + "label": "Team-Zuordnungen" + }, + "dokumente": { + "index": 1, + "sticked": false, + "label": "Dokumente" + }, + "stream": { + "index": 2, + "sticked": false + } +} diff --git a/custom/Espo/Custom/Resources/layouts/CPuls/detail.json b/custom/Espo/Custom/Resources/layouts/CPuls/detail.json new file mode 100644 index 00000000..9c82ebe4 --- /dev/null +++ b/custom/Espo/Custom/Resources/layouts/CPuls/detail.json @@ -0,0 +1,47 @@ +[ + { + "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"} + ] + ] + } +] diff --git a/custom/Espo/Custom/Resources/layouts/CPuls/list.json b/custom/Espo/Custom/Resources/layouts/CPuls/list.json new file mode 100644 index 00000000..38e8bf00 --- /dev/null +++ b/custom/Espo/Custom/Resources/layouts/CPuls/list.json @@ -0,0 +1,8 @@ +[ + {"name": "name", "width": 30}, + {"name": "status", "width": 15}, + {"name": "syncStatus", "width": 10}, + {"name": "parent", "width": 20}, + {"name": "anzahlDokumente", "width": 10}, + {"name": "createdAt", "width": 15} +] diff --git a/custom/Espo/Custom/Resources/metadata/app/api.json b/custom/Espo/Custom/Resources/metadata/app/api.json new file mode 100644 index 00000000..3d053e3f --- /dev/null +++ b/custom/Espo/Custom/Resources/metadata/app/api.json @@ -0,0 +1,14 @@ +{ + "routes": [ + { + "route": "/CPuls/:id/aktiviere-teams", + "method": "put", + "actionClassName": "Espo\\Custom\\Api\\CPuls\\AktiviereTeams" + }, + { + "route": "/CPuls/:id/abschliessen-fuer-team", + "method": "post", + "actionClassName": "Espo\\Custom\\Api\\CPuls\\AbschliessenFuerTeam" + } + ] +} diff --git a/custom/Espo/Custom/Resources/metadata/clientDefs/CPuls.json b/custom/Espo/Custom/Resources/metadata/clientDefs/CPuls.json index 2b4246ff..932982f2 100644 --- a/custom/Espo/Custom/Resources/metadata/clientDefs/CPuls.json +++ b/custom/Espo/Custom/Resources/metadata/clientDefs/CPuls.json @@ -1,37 +1,18 @@ { - "controller": "controllers/record", - "boolFilterList": [ - "onlyMy" - ], - "sidePanels": { - "detail": [ - { - "name": "activities", - "reference": "activities" - }, - { - "name": "history", - "reference": "history" - }, - { - "name": "tasks", - "reference": "tasks" - } - ] + "controller": "controllers/record", + "iconClass": "fas fa-heartbeat", + "color": "#e74c3c", + "filterList": [ + "meineOffenen", + { + "name": "bereit" }, - "bottomPanels": { - "detail": [ - { - "name": "activities", - "reference": "activities", - "disabled": true - }, - { - "name": "history", - "reference": "history", - "disabled": true - } - ] - }, - "iconClass": "fas fa-heart-pulse" -} \ No newline at end of file + { + "name": "inReview" + } + ], + "boolFilterList": [ + "onlyMy" + ], + "defaultFilterPreset": "meineOffenen" +} diff --git a/custom/Espo/Custom/Resources/metadata/entityDefs/CDokumente.json b/custom/Espo/Custom/Resources/metadata/entityDefs/CDokumente.json index a2d7db2e..4e65bb1c 100644 --- a/custom/Espo/Custom/Resources/metadata/entityDefs/CDokumente.json +++ b/custom/Espo/Custom/Resources/metadata/entityDefs/CDokumente.json @@ -105,6 +105,11 @@ "default": "pending_sync", "tooltip": true, "isCustom": true + }, + "puls": { + "type": "link", + "entity": "CPuls", + "isCustom": true } }, "links": { @@ -138,6 +143,12 @@ "skipOrmDefs": true, "utility": true }, + "puls": { + "type": "belongsTo", + "entity": "CPuls", + "foreign": "dokumente", + "isCustom": true + }, "contactsvmhdokumente": { "type": "hasMany", "relationName": "cDokumenteContact", diff --git a/custom/Espo/Custom/Resources/metadata/entityDefs/CPuls.json b/custom/Espo/Custom/Resources/metadata/entityDefs/CPuls.json index d521dd6a..a2a06c4e 100644 --- a/custom/Espo/Custom/Resources/metadata/entityDefs/CPuls.json +++ b/custom/Espo/Custom/Resources/metadata/entityDefs/CPuls.json @@ -3,10 +3,97 @@ "name": { "type": "varchar", "required": true, - "pattern": "$noBadCharacters" + "maxLength": 255, + "trim": true, + "isCustom": true }, - "description": { - "type": "text" + "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", @@ -18,25 +105,43 @@ }, "createdBy": { "type": "link", - "readOnly": true, - "view": "views/fields/user" + "entity": "User", + "readOnly": true }, "modifiedBy": { "type": "link", - "readOnly": true, - "view": "views/fields/user" + "entity": "User", + "readOnly": true }, "assignedUser": { "type": "link", - "required": false, - "view": "views/fields/assigned-user" + "entity": "User", + "isCustom": true }, "teams": { "type": "linkMultiple", - "view": "views/fields/teams" + "isCustom": true } }, "links": { + "parent": { + "type": "belongsToParent", + "entityList": [ + "CVmhRumungsklage", + "CMietinkasso", + "CKuendigung" + ] + }, + "dokumente": { + "type": "hasMany", + "entity": "CDokumente", + "foreign": "puls" + }, + "teamZuordnungen": { + "type": "hasMany", + "entity": "CPulsTeamZuordnung", + "foreign": "puls" + }, "createdBy": { "type": "belongsTo", "entity": "User" @@ -49,62 +154,37 @@ "type": "belongsTo", "entity": "User" }, + "finalisiertVon": { + "type": "belongsTo", + "entity": "User" + }, "teams": { "type": "hasMany", "entity": "Team", - "relationName": "entityTeam", - "layoutRelationshipsDisabled": true - }, - "meetings": { - "type": "hasMany", - "entity": "Meeting", - "foreign": "parent" - }, - "calls": { - "type": "hasMany", - "entity": "Call", - "foreign": "parent" - }, - "tasks": { - "type": "hasChildren", - "entity": "Task", - "foreign": "parent" - }, - "emails": { - "type": "hasChildren", - "entity": "Email", - "foreign": "parent", + "relationName": "EntityTeam", "layoutRelationshipsDisabled": true } }, "collection": { "orderBy": "createdAt", - "order": "desc" + "order": "desc", + "textFilterFields": ["name", "zusammenfassung"] }, "indexes": { - "name": { - "columns": [ - "name", - "deleted" - ] + "parent": { + "columns": ["parentType", "parentId"] }, - "assignedUser": { - "columns": [ - "assignedUserId", - "deleted" - ] + "status": { + "columns": ["status"] + }, + "syncStatus": { + "columns": ["syncStatus"] + }, + "finalisiert": { + "columns": ["finalisiert"] }, "createdAt": { - "columns": [ - "createdAt" - ] - }, - "createdAtId": { - "unique": true, - "columns": [ - "createdAt", - "id" - ] + "columns": ["createdAt"] } } } \ No newline at end of file diff --git a/custom/Espo/Custom/Resources/metadata/entityDefs/CPulsTeamZuordnung.json b/custom/Espo/Custom/Resources/metadata/entityDefs/CPulsTeamZuordnung.json new file mode 100644 index 00000000..512a3ca3 --- /dev/null +++ b/custom/Espo/Custom/Resources/metadata/entityDefs/CPulsTeamZuordnung.json @@ -0,0 +1,100 @@ +{ + "fields": { + "name": { + "type": "varchar", + "notStorable": true, + "select": { + "select": "CONCAT:(team.name, ' - ', puls.name)" + }, + "orderBy": { + "order": [ + ["team.name", "{direction}"] + ] + } + }, + "puls": { + "type": "link", + "entity": "CPuls", + "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": { + "puls": { + "type": "belongsTo", + "entity": "CPuls", + "foreign": "teamZuordnungen" + }, + "team": { + "type": "belongsTo", + "entity": "Team" + }, + "abgeschlossenVon": { + "type": "belongsTo", + "entity": "User" + } + }, + "collection": { + "orderBy": "createdAt", + "order": "desc" + }, + "indexes": { + "pulsTeam": { + "columns": ["pulsId", "teamId"], + "unique": true + }, + "aktiv": { + "columns": ["aktiv"] + }, + "abgeschlossen": { + "columns": ["abgeschlossen"] + } + } +} diff --git a/custom/Espo/Custom/Resources/metadata/entityDefs/Team.json b/custom/Espo/Custom/Resources/metadata/entityDefs/Team.json new file mode 100644 index 00000000..46e916d7 --- /dev/null +++ b/custom/Espo/Custom/Resources/metadata/entityDefs/Team.json @@ -0,0 +1,16 @@ +{ + "fields": { + "teamKategorie": { + "type": "enum", + "options": [ + "Anwalt", + "Mandatsbetreuung", + "Zwangsvollstreckung", + "Sonstiges" + ], + "default": "Sonstiges", + "isCustom": true, + "tooltip": true + } + } +} diff --git a/custom/Espo/Custom/Resources/metadata/entityDefs/User.json b/custom/Espo/Custom/Resources/metadata/entityDefs/User.json index fa337651..d5c846ad 100644 --- a/custom/Espo/Custom/Resources/metadata/entityDefs/User.json +++ b/custom/Espo/Custom/Resources/metadata/entityDefs/User.json @@ -2,6 +2,23 @@ "fields": { "cCallQueues": { "type": "linkOne" + }, + "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": { @@ -10,6 +27,11 @@ "foreign": "user", "entity": "CCallQueues", "isCustom": true + }, + "vertretung": { + "type": "belongsTo", + "entity": "User", + "isCustom": true } } } \ No newline at end of file diff --git a/custom/Espo/Custom/Resources/metadata/formula/CPuls.json b/custom/Espo/Custom/Resources/metadata/formula/CPuls.json new file mode 100644 index 00000000..f42fd963 --- /dev/null +++ b/custom/Espo/Custom/Resources/metadata/formula/CPuls.json @@ -0,0 +1,3 @@ +{ + "beforeSaveApiScript": "// Verhindere Abschluss bei unclean Status\nif (\n (status == 'Abgeschlossen' || entity\\isAttributeChanged('status'))\n && syncStatus == 'unclean'\n) {\n recordService\\throwBadRequest('Puls kann nicht abgeschlossen werden: Neue Dokumente vorhanden (Status: unclean). Bitte warten Sie auf die KI-Analyse.');\n}\n\n// Verhindere Änderungen an finalisiertem Puls\nif (\n finalisiert == true\n && entity\\isAttributeChanged('finalisiert') == false\n && (entity\\isAttributeChanged('status') || entity\\isAttributeChanged('syncStatus'))\n) {\n recordService\\throwBadRequest('Puls ist finalisiert. Neue Dokumente erzeugen automatisch einen neuen Block.');\n}" +} diff --git a/custom/Espo/Custom/Resources/metadata/scopes/CPuls.json b/custom/Espo/Custom/Resources/metadata/scopes/CPuls.json index 4fc6d49b..74384efe 100644 --- a/custom/Espo/Custom/Resources/metadata/scopes/CPuls.json +++ b/custom/Espo/Custom/Resources/metadata/scopes/CPuls.json @@ -2,22 +2,16 @@ "entity": true, "layouts": true, "tab": true, - "acl": true, - "aclPortal": true, - "aclPortalLevelList": [ - "all", - "account", - "contact", - "own", - "no" - ], + "acl": "recordAllTeamOwnNo", + "aclPortal": false, "customizable": true, - "importable": true, + "importable": false, "notifications": true, "stream": true, "disabled": false, - "type": "BasePlus", + "type": "Base", "module": "Custom", "object": true, - "isCustom": true -} \ No newline at end of file + "isCustom": true, + "calendar": false +} diff --git a/custom/Espo/Custom/Resources/metadata/scopes/CPulsTeamZuordnung.json b/custom/Espo/Custom/Resources/metadata/scopes/CPulsTeamZuordnung.json new file mode 100644 index 00000000..fc104aac --- /dev/null +++ b/custom/Espo/Custom/Resources/metadata/scopes/CPulsTeamZuordnung.json @@ -0,0 +1,13 @@ +{ + "entity": true, + "tab": false, + "acl": "recordAllTeamNo", + "aclPortal": false, + "customizable": true, + "stream": false, + "disabled": false, + "type": "Base", + "module": "Custom", + "object": true, + "isCustom": true +} diff --git a/data/config.php b/data/config.php index 1f592181..cdb874ab 100644 --- a/data/config.php +++ b/data/config.php @@ -358,7 +358,7 @@ return [ 0 => 'youtube.com', 1 => 'google.com' ], - 'microtime' => 1770972794.710849, + 'microtime' => 1770973606.273594, '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 7a31742a..ffc2cbdc 100644 --- a/data/state.php +++ b/data/state.php @@ -1,7 +1,7 @@ 1770972795, - 'microtimeState' => 1770972795.029599, + 'cacheTimestamp' => 1770973606, + 'microtimeState' => 1770973606.458758, 'currencyRates' => [ 'EUR' => 1.0 ],