diff --git a/custom/Espo/Custom/Hooks/CAkten/UpdateLastSyncFromDocuments.php b/custom/Espo/Custom/Hooks/CAkten/UpdateLastSyncFromDocuments.php index 777c16cf..fa36a3a8 100644 --- a/custom/Espo/Custom/Hooks/CAkten/UpdateLastSyncFromDocuments.php +++ b/custom/Espo/Custom/Hooks/CAkten/UpdateLastSyncFromDocuments.php @@ -29,9 +29,7 @@ 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. + // Single aggregation row — CASE inside MAX() avoids the mixed-aggregate bug. $stmt = $pdo->prepare( "SELECT MAX(last_sync_timestamp) AS maxAdvLastSync, @@ -48,6 +46,12 @@ class UpdateLastSyncFromDocuments implements BeforeSave OR ai_sync_status IS NULL OR ai_sync_status = '' THEN 1 ELSE 0 END) AS aiWorstLevel, + -- ai_sync_status = unclean triggers graphParsingStatus + SUM(CASE WHEN ai_sync_status = 'unclean' THEN 1 ELSE 0 END) AS aiUncleanCount, + -- aiParsingStatus buckets + SUM(CASE WHEN ai_parsing_status = 'parsing' THEN 1 ELSE 0 END) AS parseParsingCount, + SUM(CASE WHEN ai_parsing_status = 'complete' THEN 1 ELSE 0 END) AS parseCompleteCount, + SUM(CASE WHEN ai_parsing_status = 'failed' THEN 1 ELSE 0 END) AS parseFailedCount, COUNT(*) AS docCount FROM c_dokumente WHERE c_akten_id = :aktenId AND deleted = 0" @@ -58,10 +62,13 @@ class UpdateLastSyncFromDocuments implements BeforeSave if (!$row || (int)$row['docCount'] === 0) { $entity->set('syncStatus', 'unclean'); $entity->set('aiSyncStatus', 'unclean'); + $entity->set('aiParsingStatus', 'unknown'); return; } - // Timestamps setzen + $docCount = (int)$row['docCount']; + + // ── Timestamps ───────────────────────────────────────────────── if (!empty($row['maxAdvLastSync'])) { $entity->set('lastSync', $row['maxAdvLastSync']); } @@ -69,7 +76,7 @@ class UpdateLastSyncFromDocuments implements BeforeSave $entity->set('aiLastSync', $row['maxAiLastSync']); } - // Advoware Status setzen (worst-case über alle Dokumente) + // ── Advoware Sync Status (worst-case) ────────────────────────── $advLevel = (int)($row['advWorstLevel'] ?? 0); if ($advLevel >= 2) { $entity->set('syncStatus', 'failed'); @@ -79,7 +86,7 @@ class UpdateLastSyncFromDocuments implements BeforeSave $entity->set('syncStatus', 'synced'); } - // AI Status setzen (worst-case über alle Dokumente) + // ── AI Sync Status (worst-case) ──────────────────────────────── $aiLevel = (int)($row['aiWorstLevel'] ?? 0); if ($aiLevel >= 2) { $entity->set('aiSyncStatus', 'failed'); @@ -89,6 +96,31 @@ class UpdateLastSyncFromDocuments implements BeforeSave $entity->set('aiSyncStatus', 'synced'); } + // ── AI Parsing Status (aggregated across all documents) ───────── + // Priority: parsing > unknown > complete_with_failures > complete + $parseParsing = (int)($row['parseParsingCount'] ?? 0); + $parseComplete = (int)($row['parseCompleteCount'] ?? 0); + $parseFailed = (int)($row['parseFailedCount'] ?? 0); + $parseUnknown = $docCount - $parseParsing - $parseComplete - $parseFailed; + + if ($parseParsing > 0) { + $entity->set('aiParsingStatus', 'parsing'); + } elseif ($parseUnknown > 0) { + $entity->set('aiParsingStatus', 'unknown'); + } elseif ($parseFailed > 0) { + // No unknown/parsing left, but some failures + $entity->set('aiParsingStatus', 'complete_with_failures'); + } else { + $entity->set('aiParsingStatus', 'complete'); + } + + // ── Graph Parsing Status (only auto-set to unclean, never reset) ─ + // If any document's AI sync status is currently unclean → graph is stale. + // Any other transition (back to complete, etc.) must be done manually. + if ((int)($row['aiUncleanCount'] ?? 0) > 0) { + $entity->set('graphParsingStatus', 'unclean'); + } + } catch (\Exception $e) { $GLOBALS['log']->error('CAkten UpdateLastSyncFromDocuments Hook Error: ' . $e->getMessage()); } diff --git a/custom/Espo/Custom/Hooks/CDokumente/SyncStatusOnRelate.php b/custom/Espo/Custom/Hooks/CDokumente/SyncStatusOnRelate.php index 02f46e07..61bcc470 100644 --- a/custom/Espo/Custom/Hooks/CDokumente/SyncStatusOnRelate.php +++ b/custom/Espo/Custom/Hooks/CDokumente/SyncStatusOnRelate.php @@ -68,6 +68,7 @@ class SyncStatusOnRelate implements AfterRelate, AfterUnrelate $entity->set('cAktenId', $advowareAkten->getId()); $entity->set('syncStatus', 'unclean'); $entity->set('aiSyncStatus', 'unclean'); + $entity->set('aiParsingStatus', 'unknown'); $this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]); $this->triggerAkteUpdate($advowareAkten->getId()); @@ -75,6 +76,7 @@ class SyncStatusOnRelate implements AfterRelate, AfterUnrelate // Kein Akte-Link — trotzdem Sync-Status auf unclean setzen $entity->set('syncStatus', 'unclean'); $entity->set('aiSyncStatus', 'unclean'); + $entity->set('aiParsingStatus', 'unknown'); $this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]); } diff --git a/custom/Espo/Custom/Hooks/CDokumente/UpdateJunctionSyncStatus.php b/custom/Espo/Custom/Hooks/CDokumente/UpdateJunctionSyncStatus.php index 941bae3d..6a5ec281 100644 --- a/custom/Espo/Custom/Hooks/CDokumente/UpdateJunctionSyncStatus.php +++ b/custom/Espo/Custom/Hooks/CDokumente/UpdateJunctionSyncStatus.php @@ -48,6 +48,7 @@ class UpdateJunctionSyncStatus implements AfterSave, AfterRemove if ($newAktenId) { $entity->set('syncStatus', 'unclean'); $entity->set('aiSyncStatus', 'unclean'); + $entity->set('aiParsingStatus', 'unknown'); $this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]); $this->triggerAkteUpdate($newAktenId); } @@ -70,6 +71,7 @@ class UpdateJunctionSyncStatus implements AfterSave, AfterRemove $entity->set('syncStatus', 'unclean'); $entity->set('aiSyncStatus', 'unclean'); + $entity->set('aiParsingStatus', 'unknown'); $this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]); $this->triggerAkteUpdate($entity->get('cAktenId')); diff --git a/custom/Espo/Custom/Resources/i18n/de_DE/CAkten.json b/custom/Espo/Custom/Resources/i18n/de_DE/CAkten.json index 785eb203..1a26f645 100644 --- a/custom/Espo/Custom/Resources/i18n/de_DE/CAkten.json +++ b/custom/Espo/Custom/Resources/i18n/de_DE/CAkten.json @@ -27,7 +27,9 @@ "globalLastSync": "Globaler letzter Sync", "syncSchalter": "Sync aktiv", "dokumentes": "Dokumente", - "rubrum": "Rubrum" + "rubrum": "Rubrum", + "aiParsingStatus": "AI Parsing-Status", + "graphParsingStatus": "Graph Parsing-Status" }, "options": { "syncStatus": { @@ -65,6 +67,18 @@ "unclean": "Nicht synchronisiert", "pending_sync": "Synchronisierung ausstehend", "failed": "Fehlgeschlagen" + }, + "aiParsingStatus": { + "unknown": "Unbekannt", + "parsing": "In Verarbeitung", + "complete": "Abgeschlossen", + "complete_with_failures": "Abgeschlossen mit Fehlern" + }, + "graphParsingStatus": { + "no_graph": "Kein Graph", + "parsing": "In Verarbeitung", + "complete": "Abgeschlossen", + "unclean": "Veraltet" } }, "tooltips": { diff --git a/custom/Espo/Custom/Resources/i18n/de_DE/CDokumente.json b/custom/Espo/Custom/Resources/i18n/de_DE/CDokumente.json index 9f7a9d46..a268ccda 100644 --- a/custom/Espo/Custom/Resources/i18n/de_DE/CDokumente.json +++ b/custom/Espo/Custom/Resources/i18n/de_DE/CDokumente.json @@ -30,7 +30,8 @@ "aiSyncHash": "AI Sync-Hash", "aiSyncStatus": "AI Sync-Status", "aiLastSync": "AI Letzter Sync", - "aiFileId": "AI File-ID" + "aiFileId": "AI File-ID", + "aiParsingStatus": "AI Parsing-Status" }, "links": { "contactsvmhdokumente": "Freigegebene Nutzer", @@ -77,6 +78,12 @@ "synced": "Synchronisiert", "failed": "Fehler", "unsupported": "Nicht unterstützt" + }, + "aiParsingStatus": { + "unknown": "Unbekannt", + "parsing": "In Verarbeitung", + "complete": "Abgeschlossen", + "failed": "Fehlgeschlagen" } } } \ No newline at end of file diff --git a/custom/Espo/Custom/Resources/i18n/en_US/CAkten.json b/custom/Espo/Custom/Resources/i18n/en_US/CAkten.json index a11b91dd..cb7dde5f 100644 --- a/custom/Espo/Custom/Resources/i18n/en_US/CAkten.json +++ b/custom/Espo/Custom/Resources/i18n/en_US/CAkten.json @@ -28,7 +28,9 @@ "globalLastSync": "Global Last Sync", "syncSchalter": "Sync Active", "dokumentes": "Documents", - "rubrum": "Rubrum" + "rubrum": "Rubrum", + "aiParsingStatus": "AI Parsing Status", + "graphParsingStatus": "Graph Parsing Status" }, "options": { "syncStatus": { @@ -66,6 +68,18 @@ "unclean": "Not Synchronized", "pending_sync": "Synchronization Pending", "failed": "Failed" + }, + "aiParsingStatus": { + "unknown": "Unknown", + "parsing": "Parsing", + "complete": "Complete", + "complete_with_failures": "Complete with Failures" + }, + "graphParsingStatus": { + "no_graph": "No Graph", + "parsing": "Parsing", + "complete": "Complete", + "unclean": "Stale" } }, "tooltips": { diff --git a/custom/Espo/Custom/Resources/i18n/en_US/CDokumente.json b/custom/Espo/Custom/Resources/i18n/en_US/CDokumente.json index 7d6be4ec..f0e3343d 100644 --- a/custom/Espo/Custom/Resources/i18n/en_US/CDokumente.json +++ b/custom/Espo/Custom/Resources/i18n/en_US/CDokumente.json @@ -30,7 +30,8 @@ "aiSyncHash": "AI Sync Hash", "aiSyncStatus": "AI Sync Status", "aiLastSync": "AI Last Sync", - "aiFileId": "AI File ID" + "aiFileId": "AI File ID", + "aiParsingStatus": "AI Parsing Status" }, "links": { "contactsvmhdokumente": "Portal Users", @@ -83,6 +84,12 @@ "synced": "Synchronized", "failed": "Failed", "unsupported": "Unsupported" + }, + "aiParsingStatus": { + "unknown": "Unknown", + "parsing": "Parsing", + "complete": "Complete", + "failed": "Failed" } } } \ No newline at end of file diff --git a/custom/Espo/Custom/Resources/layouts/CAkten/detail.json b/custom/Espo/Custom/Resources/layouts/CAkten/detail.json index aec1d791..ef0a9613 100644 --- a/custom/Espo/Custom/Resources/layouts/CAkten/detail.json +++ b/custom/Espo/Custom/Resources/layouts/CAkten/detail.json @@ -74,6 +74,14 @@ "name": "aiProvider" }, {} + ], + [ + { + "name": "aiParsingStatus" + }, + { + "name": "graphParsingStatus" + } ] ], "style": "default", diff --git a/custom/Espo/Custom/Resources/layouts/CDokumente/detail.json b/custom/Espo/Custom/Resources/layouts/CDokumente/detail.json index 8a941e93..d42ff834 100644 --- a/custom/Espo/Custom/Resources/layouts/CDokumente/detail.json +++ b/custom/Espo/Custom/Resources/layouts/CDokumente/detail.json @@ -122,7 +122,9 @@ { "name": "aiSyncHash" }, - {"isFull": false} + { + "name": "aiParsingStatus" + } ] ], "dynamicLogicVisible": null, diff --git a/custom/Espo/Custom/Resources/layouts/CDokumente/listForAkten.json b/custom/Espo/Custom/Resources/layouts/CDokumente/listForAkten.json index bacda3b3..acc43d21 100644 --- a/custom/Espo/Custom/Resources/layouts/CDokumente/listForAkten.json +++ b/custom/Espo/Custom/Resources/layouts/CDokumente/listForAkten.json @@ -19,6 +19,10 @@ "name": "aiSyncStatus", "align": "left" }, + { + "name": "aiParsingStatus", + "align": "left" + }, { "name": "aiLastSync", "align": "left" @@ -37,4 +41,4 @@ "name": "createdAt", "align": "left" } -] +] \ No newline at end of file diff --git a/custom/Espo/Custom/Resources/metadata/entityDefs/CAkten.json b/custom/Espo/Custom/Resources/metadata/entityDefs/CAkten.json index c55d0f5a..9f9f0f34 100644 --- a/custom/Espo/Custom/Resources/metadata/entityDefs/CAkten.json +++ b/custom/Espo/Custom/Resources/metadata/entityDefs/CAkten.json @@ -216,6 +216,45 @@ "maxLength": 500, "tooltip": true, "isCustom": true + }, + "aiParsingStatus": { + "type": "enum", + "required": false, + "options": [ + "unknown", + "parsing", + "complete", + "complete_with_failures" + ], + "style": { + "unknown": "default", + "parsing": "info", + "complete": "success", + "complete_with_failures": "warning" + }, + "default": "unknown", + "readOnly": true, + "tooltip": true, + "isCustom": true + }, + "graphParsingStatus": { + "type": "enum", + "required": false, + "options": [ + "no_graph", + "parsing", + "complete", + "unclean" + ], + "style": { + "no_graph": "default", + "parsing": "info", + "complete": "success", + "unclean": "warning" + }, + "default": "no_graph", + "tooltip": true, + "isCustom": true } }, "links": { diff --git a/custom/Espo/Custom/Resources/metadata/entityDefs/CDokumente.json b/custom/Espo/Custom/Resources/metadata/entityDefs/CDokumente.json index 715400b7..01310148 100644 --- a/custom/Espo/Custom/Resources/metadata/entityDefs/CDokumente.json +++ b/custom/Espo/Custom/Resources/metadata/entityDefs/CDokumente.json @@ -213,6 +213,24 @@ "maxLength": 255, "tooltip": true, "isCustom": true + }, + "aiParsingStatus": { + "type": "enum", + "options": [ + "unknown", + "parsing", + "complete", + "failed" + ], + "style": { + "unknown": "default", + "parsing": "info", + "complete": "success", + "failed": "danger" + }, + "default": "unknown", + "tooltip": true, + "isCustom": true } }, "links": { diff --git a/data/config.php b/data/config.php index c97340a4..2b39b6ea 100644 --- a/data/config.php +++ b/data/config.php @@ -359,7 +359,7 @@ return [ 0 => 'youtube.com', 1 => 'google.com' ], - 'microtime' => 1774603769.791242, + 'microtime' => 1774604561.068253, '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 b31f4b9c..fccaf198 100644 --- a/data/state.php +++ b/data/state.php @@ -1,7 +1,7 @@ 1774603769, - 'microtimeState' => 1774603769.963245, + 'cacheTimestamp' => 1774604771, + 'microtimeState' => 1774604771.465835, 'currencyRates' => [ 'EUR' => 1.0 ],