From 2569003f3304e6f8e370eb780715dd1fe4f7389d Mon Sep 17 00:00:00 2001 From: Chaim Date: Tue, 14 Apr 2026 06:02:27 +0000 Subject: [PATCH] fix: broaden task detection to include Contact-linked tasks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tasks created with parentType='Contact' (e.g. preparation calls linked to a case's contact) were invisible to AlertCalculator, CaseContextBuilder, and OfficeContextBuilder — all of which only queried parentType='Case'. This caused false "no preparation task" alerts in Shira's daily standup even when preparation tasks existed, because they were linked to the case's Contact instead of the Case entity directly. Changes: - AlertCalculator: getSummary, findOverdueTasks, findUpcomingTasks, getWorkloadByUser now query parentType=['Case','Contact'] - findUpcomingHearingsWithoutPrep: new caseHasPreparation() helper checks Case tasks, Contact tasks, and NhActivity-linked tasks - CaseContextBuilder.getOpenTasks: OR query across Case + Contact IDs - OfficeContextBuilder.buildCaseDrillDown: same OR query pattern - All three classes gain a getCaseContactIds() helper Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Services/AlertCalculator.php | 116 ++++++++++++++++-- .../Services/CaseContextBuilder.php | 37 +++++- .../Services/OfficeContextBuilder.php | 33 ++++- 3 files changed, 172 insertions(+), 14 deletions(-) diff --git a/files/custom/Espo/Modules/SmartAssistant/Services/AlertCalculator.php b/files/custom/Espo/Modules/SmartAssistant/Services/AlertCalculator.php index 8122299..66a29b4 100644 --- a/files/custom/Espo/Modules/SmartAssistant/Services/AlertCalculator.php +++ b/files/custom/Espo/Modules/SmartAssistant/Services/AlertCalculator.php @@ -46,7 +46,7 @@ class AlertCalculator ->where(['status!=' => self::CLOSED_STATUSES, 'deleted' => false])->count(); $totalOverdueTasks = $this->entityManager->getRDBRepository('Task') - ->where(['status!=' => ['Completed', 'Canceled', 'Deferred'], 'dateEnd<' => $today, 'dateEnd!=' => null, 'deleted' => false, 'parentType' => 'Case'])->count(); + ->where(['status!=' => ['Completed', 'Canceled', 'Deferred'], 'dateEnd<' => $today, 'dateEnd!=' => null, 'deleted' => false, 'parentType' => ['Case', 'Contact']])->count(); $upcomingHearings = $this->entityManager->getRDBRepository('Case') ->where(['cNextHearing>=' => $today, 'cNextHearing<=' => $weekEnd, 'status!=' => self::CLOSED_STATUSES, 'deleted' => false])->count(); @@ -106,7 +106,7 @@ class AlertCalculator $tasks = $this->entityManager->getRDBRepository('Task') ->select(['id', 'name', 'dateEnd', 'status', 'parentId', 'parentType', 'parentName', 'assignedUserName']) - ->where(['status!=' => ['Completed', 'Canceled', 'Deferred'], 'dateEnd<' => $today, 'dateEnd!=' => null, 'deleted' => false, 'parentType' => 'Case']) + ->where(['status!=' => ['Completed', 'Canceled', 'Deferred'], 'dateEnd<' => $today, 'dateEnd!=' => null, 'deleted' => false, 'parentType' => ['Case', 'Contact']]) ->order('dateEnd', 'ASC')->limit(0, 50)->find(); foreach ($tasks as $task) { @@ -132,7 +132,7 @@ class AlertCalculator $recentTaskThreshold = date('Y-m-d H:i:s', strtotime('-7 days')); $cases = $this->entityManager->getRDBRepository('Case') - ->select(['id', 'name', 'cNextHearing', 'assignedUserName', 'cLastActivityAt']) + ->select(['id', 'name', 'cNextHearing', 'assignedUserName', 'cLastActivityAt', 'contactId']) ->where(['cNextHearing>=' => $today, 'cNextHearing<=' => $threshold, 'status!=' => self::CLOSED_STATUSES, 'deleted' => false]) ->order('cNextHearing', 'ASC')->find(); @@ -140,13 +140,7 @@ class AlertCalculator $lastActivity = $case->get('cLastActivityAt'); if ($lastActivity && $lastActivity >= $recentActivityThreshold) continue; - $openTaskCount = $this->entityManager->getRDBRepository('Task') - ->where(['parentId' => $case->get('id'), 'parentType' => 'Case', 'status!=' => ['Completed', 'Canceled', 'Deferred'], 'deleted' => false])->count(); - if ($openTaskCount > 0) continue; - - $recentCompleted = $this->entityManager->getRDBRepository('Task') - ->where(['parentId' => $case->get('id'), 'parentType' => 'Case', 'status' => 'Completed', 'modifiedAt>=' => $recentTaskThreshold, 'deleted' => false])->count(); - if ($recentCompleted > 0) continue; + if ($this->caseHasPreparation($case, $recentTaskThreshold)) continue; $daysUntil = (new \DateTime())->diff(new \DateTime($case->get('cNextHearing')))->days; $severity = ($daysUntil <= 1) ? 'critical' : 'warning'; @@ -162,6 +156,104 @@ class AlertCalculator return $alerts; } + /** + * Check if a case has any evidence of hearing preparation: + * 1. Tasks linked directly to the case (parentType='Case') + * 2. Tasks linked to the case's contacts (parentType='Contact') + * 3. Tasks linked through NhActivity records for this case + */ + private function caseHasPreparation(\Espo\ORM\Entity $case, string $recentTaskThreshold): bool + { + $caseId = $case->get('id'); + + // Collect all parentId+parentType pairs that relate to this case + $parentConditions = [ + ['parentType' => 'Case', 'parentId' => $caseId], + ]; + + // Also check tasks linked to the case's contacts + $contactIds = $this->getCaseContactIds($case); + if (!empty($contactIds)) { + $parentConditions[] = ['parentType' => 'Contact', 'parentId' => $contactIds]; + } + + // Check 1: Any open tasks linked to the case or its contacts + $openTaskCount = $this->entityManager->getRDBRepository('Task') + ->where([ + 'OR' => $parentConditions, + 'status!=' => ['Completed', 'Canceled', 'Deferred'], + 'deleted' => false, + ])->count(); + if ($openTaskCount > 0) return true; + + // Check 2: Any recently completed tasks linked to the case or its contacts + $recentCompleted = $this->entityManager->getRDBRepository('Task') + ->where([ + 'OR' => $parentConditions, + 'status' => 'Completed', + 'modifiedAt>=' => $recentTaskThreshold, + 'deleted' => false, + ])->count(); + if ($recentCompleted > 0) return true; + + // Check 3: Tasks linked through NhActivity records for this case + $nhActivityTaskCount = $this->entityManager->getRDBRepository('NhActivity') + ->where([ + 'caseId' => $caseId, + 'taskId!=' => null, + 'deleted' => false, + ])->count(); + + if ($nhActivityTaskCount > 0) { + $nhActivities = $this->entityManager->getRDBRepository('NhActivity') + ->select(['taskId']) + ->where([ + 'caseId' => $caseId, + 'taskId!=' => null, + 'deleted' => false, + ])->find(); + + $taskIds = []; + foreach ($nhActivities as $nha) { + $taskIds[] = $nha->get('taskId'); + } + + $activeNhTasks = $this->entityManager->getRDBRepository('Task') + ->where([ + 'id' => $taskIds, + 'status!=' => ['Canceled'], + 'deleted' => false, + ])->count(); + if ($activeNhTasks > 0) return true; + } + + return false; + } + + private function getCaseContactIds(\Espo\ORM\Entity $case): array + { + $contactIds = []; + + $primaryContactId = $case->get('contactId'); + if ($primaryContactId) { + $contactIds[] = $primaryContactId; + } + + $contacts = $this->entityManager->getRDBRepository('Case') + ->getRelation($case, 'contacts') + ->select(['id']) + ->find(); + + foreach ($contacts as $contact) { + $id = $contact->get('id'); + if (!in_array($id, $contactIds)) { + $contactIds[] = $id; + } + } + + return $contactIds; + } + private function findUnassignedNewCases(): array { $alerts = []; @@ -191,7 +283,7 @@ class AlertCalculator $tasks = $this->entityManager->getRDBRepository('Task') ->select(['id', 'name', 'dateEnd', 'parentId', 'parentType', 'parentName', 'assignedUserName']) - ->where(['status!=' => ['Completed', 'Canceled', 'Deferred'], 'dateEnd>=' => $today, 'dateEnd<=' => $threshold, 'deleted' => false, 'parentType' => 'Case']) + ->where(['status!=' => ['Completed', 'Canceled', 'Deferred'], 'dateEnd>=' => $today, 'dateEnd<=' => $threshold, 'deleted' => false, 'parentType' => ['Case', 'Contact']]) ->order('dateEnd', 'ASC')->limit(0, 30)->find(); foreach ($tasks as $task) { @@ -237,7 +329,7 @@ class AlertCalculator foreach (array_keys($userCounts) as $uid) { $userCounts[$uid]['openTasks'] = $this->entityManager->getRDBRepository('Task') - ->where(['assignedUserId' => $uid, 'status!=' => ['Completed', 'Canceled', 'Deferred'], 'deleted' => false, 'parentType' => 'Case'])->count(); + ->where(['assignedUserId' => $uid, 'status!=' => ['Completed', 'Canceled', 'Deferred'], 'deleted' => false, 'parentType' => ['Case', 'Contact']])->count(); } return array_values($userCounts); diff --git a/files/custom/Espo/Modules/SmartAssistant/Services/CaseContextBuilder.php b/files/custom/Espo/Modules/SmartAssistant/Services/CaseContextBuilder.php index efbd23b..fb0c05a 100644 --- a/files/custom/Espo/Modules/SmartAssistant/Services/CaseContextBuilder.php +++ b/files/custom/Espo/Modules/SmartAssistant/Services/CaseContextBuilder.php @@ -82,9 +82,18 @@ class CaseContextBuilder private function getOpenTasks(string $caseId): array { + $parentConditions = [ + ['parentType' => 'Case', 'parentId' => $caseId], + ]; + + $contactIds = $this->getCaseContactIds($caseId); + if (!empty($contactIds)) { + $parentConditions[] = ['parentType' => 'Contact', 'parentId' => $contactIds]; + } + $tasks = []; $collection = $this->entityManager->getRDBRepository('Task') - ->where(['parentType' => 'Case', 'parentId' => $caseId, 'status!=' => ['Completed', 'Canceled']]) + ->where(['OR' => $parentConditions, 'status!=' => ['Completed', 'Canceled']]) ->order('dateEnd', 'ASC')->limit(0, 10)->find(); foreach ($collection as $t) { @@ -97,6 +106,32 @@ class CaseContextBuilder return $tasks; } + private function getCaseContactIds(string $caseId): array + { + $case = $this->entityManager->getEntityById('Case', $caseId); + if (!$case) return []; + + $contactIds = []; + $primaryContactId = $case->get('contactId'); + if ($primaryContactId) { + $contactIds[] = $primaryContactId; + } + + $contacts = $this->entityManager->getRDBRepository('Case') + ->getRelation($case, 'contacts') + ->select(['id']) + ->find(); + + foreach ($contacts as $contact) { + $id = $contact->get('id'); + if (!in_array($id, $contactIds)) { + $contactIds[] = $id; + } + } + + return $contactIds; + } + private function getUpcomingMeetings(string $caseId): array { $meetings = []; diff --git a/files/custom/Espo/Modules/SmartAssistant/Services/OfficeContextBuilder.php b/files/custom/Espo/Modules/SmartAssistant/Services/OfficeContextBuilder.php index 8f46ec9..9ac11db 100644 --- a/files/custom/Espo/Modules/SmartAssistant/Services/OfficeContextBuilder.php +++ b/files/custom/Espo/Modules/SmartAssistant/Services/OfficeContextBuilder.php @@ -58,10 +58,18 @@ class OfficeContextBuilder $contacts[] = ['name' => $c->get('name'), 'phone' => $c->get('phoneNumber'), 'email' => $c->get('emailAddress')]; } + $parentConditions = [ + ['parentType' => 'Case', 'parentId' => $caseId], + ]; + $contactIds = $this->getCaseContactIds($case); + if (!empty($contactIds)) { + $parentConditions[] = ['parentType' => 'Contact', 'parentId' => $contactIds]; + } + $tasks = []; foreach ($this->entityManager->getRDBRepository('Task') ->select(['id', 'name', 'status', 'dateEnd', 'assignedUserName']) - ->where(['parentId' => $caseId, 'parentType' => 'Case', 'status!=' => ['Completed', 'Canceled'], 'deleted' => false]) + ->where(['OR' => $parentConditions, 'status!=' => ['Completed', 'Canceled'], 'deleted' => false]) ->order('dateEnd', 'ASC')->limit(0, 20)->find() as $t) { $tasks[] = ['name' => $t->get('name'), 'status' => $t->get('status'), 'dateEnd' => $t->get('dateEnd'), 'assignedUser' => $t->get('assignedUserName')]; } @@ -76,4 +84,27 @@ class OfficeContextBuilder return ['case' => $caseData, 'contacts' => $contacts, 'openTasks' => $tasks, 'recentNotes' => $notes]; } + + private function getCaseContactIds(\Espo\ORM\Entity $case): array + { + $contactIds = []; + $primaryContactId = $case->get('contactId'); + if ($primaryContactId) { + $contactIds[] = $primaryContactId; + } + + $contacts = $this->entityManager->getRDBRepository('Case') + ->getRelation($case, 'contacts') + ->select(['id']) + ->find(); + + foreach ($contacts as $contact) { + $id = $contact->get('id'); + if (!in_array($id, $contactIds)) { + $contactIds[] = $id; + } + } + + return $contactIds; + } }