f616065585
Cards "תיקים פתוחים" and "דיונים השבוע" showed counts but clicking revealed "אין פריטים להצגה" because card counts came from direct DB queries while detail filtered from alerts with stricter criteria. Added getOpenCaseItems() and getUpcomingHearingItems() to AlertCalculator, included them in summary API response, and wired dashlet to use them. Also: renamed extension to SmartAssistant, title to "שירה — עוזרת אישית". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
339 lines
15 KiB
PHP
339 lines
15 KiB
PHP
<?php
|
|
|
|
namespace Espo\Modules\SmartAssistant\Services;
|
|
|
|
use Espo\Core\Acl;
|
|
use Espo\ORM\EntityManager;
|
|
|
|
class AlertCalculator
|
|
{
|
|
private EntityManager $entityManager;
|
|
private Acl $acl;
|
|
|
|
private const CLOSED_STATUSES = ['ClosedSettlement', 'ClosedJudgment', 'Closed', 'Rejected'];
|
|
|
|
public function __construct(EntityManager $entityManager, Acl $acl)
|
|
{
|
|
$this->entityManager = $entityManager;
|
|
$this->acl = $acl;
|
|
}
|
|
|
|
public function calculateAlerts(array $thresholds = []): array
|
|
{
|
|
$inactivityWarningDays = $thresholds['inactivityWarningDays'] ?? 14;
|
|
$inactivityCriticalDays = $thresholds['inactivityCriticalDays'] ?? 30;
|
|
$upcomingHearingDays = $thresholds['upcomingHearingDays'] ?? 7;
|
|
|
|
$alerts = [];
|
|
$alerts = array_merge($alerts, $this->findInactiveCases($inactivityWarningDays, $inactivityCriticalDays));
|
|
$alerts = array_merge($alerts, $this->findOverdueTasks());
|
|
$alerts = array_merge($alerts, $this->findUpcomingHearingsWithoutPrep($upcomingHearingDays));
|
|
$alerts = array_merge($alerts, $this->findUnassignedNewCases());
|
|
$alerts = array_merge($alerts, $this->findUpcomingTasks());
|
|
|
|
$severityOrder = ['critical' => 0, 'warning' => 1, 'info' => 2];
|
|
usort($alerts, fn($a, $b) => ($severityOrder[$a['severity']] ?? 3) <=> ($severityOrder[$b['severity']] ?? 3));
|
|
|
|
return $alerts;
|
|
}
|
|
|
|
public function getSummary(): array
|
|
{
|
|
$today = date('Y-m-d');
|
|
$weekEnd = date('Y-m-d', strtotime('+7 days'));
|
|
|
|
$totalOpenCases = $this->entityManager->getRDBRepository('Case')
|
|
->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();
|
|
|
|
$upcomingHearings = $this->entityManager->getRDBRepository('Case')
|
|
->where(['cNextHearing>=' => $today, 'cNextHearing<=' => $weekEnd, 'status!=' => self::CLOSED_STATUSES, 'deleted' => false])->count();
|
|
|
|
$alerts = $this->calculateAlerts();
|
|
$criticalCount = count(array_filter($alerts, fn($a) => $a['severity'] === 'critical'));
|
|
$warningCount = count(array_filter($alerts, fn($a) => $a['severity'] === 'warning'));
|
|
|
|
return [
|
|
'totalOpenCases' => $totalOpenCases,
|
|
'totalOverdueTasks' => $totalOverdueTasks,
|
|
'upcomingHearings7d' => $upcomingHearings,
|
|
'casesNeedingAttention' => $criticalCount + $warningCount,
|
|
'criticalAlerts' => $criticalCount,
|
|
'warningAlerts' => $warningCount,
|
|
];
|
|
}
|
|
|
|
private function findInactiveCases(int $warningDays, int $criticalDays): array
|
|
{
|
|
$alerts = [];
|
|
$now = new \DateTime();
|
|
$warningThreshold = (new \DateTime())->modify("-{$warningDays} days")->format('Y-m-d H:i:s');
|
|
$criticalThreshold = (new \DateTime())->modify("-{$criticalDays} days")->format('Y-m-d H:i:s');
|
|
|
|
$cases = $this->entityManager->getRDBRepository('Case')
|
|
->select(['id', 'name', 'status', 'assignedUserName', 'cLastActivityAt', 'createdAt'])
|
|
->where([
|
|
'status!=' => self::CLOSED_STATUSES, 'deleted' => false,
|
|
'OR' => [
|
|
['cLastActivityAt<' => $warningThreshold, 'cLastActivityAt!=' => null],
|
|
['cLastActivityAt' => null, 'createdAt<' => $warningThreshold],
|
|
],
|
|
])->order('cLastActivityAt', 'ASC')->limit(0, 50)->find();
|
|
|
|
foreach ($cases as $case) {
|
|
$lastActivity = $case->get('cLastActivityAt') ?? $case->get('createdAt');
|
|
if (!$lastActivity) continue;
|
|
$daysSince = $now->diff(new \DateTime($lastActivity))->days;
|
|
$severity = ($lastActivity < $criticalThreshold) ? 'critical' : 'warning';
|
|
|
|
$alerts[] = [
|
|
'type' => 'inactive_case', 'severity' => $severity,
|
|
'caseId' => $case->get('id'), 'caseName' => $case->get('name'),
|
|
'assignedUser' => $case->get('assignedUserName'),
|
|
'daysSinceActivity' => $daysSince,
|
|
'message' => $case->get('name') . ' - ' . $daysSince . ' ימים ללא פעילות',
|
|
];
|
|
}
|
|
return $alerts;
|
|
}
|
|
|
|
private function findOverdueTasks(): array
|
|
{
|
|
$alerts = [];
|
|
$today = date('Y-m-d');
|
|
|
|
$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'])
|
|
->order('dateEnd', 'ASC')->limit(0, 50)->find();
|
|
|
|
foreach ($tasks as $task) {
|
|
$daysOverdue = (new \DateTime())->diff(new \DateTime($task->get('dateEnd')))->days;
|
|
$alerts[] = [
|
|
'type' => 'overdue_task', 'severity' => 'critical',
|
|
'taskId' => $task->get('id'), 'taskName' => $task->get('name'),
|
|
'caseId' => $task->get('parentId'), 'caseName' => $task->get('parentName'),
|
|
'assignedUser' => $task->get('assignedUserName'),
|
|
'daysOverdue' => $daysOverdue,
|
|
'message' => $task->get('parentName') . ' - משימה "' . $task->get('name') . '" באיחור ' . $daysOverdue . ' ימים',
|
|
];
|
|
}
|
|
return $alerts;
|
|
}
|
|
|
|
private function findUpcomingHearingsWithoutPrep(int $hearingDays): array
|
|
{
|
|
$alerts = [];
|
|
$today = date('Y-m-d');
|
|
$threshold = date('Y-m-d', strtotime("+{$hearingDays} days"));
|
|
$recentActivityThreshold = date('Y-m-d H:i:s', strtotime('-3 days'));
|
|
$recentTaskThreshold = date('Y-m-d H:i:s', strtotime('-7 days'));
|
|
|
|
$cases = $this->entityManager->getRDBRepository('Case')
|
|
->select(['id', 'name', 'cNextHearing', 'assignedUserName', 'cLastActivityAt'])
|
|
->where(['cNextHearing>=' => $today, 'cNextHearing<=' => $threshold, 'status!=' => self::CLOSED_STATUSES, 'deleted' => false])
|
|
->order('cNextHearing', 'ASC')->find();
|
|
|
|
foreach ($cases as $case) {
|
|
$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;
|
|
|
|
$daysUntil = (new \DateTime())->diff(new \DateTime($case->get('cNextHearing')))->days;
|
|
$severity = ($daysUntil <= 1) ? 'critical' : 'warning';
|
|
|
|
$alerts[] = [
|
|
'type' => 'hearing_no_prep', 'severity' => $severity,
|
|
'caseId' => $case->get('id'), 'caseName' => $case->get('name'),
|
|
'assignedUser' => $case->get('assignedUserName'),
|
|
'hearingDate' => $case->get('cNextHearing'), 'daysUntil' => $daysUntil,
|
|
'message' => $case->get('name') . ' - דיון בעוד ' . $daysUntil . ' ימים, אין משימת הכנה',
|
|
];
|
|
}
|
|
return $alerts;
|
|
}
|
|
|
|
private function findUnassignedNewCases(): array
|
|
{
|
|
$alerts = [];
|
|
$threshold = date('Y-m-d H:i:s', strtotime('-3 days'));
|
|
|
|
$cases = $this->entityManager->getRDBRepository('Case')
|
|
->select(['id', 'name', 'createdAt'])->where(['status' => 'New', 'createdAt<' => $threshold, 'deleted' => false])
|
|
->order('createdAt', 'ASC')->limit(0, 20)->find();
|
|
|
|
foreach ($cases as $case) {
|
|
$daysSince = (new \DateTime())->diff(new \DateTime($case->get('createdAt')))->days;
|
|
$alerts[] = [
|
|
'type' => 'unassigned_case', 'severity' => 'warning',
|
|
'caseId' => $case->get('id'), 'caseName' => $case->get('name'),
|
|
'daysSinceCreated' => $daysSince,
|
|
'message' => $case->get('name') . ' - תיק חדש לא שובץ (' . $daysSince . ' ימים)',
|
|
];
|
|
}
|
|
return $alerts;
|
|
}
|
|
|
|
private function findUpcomingTasks(): array
|
|
{
|
|
$alerts = [];
|
|
$today = date('Y-m-d');
|
|
$threshold = date('Y-m-d', strtotime('+3 days'));
|
|
|
|
$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'])
|
|
->order('dateEnd', 'ASC')->limit(0, 30)->find();
|
|
|
|
foreach ($tasks as $task) {
|
|
$daysUntil = (new \DateTime())->diff(new \DateTime($task->get('dateEnd')))->days;
|
|
$alerts[] = [
|
|
'type' => 'upcoming_task', 'severity' => 'info',
|
|
'taskId' => $task->get('id'), 'taskName' => $task->get('name'),
|
|
'caseId' => $task->get('parentId'), 'caseName' => $task->get('parentName'),
|
|
'assignedUser' => $task->get('assignedUserName'),
|
|
'daysUntil' => $daysUntil,
|
|
'message' => $task->get('parentName') . ' - משימה "' . $task->get('name') . '" בעוד ' . $daysUntil . ' ימים',
|
|
];
|
|
}
|
|
return $alerts;
|
|
}
|
|
|
|
public function getCasesByStatus(): array
|
|
{
|
|
$result = [];
|
|
$statuses = ['New', 'Assigned', 'Pending', 'PendingHearing', 'PendingDecision', 'PendingResponse', 'Negotiation', 'Suspended', 'ClosedSettlement', 'ClosedJudgment', 'Closed', 'Rejected'];
|
|
|
|
foreach ($statuses as $status) {
|
|
$count = $this->entityManager->getRDBRepository('Case')->where(['status' => $status, 'deleted' => false])->count();
|
|
if ($count > 0) $result[$status] = $count;
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
public function getWorkloadByUser(): array
|
|
{
|
|
$userCounts = [];
|
|
$cases = $this->entityManager->getRDBRepository('Case')
|
|
->select(['assignedUserId', 'assignedUserName'])
|
|
->where(['status!=' => self::CLOSED_STATUSES, 'deleted' => false, 'assignedUserId!=' => null])->find();
|
|
|
|
foreach ($cases as $case) {
|
|
$uid = $case->get('assignedUserId');
|
|
if (!isset($userCounts[$uid])) {
|
|
$userCounts[$uid] = ['userId' => $uid, 'userName' => $case->get('assignedUserName'), 'openCases' => 0, 'openTasks' => 0];
|
|
}
|
|
$userCounts[$uid]['openCases']++;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
return array_values($userCounts);
|
|
}
|
|
|
|
public function getOpenCaseItems(int $limit = 30): array
|
|
{
|
|
$items = [];
|
|
$cases = $this->entityManager->getRDBRepository('Case')
|
|
->select(['id', 'name', 'status', 'assignedUserName', 'cLastActivityAt', 'createdAt'])
|
|
->where(['status!=' => self::CLOSED_STATUSES, 'deleted' => false])
|
|
->order('createdAt', 'DESC')
|
|
->limit(0, $limit)
|
|
->find();
|
|
|
|
$statusLabels = [
|
|
'New' => 'חדש', 'Assigned' => 'שובץ', 'Pending' => 'ממתין',
|
|
'PendingHearing' => 'ממתין לדיון', 'PendingDecision' => 'ממתין להחלטה',
|
|
'PendingResponse' => 'ממתין לתגובה', 'Negotiation' => 'משא ומתן', 'Suspended' => 'מושהה',
|
|
];
|
|
|
|
foreach ($cases as $case) {
|
|
$status = $case->get('status');
|
|
$label = $statusLabels[$status] ?? $status;
|
|
$assigned = $case->get('assignedUserName');
|
|
|
|
$items[] = [
|
|
'type' => 'open_case',
|
|
'severity' => 'info',
|
|
'caseId' => $case->get('id'),
|
|
'caseName' => $case->get('name'),
|
|
'message' => $case->get('name') . ' — ' . $label . ($assigned ? ' (' . $assigned . ')' : ''),
|
|
];
|
|
}
|
|
return $items;
|
|
}
|
|
|
|
public function getUpcomingHearingItems(int $days = 7): array
|
|
{
|
|
$today = date('Y-m-d');
|
|
$threshold = date('Y-m-d', strtotime("+{$days} days"));
|
|
$items = [];
|
|
|
|
$cases = $this->entityManager->getRDBRepository('Case')
|
|
->select(['id', 'name', 'cNextHearing', 'cCourt', 'assignedUserName'])
|
|
->where([
|
|
'cNextHearing>=' => $today,
|
|
'cNextHearing<=' => $threshold,
|
|
'status!=' => self::CLOSED_STATUSES,
|
|
'deleted' => false,
|
|
])
|
|
->order('cNextHearing', 'ASC')
|
|
->find();
|
|
|
|
foreach ($cases as $case) {
|
|
$daysUntil = (new \DateTime())->diff(new \DateTime($case->get('cNextHearing')))->days;
|
|
$court = $case->get('cCourt');
|
|
$severity = ($daysUntil <= 1) ? 'warning' : 'info';
|
|
|
|
$msg = $case->get('name') . ' — דיון בעוד ' . $daysUntil . ' ימים';
|
|
if ($court) {
|
|
$msg .= ' (' . $court . ')';
|
|
}
|
|
|
|
$items[] = [
|
|
'type' => 'upcoming_hearing',
|
|
'severity' => $severity,
|
|
'caseId' => $case->get('id'),
|
|
'caseName' => $case->get('name'),
|
|
'hearingDate' => $case->get('cNextHearing'),
|
|
'daysUntil' => $daysUntil,
|
|
'message' => $msg,
|
|
];
|
|
}
|
|
return $items;
|
|
}
|
|
|
|
public function getUpcomingHearings(int $days = 7): array
|
|
{
|
|
$today = date('Y-m-d');
|
|
$threshold = date('Y-m-d', strtotime("+{$days} days"));
|
|
|
|
$result = [];
|
|
$cases = $this->entityManager->getRDBRepository('Case')
|
|
->select(['id', 'name', 'cNextHearing', 'cCourt', 'cJudge', 'assignedUserName', 'status'])
|
|
->where(['cNextHearing>=' => $today, 'cNextHearing<=' => $threshold, 'status!=' => self::CLOSED_STATUSES, 'deleted' => false])
|
|
->order('cNextHearing', 'ASC')->find();
|
|
|
|
foreach ($cases as $case) {
|
|
$result[] = [
|
|
'caseId' => $case->get('id'), 'caseName' => $case->get('name'),
|
|
'hearingDate' => $case->get('cNextHearing'), 'court' => $case->get('cCourt'),
|
|
'judge' => $case->get('cJudge'), 'assignedUser' => $case->get('assignedUserName'),
|
|
];
|
|
}
|
|
return $result;
|
|
}
|
|
}
|