get('skipHooks')) { return; } if ($entity->isNew()) { return; } try { $pdo = $this->entityManager->getPDO(); $aktenId = $entity->getId(); // Single aggregation row — CASE inside MAX() avoids the mixed-aggregate bug. $stmt = $pdo->prepare( "SELECT MAX(last_sync_timestamp) AS maxAdvLastSync, 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, -- 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" ); $stmt->execute([':aktenId' => $aktenId]); $row = $stmt->fetch(\PDO::FETCH_ASSOC); if (!$row || (int)$row['docCount'] === 0) { $entity->set('syncStatus', 'unclean'); $entity->set('aiSyncStatus', 'unclean'); $entity->set('aiParsingStatus', 'unknown'); return; } $docCount = (int)$row['docCount']; // ── Timestamps ───────────────────────────────────────────────── if (!empty($row['maxAdvLastSync'])) { $entity->set('lastSync', $row['maxAdvLastSync']); } if (!empty($row['maxAiLastSync'])) { $entity->set('aiLastSync', $row['maxAiLastSync']); } // ── Advoware Sync Status (worst-case) ────────────────────────── $advLevel = (int)($row['advWorstLevel'] ?? 0); if ($advLevel >= 2) { $entity->set('syncStatus', 'failed'); } elseif ($advLevel === 1) { $entity->set('syncStatus', 'unclean'); } else { $entity->set('syncStatus', 'synced'); } // ── AI Sync Status (worst-case) ──────────────────────────────── $aiLevel = (int)($row['aiWorstLevel'] ?? 0); if ($aiLevel >= 2) { $entity->set('aiSyncStatus', 'failed'); } elseif ($aiLevel === 1) { $entity->set('aiSyncStatus', 'unclean'); } else { $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()); } } }