Add sync status and last sync fields to CAIKnowledge and CAdvowareAkten entities, update layouts and metadata. Implement hooks for global sync status management.

This commit is contained in:
2026-03-11 19:29:55 +01:00
parent 986cafcfd6
commit 9411337939
19 changed files with 419 additions and 8 deletions

View File

@@ -0,0 +1,75 @@
<?php
namespace Espo\Custom\Hooks\CAIKnowledge;
use Espo\ORM\Entity;
use Espo\ORM\Repository\Option\SaveOptions;
use Espo\Core\Hook\Hook\BeforeSave;
/**
* Hook: Prüft Junction-Table und aktualisiert globalen syncStatus
* basierend auf den syncstatus-Werten der verknüpften Dokumente
*/
class CheckGlobalSyncStatus implements BeforeSave
{
public function __construct(
private \Espo\ORM\EntityManager $entityManager
) {}
public function beforeSave(Entity $entity, SaveOptions $options): void
{
// Überspringe, wenn skipHooks gesetzt ist (verhindert Loops)
if ($options->get('skipHooks')) {
return;
}
// Nur wenn Entity bereits existiert (nicht bei Create)
if ($entity->isNew()) {
return;
}
try {
// Hole alle verknüpften Dokumente mit ihren syncstatus-Werten aus der Junction-Tabelle
$query = $this->entityManager->getQueryBuilder()
->select(['syncstatus'])
->from('CAIKnowledgeDokumente')
->where([
'cAIKnowledgeId' => $entity->getId(),
'deleted' => false
])
->build();
$pdoStatement = $this->entityManager->getQueryExecutor()->execute($query);
$rows = $pdoStatement->fetchAll(\PDO::FETCH_ASSOC);
// Wenn keine Dokumente verknüpft, setze auf "unclean"
if (empty($rows)) {
$entity->set('syncStatus', 'unclean');
return;
}
// Prüfe, ob irgendein Dokument "new" oder "unclean" ist
$hasUnsynced = false;
foreach ($rows as $row) {
$status = $row['syncstatus'] ?? null;
if ($status === 'new' || $status === 'unclean' || $status === null || $status === '') {
$hasUnsynced = true;
break;
}
}
// Setze globalen Status
if ($hasUnsynced) {
$entity->set('syncStatus', 'unclean');
} else {
// Alle Dokumente sind "synced"
$entity->set('syncStatus', 'synced');
$entity->set('lastSync', date('Y-m-d H:i:s'));
}
} catch (\Exception $e) {
// Bei Fehler loggen und Status auf "unclean" setzen
$GLOBALS['log']->error('CAIKnowledge CheckGlobalSyncStatus Hook Error: ' . $e->getMessage());
$entity->set('syncStatus', 'unclean');
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Espo\Custom\Hooks\CAIKnowledge;
use Espo\ORM\Entity;
use Espo\Core\Hook\Hook\AfterRelate;
/**
* Hook: Setzt Dokument-Sync-Status auf "new" beim Verknüpfen und
* globalen syncStatus auf "unclean"
*/
class DokumenteSyncStatus implements AfterRelate
{
public function __construct(
private \Espo\ORM\EntityManager $entityManager
) {}
public function afterRelate(
Entity $entity,
string $relationName,
Entity $foreignEntity,
array $columnData,
\Espo\ORM\Repository\Option\RelateOptions $options
): void {
// Nur für dokumentes-Beziehung
if ($relationName !== 'dokumentes') {
return;
}
// Setze Sync-Status des Dokuments in der Junction-Tabelle auf "new"
$repository = $this->entityManager->getRDBRepository('CAIKnowledge');
try {
$repository->getRelation($entity, 'dokumentes')->updateColumns(
$foreignEntity,
['syncstatus' => 'new']
);
// Setze globalen syncStatus auf "unclean"
$entity->set('syncStatus', 'unclean');
$this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]);
} catch (\Exception $e) {
// Fehler loggen, aber nicht werfen (um Verknüpfung nicht zu blockieren)
$GLOBALS['log']->error('CAIKnowledge DokumenteSyncStatus Hook Error: ' . $e->getMessage());
}
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Espo\Custom\Hooks\CAdvowareAkten;
use Espo\ORM\Entity;
use Espo\ORM\Repository\Option\SaveOptions;
use Espo\Core\Hook\Hook\BeforeSave;
/**
* Hook: Prüft Junction-Table und aktualisiert globalen syncStatus
* basierend auf den syncstatus-Werten der verknüpften Dokumente
*/
class CheckGlobalSyncStatus implements BeforeSave
{
public function __construct(
private \Espo\ORM\EntityManager $entityManager
) {}
public function beforeSave(Entity $entity, SaveOptions $options): void
{
// Überspringe, wenn skipHooks gesetzt ist (verhindert Loops)
if ($options->get('skipHooks')) {
return;
}
// Nur wenn Entity bereits existiert (nicht bei Create)
if ($entity->isNew()) {
return;
}
try {
// Hole alle verknüpften Dokumente mit ihren syncstatus-Werten aus der Junction-Tabelle
$query = $this->entityManager->getQueryBuilder()
->select(['syncstatus'])
->from('CAdvowareAktenDokumente')
->where([
'cAdvowareAktenId' => $entity->getId(),
'deleted' => false
])
->build();
$pdoStatement = $this->entityManager->getQueryExecutor()->execute($query);
$rows = $pdoStatement->fetchAll(\PDO::FETCH_ASSOC);
// Wenn keine Dokumente verknüpft, setze auf "unclean"
if (empty($rows)) {
$entity->set('syncStatus', 'unclean');
return;
}
// Prüfe, ob irgendein Dokument "new" oder "unclean" ist
$hasUnsynced = false;
foreach ($rows as $row) {
$status = $row['syncstatus'] ?? null;
if ($status === 'new' || $status === 'unclean' || $status === null || $status === '') {
$hasUnsynced = true;
break;
}
}
// Setze globalen Status
if ($hasUnsynced) {
$entity->set('syncStatus', 'unclean');
} else {
// Alle Dokumente sind "synced"
$entity->set('syncStatus', 'synced');
$entity->set('lastSync', date('Y-m-d H:i:s'));
}
} catch (\Exception $e) {
// Bei Fehler loggen und Status auf "unclean" setzen
$GLOBALS['log']->error('CAdvowareAkten CheckGlobalSyncStatus Hook Error: ' . $e->getMessage());
$entity->set('syncStatus', 'unclean');
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Espo\Custom\Hooks\CAdvowareAkten;
use Espo\ORM\Entity;
use Espo\Core\Hook\Hook\AfterRelate;
/**
* Hook: Setzt Dokument-Sync-Status auf "new" beim Verknüpfen und
* globalen syncStatus auf "unclean"
*/
class DokumenteSyncStatus implements AfterRelate
{
public function __construct(
private \Espo\ORM\EntityManager $entityManager
) {}
public function afterRelate(
Entity $entity,
string $relationName,
Entity $foreignEntity,
array $columnData,
\Espo\ORM\Repository\Option\RelateOptions $options
): void {
// Nur für dokumentes-Beziehung
if ($relationName !== 'dokumentes') {
return;
}
// Setze Sync-Status des Dokuments in der Junction-Tabelle auf "new"
$repository = $this->entityManager->getRDBRepository('CAdvowareAkten');
try {
$repository->getRelation($entity, 'dokumentes')->updateColumns(
$foreignEntity,
['syncstatus' => 'new']
);
// Setze globalen syncStatus auf "unclean"
$entity->set('syncStatus', 'unclean');
$this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]);
} catch (\Exception $e) {
// Fehler loggen, aber nicht werfen (um Verknüpfung nicht zu blockieren)
$GLOBALS['log']->error('CAdvowareAkten DokumenteSyncStatus Hook Error: ' . $e->getMessage());
}
}
}

View File

@@ -7,6 +7,8 @@
"vmhRumungsklage": "Räumungsklage",
"mietinkasso": "Mietinkasso",
"datenbankId": "Datenbank-ID",
"syncStatus": "Sync-Status",
"lastSync": "Letzte Synchronisation",
"dokumenteAiDocumentId": "AI Document ID",
"dokumenteSyncstatus": "Sync-Status",
"dokumenteLastSync": "Letzter Sync"
@@ -15,5 +17,16 @@
"dokumentes": "Dokumente",
"vmhRumungsklage": "Räumungsklage",
"mietinkasso": "Mietinkasso"
},
"options": {
"syncStatus": {
"synced": "Synchronisiert",
"unclean": "Nicht synchronisiert"
}
},
"tooltips": {
"syncStatus": "Globaler Synchronisationsstatus: synced = Alle Dokumente synchronisiert, unclean = Mindestens ein Dokument ist neu oder hat Änderungen. Wird automatisch basierend auf den Dokumenten-Status aktualisiert.",
"lastSync": "Zeitpunkt der letzten erfolgreichen Synchronisation aller Dokumente",
"datenbankId": "Eindeutige ID in der AI-Datenbank"
}
}

View File

@@ -14,10 +14,23 @@
"mietinkasso": "Mietinkasso",
"aktenzeichen": "Aktenzeichen",
"aktennummer": "Aktennummer",
"pfad": "Dateipfad",
"aktenpfad": "Aktenpfad (Windows)",
"syncStatus": "Sync-Status",
"lastSync": "Letzte Synchronisation",
"dokumentes": "Dokumente",
"dokumenteHnr": "HNR",
"dokumenteSyncstatus": "Sync-Status",
"dokumenteLastSync": "Letzter Sync"
},
"options": {
"syncStatus": {
"synced": "Synchronisiert",
"unclean": "Nicht synchronisiert"
}
},
"tooltips": {
"syncStatus": "Globaler Synchronisationsstatus: synced = Alle Dokumente synchronisiert, unclean = Mindestens ein Dokument ist neu oder hat Änderungen. Wird automatisch basierend auf den Dokumenten-Status aktualisiert.",
"lastSync": "Zeitpunkt der letzten erfolgreichen Synchronisation aller Dokumente",
"aktenpfad": "Windows-Dateipfad zur Akte in Advoware"
}
}

View File

@@ -4,6 +4,8 @@
"vmhRumungsklage": "Räumungsklage",
"mietinkasso": "Mietinkasso",
"datenbankId": "Database ID",
"syncStatus": "Sync Status",
"lastSync": "Last Synchronization",
"dokumenteAiDocumentId": "AI Document ID",
"dokumenteSyncstatus": "Sync Status",
"dokumenteLastSync": "Last Sync"
@@ -15,5 +17,16 @@
},
"labels": {
"Create CAIKnowledge": "Create AI Knowledge"
},
"options": {
"syncStatus": {
"synced": "Synchronized",
"unclean": "Not Synchronized"
}
},
"tooltips": {
"syncStatus": "Global synchronization status: synced = All documents synchronized, unclean = At least one document is new or has changes. Updated automatically based on document status.",
"lastSync": "Timestamp of the last successful synchronization of all documents",
"datenbankId": "Unique ID in the AI database"
}
}

View File

@@ -4,7 +4,9 @@
"mietinkasso": "Mietinkasso",
"aktenzeichen": "Aktenzeichen",
"aktennummer": "Aktennummer",
"pfad": "File Path",
"aktenpfad": "File Path (Windows)",
"syncStatus": "Sync Status",
"lastSync": "Last Synchronization",
"dokumentes": "Dokumente",
"dokumenteHnr": "HNR",
"dokumenteSyncstatus": "Sync Status",
@@ -20,5 +22,16 @@
},
"labels": {
"Create CAdvowareAkten": "Create Advoware Akten"
},
"options": {
"syncStatus": {
"synced": "Synchronized",
"unclean": "Not Synchronized"
}
},
"tooltips": {
"syncStatus": "Global synchronization status: synced = All documents synchronized, unclean = At least one document is new or has changes. Updated automatically based on document status.",
"lastSync": "Timestamp of the last successful synchronization of all documents",
"aktenpfad": "Windows file path to the file in Advoware"
}
}

View File

@@ -9,5 +9,14 @@
},
"dokumentes": {
"index": 1
},
"_tabBreak_1": {
"index": 2,
"tabBreak": true,
"tabLabel": "Ereignisse"
},
"stream": {
"sticked": false,
"index": 3
}
}

View File

@@ -0,0 +1,24 @@
[
{
"rows": [
[
{
"name": "name"
},
{
"name": "datenbankId"
}
],
[
{
"name": "syncStatus"
},
{
"name": "lastSync"
}
]
],
"style": "default",
"label": "Overview"
}
]

View File

@@ -15,5 +15,14 @@
},
"dokumentes": {
"index": 1
},
"_tabBreak_1": {
"index": 2,
"tabBreak": true,
"tabLabel": "Ereignisse"
},
"stream": {
"sticked": false,
"index": 3
}
}

View File

@@ -5,11 +5,24 @@
{
"name": "name"
},
{
"name": "aktenzeichen"
}
],
[
{
"name": "aktennummer"
},
{
"name": "aktenzeichen"
"name": "aktenpfad"
}
],
[
{
"name": "syncStatus"
},
{
"name": "lastSync"
}
]
],

View File

@@ -3,6 +3,16 @@
"boolFilterList": [
"onlyMy"
],
"bottomPanels": {
"detail": [
{
"name": "stream",
"label": "Ereignisse",
"reference": "stream",
"stacked": false
}
]
},
"relationshipPanels": {
"dokumentes": {
"layout": "listForAIKnowledge",

View File

@@ -30,6 +30,12 @@
"name": "history",
"reference": "history",
"disabled": true
},
{
"name": "stream",
"label": "Ereignisse",
"reference": "stream",
"stacked": false
}
]
},

View File

@@ -47,6 +47,28 @@
"tooltip": true,
"isCustom": true
},
"syncStatus": {
"type": "enum",
"required": false,
"options": [
"synced",
"unclean"
],
"style": {
"synced": "success",
"unclean": "warning"
},
"default": "unclean",
"tooltip": true,
"isCustom": true
},
"lastSync": {
"type": "datetime",
"required": false,
"readOnly": true,
"tooltip": true,
"isCustom": true
},
"dokumenteAiDocumentId": {
"type": "varchar",
"notStorable": true,

View File

@@ -56,12 +56,34 @@
"readOnlyAfterCreate": true,
"isCustom": true
},
"pfad": {
"aktenpfad": {
"type": "varchar",
"maxLength": 500,
"tooltip": true,
"isCustom": true
},
"syncStatus": {
"type": "enum",
"required": false,
"options": [
"synced",
"unclean"
],
"style": {
"synced": "success",
"unclean": "warning"
},
"default": "unclean",
"tooltip": true,
"isCustom": true
},
"lastSync": {
"type": "datetime",
"required": false,
"readOnly": true,
"tooltip": true,
"isCustom": true
},
"dokumenteHnr": {
"type": "int",
"notStorable": true,

View File

@@ -14,7 +14,7 @@
"customizable": true,
"importable": true,
"notifications": true,
"stream": false,
"stream": true,
"disabled": false,
"type": "BasePlus",
"module": "Custom",

View File

@@ -360,7 +360,7 @@ return [
0 => 'youtube.com',
1 => 'google.com'
],
'microtime' => 1773251210.764005,
'microtime' => 1773253602.105787,
'siteUrl' => 'https://crm.bitbylaw.com',
'fullTextSearchMinLength' => 4,
'webSocketUrl' => 'ws://api.bitbylaw.com:5000/espocrm/ws',

View File

@@ -1,7 +1,7 @@
<?php
return [
'cacheTimestamp' => 1773251210,
'microtimeState' => 1773251210.899627,
'cacheTimestamp' => 1773253761,
'microtimeState' => 1773253761.495155,
'currencyRates' => [
'EUR' => 1.0
],