From bf7eaa965f9f5dac035728a3432fd692f219e0ff Mon Sep 17 00:00:00 2001 From: bsiggel Date: Fri, 13 Feb 2026 10:28:21 +0100 Subject: [PATCH] feat: Refactor CPuls entity and related resources; enhance localization, update layouts, and improve validation logic --- .../Custom/Api/CPuls/AbschliessenFuerTeam.php | 139 +++++++----------- .../Custom/Hooks/CPuls/UpdateTeamStats.php | 3 +- .../Custom/Resources/i18n/de_DE/CPuls.json | 47 +++--- .../Custom/Resources/i18n/en_US/CPuls.json | 49 +++--- .../layouts/CPuls/bottomPanelsDetail.json | 28 ++-- .../layouts/CPuls/defaultSidePanel.json | 11 ++ .../Resources/layouts/CPuls/detail.json | 31 +++- .../Resources/metadata/entityDefs/CPuls.json | 44 ++---- .../entityDefs/CPulsTeamZuordnung.json | 6 +- .../Resources/metadata/formula/CPuls.json | 2 +- data/config.php | 2 +- data/state.php | 4 +- 12 files changed, 175 insertions(+), 191 deletions(-) create mode 100644 custom/Espo/Custom/Resources/layouts/CPuls/defaultSidePanel.json diff --git a/custom/Espo/Custom/Api/CPuls/AbschliessenFuerTeam.php b/custom/Espo/Custom/Api/CPuls/AbschliessenFuerTeam.php index b6b5666c..6e855028 100644 --- a/custom/Espo/Custom/Api/CPuls/AbschliessenFuerTeam.php +++ b/custom/Espo/Custom/Api/CPuls/AbschliessenFuerTeam.php @@ -1,121 +1,90 @@ getRouteParam('id'); - $data = $request->getParsedBody(); - $teamId = $data->teamId ?? null; - - if (!$pulsId || !$teamId) { - throw new BadRequest('pulsId oder teamId fehlt'); + $id = $request->getRouteParam('id'); + if (!$id) { + throw new BadRequest('ID 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); - + + $puls = $this->entityManager->getEntityById('CPuls', $id); if (!$puls) { - throw new NotFound('Puls nicht gefunden'); + 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'); + + // Prüfe, ob bereits finalisiert + if ($puls->get('status') === 'Finalisiert') { + throw new Forbidden('Puls wurde bereits finalisiert.'); } - - // 4. Finde Zuordnung - $zuordnung = $this->entityManager - ->getRDBRepository('CPulsTeamZuordnung') + + // Prüfe syncStatus + if ($puls->get('syncStatus') === 'unclean') { + throw new BadRequest('Neue Dokumente vorhanden. Bitte warten Sie auf die KI-Analyse.'); + } + + // Hole aktuelle Team-ID des Benutzers + $user = $this->entityManager->getEntityById('User', $request->getServerParam('ESPO_USER_ID')); + if (!$user) { + throw new Forbidden('Benutzer nicht gefunden.'); + } + + $teamIds = $user->getLinkMultipleIdList('teams'); + if (empty($teamIds)) { + throw new Forbidden('Sie sind keinem Team zugeordnet.'); + } + + // Finde Zuordnung für erstes Team des Benutzers + $zuordnung = $this->entityManager->getRDBRepositoryByClass('CPulsTeamZuordnung') ->where([ - 'pulsId' => $pulsId, - 'teamId' => $teamId, + 'pulsId' => $id, + 'teamId' => $teamIds, 'aktiv' => true ]) ->findOne(); - + if (!$zuordnung) { - throw new NotFound('Team-Zuordnung nicht gefunden oder nicht aktiv'); + throw new Forbidden('Sie sind diesem Puls nicht zugeordnet oder nicht aktiv.'); } - - // 5. Bereits abgeschlossen? - if ($zuordnung->get('abgeschlossen')) { - return Response::json([ - 'success' => true, - 'message' => 'Bereits abgeschlossen', - 'alreadyCompleted' => true - ]); - } - - // 6. Abschluss setzen + + // Markiere Zuordnung als abgeschlossen $zuordnung->set([ 'abgeschlossen' => true, 'abgeschlossenAm' => date('Y-m-d H:i:s'), - 'abgeschlossenVonId' => $this->user->getId() + 'abgeschlossenVon' => $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'); - } - + + // FIRST-READ-CLOSES: Finalisiere den Puls sofort + $puls->set([ + 'status' => 'Finalisiert', + 'finalisiertAm' => date('Y-m-d H:i:s'), + 'finalisiertVon' => $user->getId() + ]); $this->entityManager->saveEntity($puls); - - $this->log->info("Team {$teamId} hat Puls {$pulsId} abgeschlossen"); - - return Response::json([ + + $GLOBALS['log']->info("Puls {$id} durch Team {$zuordnung->get('teamId')} finalisiert (First-Read-Closes)."); + + return ResponseComposer::json([ 'success' => true, - 'status' => $puls->get('status'), - 'finalisiert' => $puls->get('finalisiert'), - 'offeneTeams' => $offeneTeams + 'finalized' => true, + 'message' => 'Puls wurde finalisiert (First-Read-Closes).' ]); } } diff --git a/custom/Espo/Custom/Hooks/CPuls/UpdateTeamStats.php b/custom/Espo/Custom/Hooks/CPuls/UpdateTeamStats.php index cfdd1987..f4bb66f5 100644 --- a/custom/Espo/Custom/Hooks/CPuls/UpdateTeamStats.php +++ b/custom/Espo/Custom/Hooks/CPuls/UpdateTeamStats.php @@ -2,6 +2,7 @@ namespace Espo\Custom\Hooks\CPuls; use Espo\ORM\Entity; +use Espo\ORM\Repository\Option\SaveOptions; use Espo\Core\Hook\Hook\BeforeSave; class UpdateTeamStats implements BeforeSave @@ -10,7 +11,7 @@ class UpdateTeamStats implements BeforeSave private \Espo\ORM\EntityManager $entityManager ) {} - public function beforeSave(Entity $entity, array $options): void + public function beforeSave(Entity $entity, SaveOptions $options): void { // Zähle Dokumente if ($entity->isNew() || $entity->isAttributeChanged('id')) { diff --git a/custom/Espo/Custom/Resources/i18n/de_DE/CPuls.json b/custom/Espo/Custom/Resources/i18n/de_DE/CPuls.json index 973ead70..096a1bd9 100644 --- a/custom/Espo/Custom/Resources/i18n/de_DE/CPuls.json +++ b/custom/Espo/Custom/Resources/i18n/de_DE/CPuls.json @@ -1,37 +1,35 @@ { "labels": { - "Create CPuls": "Puls erstellen", "CPuls": "Puls", - "cPuls": "Pulse" - }, - "fields": { - "name": "Bezeichnung", + "CPulse": "Pulse", + "Create CPuls": "Puls erstellen", + "name": "Name", "status": "Status", - "syncStatus": "Synchronisations-Status", + "syncStatus": "Synchronisierungs-Status", "kiAnalyse": "KI-Analyse", "zusammenfassung": "Zusammenfassung", "anzahlDokumente": "Anzahl Dokumente", - "anzahlTeamsAktiv": "Teams (aktiv)", - "anzahlTeamsAbgeschlossen": "Teams (abgeschlossen)", - "finalisiert": "Finalisiert", - "finalisierungsGrund": "Finalisierungsgrund", + "anzahlTeamsAktiv": "Teams aktiv", + "anzahlTeamsAbgeschlossen": "Teams abgeschlossen", "finalisiertAm": "Finalisiert am", "finalisiertVon": "Finalisiert von", + "mandantMitteilung": "Mitteilung an Mandant", + "mandantMitteilungText": "Mitteilungstext", "parent": "Vorgang", - "dokumente": "Dokumente", - "teamZuordnungen": "Team-Zuordnungen" + "assignedUser": "Zugewiesen" }, "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" + "syncStatus": "Status der KI-Analyse für neue Dokumente", + "kiAnalyse": "Vollständige Analyse der KI zu den Dokumenten", + "zusammenfassung": "Kurze Zusammenfassung der KI-Analyse", + "finalisiertAm": "Zeitpunkt, zu dem dieser Puls finalisiert wurde", + "finalisiertVon": "Benutzer, der den Puls durch Abschluss finalisiert hat", + "mandantMitteilung": "Soll eine Mitteilung an den Mandanten versendet werden?", + "mandantMitteilungText": "Text der Mitteilung an den Mandanten" }, "options": { "status": { @@ -40,19 +38,12 @@ "Bereit": "Bereit", "In Review": "In Review", "Teilweise abgeschlossen": "Teilweise abgeschlossen", - "Abgeschlossen": "Abgeschlossen" + "Abgeschlossen": "Abgeschlossen", + "Finalisiert": "Finalisiert" }, "syncStatus": { - "clean": "Aktuell", + "clean": "Synchron", "unclean": "Ausstehend" - }, - "finalisierungsGrund": { - "Erstes Team": "Erstes Team", - "Manuell": "Manuell", - "Automatisch": "Automatisch" } - }, - "presetFilters": { - "meineOffenen": "Meine offenen Pulse" } } diff --git a/custom/Espo/Custom/Resources/i18n/en_US/CPuls.json b/custom/Espo/Custom/Resources/i18n/en_US/CPuls.json index e991dd14..ba200729 100644 --- a/custom/Espo/Custom/Resources/i18n/en_US/CPuls.json +++ b/custom/Espo/Custom/Resources/i18n/en_US/CPuls.json @@ -1,58 +1,49 @@ { "labels": { - "Create CPuls": "Create Pulse", "CPuls": "Pulse", - "cPuls": "Pulses" - }, - "fields": { + "CPulse": "Pulses", + "Create CPuls": "Create Pulse", "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", + "anzahlDokumente": "Document Count", + "anzahlTeamsAktiv": "Active Teams", + "anzahlTeamsAbgeschlossen": "Completed Teams", "finalisiertAm": "Finalized At", "finalisiertVon": "Finalized By", - "parent": "Parent Record", - "dokumente": "Documents", - "teamZuordnungen": "Team Assignments" + "mandantMitteilung": "Client Notification", + "mandantMitteilungText": "Notification Text", + "parent": "Case", + "assignedUser": "Assigned To" }, "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" + "syncStatus": "AI analysis status for new documents", + "kiAnalyse": "Complete AI analysis of the documents", + "zusammenfassung": "Brief summary of the AI analysis", + "finalisiertAm": "Date and time when this pulse was finalized", + "finalisiertVon": "User who finalized the pulse by completion", + "mandantMitteilung": "Should a notification be sent to the client?", + "mandantMitteilungText": "Text of the notification to the client" }, "options": { "status": { "Neu": "New", - "In Verarbeitung": "Processing", + "In Verarbeitung": "In Progress", "Bereit": "Ready", "In Review": "In Review", "Teilweise abgeschlossen": "Partially Completed", - "Abgeschlossen": "Completed" + "Abgeschlossen": "Completed", + "Finalisiert": "Finalized" }, "syncStatus": { - "clean": "Up-to-date", + "clean": "Synchronized", "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 index e2d9c874..46b2b516 100644 --- a/custom/Espo/Custom/Resources/layouts/CPuls/bottomPanelsDetail.json +++ b/custom/Espo/Custom/Resources/layouts/CPuls/bottomPanelsDetail.json @@ -1,17 +1,27 @@ { - "teamZuordnungen": { + "_delimiter_": { + "disabled": true + }, + "activities": { + "disabled": true + }, + "history": { + "disabled": true + }, + "_tabBreak_0": { "index": 0, - "sticked": true, - "style": "info", - "label": "Team-Zuordnungen" + "tabBreak": true, + "tabLabel": "Dokumente" }, "dokumente": { - "index": 1, - "sticked": false, - "label": "Dokumente" + "index": 1 + }, + "_tabBreak_1": { + "index": 2, + "tabBreak": true, + "tabLabel": "Aktivitäten" }, "stream": { - "index": 2, - "sticked": false + "index": 3 } } diff --git a/custom/Espo/Custom/Resources/layouts/CPuls/defaultSidePanel.json b/custom/Espo/Custom/Resources/layouts/CPuls/defaultSidePanel.json new file mode 100644 index 00000000..98fe7b40 --- /dev/null +++ b/custom/Espo/Custom/Resources/layouts/CPuls/defaultSidePanel.json @@ -0,0 +1,11 @@ +[ + { + "name": ":assignedUser" + }, + { + "name": "teams" + }, + { + "name": "teamZuordnungen" + } +] diff --git a/custom/Espo/Custom/Resources/layouts/CPuls/detail.json b/custom/Espo/Custom/Resources/layouts/CPuls/detail.json index 9c82ebe4..2a825f88 100644 --- a/custom/Espo/Custom/Resources/layouts/CPuls/detail.json +++ b/custom/Espo/Custom/Resources/layouts/CPuls/detail.json @@ -1,6 +1,8 @@ [ { "label": "Übersicht", + "style": "default", + "tabBreak": false, "rows": [ [ {"name": "name"}, @@ -14,10 +16,6 @@ {"name": "anzahlDokumente"}, {"name": "anzahlTeamsAktiv"} ], - [ - {"name": "finalisiert"}, - {"name": "finalisierungsGrund"} - ], [ {"name": "zusammenfassung", "span": 2} ] @@ -25,6 +23,8 @@ }, { "label": "KI-Analyse", + "style": "default", + "tabBreak": false, "rows": [ [ {"name": "kiAnalyse", "span": 2} @@ -32,7 +32,24 @@ ] }, { - "label": "System", + "label": "Mandant", + "style": "default", + "tabBreak": false, + "rows": [ + [ + {"name": "mandantMitteilung"}, + {} + ], + [ + {"name": "mandantMitteilungText", "span": 2} + ] + ] + }, + { + "label": "Metadaten", + "style": "default", + "tabBreak": true, + "tabLabel": "Erweitert", "rows": [ [ {"name": "createdAt"}, @@ -41,6 +58,10 @@ [ {"name": "createdBy"}, {"name": "modifiedBy"} + ], + [ + {"name": "finalisiertAm"}, + {"name": "finalisiertVon"} ] ] } diff --git a/custom/Espo/Custom/Resources/metadata/entityDefs/CPuls.json b/custom/Espo/Custom/Resources/metadata/entityDefs/CPuls.json index a2a06c4e..e22abe3a 100644 --- a/custom/Espo/Custom/Resources/metadata/entityDefs/CPuls.json +++ b/custom/Espo/Custom/Resources/metadata/entityDefs/CPuls.json @@ -15,7 +15,8 @@ "Bereit", "In Review", "Teilweise abgeschlossen", - "Abgeschlossen" + "Abgeschlossen", + "Finalisiert" ], "default": "Neu", "required": true, @@ -26,7 +27,8 @@ "Bereit": "success", "In Review": "warning", "Teilweise abgeschlossen": "info", - "Abgeschlossen": "success" + "Abgeschlossen": "success", + "Finalisiert": "danger" } }, "syncStatus": { @@ -66,24 +68,6 @@ "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, @@ -95,6 +79,17 @@ "readOnly": true, "isCustom": true }, + "mandantMitteilung": { + "type": "bool", + "default": false, + "isCustom": true, + "tooltip": true + }, + "mandantMitteilungText": { + "type": "text", + "isCustom": true, + "tooltip": true + }, "createdAt": { "type": "datetime", "readOnly": true @@ -117,10 +112,6 @@ "type": "link", "entity": "User", "isCustom": true - }, - "teams": { - "type": "linkMultiple", - "isCustom": true } }, "links": { @@ -180,11 +171,8 @@ "syncStatus": { "columns": ["syncStatus"] }, - "finalisiert": { - "columns": ["finalisiert"] - }, "createdAt": { "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 index 512a3ca3..ad507576 100644 --- a/custom/Espo/Custom/Resources/metadata/entityDefs/CPulsTeamZuordnung.json +++ b/custom/Espo/Custom/Resources/metadata/entityDefs/CPulsTeamZuordnung.json @@ -4,12 +4,14 @@ "type": "varchar", "notStorable": true, "select": { - "select": "CONCAT:(team.name, ' - ', puls.name)" + "select": "CONCAT:(team.name, ' - ', puls.name)", + "leftJoins": ["team", "puls"] }, "orderBy": { "order": [ ["team.name", "{direction}"] - ] + ], + "leftJoins": ["team"] } }, "puls": { diff --git a/custom/Espo/Custom/Resources/metadata/formula/CPuls.json b/custom/Espo/Custom/Resources/metadata/formula/CPuls.json index f42fd963..fe5a59e0 100644 --- a/custom/Espo/Custom/Resources/metadata/formula/CPuls.json +++ b/custom/Espo/Custom/Resources/metadata/formula/CPuls.json @@ -1,3 +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}" + "beforeSaveApiScript": "// Verhindere Abschluss bei unclean Status\nif (\n entity\\isAttributeChanged('status') &&\n status == 'Abgeschlossen' &&\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 status == 'Finalisiert' &&\n !entity\\isAttributeChanged('status') &&\n (entity\\isAttributeChanged('syncStatus'))\n) {\n recordService\\throwBadRequest('Puls ist finalisiert. Neue Dokumente erzeugen automatisch einen neuen Block.');\n}" } diff --git a/data/config.php b/data/config.php index cdb874ab..f90bae33 100644 --- a/data/config.php +++ b/data/config.php @@ -358,7 +358,7 @@ return [ 0 => 'youtube.com', 1 => 'google.com' ], - 'microtime' => 1770973606.273594, + 'microtime' => 1770974863.576723, '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 ffc2cbdc..b0ffe821 100644 --- a/data/state.php +++ b/data/state.php @@ -1,7 +1,7 @@ 1770973606, - 'microtimeState' => 1770973606.458758, + 'cacheTimestamp' => 1770974863, + 'microtimeState' => 1770974863.697017, 'currencyRates' => [ 'EUR' => 1.0 ],