From fa3c92379f66c1083eec926e95775d3c350f7a9c Mon Sep 17 00:00:00 2001 From: bsiggel Date: Fri, 27 Mar 2026 10:31:12 +0100 Subject: [PATCH] Refactor document propagation and sync status hooks; deprecate CAkten hooks and implement new logic in CDokumente; update microtime values in config and state files --- .../Hooks/CAkten/PropagateDocumentsUp.php | 72 +------ .../CAkten/UpdateLastSyncFromDocuments.php | 94 +++------ .../Hooks/CDokumente/PropagateDocumentsUp.php | 90 +++++++++ .../Hooks/CDokumente/SyncStatusOnRelate.php | 191 ++++++++++++++++++ .../Hooks/CMietinkasso/PropagateDocuments.php | 3 +- .../CVmhRumungsklage/PropagateDocuments.php | 3 +- data/config.php | 2 +- data/state.php | 4 +- 8 files changed, 325 insertions(+), 134 deletions(-) create mode 100644 custom/Espo/Custom/Hooks/CDokumente/PropagateDocumentsUp.php create mode 100644 custom/Espo/Custom/Hooks/CDokumente/SyncStatusOnRelate.php diff --git a/custom/Espo/Custom/Hooks/CAkten/PropagateDocumentsUp.php b/custom/Espo/Custom/Hooks/CAkten/PropagateDocumentsUp.php index db2485ef..27eaf201 100644 --- a/custom/Espo/Custom/Hooks/CAkten/PropagateDocumentsUp.php +++ b/custom/Espo/Custom/Hooks/CAkten/PropagateDocumentsUp.php @@ -5,79 +5,15 @@ use Espo\ORM\Entity; use Espo\Core\Hook\Hook\AfterSave; /** - * Hook: Propagiert Dokumenten-Änderungen von Akten nach oben zu Räumungsklage/Mietinkasso + * DEPRECATED / DEAD CODE — dieser Hook war in der falschen Namespace. * - * Wenn ein Dokument einer Akte zugewiesen wird (via cAktenId): - * → verknüpfe mit verbundener Räumungsklage/Mietinkasso + * CAkten-Hooks feuern bei CAkten-Saves, nicht bei CDokumente-Saves. + * Die Logik wurde in Hooks/CDokumente/PropagateDocumentsUp.php verschoben. */ class PropagateDocumentsUp implements AfterSave { - private static array $processing = []; - - public function __construct( - private \Espo\ORM\EntityManager $entityManager - ) {} - public function afterSave(Entity $entity, \Espo\ORM\Repository\Option\SaveOptions $options): void { - // Only process when cAktenId changed - if (!$entity->isAttributeChanged('cAktenId')) { - return; - } - - $akteId = $entity->get('cAktenId'); - if (!$akteId) { - return; - } - - $key = $akteId . '-' . $entity->getId() . '-propagate'; - if (isset(self::$processing[$key])) { - return; - } - self::$processing[$key] = true; - - try { - $akte = $this->entityManager->getEntity('CAkten', $akteId); - if (!$akte) { - return; - } - - $raumungsklage = $this->entityManager - ->getRDBRepository('CAkten') - ->getRelation($akte, 'vmhRumungsklage') - ->findOne(); - - if ($raumungsklage) { - $this->relateDocument($raumungsklage, 'dokumentesvmhraumungsklage', $entity); - } - - $mietinkasso = $this->entityManager - ->getRDBRepository('CAkten') - ->getRelation($akte, 'mietinkasso') - ->findOne(); - - if ($mietinkasso) { - $this->relateDocument($mietinkasso, 'dokumentesmietinkasso', $entity); - } - - } catch (\Exception $e) { - $GLOBALS['log']->error('CAkten PropagateDocumentsUp Error: ' . $e->getMessage()); - } finally { - unset(self::$processing[$key]); - } - } - - private function relateDocument(Entity $parentEntity, string $relationName, Entity $document): void - { - $repository = $this->entityManager->getRDBRepository($parentEntity->getEntityType()); - $relation = $repository->getRelation($parentEntity, $relationName); - - $isRelated = $relation - ->where(['id' => $document->getId()]) - ->findOne(); - - if (!$isRelated) { - $relation->relate($document); - } + // intentionally empty — see Hooks/CDokumente/PropagateDocumentsUp.php } } diff --git a/custom/Espo/Custom/Hooks/CAkten/UpdateLastSyncFromDocuments.php b/custom/Espo/Custom/Hooks/CAkten/UpdateLastSyncFromDocuments.php index a3d1c64d..777c16cf 100644 --- a/custom/Espo/Custom/Hooks/CAkten/UpdateLastSyncFromDocuments.php +++ b/custom/Espo/Custom/Hooks/CAkten/UpdateLastSyncFromDocuments.php @@ -29,89 +29,61 @@ class UpdateLastSyncFromDocuments implements BeforeSave $pdo = $this->entityManager->getPDO(); $aktenId = $entity->getId(); + // Einzelne Zeile mit aggregierten Worst-Case-Werten über alle Dokumente. + // CASE-Ausdrücke in MAX() vermeiden das GROUP-BY-Problem bei gemischten + // Aggregat- und Nicht-Aggregat-Spalten. $stmt = $pdo->prepare( "SELECT MAX(last_sync_timestamp) AS maxAdvLastSync, - MAX(ai_last_sync) AS maxAiLastSync, - sync_status, - ai_sync_status + MAX(ai_last_sync) AS maxAiLastSync, + MAX(CASE + WHEN sync_status = 'failed' THEN 2 + WHEN sync_status IN ('new','unclean') + OR sync_status IS NULL OR sync_status = '' THEN 1 + ELSE 0 + END) AS advWorstLevel, + MAX(CASE + WHEN ai_sync_status = 'failed' THEN 2 + WHEN ai_sync_status IN ('new','unclean') + OR ai_sync_status IS NULL OR ai_sync_status = '' THEN 1 + ELSE 0 + END) AS aiWorstLevel, + COUNT(*) AS docCount FROM c_dokumente WHERE c_akten_id = :aktenId AND deleted = 0" ); $stmt->execute([':aktenId' => $aktenId]); - $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC); + $row = $stmt->fetch(\PDO::FETCH_ASSOC); - if (empty($rows)) { + if (!$row || (int)$row['docCount'] === 0) { $entity->set('syncStatus', 'unclean'); $entity->set('aiSyncStatus', 'unclean'); return; } - // Timestamps - $maxAdvLastSync = null; - $maxAiLastSync = null; - - // Status-Tracker - $advHasFailed = false; - $advHasUnsynced = false; - $aiHasFailed = false; - $aiHasUnsynced = false; - - foreach ($rows as $row) { - // Advoware: neuester Timestamp - if (!empty($row['maxAdvLastSync'])) { - if ($maxAdvLastSync === null || $row['maxAdvLastSync'] > $maxAdvLastSync) { - $maxAdvLastSync = $row['maxAdvLastSync']; - } - } - - // AI: neuester Timestamp - if (!empty($row['maxAiLastSync'])) { - if ($maxAiLastSync === null || $row['maxAiLastSync'] > $maxAiLastSync) { - $maxAiLastSync = $row['maxAiLastSync']; - } - } - - // Advoware: schlechtester Status - $advStatus = $row['sync_status'] ?? null; - if ($advStatus === 'failed') { - $advHasFailed = true; - } elseif ($advStatus === 'new' || $advStatus === 'unclean' || $advStatus === null || $advStatus === '') { - $advHasUnsynced = true; - } - - // AI: schlechtester Status - $aiStatus = $row['ai_sync_status'] ?? null; - if ($aiStatus === 'failed') { - $aiHasFailed = true; - } elseif ($aiStatus === 'new' || $aiStatus === 'unclean' || $aiStatus === null || $aiStatus === '') { - $aiHasUnsynced = true; - } + // Timestamps setzen + if (!empty($row['maxAdvLastSync'])) { + $entity->set('lastSync', $row['maxAdvLastSync']); + } + if (!empty($row['maxAiLastSync'])) { + $entity->set('aiLastSync', $row['maxAiLastSync']); } - // Advoware Timestamp setzen - if ($maxAdvLastSync !== null) { - $entity->set('lastSync', $maxAdvLastSync); - } - - // AI Timestamp setzen - if ($maxAiLastSync !== null) { - $entity->set('aiLastSync', $maxAiLastSync); - } - - // Advoware Status setzen (worst-case) - if ($advHasFailed) { + // Advoware Status setzen (worst-case über alle Dokumente) + $advLevel = (int)($row['advWorstLevel'] ?? 0); + if ($advLevel >= 2) { $entity->set('syncStatus', 'failed'); - } elseif ($advHasUnsynced) { + } elseif ($advLevel === 1) { $entity->set('syncStatus', 'unclean'); } else { $entity->set('syncStatus', 'synced'); } - // AI Status setzen (worst-case) - if ($aiHasFailed) { + // AI Status setzen (worst-case über alle Dokumente) + $aiLevel = (int)($row['aiWorstLevel'] ?? 0); + if ($aiLevel >= 2) { $entity->set('aiSyncStatus', 'failed'); - } elseif ($aiHasUnsynced) { + } elseif ($aiLevel === 1) { $entity->set('aiSyncStatus', 'unclean'); } else { $entity->set('aiSyncStatus', 'synced'); diff --git a/custom/Espo/Custom/Hooks/CDokumente/PropagateDocumentsUp.php b/custom/Espo/Custom/Hooks/CDokumente/PropagateDocumentsUp.php new file mode 100644 index 00000000..79a56d41 --- /dev/null +++ b/custom/Espo/Custom/Hooks/CDokumente/PropagateDocumentsUp.php @@ -0,0 +1,90 @@ +get('skipHooks')) { + return; + } + + $akteId = $entity->get('cAktenId'); + if (!$akteId) { + return; + } + + // Nur wenn cAktenId neu gesetzt oder geändert wurde + if (!$entity->isNew() && !$entity->isAttributeChanged('cAktenId')) { + return; + } + + $key = $akteId . '-' . $entity->getId() . '-propagate-up'; + if (isset(self::$processing[$key])) { + return; + } + self::$processing[$key] = true; + + try { + $akte = $this->entityManager->getEntity('CAkten', $akteId); + if (!$akte) { + return; + } + + $raumungsklage = $this->entityManager + ->getRDBRepository('CAkten') + ->getRelation($akte, 'vmhRumungsklage') + ->findOne(); + + if ($raumungsklage) { + $this->relateDocument($raumungsklage, 'dokumentesvmhraumungsklage', $entity); + } + + $mietinkasso = $this->entityManager + ->getRDBRepository('CAkten') + ->getRelation($akte, 'mietinkasso') + ->findOne(); + + if ($mietinkasso) { + $this->relateDocument($mietinkasso, 'dokumentesmietinkasso', $entity); + } + + } catch (\Exception $e) { + $GLOBALS['log']->error('CDokumente PropagateDocumentsUp Error: ' . $e->getMessage()); + } finally { + unset(self::$processing[$key]); + } + } + + private function relateDocument(Entity $parentEntity, string $relationName, Entity $document): void + { + $repository = $this->entityManager->getRDBRepository($parentEntity->getEntityType()); + $relation = $repository->getRelation($parentEntity, $relationName); + + $isRelated = $relation + ->where(['id' => $document->getId()]) + ->findOne(); + + if (!$isRelated) { + $relation->relate($document); + } + } +} diff --git a/custom/Espo/Custom/Hooks/CDokumente/SyncStatusOnRelate.php b/custom/Espo/Custom/Hooks/CDokumente/SyncStatusOnRelate.php new file mode 100644 index 00000000..02f46e07 --- /dev/null +++ b/custom/Espo/Custom/Hooks/CDokumente/SyncStatusOnRelate.php @@ -0,0 +1,191 @@ + ['CVmhRumungsklage', 'advowareAkten', 'aIKnowledge'], + 'mietinkassosdokumente' => ['CMietinkasso', 'advowareAkten', 'aIKnowledge'], + ]; + + /** @var array */ + private static array $processing = []; + + public function __construct( + private \Espo\ORM\EntityManager $entityManager + ) {} + + // ------------------------------------------------------------------------- + + public function afterRelate( + Entity $entity, + string $relationName, + Entity $foreignEntity, + array $columnData, + RelateOptions $options + ): void { + if (!isset(self::RELATION_MAP[$relationName])) { + return; + } + + $key = $entity->getId() . '-' . $foreignEntity->getId() . '-' . $relationName . '-relate'; + if (isset(self::$processing[$key])) { + return; + } + self::$processing[$key] = true; + + try { + [, $aktenRelation, $aiKnowledgeRelation] = self::RELATION_MAP[$relationName]; + + // AdvowareAkten über die Parent-Entity ermitteln + $advowareAkten = $this->entityManager + ->getRDBRepository($foreignEntity->getEntityType()) + ->getRelation($foreignEntity, $aktenRelation) + ->findOne(); + + if ($advowareAkten) { + $entity->set('cAktenId', $advowareAkten->getId()); + $entity->set('syncStatus', 'unclean'); + $entity->set('aiSyncStatus', 'unclean'); + $this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]); + + $this->triggerAkteUpdate($advowareAkten->getId()); + } else { + // Kein Akte-Link — trotzdem Sync-Status auf unclean setzen + $entity->set('syncStatus', 'unclean'); + $entity->set('aiSyncStatus', 'unclean'); + $this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]); + } + + // AIKnowledge-Verknüpfung propagieren + $aiKnowledge = $this->entityManager + ->getRDBRepository($foreignEntity->getEntityType()) + ->getRelation($foreignEntity, $aiKnowledgeRelation) + ->findOne(); + + if ($aiKnowledge) { + $this->relateIfNotAlready($aiKnowledge, 'dokumentes', $entity); + } + + } catch (\Exception $e) { + $GLOBALS['log']->error( + 'CDokumente SyncStatusOnRelate afterRelate (' . $relationName . ') Error: ' . $e->getMessage() + ); + } finally { + unset(self::$processing[$key]); + } + } + + // ------------------------------------------------------------------------- + + public function afterUnrelate( + Entity $entity, + string $relationName, + Entity $foreignEntity, + UnrelateOptions $options + ): void { + if (!isset(self::RELATION_MAP[$relationName])) { + return; + } + + $key = $entity->getId() . '-' . $foreignEntity->getId() . '-' . $relationName . '-unrelate'; + if (isset(self::$processing[$key])) { + return; + } + self::$processing[$key] = true; + + try { + [, $aktenRelation, $aiKnowledgeRelation] = self::RELATION_MAP[$relationName]; + + // Alte Akte ermitteln + $advowareAkten = $this->entityManager + ->getRDBRepository($foreignEntity->getEntityType()) + ->getRelation($foreignEntity, $aktenRelation) + ->findOne(); + + $oldAkteId = null; + + if ($advowareAkten && $entity->get('cAktenId') === $advowareAkten->getId()) { + $oldAkteId = $advowareAkten->getId(); + $entity->set('cAktenId', null); + $entity->set('syncStatus', 'unclean'); + $entity->set('aiSyncStatus', 'unclean'); + $this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]); + } + + if ($oldAkteId) { + $this->triggerAkteUpdate($oldAkteId); + } + + // AIKnowledge-Verknüpfung entfernen + $aiKnowledge = $this->entityManager + ->getRDBRepository($foreignEntity->getEntityType()) + ->getRelation($foreignEntity, $aiKnowledgeRelation) + ->findOne(); + + if ($aiKnowledge) { + $this->unrelateIfExists($aiKnowledge, 'dokumentes', $entity); + } + + } catch (\Exception $e) { + $GLOBALS['log']->error( + 'CDokumente SyncStatusOnRelate afterUnrelate (' . $relationName . ') Error: ' . $e->getMessage() + ); + } finally { + unset(self::$processing[$key]); + } + } + + // ------------------------------------------------------------------------- + + private function triggerAkteUpdate(string $akteId): void + { + $akte = $this->entityManager->getEntityById('CAkten', $akteId); + if ($akte) { + $this->entityManager->saveEntity($akte, ['silent' => true]); + } + } + + private function relateIfNotAlready(Entity $parentEntity, string $relationName, Entity $document): void + { + $repository = $this->entityManager->getRDBRepository($parentEntity->getEntityType()); + $relation = $repository->getRelation($parentEntity, $relationName); + + $isRelated = $relation->where(['id' => $document->getId()])->findOne(); + if (!$isRelated) { + $relation->relate($document); + } + } + + private function unrelateIfExists(Entity $parentEntity, string $relationName, Entity $document): void + { + $repository = $this->entityManager->getRDBRepository($parentEntity->getEntityType()); + $relation = $repository->getRelation($parentEntity, $relationName); + + $isRelated = $relation->where(['id' => $document->getId()])->findOne(); + if ($isRelated) { + $relation->unrelate($document); + } + } +} diff --git a/custom/Espo/Custom/Hooks/CMietinkasso/PropagateDocuments.php b/custom/Espo/Custom/Hooks/CMietinkasso/PropagateDocuments.php index f867cb13..b748c7e1 100644 --- a/custom/Espo/Custom/Hooks/CMietinkasso/PropagateDocuments.php +++ b/custom/Espo/Custom/Hooks/CMietinkasso/PropagateDocuments.php @@ -48,7 +48,8 @@ class PropagateDocuments implements AfterRelate, AfterUnrelate // Set direct belongsTo relationship on document if ($advowareAkten) { $foreignEntity->set('cAktenId', $advowareAkten->getId()); - $foreignEntity->set('syncStatus', 'new'); // Mark as new for Advoware sync + $foreignEntity->set('syncStatus', 'unclean'); // Advoware-Sync ausstehend + $foreignEntity->set('aiSyncStatus', 'unclean'); // AI-Sync ausstehend $this->entityManager->saveEntity($foreignEntity, ['silent' => true, 'skipHooks' => true]); // Akte über neue Verlinkung informieren → syncStatus auf unclean diff --git a/custom/Espo/Custom/Hooks/CVmhRumungsklage/PropagateDocuments.php b/custom/Espo/Custom/Hooks/CVmhRumungsklage/PropagateDocuments.php index 1731df66..b4940f68 100644 --- a/custom/Espo/Custom/Hooks/CVmhRumungsklage/PropagateDocuments.php +++ b/custom/Espo/Custom/Hooks/CVmhRumungsklage/PropagateDocuments.php @@ -48,7 +48,8 @@ class PropagateDocuments implements AfterRelate, AfterUnrelate // Set direct belongsTo relationship on document if ($advowareAkten) { $foreignEntity->set('cAktenId', $advowareAkten->getId()); - $foreignEntity->set('syncStatus', 'new'); // Mark as new for Advoware sync + $foreignEntity->set('syncStatus', 'unclean'); // Advoware-Sync ausstehend + $foreignEntity->set('aiSyncStatus', 'unclean'); // AI-Sync ausstehend $this->entityManager->saveEntity($foreignEntity, ['silent' => true, 'skipHooks' => true]); // Akte über neue Verlinkung informieren → syncStatus auf unclean diff --git a/data/config.php b/data/config.php index 64ab90e2..c97340a4 100644 --- a/data/config.php +++ b/data/config.php @@ -359,7 +359,7 @@ return [ 0 => 'youtube.com', 1 => 'google.com' ], - 'microtime' => 1774562639.614825, + 'microtime' => 1774603769.791242, '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 3f14d5b8..b31f4b9c 100644 --- a/data/state.php +++ b/data/state.php @@ -1,7 +1,7 @@ 1774566125, - 'microtimeState' => 1774566125.048162, + 'cacheTimestamp' => 1774603769, + 'microtimeState' => 1774603769.963245, 'currencyRates' => [ 'EUR' => 1.0 ],